Skip to main content

running_process/broker/server/
connection.rs

1//! Framed broker connection handling for the v1 Hello path.
2//!
3//! This module keeps the wire I/O boundary separate from
4//! [`HelloHandler`]. The long-lived accept loop can call the same
5//! single-connection function after binding the platform pipe/socket and
6//! verifying peer credentials.
7
8use std::collections::HashMap;
9use std::io::{self, Read, Write};
10use std::sync::Arc;
11use std::thread;
12
13use prost::Message;
14
15use crate::broker::protocol::{
16    hello_reply::Result as HelloReplyResult, read_frame_with_cap, write_frame, ErrorCode, Frame,
17    FrameKind, FramingError, HelloReply, PayloadEncoding, Refused, CONTROL_PAYLOAD_PROTOCOL,
18    MAX_HELLO_BYTES, PROTOCOL_VERSION,
19};
20use crate::broker::server::deadline_stream::{hello_read_deadline, with_nonblocking_deadline};
21use crate::broker::server::{HelloHandler, HelloRouter, PeerIdentity};
22use crate::platform::ipc;
23
24/// Peer credential policy applied before reading a Hello frame.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub enum PeerCredentialPolicy {
27    /// Accept any peer whose platform credentials can be read.
28    AllowAny,
29    /// Accept only peers whose UID or SID exactly matches `uid_or_sid`.
30    OwnerOnly {
31        /// Expected owner UID or SID string.
32        uid_or_sid: String,
33    },
34}
35
36impl PeerCredentialPolicy {
37    /// Build a permissive policy.
38    pub fn allow_any() -> Self {
39        Self::AllowAny
40    }
41
42    /// Build a policy that accepts only one owner UID or SID.
43    pub fn owner_only(uid_or_sid: impl Into<String>) -> Self {
44        Self::OwnerOnly {
45            uid_or_sid: uid_or_sid.into(),
46        }
47    }
48
49    /// Build an owner-only policy for the current platform user.
50    pub fn current_user() -> Option<Self> {
51        ipc::current_user_id().ok().map(Self::owner_only)
52    }
53
54    /// Return true when `peer` is authorized by this policy.
55    pub fn allows(&self, peer: &PeerIdentity) -> bool {
56        match self {
57            Self::AllowAny => true,
58            Self::OwnerOnly { uid_or_sid } => {
59                !uid_or_sid.is_empty() && peer.uid_or_sid == *uid_or_sid
60            }
61        }
62    }
63}
64
65/// Handles a decoded broker Hello frame and returns the protocol reply.
66///
67/// This keeps the frame I/O boundary independent from the concrete routing
68/// strategy. Tests and preloaded-backend serve mode can use [`HelloHandler`], while the
69/// broker accept loop can route through [`HelloRouter`].
70pub trait HelloResponder {
71    /// Decode and answer a broker Hello frame for an OS-verified peer.
72    fn handle_frame(&self, frame: Frame, peer: PeerIdentity) -> HelloReply;
73}
74
75impl HelloResponder for HelloHandler {
76    fn handle_frame(&self, frame: Frame, peer: PeerIdentity) -> HelloReply {
77        Self::handle_frame(self, frame, peer)
78    }
79}
80
81impl HelloResponder for HelloRouter<'_> {
82    fn handle_frame(&self, frame: Frame, peer: PeerIdentity) -> HelloReply {
83        Self::handle_frame(self, frame, peer)
84    }
85}
86
87/// Handle one already-accepted broker connection.
88///
89/// The connection reads exactly one v1-framed [`Frame`], decodes the
90/// embedded `Hello`, writes one v1-framed response [`Frame`] containing
91/// a `HelloReply`, then returns the reply for metrics/logging callers.
92pub fn handle_hello_connection<S: Read + Write>(
93    stream: &mut S,
94    handler: &HelloHandler,
95    peer: PeerIdentity,
96) -> Result<HelloReply, BrokerConnectionError> {
97    handle_hello_connection_with(stream, handler, peer)
98}
99
100/// Handle one already-accepted broker connection with a pluggable responder.
101///
102/// The framed wire behavior is identical to [`handle_hello_connection`]; only
103/// the decoded Hello routing strategy is supplied by the caller.
104pub fn handle_hello_connection_with<S, R>(
105    stream: &mut S,
106    responder: &R,
107    peer: PeerIdentity,
108) -> Result<HelloReply, BrokerConnectionError>
109where
110    S: Read + Write,
111    R: HelloResponder + ?Sized,
112{
113    handle_hello_connection_with_peer_policy(
114        stream,
115        responder,
116        peer,
117        &PeerCredentialPolicy::allow_any(),
118    )
119    .map(|reply| reply.expect("allow-any policy must not drop peers"))
120}
121
122/// Handle one already-accepted broker connection with an explicit peer policy.
123///
124/// Returns `Ok(None)` when the policy rejects the peer. The caller should drop
125/// the stream without writing a `HelloReply`; this is the broker's silent
126/// foreign-peer rejection path.
127pub fn handle_hello_connection_with_peer_policy<S, R>(
128    stream: &mut S,
129    responder: &R,
130    peer: PeerIdentity,
131    peer_policy: &PeerCredentialPolicy,
132) -> Result<Option<HelloReply>, BrokerConnectionError>
133where
134    S: Read + Write,
135    R: HelloResponder + ?Sized,
136{
137    if !peer_policy.allows(&peer) {
138        return Ok(None);
139    }
140
141    let request_bytes = match read_frame_with_cap(stream, MAX_HELLO_BYTES) {
142        Ok(bytes) => bytes,
143        Err(err) => {
144            let reply = reply_for_framing_error(&err);
145            write_response_frame(stream, None, &reply)?;
146            return Ok(Some(reply));
147        }
148    };
149
150    let request_frame = match Frame::decode(request_bytes.as_slice()) {
151        Ok(frame) => frame,
152        Err(_) => {
153            let reply = refused_reply(ErrorCode::ErrorPeerRejected, "malformed broker Frame", 0);
154            write_response_frame(stream, None, &reply)?;
155            return Ok(Some(reply));
156        }
157    };
158
159    let reply = responder.handle_frame(request_frame.clone(), peer);
160    write_response_frame(stream, Some(&request_frame), &reply)?;
161    Ok(Some(reply))
162}
163
164/// Run one blocking local-socket accept and serve exactly one Hello.
165///
166/// This is a testable stepping stone toward the full Phase 4 accept
167/// loop. It binds the platform local socket, accepts one peer, derives
168/// available OS peer credentials, serves one framed Hello exchange, and
169/// returns.
170pub fn serve_one_local_socket(
171    socket_path: &str,
172    handler: &HelloHandler,
173) -> Result<HelloReply, BrokerConnectionError> {
174    serve_one_local_socket_with(socket_path, handler)
175}
176
177/// Run one blocking local-socket accept and serve exactly one Hello with a
178/// pluggable responder.
179pub fn serve_one_local_socket_with<R>(
180    socket_path: &str,
181    responder: &R,
182) -> Result<HelloReply, BrokerConnectionError>
183where
184    R: HelloResponder + ?Sized,
185{
186    serve_one_local_socket_with_peer_policy(
187        socket_path,
188        responder,
189        &PeerCredentialPolicy::allow_any(),
190    )
191    .map(|reply| reply.expect("allow-any policy must not drop peers"))
192}
193
194/// Run one blocking local-socket accept with an explicit peer policy.
195pub fn serve_one_local_socket_with_peer_policy<R>(
196    socket_path: &str,
197    responder: &R,
198    peer_policy: &PeerCredentialPolicy,
199) -> Result<Option<HelloReply>, BrokerConnectionError>
200where
201    R: HelloResponder + ?Sized,
202{
203    let listener = bind_local_socket(socket_path)?;
204    let cleanup = LocalSocketCleanup(socket_path);
205    let result = (|| {
206        let mut stream = listener.accept()?;
207        let peer = peer_identity_from_stream(&stream)?;
208        with_nonblocking_deadline(&mut stream, hello_read_deadline(), |stream| {
209            handle_hello_connection_with_peer_policy(stream, responder, peer, peer_policy)
210        })
211    })();
212    drop(listener);
213    drop(cleanup);
214    result
215}
216
217/// Run a bounded blocking local-socket accept loop.
218///
219/// This is the synchronous Phase 4 test harness for the Hello accept
220/// path. It accepts `connection_count` peers, handles each connection
221/// on a worker thread, waits for all workers, then returns.
222pub fn serve_local_socket_connections(
223    socket_path: &str,
224    handler: Arc<HelloHandler>,
225    connection_count: usize,
226) -> Result<(), BrokerConnectionError> {
227    serve_local_socket_connections_with_peer_policy(
228        socket_path,
229        handler,
230        connection_count,
231        &PeerCredentialPolicy::allow_any(),
232    )
233}
234
235/// Run a bounded blocking local-socket accept loop with an explicit peer policy.
236pub fn serve_local_socket_connections_with_peer_policy(
237    socket_path: &str,
238    handler: Arc<HelloHandler>,
239    connection_count: usize,
240    peer_policy: &PeerCredentialPolicy,
241) -> Result<(), BrokerConnectionError> {
242    if connection_count == 0 {
243        return Ok(());
244    }
245
246    let listener = bind_local_socket(socket_path)?;
247    let cleanup = LocalSocketCleanup(socket_path);
248    let result = (|| {
249        let mut workers = Vec::with_capacity(connection_count);
250        let peer_policy = Arc::new(peer_policy.clone());
251
252        for _ in 0..connection_count {
253            let mut stream = listener.accept()?;
254            let peer = peer_identity_from_stream(&stream)?;
255            let handler = Arc::clone(&handler);
256            let peer_policy = Arc::clone(&peer_policy);
257            workers.push(thread::spawn(move || {
258                with_nonblocking_deadline(&mut stream, hello_read_deadline(), |stream| {
259                    handle_hello_connection_with_peer_policy(
260                        stream,
261                        handler.as_ref(),
262                        peer,
263                        peer_policy.as_ref(),
264                    )
265                    .map(|_| ())
266                })
267            }));
268        }
269
270        for worker in workers {
271            match worker.join() {
272                Ok(Ok(())) => {}
273                Ok(Err(err)) => return Err(err),
274                Err(_) => return Err(BrokerConnectionError::WorkerPanic),
275            }
276        }
277        Ok(())
278    })();
279    drop(listener);
280    drop(cleanup);
281    result
282}
283
284/// Run a bounded blocking local-socket accept loop with a pluggable responder.
285///
286/// This serves accepted connections sequentially so responders may borrow
287/// broker-owned state that is not safe to share across worker threads, such as
288/// platform process handles in the backend registry.
289pub fn serve_local_socket_connections_with<R>(
290    socket_path: &str,
291    responder: &R,
292    connection_count: usize,
293) -> Result<(), BrokerConnectionError>
294where
295    R: HelloResponder + ?Sized,
296{
297    serve_local_socket_connections_with_policy(
298        socket_path,
299        responder,
300        connection_count,
301        &PeerCredentialPolicy::allow_any(),
302    )
303}
304
305/// Run a bounded pluggable-responder accept loop with an explicit peer policy.
306pub fn serve_local_socket_connections_with_policy<R>(
307    socket_path: &str,
308    responder: &R,
309    connection_count: usize,
310    peer_policy: &PeerCredentialPolicy,
311) -> Result<(), BrokerConnectionError>
312where
313    R: HelloResponder + ?Sized,
314{
315    if connection_count == 0 {
316        return Ok(());
317    }
318
319    let listener = bind_local_socket(socket_path)?;
320    let cleanup = LocalSocketCleanup(socket_path);
321    let result = (|| {
322        for _ in 0..connection_count {
323            let mut stream = listener.accept()?;
324            let peer = peer_identity_from_stream(&stream)?;
325            let _ = with_nonblocking_deadline(&mut stream, hello_read_deadline(), |stream| {
326                handle_hello_connection_with_peer_policy(stream, responder, peer, peer_policy)
327            })?;
328        }
329        Ok(())
330    })();
331    drop(listener);
332    drop(cleanup);
333    result
334}
335
336/// Convert the broker's platform socket path/name string into an
337/// `interprocess` local-socket name.
338pub fn local_socket_name(socket_path: &str) -> io::Result<interprocess::local_socket::Name<'_>> {
339    super::singleton_bind::wrap_socket_name(socket_path).map_err(io::Error::other)
340}
341
342/// Errors raised while serving a framed broker Hello connection.
343#[derive(Debug, thiserror::Error)]
344pub enum BrokerConnectionError {
345    /// v1 framing failed.
346    #[error(transparent)]
347    Framing(#[from] FramingError),
348    /// The response frame could not be encoded.
349    #[error("failed to encode broker response Frame: {0}")]
350    EncodeFrame(prost::EncodeError),
351    /// Local socket I/O failed.
352    #[error(transparent)]
353    Io(#[from] io::Error),
354    /// A connection worker thread panicked.
355    #[error("broker connection worker panicked")]
356    WorkerPanic,
357}
358
359pub(super) fn bind_local_socket(socket_path: &str) -> Result<ipc::Listener, BrokerConnectionError> {
360    let endpoint = ipc::Endpoint::new(socket_path.to_owned())?;
361    prepare_local_socket_path(&endpoint)?;
362    let listener = ipc::Listener::bind_owner_only(&endpoint)?;
363    Ok(listener)
364}
365
366pub(super) struct LocalSocketCleanup<'a>(pub(super) &'a str);
367
368impl Drop for LocalSocketCleanup<'_> {
369    fn drop(&mut self) {
370        cleanup_local_socket_path(self.0);
371    }
372}
373
374pub(super) fn write_response_frame<W: Write>(
375    writer: &mut W,
376    request_frame: Option<&Frame>,
377    reply: &HelloReply,
378) -> Result<(), BrokerConnectionError> {
379    let response_frame = Frame {
380        envelope_version: PROTOCOL_VERSION,
381        kind: FrameKind::Response as i32,
382        payload_protocol: CONTROL_PAYLOAD_PROTOCOL,
383        payload: reply.encode_to_vec(),
384        request_id: request_frame.map_or(0, |frame| frame.request_id),
385        payload_encoding: PayloadEncoding::None as i32,
386        deadline_unix_ms: 0,
387        traceparent: request_frame
388            .map(|frame| frame.traceparent.clone())
389            .unwrap_or_default(),
390        tracestate: request_frame
391            .map(|frame| frame.tracestate.clone())
392            .unwrap_or_default(),
393    };
394    let mut response_bytes = Vec::new();
395    response_frame
396        .encode(&mut response_bytes)
397        .map_err(BrokerConnectionError::EncodeFrame)?;
398    write_frame(writer, &response_bytes)?;
399    Ok(())
400}
401
402pub(super) fn reply_for_framing_error(error: &FramingError) -> HelloReply {
403    match error {
404        FramingError::UnsupportedFramingVersion { .. } => refused_reply(
405            ErrorCode::ErrorVersionUnsupported,
406            "unsupported framing version",
407            0,
408        ),
409        FramingError::FrameTooLarge { .. } => refused_reply(
410            ErrorCode::ErrorPeerRejected,
411            "initial Hello frame exceeds 64 KiB",
412            0,
413        ),
414        FramingError::UnexpectedEof { .. } | FramingError::Io(_) => {
415            refused_reply(ErrorCode::ErrorPeerRejected, "incomplete Hello frame", 0)
416        }
417        // Buffer-level codec variant (#412); the stream-level reads used
418        // here never produce it, but a malformed body is a peer fault.
419        FramingError::Decode(_) => {
420            refused_reply(ErrorCode::ErrorPeerRejected, "malformed Hello frame", 0)
421        }
422    }
423}
424
425pub(super) fn refused_reply(
426    code: ErrorCode,
427    reason: impl Into<String>,
428    retry_after_ms: u64,
429) -> HelloReply {
430    HelloReply {
431        result: Some(HelloReplyResult::Refused(Refused {
432            reason: reason.into(),
433            daemon_min_protocol: PROTOCOL_VERSION,
434            daemon_max_protocol: PROTOCOL_VERSION,
435            code: code as i32,
436            details: HashMap::new(),
437            retry_after_ms,
438        })),
439    }
440}
441
442pub fn peer_identity_from_stream<S>(stream: &S) -> Result<PeerIdentity, BrokerConnectionError>
443where
444    S: ipc::PeerIdentitySource + ?Sized,
445{
446    peer_identity_from_source(stream)
447}
448
449/// Async analog of [`peer_identity_from_stream`] for tokio-`interprocess`
450/// streams (soldr#2365, the async v2 broker serve path).
451///
452/// `peer_creds()` is a synchronous `getsockopt`/`GetNamedPipeClientProcessId`
453/// query — it does not block on I/O — so it is safe to call from an async
454/// accept loop without yielding. The credential extraction is identical to the
455/// sync path (shared `peer_identity_from_peer_creds`).
456#[cfg(feature = "client-async")]
457pub fn peer_identity_from_tokio_stream<S>(stream: &S) -> Result<PeerIdentity, BrokerConnectionError>
458where
459    S: ipc::PeerIdentitySource + ?Sized,
460{
461    peer_identity_from_source(stream)
462}
463
464fn peer_identity_from_source<S>(stream: &S) -> Result<PeerIdentity, BrokerConnectionError>
465where
466    S: ipc::PeerIdentitySource + ?Sized,
467{
468    let peer = ipc::PeerIdentitySource::ipc_peer_identity(stream)?;
469    Ok(PeerIdentity {
470        pid: peer.pid,
471        uid_or_sid: peer.user_id,
472    })
473}
474
475fn prepare_local_socket_path(endpoint: &ipc::Endpoint) -> io::Result<()> {
476    endpoint.ensure_owner_private_parent()?;
477    if endpoint.target_exists()? {
478        return Err(io::Error::new(
479            io::ErrorKind::AlreadyExists,
480            "broker local socket path already exists",
481        ));
482    }
483    Ok(())
484}
485
486fn cleanup_local_socket_path(socket_path: &str) {
487    if let Ok(endpoint) = ipc::Endpoint::new(socket_path.to_owned()) {
488        let _ = endpoint.retire();
489    }
490}
491
492#[cfg(test)]
493mod endpoint_tests {
494    use super::local_socket_name;
495
496    #[test]
497    fn resolved_pipe_has_one_canonical_interprocess_name() {
498        use crate::broker::server::singleton_bind::{resolve_socket_path, wrap_socket_name};
499
500        let resolved = resolve_socket_path(&format!(
501            "rpb-v2-canonical-name-test-{}",
502            std::process::id()
503        ))
504        .expect("resolve Windows pipe path");
505        let server = local_socket_name(&resolved).expect("server name");
506        let client = wrap_socket_name(&resolved).expect("client name");
507
508        assert_eq!(
509            server, client,
510            "server bind and client dial must normalize a resolved pipe identically"
511        );
512    }
513}