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