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 interprocess::local_socket::traits::Listener;
14use prost::Message;
15
16use crate::broker::protocol::{
17    hello_reply::Result as HelloReplyResult, read_frame_with_cap, write_frame, ErrorCode, Frame,
18    FrameKind, FramingError, HelloReply, PayloadEncoding, Refused, CONTROL_PAYLOAD_PROTOCOL,
19    MAX_HELLO_BYTES, PROTOCOL_VERSION,
20};
21use crate::broker::server::deadline_stream::{hello_read_deadline, with_nonblocking_deadline};
22use crate::broker::server::{HelloHandler, HelloRouter, PeerIdentity};
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        #[cfg(unix)]
52        {
53            Some(Self::owner_only(unsafe { libc::geteuid() }.to_string()))
54        }
55
56        #[cfg(windows)]
57        {
58            current_process_user_sid().ok().map(Self::owner_only)
59        }
60    }
61
62    /// Return true when `peer` is authorized by this policy.
63    pub fn allows(&self, peer: &PeerIdentity) -> bool {
64        match self {
65            Self::AllowAny => true,
66            Self::OwnerOnly { uid_or_sid } => {
67                !uid_or_sid.is_empty() && peer.uid_or_sid == *uid_or_sid
68            }
69        }
70    }
71}
72
73/// Handles a decoded broker Hello frame and returns the protocol reply.
74///
75/// This keeps the frame I/O boundary independent from the concrete routing
76/// strategy. Tests and preloaded-backend serve mode can use [`HelloHandler`], while the
77/// broker accept loop can route through [`HelloRouter`].
78pub trait HelloResponder {
79    /// Decode and answer a broker Hello frame for an OS-verified peer.
80    fn handle_frame(&self, frame: Frame, peer: PeerIdentity) -> HelloReply;
81}
82
83impl HelloResponder for HelloHandler {
84    fn handle_frame(&self, frame: Frame, peer: PeerIdentity) -> HelloReply {
85        Self::handle_frame(self, frame, peer)
86    }
87}
88
89impl HelloResponder for HelloRouter<'_> {
90    fn handle_frame(&self, frame: Frame, peer: PeerIdentity) -> HelloReply {
91        Self::handle_frame(self, frame, peer)
92    }
93}
94
95/// Handle one already-accepted broker connection.
96///
97/// The connection reads exactly one v1-framed [`Frame`], decodes the
98/// embedded `Hello`, writes one v1-framed response [`Frame`] containing
99/// a `HelloReply`, then returns the reply for metrics/logging callers.
100pub fn handle_hello_connection<S: Read + Write>(
101    stream: &mut S,
102    handler: &HelloHandler,
103    peer: PeerIdentity,
104) -> Result<HelloReply, BrokerConnectionError> {
105    handle_hello_connection_with(stream, handler, peer)
106}
107
108/// Handle one already-accepted broker connection with a pluggable responder.
109///
110/// The framed wire behavior is identical to [`handle_hello_connection`]; only
111/// the decoded Hello routing strategy is supplied by the caller.
112pub fn handle_hello_connection_with<S, R>(
113    stream: &mut S,
114    responder: &R,
115    peer: PeerIdentity,
116) -> Result<HelloReply, BrokerConnectionError>
117where
118    S: Read + Write,
119    R: HelloResponder + ?Sized,
120{
121    handle_hello_connection_with_peer_policy(
122        stream,
123        responder,
124        peer,
125        &PeerCredentialPolicy::allow_any(),
126    )
127    .map(|reply| reply.expect("allow-any policy must not drop peers"))
128}
129
130/// Handle one already-accepted broker connection with an explicit peer policy.
131///
132/// Returns `Ok(None)` when the policy rejects the peer. The caller should drop
133/// the stream without writing a `HelloReply`; this is the broker's silent
134/// foreign-peer rejection path.
135pub fn handle_hello_connection_with_peer_policy<S, R>(
136    stream: &mut S,
137    responder: &R,
138    peer: PeerIdentity,
139    peer_policy: &PeerCredentialPolicy,
140) -> Result<Option<HelloReply>, BrokerConnectionError>
141where
142    S: Read + Write,
143    R: HelloResponder + ?Sized,
144{
145    if !peer_policy.allows(&peer) {
146        return Ok(None);
147    }
148
149    let request_bytes = match read_frame_with_cap(stream, MAX_HELLO_BYTES) {
150        Ok(bytes) => bytes,
151        Err(err) => {
152            let reply = reply_for_framing_error(&err);
153            write_response_frame(stream, None, &reply)?;
154            return Ok(Some(reply));
155        }
156    };
157
158    let request_frame = match Frame::decode(request_bytes.as_slice()) {
159        Ok(frame) => frame,
160        Err(_) => {
161            let reply = refused_reply(ErrorCode::ErrorPeerRejected, "malformed broker Frame", 0);
162            write_response_frame(stream, None, &reply)?;
163            return Ok(Some(reply));
164        }
165    };
166
167    let reply = responder.handle_frame(request_frame.clone(), peer);
168    write_response_frame(stream, Some(&request_frame), &reply)?;
169    Ok(Some(reply))
170}
171
172/// Run one blocking local-socket accept and serve exactly one Hello.
173///
174/// This is a testable stepping stone toward the full Phase 4 accept
175/// loop. It binds the platform local socket, accepts one peer, derives
176/// available OS peer credentials, serves one framed Hello exchange, and
177/// returns.
178pub fn serve_one_local_socket(
179    socket_path: &str,
180    handler: &HelloHandler,
181) -> Result<HelloReply, BrokerConnectionError> {
182    serve_one_local_socket_with(socket_path, handler)
183}
184
185/// Run one blocking local-socket accept and serve exactly one Hello with a
186/// pluggable responder.
187pub fn serve_one_local_socket_with<R>(
188    socket_path: &str,
189    responder: &R,
190) -> Result<HelloReply, BrokerConnectionError>
191where
192    R: HelloResponder + ?Sized,
193{
194    serve_one_local_socket_with_peer_policy(
195        socket_path,
196        responder,
197        &PeerCredentialPolicy::allow_any(),
198    )
199    .map(|reply| reply.expect("allow-any policy must not drop peers"))
200}
201
202/// Run one blocking local-socket accept with an explicit peer policy.
203pub fn serve_one_local_socket_with_peer_policy<R>(
204    socket_path: &str,
205    responder: &R,
206    peer_policy: &PeerCredentialPolicy,
207) -> Result<Option<HelloReply>, BrokerConnectionError>
208where
209    R: HelloResponder + ?Sized,
210{
211    let listener = bind_local_socket(socket_path)?;
212    let cleanup = LocalSocketCleanup(socket_path);
213    let result = (|| {
214        let mut stream = listener.accept()?;
215        let peer = peer_identity_from_stream(&stream)?;
216        with_nonblocking_deadline(&mut stream, hello_read_deadline(), |stream| {
217            handle_hello_connection_with_peer_policy(stream, responder, peer, peer_policy)
218        })
219    })();
220    drop(listener);
221    drop(cleanup);
222    result
223}
224
225/// Run a bounded blocking local-socket accept loop.
226///
227/// This is the synchronous Phase 4 test harness for the Hello accept
228/// path. It accepts `connection_count` peers, handles each connection
229/// on a worker thread, waits for all workers, then returns.
230pub fn serve_local_socket_connections(
231    socket_path: &str,
232    handler: Arc<HelloHandler>,
233    connection_count: usize,
234) -> Result<(), BrokerConnectionError> {
235    serve_local_socket_connections_with_peer_policy(
236        socket_path,
237        handler,
238        connection_count,
239        &PeerCredentialPolicy::allow_any(),
240    )
241}
242
243/// Run a bounded blocking local-socket accept loop with an explicit peer policy.
244pub fn serve_local_socket_connections_with_peer_policy(
245    socket_path: &str,
246    handler: Arc<HelloHandler>,
247    connection_count: usize,
248    peer_policy: &PeerCredentialPolicy,
249) -> Result<(), BrokerConnectionError> {
250    if connection_count == 0 {
251        return Ok(());
252    }
253
254    let listener = bind_local_socket(socket_path)?;
255    let cleanup = LocalSocketCleanup(socket_path);
256    let result = (|| {
257        let mut workers = Vec::with_capacity(connection_count);
258        let peer_policy = Arc::new(peer_policy.clone());
259
260        for _ in 0..connection_count {
261            let mut stream = listener.accept()?;
262            let peer = peer_identity_from_stream(&stream)?;
263            let handler = Arc::clone(&handler);
264            let peer_policy = Arc::clone(&peer_policy);
265            workers.push(thread::spawn(move || {
266                with_nonblocking_deadline(&mut stream, hello_read_deadline(), |stream| {
267                    handle_hello_connection_with_peer_policy(
268                        stream,
269                        handler.as_ref(),
270                        peer,
271                        peer_policy.as_ref(),
272                    )
273                    .map(|_| ())
274                })
275            }));
276        }
277
278        for worker in workers {
279            match worker.join() {
280                Ok(Ok(())) => {}
281                Ok(Err(err)) => return Err(err),
282                Err(_) => return Err(BrokerConnectionError::WorkerPanic),
283            }
284        }
285        Ok(())
286    })();
287    drop(listener);
288    drop(cleanup);
289    result
290}
291
292/// Run a bounded blocking local-socket accept loop with a pluggable responder.
293///
294/// This serves accepted connections sequentially so responders may borrow
295/// broker-owned state that is not safe to share across worker threads, such as
296/// platform process handles in the backend registry.
297pub fn serve_local_socket_connections_with<R>(
298    socket_path: &str,
299    responder: &R,
300    connection_count: usize,
301) -> Result<(), BrokerConnectionError>
302where
303    R: HelloResponder + ?Sized,
304{
305    serve_local_socket_connections_with_policy(
306        socket_path,
307        responder,
308        connection_count,
309        &PeerCredentialPolicy::allow_any(),
310    )
311}
312
313/// Run a bounded pluggable-responder accept loop with an explicit peer policy.
314pub fn serve_local_socket_connections_with_policy<R>(
315    socket_path: &str,
316    responder: &R,
317    connection_count: usize,
318    peer_policy: &PeerCredentialPolicy,
319) -> Result<(), BrokerConnectionError>
320where
321    R: HelloResponder + ?Sized,
322{
323    if connection_count == 0 {
324        return Ok(());
325    }
326
327    let listener = bind_local_socket(socket_path)?;
328    let cleanup = LocalSocketCleanup(socket_path);
329    let result = (|| {
330        for _ in 0..connection_count {
331            let mut stream = listener.accept()?;
332            let peer = peer_identity_from_stream(&stream)?;
333            let _ = with_nonblocking_deadline(&mut stream, hello_read_deadline(), |stream| {
334                handle_hello_connection_with_peer_policy(stream, responder, peer, peer_policy)
335            })?;
336        }
337        Ok(())
338    })();
339    drop(listener);
340    drop(cleanup);
341    result
342}
343
344/// Convert the broker's platform socket path/name string into an
345/// `interprocess` local-socket name.
346pub fn local_socket_name(socket_path: &str) -> io::Result<interprocess::local_socket::Name<'_>> {
347    #[cfg(unix)]
348    {
349        use interprocess::local_socket::{GenericFilePath, ToFsName};
350        socket_path.to_fs_name::<GenericFilePath>()
351    }
352
353    #[cfg(windows)]
354    {
355        use interprocess::local_socket::{GenericNamespaced, ToNsName};
356        socket_path.to_ns_name::<GenericNamespaced>()
357    }
358}
359
360/// Errors raised while serving a framed broker Hello connection.
361#[derive(Debug, thiserror::Error)]
362pub enum BrokerConnectionError {
363    /// v1 framing failed.
364    #[error(transparent)]
365    Framing(#[from] FramingError),
366    /// The response frame could not be encoded.
367    #[error("failed to encode broker response Frame: {0}")]
368    EncodeFrame(prost::EncodeError),
369    /// Local socket I/O failed.
370    #[error(transparent)]
371    Io(#[from] io::Error),
372    /// A connection worker thread panicked.
373    #[error("broker connection worker panicked")]
374    WorkerPanic,
375}
376
377pub(super) fn bind_local_socket(
378    socket_path: &str,
379) -> Result<interprocess::local_socket::Listener, BrokerConnectionError> {
380    use interprocess::local_socket::ListenerOptions;
381
382    prepare_local_socket_path(socket_path)?;
383    let name = local_socket_name(socket_path)?;
384    let listener = ListenerOptions::new().name(name).create_sync()?;
385    secure_local_socket_path(socket_path)?;
386    Ok(listener)
387}
388
389pub(super) struct LocalSocketCleanup<'a>(pub(super) &'a str);
390
391impl Drop for LocalSocketCleanup<'_> {
392    fn drop(&mut self) {
393        cleanup_local_socket_path(self.0);
394    }
395}
396
397pub(super) fn write_response_frame<W: Write>(
398    writer: &mut W,
399    request_frame: Option<&Frame>,
400    reply: &HelloReply,
401) -> Result<(), BrokerConnectionError> {
402    let response_frame = Frame {
403        envelope_version: PROTOCOL_VERSION,
404        kind: FrameKind::Response as i32,
405        payload_protocol: CONTROL_PAYLOAD_PROTOCOL,
406        payload: reply.encode_to_vec(),
407        request_id: request_frame.map_or(0, |frame| frame.request_id),
408        payload_encoding: PayloadEncoding::None as i32,
409        deadline_unix_ms: 0,
410        traceparent: request_frame
411            .map(|frame| frame.traceparent.clone())
412            .unwrap_or_default(),
413        tracestate: request_frame
414            .map(|frame| frame.tracestate.clone())
415            .unwrap_or_default(),
416    };
417    let mut response_bytes = Vec::new();
418    response_frame
419        .encode(&mut response_bytes)
420        .map_err(BrokerConnectionError::EncodeFrame)?;
421    write_frame(writer, &response_bytes)?;
422    Ok(())
423}
424
425pub(super) fn reply_for_framing_error(error: &FramingError) -> HelloReply {
426    match error {
427        FramingError::UnsupportedFramingVersion { .. } => refused_reply(
428            ErrorCode::ErrorVersionUnsupported,
429            "unsupported framing version",
430            0,
431        ),
432        FramingError::FrameTooLarge { .. } => refused_reply(
433            ErrorCode::ErrorPeerRejected,
434            "initial Hello frame exceeds 64 KiB",
435            0,
436        ),
437        FramingError::UnexpectedEof { .. } | FramingError::Io(_) => {
438            refused_reply(ErrorCode::ErrorPeerRejected, "incomplete Hello frame", 0)
439        }
440        // Buffer-level codec variant (#412); the stream-level reads used
441        // here never produce it, but a malformed body is a peer fault.
442        FramingError::Decode(_) => {
443            refused_reply(ErrorCode::ErrorPeerRejected, "malformed Hello frame", 0)
444        }
445    }
446}
447
448pub(super) fn refused_reply(
449    code: ErrorCode,
450    reason: impl Into<String>,
451    retry_after_ms: u64,
452) -> HelloReply {
453    HelloReply {
454        result: Some(HelloReplyResult::Refused(Refused {
455            reason: reason.into(),
456            daemon_min_protocol: PROTOCOL_VERSION,
457            daemon_max_protocol: PROTOCOL_VERSION,
458            code: code as i32,
459            details: HashMap::new(),
460            retry_after_ms,
461        })),
462    }
463}
464
465pub(super) fn peer_identity_from_stream(
466    stream: &interprocess::local_socket::Stream,
467) -> Result<PeerIdentity, BrokerConnectionError> {
468    use interprocess::local_socket::traits::StreamCommon;
469
470    let creds = stream.peer_creds()?;
471    #[cfg(unix)]
472    let pid = creds
473        .pid()
474        .and_then(|pid| u32::try_from(pid).ok())
475        .unwrap_or(0);
476
477    #[cfg(windows)]
478    let pid = creds.pid().unwrap_or(0);
479
480    #[cfg(unix)]
481    let uid_or_sid = creds.euid().map(|uid| uid.to_string()).unwrap_or_default();
482
483    #[cfg(windows)]
484    let uid_or_sid = if pid == 0 {
485        String::new()
486    } else {
487        process_user_sid(pid).unwrap_or_default()
488    };
489
490    Ok(PeerIdentity { pid, uid_or_sid })
491}
492
493#[cfg(windows)]
494fn current_process_user_sid() -> io::Result<String> {
495    process_user_sid(std::process::id())
496}
497
498#[cfg(windows)]
499fn process_user_sid(pid: u32) -> io::Result<String> {
500    use std::ptr;
501    use winapi::um::processthreadsapi::{OpenProcess, OpenProcessToken};
502    use winapi::um::winnt::{
503        TokenUser, HANDLE, PROCESS_QUERY_LIMITED_INFORMATION, TOKEN_QUERY, TOKEN_USER,
504    };
505
506    unsafe {
507        let process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
508        if process.is_null() {
509            return Err(io::Error::last_os_error());
510        }
511        let _process_guard = WindowsHandle(process);
512
513        let mut token: HANDLE = ptr::null_mut();
514        if OpenProcessToken(process, TOKEN_QUERY, &mut token) == 0 {
515            return Err(io::Error::last_os_error());
516        }
517        let _token_guard = WindowsHandle(token);
518
519        let mut required_size = 0_u32;
520        let _ = winapi::um::securitybaseapi::GetTokenInformation(
521            token,
522            TokenUser,
523            ptr::null_mut(),
524            0,
525            &mut required_size,
526        );
527        if required_size == 0 {
528            return Err(io::Error::last_os_error());
529        }
530
531        let mut buffer = vec![0_u8; required_size as usize];
532        if winapi::um::securitybaseapi::GetTokenInformation(
533            token,
534            TokenUser,
535            buffer.as_mut_ptr().cast(),
536            required_size,
537            &mut required_size,
538        ) == 0
539        {
540            return Err(io::Error::last_os_error());
541        }
542
543        let token_user: *const TOKEN_USER = buffer.as_ptr().cast();
544        let sid = (*token_user).User.Sid;
545        sid_to_stable_string(sid)
546    }
547}
548
549#[cfg(windows)]
550struct WindowsHandle(winapi::um::winnt::HANDLE);
551
552#[cfg(windows)]
553impl Drop for WindowsHandle {
554    fn drop(&mut self) {
555        unsafe {
556            winapi::um::handleapi::CloseHandle(self.0);
557        }
558    }
559}
560
561#[cfg(windows)]
562unsafe fn sid_to_stable_string(sid: winapi::um::winnt::PSID) -> io::Result<String> {
563    use winapi::um::securitybaseapi::{GetLengthSid, IsValidSid};
564
565    if sid.is_null() || IsValidSid(sid) == 0 {
566        return Err(io::Error::other("invalid Windows SID"));
567    }
568    let len = GetLengthSid(sid) as usize;
569    if len == 0 || len > 1024 {
570        return Err(io::Error::other(format!(
571            "implausible Windows SID length {len}"
572        )));
573    }
574    let bytes = std::slice::from_raw_parts(sid.cast::<u8>(), len);
575    let mut out = String::with_capacity("windows-sid:".len() + len * 2);
576    out.push_str("windows-sid:");
577    for byte in bytes {
578        out.push(nibble_to_hex(byte >> 4));
579        out.push(nibble_to_hex(byte & 0x0f));
580    }
581    Ok(out)
582}
583
584#[cfg(windows)]
585fn nibble_to_hex(nibble: u8) -> char {
586    match nibble {
587        0..=9 => (b'0' + nibble) as char,
588        10..=15 => (b'a' + (nibble - 10)) as char,
589        _ => unreachable!("nibble out of range"),
590    }
591}
592
593fn prepare_local_socket_path(socket_path: &str) -> io::Result<()> {
594    #[cfg(unix)]
595    {
596        let path = std::path::Path::new(socket_path);
597        if let Some(parent) = path.parent() {
598            std::fs::create_dir_all(parent)?;
599        }
600        match std::fs::symlink_metadata(path) {
601            Ok(_) => {
602                return Err(io::Error::new(
603                    io::ErrorKind::AlreadyExists,
604                    "broker local socket path already exists",
605                ));
606            }
607            Err(err) if err.kind() == io::ErrorKind::NotFound => {}
608            Err(err) => return Err(err),
609        }
610    }
611
612    #[cfg(windows)]
613    let _ = socket_path;
614
615    Ok(())
616}
617
618fn secure_local_socket_path(socket_path: &str) -> io::Result<()> {
619    #[cfg(unix)]
620    {
621        use std::os::unix::fs::PermissionsExt;
622
623        let perms = std::fs::Permissions::from_mode(0o600);
624        std::fs::set_permissions(socket_path, perms)?;
625    }
626
627    #[cfg(windows)]
628    let _ = socket_path;
629
630    Ok(())
631}
632
633fn cleanup_local_socket_path(socket_path: &str) {
634    #[cfg(unix)]
635    {
636        let _ = std::fs::remove_file(socket_path);
637    }
638
639    #[cfg(windows)]
640    let _ = socket_path;
641}