Skip to main content

running_process/broker/server/
control_socket.rs

1//! Shared broker control socket dispatch for Hello and admin frames.
2//!
3//! The v1 broker uses one local socket for both client Hello negotiation and
4//! admin verbs. This module keeps the bounded synchronous serve helpers aligned
5//! with that contract while the long-lived daemon loop is still being built.
6
7use std::io::{Read, Write};
8use std::num::NonZeroUsize;
9
10use interprocess::local_socket::traits::{Listener, Stream as _};
11use prost::Message;
12
13use crate::broker::protocol::{
14    read_frame, write_frame, AdminReply, ErrorCode, Frame, FramingError, HelloReply,
15    MAX_HELLO_BYTES,
16};
17
18use super::admin::{handle_admin_frame, AdminFrameError, AdminSnapshot, ADMIN_PAYLOAD_PROTOCOL};
19use super::connection::{
20    bind_local_socket, peer_identity_from_stream, refused_reply, reply_for_framing_error,
21    write_response_frame, BrokerConnectionError, HelloResponder, LocalSocketCleanup,
22    PeerCredentialPolicy,
23};
24use super::deadline_stream::{hello_read_deadline, DeadlineStream};
25use super::fd_pressure::{FdPressureDecision, FdPressureGuard};
26use super::hello_handler::PeerIdentity;
27
28/// Result of handling one control socket connection.
29#[derive(Clone, Debug, PartialEq)]
30pub enum ControlSocketReply {
31    /// Peer was rejected by credential policy before any bytes were read.
32    DroppedPeer,
33    /// The connection was handled as a Hello exchange.
34    Hello(HelloReply),
35    /// The connection was handled as an admin request.
36    Admin(AdminReply),
37}
38
39/// Connection limit for a broker control-socket accept loop.
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum ControlSocketConnectionLimit {
42    /// Accept exactly this many connections, then return.
43    Bounded(NonZeroUsize),
44    /// Continue accepting until the process exits or binding/accepting fails.
45    Unbounded,
46}
47
48impl ControlSocketConnectionLimit {
49    fn should_continue(self, accepted: usize) -> bool {
50        match self {
51            Self::Bounded(limit) => accepted < limit.get(),
52            Self::Unbounded => true,
53        }
54    }
55}
56
57/// Handle one already-accepted broker control connection.
58pub fn handle_control_connection_with_peer_policy<S, R, F>(
59    stream: &mut S,
60    hello_responder: &R,
61    snapshot_provider: &F,
62    peer: PeerIdentity,
63    peer_policy: &PeerCredentialPolicy,
64) -> Result<ControlSocketReply, ControlSocketError>
65where
66    S: Read + Write,
67    R: HelloResponder + ?Sized,
68    F: Fn() -> AdminSnapshot + ?Sized,
69{
70    handle_control_connection_with_peer_policy_and_fd_guard(
71        stream,
72        hello_responder,
73        snapshot_provider,
74        peer,
75        peer_policy,
76        None,
77    )
78}
79
80/// Handle one already-accepted broker control connection, refusing Hello
81/// frames with `ERROR_FD_PRESSURE` while `fd_guard` reports a demotion
82/// (#390). Admin frames are always served so `status` can surface the
83/// demoted state.
84pub fn handle_control_connection_with_peer_policy_and_fd_guard<S, R, F>(
85    stream: &mut S,
86    hello_responder: &R,
87    snapshot_provider: &F,
88    peer: PeerIdentity,
89    peer_policy: &PeerCredentialPolicy,
90    fd_guard: Option<&FdPressureGuard>,
91) -> Result<ControlSocketReply, ControlSocketError>
92where
93    S: Read + Write,
94    R: HelloResponder + ?Sized,
95    F: Fn() -> AdminSnapshot + ?Sized,
96{
97    if !peer_policy.allows(&peer) {
98        return Ok(ControlSocketReply::DroppedPeer);
99    }
100
101    let request_bytes = match read_frame(stream) {
102        Ok(bytes) => bytes,
103        Err(err) => {
104            let reply = reply_for_framing_error(&err);
105            write_response_frame(stream, None, &reply)?;
106            return Ok(ControlSocketReply::Hello(reply));
107        }
108    };
109
110    let request_frame = match Frame::decode(request_bytes.as_slice()) {
111        Ok(frame) => frame,
112        Err(_) => {
113            let reply = refused_reply(ErrorCode::ErrorPeerRejected, "malformed broker Frame", 0);
114            write_response_frame(stream, None, &reply)?;
115            return Ok(ControlSocketReply::Hello(reply));
116        }
117    };
118
119    if request_frame.payload_protocol == ADMIN_PAYLOAD_PROTOCOL {
120        let snapshot = snapshot_provider();
121        let response_frame = handle_admin_frame(request_frame, &snapshot)?;
122        let reply = write_admin_response_frame(stream, &response_frame)?;
123        return Ok(ControlSocketReply::Admin(reply));
124    }
125
126    let reply = if request_bytes.len() > MAX_HELLO_BYTES {
127        refused_reply(
128            ErrorCode::ErrorPeerRejected,
129            "initial Hello frame exceeds 64 KiB",
130            0,
131        )
132    } else if let Some(guard) = fd_guard.filter(|guard| guard.is_demoted()) {
133        guard.refusal_reply()
134    } else {
135        hello_responder.handle_frame(request_frame.clone(), peer)
136    };
137    write_response_frame(stream, Some(&request_frame), &reply)?;
138    Ok(ControlSocketReply::Hello(reply))
139}
140
141/// Run a bounded local-socket accept loop that dispatches Hello and admin
142/// frames on the same endpoint.
143pub fn serve_control_socket_connections_with_policy<R, F>(
144    socket_path: &str,
145    hello_responder: &R,
146    snapshot_provider: F,
147    connection_count: usize,
148    peer_policy: &PeerCredentialPolicy,
149) -> Result<(), ControlSocketError>
150where
151    R: HelloResponder + ?Sized,
152    F: Fn() -> AdminSnapshot,
153{
154    let Some(connection_count) = NonZeroUsize::new(connection_count) else {
155        return Ok(());
156    };
157
158    serve_control_socket_connections_with_limit_and_policy(
159        socket_path,
160        hello_responder,
161        snapshot_provider,
162        ControlSocketConnectionLimit::Bounded(connection_count),
163        peer_policy,
164    )
165}
166
167/// Run a broker control-socket accept loop that dispatches Hello and admin
168/// frames on the same endpoint.
169pub fn serve_control_socket_connections_with_limit_and_policy<R, F>(
170    socket_path: &str,
171    hello_responder: &R,
172    snapshot_provider: F,
173    connection_limit: ControlSocketConnectionLimit,
174    peer_policy: &PeerCredentialPolicy,
175) -> Result<(), ControlSocketError>
176where
177    R: HelloResponder + ?Sized,
178    F: Fn() -> AdminSnapshot,
179{
180    serve_control_socket_connections_with_limit_policy_and_post_hello(
181        socket_path,
182        hello_responder,
183        snapshot_provider,
184        connection_limit,
185        peer_policy,
186        |_stream, _reply| {},
187    )
188}
189
190/// Run a broker control-socket accept loop with a post-Hello connection hook.
191///
192/// `post_hello` runs after a Hello reply has been written, with the client
193/// connection still open. The production serve path uses it to attempt the
194/// optional handle-passing handoff (#387) when negotiation issued a handoff
195/// token; the hook must stay silent toward the client on failure.
196pub fn serve_control_socket_connections_with_limit_policy_and_post_hello<R, F, H>(
197    socket_path: &str,
198    hello_responder: &R,
199    snapshot_provider: F,
200    connection_limit: ControlSocketConnectionLimit,
201    peer_policy: &PeerCredentialPolicy,
202    post_hello: H,
203) -> Result<(), ControlSocketError>
204where
205    R: HelloResponder + ?Sized,
206    F: Fn() -> AdminSnapshot,
207    H: FnMut(&mut interprocess::local_socket::Stream, &HelloReply),
208{
209    let fd_guard = FdPressureGuard::default();
210    serve_control_socket_connections_with_limit_policy_post_hello_and_fd_guard(
211        socket_path,
212        hello_responder,
213        snapshot_provider,
214        connection_limit,
215        peer_policy,
216        post_hello,
217        &fd_guard,
218    )
219}
220
221/// Run a broker control-socket accept loop with fd-pressure self-demotion
222/// (#390).
223///
224/// `fd_guard` is shared so callers can surface the demotion state in admin
225/// snapshots. When `accept()` fails with EMFILE/ENFILE the loop demotes
226/// instead of returning the error: subsequent Hello connections receive a
227/// structured `ERROR_FD_PRESSURE` refusal (admin verbs keep working), and
228/// the guard recovers automatically after a streak of successful accepts.
229#[allow(clippy::too_many_arguments)]
230pub fn serve_control_socket_connections_with_limit_policy_post_hello_and_fd_guard<R, F, H>(
231    socket_path: &str,
232    hello_responder: &R,
233    snapshot_provider: F,
234    connection_limit: ControlSocketConnectionLimit,
235    peer_policy: &PeerCredentialPolicy,
236    mut post_hello: H,
237    fd_guard: &FdPressureGuard,
238) -> Result<(), ControlSocketError>
239where
240    R: HelloResponder + ?Sized,
241    F: Fn() -> AdminSnapshot,
242    H: FnMut(&mut interprocess::local_socket::Stream, &HelloReply),
243{
244    /// Back-off between accepts while demoted so a hard fd-exhaustion loop
245    /// cannot spin the broker's CPU at 100%.
246    const FD_PRESSURE_ACCEPT_BACKOFF: std::time::Duration = std::time::Duration::from_millis(50);
247
248    let listener = bind_local_socket(socket_path)?;
249    let cleanup = LocalSocketCleanup(socket_path);
250    let result = (|| {
251        let mut accepted = 0;
252        while connection_limit.should_continue(accepted) {
253            let mut stream = match listener.accept() {
254                Ok(stream) => {
255                    fd_guard.on_accept_ok();
256                    stream
257                }
258                Err(err) => {
259                    let was_demoted = fd_guard.is_demoted();
260                    if fd_guard.on_accept_error(&err) == FdPressureDecision::Demoted {
261                        if !was_demoted {
262                            eprintln!(
263                                "running-process-broker: accept on {socket_path} demoted \
264                                 under fd pressure: {err}"
265                            );
266                        }
267                        accepted += 1;
268                        std::thread::sleep(FD_PRESSURE_ACCEPT_BACKOFF);
269                        continue;
270                    }
271                    return Err(BrokerConnectionError::Io(err).into());
272                }
273            };
274            accepted += 1;
275            let peer = peer_identity_from_stream(&stream)?;
276            // Bound the Hello/admin read against a deadline (issue #590,
277            // cluster G) so a silent or trickle peer cannot stall this
278            // single-threaded accept loop. Set the accepted stream
279            // nonblocking for the deadline-bounded handler, then restore
280            // blocking mode for the post_hello callback below.
281            let nonblocking_set = stream.set_nonblocking(true).is_ok();
282            let reply_result = {
283                let mut deadline_stream = DeadlineStream::new(&mut stream, hello_read_deadline());
284                handle_control_connection_with_peer_policy_and_fd_guard(
285                    &mut deadline_stream,
286                    hello_responder,
287                    &snapshot_provider,
288                    peer.clone(),
289                    peer_policy,
290                    Some(fd_guard),
291                )
292            };
293            if nonblocking_set {
294                let _ = stream.set_nonblocking(false);
295            }
296            let reply = reply_result?;
297            if reply == ControlSocketReply::DroppedPeer {
298                eprintln!(
299                    "running-process-broker: dropped connection on {socket_path} from peer \
300                     pid={} uid_or_sid={:?}: credential policy refused",
301                    peer.pid, peer.uid_or_sid
302                );
303            }
304            if let ControlSocketReply::Hello(hello_reply) = &reply {
305                post_hello(&mut stream, hello_reply);
306            }
307        }
308        Ok(())
309    })();
310    drop(listener);
311    drop(cleanup);
312    result
313}
314
315fn write_admin_response_frame<W: Write>(
316    writer: &mut W,
317    response_frame: &Frame,
318) -> Result<AdminReply, ControlSocketError> {
319    let mut response_bytes = Vec::new();
320    response_frame
321        .encode(&mut response_bytes)
322        .map_err(ControlSocketError::EncodeFrame)?;
323    write_frame(writer, &response_bytes)?;
324    AdminReply::decode(response_frame.payload.as_slice())
325        .map_err(ControlSocketError::DecodeAdminReply)
326}
327
328/// Errors raised while dispatching a shared broker control socket frame.
329#[derive(Debug, thiserror::Error)]
330pub enum ControlSocketError {
331    /// Hello/local-socket connection handling failed.
332    #[error(transparent)]
333    Connection(#[from] BrokerConnectionError),
334    /// Frame read/write failed.
335    #[error(transparent)]
336    Framing(#[from] FramingError),
337    /// Admin frame validation or dispatch failed.
338    #[error(transparent)]
339    AdminFrame(#[from] AdminFrameError),
340    /// The response frame could not be encoded.
341    #[error("failed to encode broker control response Frame: {0}")]
342    EncodeFrame(prost::EncodeError),
343    /// The admin response payload could not be decoded after dispatch.
344    #[error("failed to decode admin reply payload: {0}")]
345    DecodeAdminReply(prost::DecodeError),
346}
347
348#[cfg(test)]
349mod cluster_g_tests {
350    use super::*;
351    use std::time::{Duration, Instant};
352
353    struct NeverReady;
354    impl Read for NeverReady {
355        fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
356            Err(std::io::Error::new(
357                std::io::ErrorKind::WouldBlock,
358                "never ready",
359            ))
360        }
361    }
362    impl Write for NeverReady {
363        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
364            Ok(buf.len())
365        }
366        fn flush(&mut self) -> std::io::Result<()> {
367            Ok(())
368        }
369    }
370
371    #[test]
372    fn deadline_stream_read_times_out_on_silent_peer() {
373        let mut inner = NeverReady;
374        let mut ds = DeadlineStream::new(&mut inner, Instant::now() + Duration::from_millis(100));
375        let mut buf = [0u8; 4];
376        let start = Instant::now();
377        let err = ds.read(&mut buf).unwrap_err();
378        assert_eq!(err.kind(), std::io::ErrorKind::TimedOut);
379        assert!(start.elapsed() < Duration::from_secs(2), "must be bounded");
380    }
381
382    #[test]
383    fn deadline_stream_passes_ready_data_through() {
384        let data = b"hello";
385        let mut cursor = std::io::Cursor::new(data.to_vec());
386        let mut ds = DeadlineStream::new(&mut cursor, Instant::now() + Duration::from_secs(1));
387        let mut buf = [0u8; 5];
388        ds.read_exact(&mut buf).unwrap();
389        assert_eq!(&buf, data);
390    }
391}