Skip to main content

rightkit_daemon/
transport.rs

1//! Current-user-only local transport: Unix socket (macOS, other Unix) and named
2//! pipe (Windows). One request is in flight per connection (lock-step), so a
3//! connection never needs concurrent reads and writes on one handle.
4
5use crate::host_identity::{HostConnectError, HostExpectation, HostProcess};
6use crate::suite::Suite;
7use std::io::{self, Read, Write};
8use std::sync::Arc;
9
10/// Outcome of [`Listener::accept`]: an authenticated connection, a rejected peer, or one that
11/// left before it could be identified.
12pub enum Accepted {
13    /// The peer was authenticated as the current user.
14    Conn(Conn),
15    /// The peer failed OS-level authentication and was closed unread. The reason is the
16    /// typed peer error, content-free.
17    Rejected(String),
18    /// The peer left before it could be identified (a probe, or the shutdown wake-up).
19    /// Closed; not worth a rejection event.
20    Vanished,
21}
22
23/// Default for [`HostOptions::max_pipe_instances`](crate::HostOptions::max_pipe_instances).
24pub const DEFAULT_MAX_PIPE_INSTANCES: u32 = 16;
25
26/// An accepted or connected duplex stream. `peer` is who the OS reports on the other end.
27pub struct Conn {
28    pub reader: Box<dyn Read + Send>,
29    pub writer: Box<dyn Write + Send>,
30    /// Non-blocking: has the peer closed its end? Safe to call from another thread.
31    pub peer_closed: Arc<dyn Fn() -> bool + Send + Sync>,
32    pub shutdown: Arc<dyn Fn() + Send + Sync>,
33    /// Who the OS says is on the other end. Filled in for an accepted connection, where the
34    /// peer check has just run; empty for a connection this process opened.
35    pub peer: crate::peer::PeerIdentity,
36}
37
38fn fnv(s: &str) -> u32 {
39    let mut h: u32 = 0x811c_9dc5;
40    for b in s
41        .to_lowercase()
42        .replace('\\', "/")
43        .trim_end_matches('/')
44        .bytes()
45    {
46        h ^= u32::from(b);
47        h = h.wrapping_mul(0x0100_0193);
48    }
49    h
50}
51
52/// Longest Unix socket path this crate will bind: `sockaddr_un.sun_path` is 104 bytes on macOS
53/// and 108 on Linux, each including the trailing NUL, so 103 is the portable ceiling.
54pub const MAX_UNIX_SOCKET_PATH: usize = 103;
55
56/// In-suite socket paths longer than this move to the per-user temp dir. Kept below
57/// [`MAX_UNIX_SOCKET_PATH`] on purpose: it is the historical threshold and changing it would move
58/// the endpoint of services that bind today.
59const IN_SUITE_PATH_LIMIT: usize = 100;
60
61/// The endpoint for a service cannot be named within the OS socket path limit, even at the
62/// short fallback location. Names the offending path, its length and the limit.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct EndpointError {
65    pub path: String,
66    pub len: usize,
67    pub limit: usize,
68}
69
70impl std::fmt::Display for EndpointError {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(
73            f,
74            "service socket path {:?} is {} bytes, over the {}-byte limit; set a shorter TMPDIR",
75            self.path, self.len, self.limit
76        )
77    }
78}
79impl std::error::Error for EndpointError {}
80
81/// 64-bit FNV-1a. Stable across Rust versions and processes (std's hashers are not), so a
82/// client and a server derive the same fallback name.
83fn fnv1a64(s: &str) -> u64 {
84    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
85    for b in s.bytes() {
86        h ^= u64::from(b);
87        h = h.wrapping_mul(0x0000_0100_0000_01b3);
88    }
89    h
90}
91
92/// File name of the bounded fallback socket: `rk-<tag>-<fnv1a64(service)>.sock`, 33 bytes
93/// whatever the service name length (the tag is 8 hex digits, the hash 16).
94fn fallback_file_name(tag: &str, service: &str) -> String {
95    format!("rk-{tag}-{:016x}.sock", fnv1a64(service))
96}
97
98/// The fallback name used before the bounded form existed: `rk-<tag>-<service>.sock`.
99fn legacy_fallback_file_name(tag: &str, service: &str) -> String {
100    format!("rk-{tag}-{service}.sock")
101}
102
103fn suite_tag(suite: &Suite) -> String {
104    format!("{:08x}", fnv(&suite.root().to_string_lossy()))
105}
106
107/// Unix endpoint with the temp dir injected (the seam tests use). Returns the in-suite path when
108/// it fits, else the bounded fallback under `temp`, else a typed error.
109fn unix_endpoint_in(
110    suite: &Suite,
111    service: &str,
112    temp: &std::path::Path,
113) -> Result<String, EndpointError> {
114    let p = suite.service_dir(service).join(format!("{service}.sock"));
115    if p.as_os_str().len() <= IN_SUITE_PATH_LIMIT {
116        return Ok(p.to_string_lossy().into_owned());
117    }
118    let f = temp
119        .join(fallback_file_name(&suite_tag(suite), service))
120        .to_string_lossy()
121        .into_owned();
122    if f.len() <= MAX_UNIX_SOCKET_PATH {
123        Ok(f)
124    } else {
125        Err(EndpointError {
126            len: f.len(),
127            path: f,
128            limit: MAX_UNIX_SOCKET_PATH,
129        })
130    }
131}
132
133/// The pre-upgrade fallback path, only when it would have fitted the socket limit (so an old
134/// host could actually have bound it). `None` means there is nothing to stay compatible with.
135fn legacy_unix_endpoint_in(suite: &Suite, service: &str, temp: &std::path::Path) -> Option<String> {
136    let p = suite.service_dir(service).join(format!("{service}.sock"));
137    if p.as_os_str().len() <= IN_SUITE_PATH_LIMIT {
138        return None;
139    }
140    let f = temp
141        .join(legacy_fallback_file_name(&suite_tag(suite), service))
142        .to_string_lossy()
143        .into_owned();
144    (f.len() <= MAX_UNIX_SOCKET_PATH).then_some(f)
145}
146
147/// Endpoint for `service` on `suite`, or the typed reason none can be named. Windows: a pipe
148/// name that carries the user and a hash of the suite root, so an isolated suite never collides
149/// with the user's installed host (at most 96 characters for a 48-byte service name and a
150/// 20-character user, far under the 256-character pipe name limit). Unix: a socket in the
151/// private service dir, or, when that path would exceed the `sockaddr_un` limit, a bounded
152/// `rk-<tag>-<hash>.sock` in the per-user temp dir.
153///
154/// This is the single place an endpoint is computed; the host binds it and publishes it in its
155/// discovery record, and clients connect to the recorded endpoint.
156pub fn try_endpoint_for(suite: &Suite, service: &str) -> Result<String, EndpointError> {
157    if cfg!(windows) {
158        let tag = suite_tag(suite);
159        // `USERNAME` stays: it is only a label in the pipe name (so two users' pipes do not
160        // collide), never a root, trust or mode selector. A launcher that changes it changes only
161        // the name host and client compute; the pipe's current-user DACL and the client's
162        // same-user check still decide who may connect. No API alternative in the current
163        // dependency features, and none is added.
164        let user: String = std::env::var("USERNAME")
165            .unwrap_or_default()
166            .chars()
167            .filter(|c| c.is_ascii_alphanumeric())
168            .take(20)
169            .collect();
170        Ok(format!(r"\\.\pipe\rightkit-{service}-{user}-{tag}"))
171    } else {
172        unix_endpoint_in(suite, service, &std::env::temp_dir())
173    }
174}
175
176/// [`try_endpoint_for`] for callers that cannot handle the error: on failure it returns the path
177/// that did not fit, so a bind attempt fails with the OS error rather than panicking here.
178pub fn endpoint_for(suite: &Suite, service: &str) -> String {
179    try_endpoint_for(suite, service).unwrap_or_else(|e| e.path)
180}
181
182/// Endpoints a client may find a live host on, in order: the current endpoint, then (Unix only)
183/// the pre-upgrade long fallback name when it would have fitted. Compatibility decision: for one
184/// release a new client still finds an old host on the old fallback name; that name is
185/// connect-only and is never bound. Where the old name never fitted the limit no old host could
186/// have bound it, so there is no second candidate.
187pub fn endpoint_candidates(suite: &Suite, service: &str) -> Vec<String> {
188    candidates_in(suite, service, &std::env::temp_dir())
189}
190
191fn candidates_in(suite: &Suite, service: &str, temp: &std::path::Path) -> Vec<String> {
192    let mut v = Vec::new();
193    if cfg!(windows) {
194        if let Ok(e) = try_endpoint_for(suite, service) {
195            v.push(e);
196        }
197        return v;
198    }
199    if let Ok(e) = unix_endpoint_in(suite, service, temp) {
200        v.push(e);
201    }
202    if let Some(old) = legacy_unix_endpoint_in(suite, service, temp) {
203        if !v.contains(&old) {
204            v.push(old);
205        }
206    }
207    v
208}
209
210/// The first of [`endpoint_candidates`] that answers a connect.
211pub fn find_live_endpoint(suite: &Suite, service: &str) -> Option<String> {
212    endpoint_candidates(suite, service)
213        .into_iter()
214        .find(|e| probe_endpoint(e))
215}
216
217/// Does something answer at `endpoint`? (Connect, then drop.)
218pub fn probe_endpoint(endpoint: &str) -> bool {
219    connect(endpoint).is_ok()
220}
221
222#[cfg(unix)]
223mod imp {
224    use super::*;
225    use std::os::fd::AsRawFd;
226    use std::os::unix::fs::{MetadataExt, PermissionsExt};
227    use std::os::unix::net::{UnixListener, UnixStream};
228    use std::path::PathBuf;
229
230    use crate::peer::{verify_peer_is_current_user, PeerError, PeerIdentity};
231
232    extern "C" {
233        fn recv(fd: i32, buf: *mut u8, len: usize, flags: i32) -> isize;
234    }
235    // MSG_PEEK | MSG_DONTWAIT: 0x2 | 0x80 on macOS, 0x2 | 0x40 on Linux.
236    #[cfg(target_os = "macos")]
237    const PEEK_NOWAIT: i32 = 0x2 | 0x80;
238    #[cfg(not(target_os = "macos"))]
239    const PEEK_NOWAIT: i32 = 0x2 | 0x40;
240
241    pub fn current_uid() -> u32 {
242        crate::peer::current_euid()
243    }
244
245    /// Authenticate `s` with `verify` before it is wrapped: nothing is read from a
246    /// stream that fails. The stream is dropped (closed) on every non-`Conn` outcome.
247    fn admit(
248        s: UnixStream,
249        verify: impl FnOnce(&UnixStream) -> Result<PeerIdentity, PeerError>,
250    ) -> io::Result<Accepted> {
251        match verify(&s) {
252            Ok(peer) => wrap(s).map(|conn| Accepted::Conn(Conn { peer, ..conn })),
253            Err(e) if e.peer_gone() => Ok(Accepted::Vanished),
254            Err(e) => Ok(Accepted::Rejected(e.to_string())),
255        }
256    }
257
258    fn wrap(s: UnixStream) -> io::Result<Conn> {
259        let r = s.try_clone()?;
260        let w = s.try_clone()?;
261        let watch = s.try_clone()?;
262        let sd = s;
263        Ok(Conn {
264            peer: crate::peer::PeerIdentity::default(),
265            reader: Box::new(r),
266            writer: Box::new(w),
267            peer_closed: Arc::new(move || {
268                let mut b = [0u8; 1];
269                let n = unsafe { recv(watch.as_raw_fd(), b.as_mut_ptr(), 1, PEEK_NOWAIT) };
270                n == 0
271                    || (n < 0
272                        && !matches!(
273                            io::Error::last_os_error().kind(),
274                            io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
275                        ))
276            }),
277            shutdown: Arc::new(move || {
278                let _ = sd.shutdown(std::net::Shutdown::Both);
279            }),
280        })
281    }
282
283    pub struct Listener {
284        l: UnixListener,
285        path: PathBuf,
286    }
287
288    impl Listener {
289        /// `max_instances` is a Windows pipe-instance cap; a Unix socket has no such notion.
290        pub fn bind(endpoint: &str, _max_instances: u32) -> io::Result<Listener> {
291            let path = PathBuf::from(endpoint);
292            if let Ok(md) = std::fs::symlink_metadata(&path) {
293                // Only replace a dead socket that we own; never follow or unlink anything else.
294                if md.uid() != current_uid() || !md.file_type().is_socket_like() {
295                    return Err(io::Error::new(
296                        io::ErrorKind::AddrInUse,
297                        "endpoint path is occupied by something that is not our socket",
298                    ));
299                }
300                if UnixStream::connect(&path).is_ok() {
301                    return Err(io::Error::new(
302                        io::ErrorKind::AddrInUse,
303                        "a live host already serves this endpoint",
304                    ));
305                }
306                let _ = std::fs::remove_file(&path);
307            }
308            let l = UnixListener::bind(&path)?;
309            std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
310            Ok(Listener { l, path })
311        }
312        /// A peer that is not the current user is `Rejected`; the caller keeps accepting.
313        pub fn accept(&self) -> io::Result<Accepted> {
314            let (s, _) = self.l.accept()?;
315            admit(s, verify_peer_is_current_user)
316        }
317        #[cfg(test)]
318        pub fn accept_with(
319            &self,
320            verify: impl FnOnce(&UnixStream) -> Result<PeerIdentity, PeerError>,
321        ) -> io::Result<Accepted> {
322            let (s, _) = self.l.accept()?;
323            admit(s, verify)
324        }
325        pub fn wake(&self) {
326            let _ = UnixStream::connect(&self.path);
327        }
328        pub fn cleanup(&self) {
329            let _ = std::fs::remove_file(&self.path);
330        }
331    }
332
333    trait SocketLike {
334        fn is_socket_like(&self) -> bool;
335    }
336    impl SocketLike for std::fs::FileType {
337        fn is_socket_like(&self) -> bool {
338            use std::os::unix::fs::FileTypeExt;
339            self.is_socket()
340        }
341    }
342
343    /// Connect, then verify the server is the current user before any byte is exchanged
344    /// (a squatter on the endpoint path would otherwise receive our `hello`).
345    pub fn connect(endpoint: &str) -> io::Result<Conn> {
346        let s = UnixStream::connect(endpoint)?;
347        verify_peer_is_current_user(&s).map_err(io::Error::from)?;
348        wrap(s)
349    }
350
351    /// Connect, then verify WHO the host is before any byte is exchanged: the same-user check
352    /// (reported as a typed identity refusal here) and the host's executable against `expect`.
353    /// The only thing that has happened on the wire when this returns an error is the kernel-level
354    /// `connect`: the host sees a connection that closes without a byte, and the stream is dropped
355    /// (closed) on every error path.
356    pub fn connect_expecting(
357        endpoint: &str,
358        expect: &HostExpectation,
359    ) -> Result<(Conn, HostProcess), HostConnectError> {
360        let s = UnixStream::connect(endpoint).map_err(HostConnectError::Io)?;
361        let host = crate::host_identity::verify_unix_host(&s, expect)?;
362        let conn = wrap(s).map_err(HostConnectError::Io)?;
363        Ok((conn, host))
364    }
365}
366
367#[cfg(windows)]
368mod imp {
369    use super::*;
370    use crate::peer::{
371        owner_only_security_attributes, verify_pipe_peer_is_current_user,
372        verify_pipe_server_is_current_user, OwnerOnlySecurityAttributes,
373    };
374    use std::os::windows::io::RawHandle;
375    use std::sync::atomic::{AtomicBool, AtomicIsize, Ordering};
376    use std::sync::{Mutex, RwLock};
377    use windows::core::PCWSTR;
378    use windows::Win32::Foundation::{CloseHandle, HANDLE};
379    use windows::Win32::Storage::FileSystem::{
380        ReadFile, WriteFile, FILE_FLAG_FIRST_PIPE_INSTANCE, FILE_FLAG_OVERLAPPED,
381        PIPE_ACCESS_DUPLEX,
382    };
383    use windows::Win32::System::Pipes::{
384        ConnectNamedPipe, CreateNamedPipeW, PeekNamedPipe, PIPE_READMODE_BYTE,
385        PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE, PIPE_WAIT,
386    };
387    use windows::Win32::System::Threading::CreateEventW;
388    use windows::Win32::System::IO::{CancelIoEx, GetOverlappedResult, OVERLAPPED};
389
390    pub fn current_uid() -> u32 {
391        0
392    }
393
394    struct SendHandle(HANDLE);
395    unsafe impl Send for SendHandle {}
396
397    pub struct Listener {
398        name: Vec<u16>,
399        attrs: OwnerOnlySecurityAttributes,
400        /// Cap on simultaneous instances of this pipe (live connections + the one waiting).
401        max_instances: u32,
402        first: AtomicBool,
403        endpoint: String,
404        /// The first instance, created at bind time (FILE_FLAG_FIRST_PIPE_INSTANCE) and
405        /// kept open so the name is never unowned between bind and the first accept.
406        pending: Mutex<Option<SendHandle>>,
407    }
408
409    fn wide(s: &str) -> Vec<u16> {
410        s.encode_utf16().chain(std::iter::once(0)).collect()
411    }
412
413    const ERROR_BROKEN_PIPE: u32 = 109;
414    const ERROR_IO_PENDING: u32 = 997;
415    const ERROR_OPERATION_ABORTED: u32 = 995;
416    const ERROR_NO_DATA: u32 = 232;
417    const ERROR_PIPE_CONNECTED: u32 = 535;
418    const FILE_FLAG_OVERLAPPED_BITS: u32 = 0x4000_0000;
419    // CreateFile security quality-of-service flags (winbase.h).
420    const SECURITY_SQOS_PRESENT_BITS: u32 = 0x0010_0000;
421    const SECURITY_IDENTIFICATION_BITS: u32 = 0x0001_0000;
422
423    fn code(e: &windows::core::Error) -> u32 {
424        (e.code().0 as u32) & 0xFFFF
425    }
426
427    /// One pipe handle opened with FILE_FLAG_OVERLAPPED. Reads and writes each use
428    /// their own OVERLAPPED + event, so a read blocked in one thread never blocks a
429    /// write, a peer check, or `cancel` from another thread (a synchronous handle
430    /// serialises all I/O on the file object). `cancel` issues CancelIoEx, which
431    /// completes every pending operation with ERROR_OPERATION_ABORTED.
432    struct PipeInner {
433        /// `Some(handle)` until `cancel` closes it. Operations hold the read lock for
434        /// their whole (possibly blocked) duration; `cancel` takes the write lock only
435        /// after CancelIoEx has released them, so the handle is never closed under I/O.
436        h: RwLock<Option<HANDLE>>,
437        raw: AtomicIsize,
438        cancelled: AtomicBool,
439        closing: Mutex<()>,
440    }
441    unsafe impl Send for PipeInner {}
442    unsafe impl Sync for PipeInner {}
443    impl Drop for PipeInner {
444        fn drop(&mut self) {
445            if let Some(h) = self.h.get_mut().unwrap_or_else(|e| e.into_inner()).take() {
446                unsafe {
447                    let _ = CloseHandle(h);
448                }
449            }
450        }
451    }
452
453    #[derive(Clone)]
454    struct Pipe(Arc<PipeInner>);
455
456    impl Pipe {
457        fn new(h: HANDLE) -> Pipe {
458            Pipe(Arc::new(PipeInner {
459                h: RwLock::new(Some(h)),
460                raw: AtomicIsize::new(h.0 as isize),
461                cancelled: AtomicBool::new(false),
462                closing: Mutex::new(()),
463            }))
464        }
465
466        /// Interrupt every pending operation, then close the handle so the peer sees
467        /// the disconnect now rather than when the last clone drops. Idempotent.
468        fn cancel(&self) {
469            let _one = self.0.closing.lock().unwrap_or_else(|e| e.into_inner());
470            self.0.cancelled.store(true, Ordering::SeqCst);
471            let raw = HANDLE(self.0.raw.load(Ordering::SeqCst) as *mut _);
472            if self.0.h.read().map(|g| g.is_some()).unwrap_or(false) {
473                // Safe: only `cancel` (serialised by `closing`) ever closes the handle.
474                unsafe {
475                    let _ = CancelIoEx(raw, None);
476                }
477            }
478            let mut g = self.0.h.write().unwrap_or_else(|e| e.into_inner()); // waits for aborted ops to unwind
479            if let Some(h) = g.take() {
480                unsafe {
481                    let _ = CloseHandle(h);
482                }
483            }
484        }
485
486        /// Run one overlapped operation to completion. `start` issues the call with
487        /// the prepared OVERLAPPED; the wait happens here, outside any shared lock.
488        fn run(
489            &self,
490            start: impl FnOnce(HANDLE, *mut OVERLAPPED) -> windows::core::Result<()>,
491        ) -> io::Result<usize> {
492            if self.0.cancelled.load(Ordering::SeqCst) {
493                return Err(io::Error::new(
494                    io::ErrorKind::ConnectionAborted,
495                    "connection closed",
496                ));
497            }
498            let guard = self.0.h.read().unwrap_or_else(|e| e.into_inner());
499            let Some(handle) = *guard else {
500                return Err(io::Error::new(
501                    io::ErrorKind::ConnectionAborted,
502                    "connection closed",
503                ));
504            };
505            unsafe {
506                let ev =
507                    CreateEventW(None, true, false, PCWSTR::null()).map_err(io::Error::other)?;
508                let mut ov = OVERLAPPED {
509                    hEvent: ev,
510                    ..Default::default()
511                };
512                let started = start(handle, &mut ov);
513                // `cancel` may have run its CancelIoEx before this operation was issued;
514                // re-check now that it is registered so it cannot wait forever.
515                if self.0.cancelled.load(Ordering::SeqCst) {
516                    let _ = CancelIoEx(handle, Some(&ov));
517                }
518                let outcome = match started {
519                    Ok(()) => Ok(true),
520                    Err(e) if code(&e) == ERROR_IO_PENDING => Ok(true),
521                    // ConnectNamedPipe: the client connected before the call. Nothing to wait for.
522                    Err(e) if code(&e) == ERROR_PIPE_CONNECTED => Ok(false),
523                    Err(e) => Err(e),
524                };
525                let res = match outcome {
526                    Ok(false) => Ok(0),
527                    Ok(true) => {
528                        let mut n = 0u32;
529                        // bWait = true: blocks only this call; CancelIoEx releases it.
530                        match GetOverlappedResult(handle, &ov, &mut n, true) {
531                            Ok(()) => Ok(n as usize),
532                            Err(e) => Err(e),
533                        }
534                    }
535                    Err(e) => Err(e),
536                };
537                let _ = CloseHandle(ev);
538                match res {
539                    Ok(n) => Ok(n),
540                    Err(e) => match code(&e) {
541                        ERROR_BROKEN_PIPE | ERROR_NO_DATA => {
542                            Err(io::Error::new(io::ErrorKind::BrokenPipe, "pipe closed"))
543                        }
544                        ERROR_OPERATION_ABORTED => Err(io::Error::new(
545                            io::ErrorKind::ConnectionAborted,
546                            "operation cancelled",
547                        )),
548                        _ => Err(io::Error::other(e)),
549                    },
550                }
551            }
552        }
553    }
554
555    struct Half(Pipe);
556    impl Read for Half {
557        fn read(&mut self, b: &mut [u8]) -> io::Result<usize> {
558            match self
559                .0
560                .run(|h, ov| unsafe { ReadFile(h, Some(b), None, Some(ov)) })
561            {
562                Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(0), // peer closed: EOF
563                r => r,
564            }
565        }
566    }
567    impl Write for Half {
568        fn write(&mut self, b: &[u8]) -> io::Result<usize> {
569            self.0
570                .run(|h, ov| unsafe { WriteFile(h, Some(b), None, Some(ov)) })
571        }
572        fn flush(&mut self) -> io::Result<()> {
573            Ok(()) // writes complete when the pipe buffer accepts them
574        }
575    }
576
577    fn wrap(pipe: Pipe) -> Conn {
578        let (w, watch, sd) = (pipe.clone(), pipe.clone(), pipe.clone());
579        Conn {
580            peer: crate::peer::PeerIdentity::default(),
581            reader: Box::new(Half(pipe)),
582            writer: Box::new(Half(w)),
583            peer_closed: Arc::new(move || {
584                if watch.0.cancelled.load(Ordering::SeqCst) {
585                    return true;
586                }
587                // PeekNamedPipe never blocks and is not serialised with a pending read.
588                let g = watch.0.h.read().unwrap_or_else(|e| e.into_inner());
589                match *g {
590                    Some(h) => unsafe { PeekNamedPipe(h, None, 0, None, None, None).is_err() },
591                    None => true,
592                }
593            }),
594            // Real cancellation: pending reads/writes return now; the handle closes
595            // when the last clone (reader, writer, this closure) drops.
596            shutdown: Arc::new(move || sd.cancel()),
597        }
598    }
599
600    impl Listener {
601        /// `max_instances` is clamped to 1..=254 (255 means unlimited to Win32). Every
602        /// instance of one pipe name must pass the same value.
603        pub fn bind(endpoint: &str, max_instances: u32) -> io::Result<Listener> {
604            let attrs = owner_only_security_attributes()?;
605            let l = Listener {
606                name: wide(endpoint),
607                attrs,
608                max_instances: max_instances.clamp(1, 254),
609                first: AtomicBool::new(true),
610                endpoint: endpoint.to_string(),
611                pending: Mutex::new(None),
612            };
613            // Create the first instance now so a squatter fails the bind, not a later accept.
614            // FILE_FLAG_FIRST_PIPE_INSTANCE on a name whose previous owner is still closing its
615            // instances fails ERROR_ACCESS_DENIED (5): report that as the address being in use.
616            let h = l.instance().map_err(|e| {
617                if e.raw_os_error() == Some(5) {
618                    io::Error::new(
619                        io::ErrorKind::AddrInUse,
620                        format!("pipe {endpoint} still held: {e}"),
621                    )
622                } else {
623                    e
624                }
625            })?;
626            *l.pending.lock().unwrap_or_else(|e| e.into_inner()) = Some(SendHandle(h));
627            Ok(l)
628        }
629
630        fn instance(&self) -> io::Result<HANDLE> {
631            let mut open = PIPE_ACCESS_DUPLEX;
632            if self.first.swap(false, Ordering::SeqCst) {
633                open |= FILE_FLAG_FIRST_PIPE_INSTANCE;
634            }
635            let h = unsafe {
636                CreateNamedPipeW(
637                    PCWSTR(self.name.as_ptr()),
638                    open | FILE_FLAG_OVERLAPPED,
639                    PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS,
640                    self.max_instances,
641                    64 * 1024,
642                    64 * 1024,
643                    0,
644                    Some(self.attrs.raw()),
645                )
646            };
647            if h.is_invalid() {
648                return Err(io::Error::last_os_error());
649            }
650            Ok(h)
651        }
652
653        pub fn accept(&self) -> io::Result<Accepted> {
654            let pending = self
655                .pending
656                .lock()
657                .unwrap_or_else(|e| e.into_inner())
658                .take();
659            let h = match pending {
660                Some(p) => p.0,
661                None => self.instance()?,
662            };
663            let pipe = Pipe::new(h); // owns (and on error closes) the instance
664            let connected = pipe.run(|h, ov| unsafe { ConnectNamedPipe(h, Some(ov)) });
665            connected.map_err(|e| io::Error::new(e.kind(), format!("accept: {e}")))?;
666            // Authenticate before a single byte is read. A rejected instance is closed
667            // (the peer sees a disconnect); the next accept creates a fresh instance.
668            let raw = pipe.0.raw.load(Ordering::SeqCst) as RawHandle;
669            match verify_pipe_peer_is_current_user(raw) {
670                Ok(peer) => Ok(Accepted::Conn(Conn { peer, ..wrap(pipe) })),
671                Err(e) => {
672                    pipe.cancel();
673                    if e.peer_gone() {
674                        Ok(Accepted::Vanished)
675                    } else {
676                        Ok(Accepted::Rejected(e.to_string()))
677                    }
678                }
679            }
680        }
681        pub fn wake(&self) {
682            let _ = open_overlapped(&self.endpoint);
683        }
684        pub fn cleanup(&self) {}
685    }
686
687    /// Connect, then verify the server process runs as the current user before any byte is
688    /// exchanged. The pipe is opened with SQOS + SECURITY_IDENTIFICATION, so the server can
689    /// identify the caller but never impersonate it.
690    pub fn connect(endpoint: &str) -> io::Result<Conn> {
691        let pipe = open_overlapped(endpoint)?;
692        let raw = pipe.0.raw.load(Ordering::SeqCst) as RawHandle;
693        if let Err(e) = verify_pipe_server_is_current_user(raw) {
694            pipe.cancel();
695            return Err(e.into());
696        }
697        Ok(wrap(pipe))
698    }
699
700    /// Connect, verify the server is the current user, then verify WHICH program it is before any
701    /// byte is exchanged (see `host_identity::verify_pipe_host`); the pipe is cancelled (closed)
702    /// on every error path.
703    pub fn connect_expecting(
704        endpoint: &str,
705        expect: &HostExpectation,
706    ) -> Result<(Conn, HostProcess), HostConnectError> {
707        let pipe = open_overlapped(endpoint).map_err(HostConnectError::Io)?;
708        let raw = pipe.0.raw.load(Ordering::SeqCst) as RawHandle;
709        if let Err(e) = verify_pipe_server_is_current_user(raw) {
710            pipe.cancel();
711            return Err(HostConnectError::Io(e.into()));
712        }
713        match crate::host_identity::verify_pipe_host(raw, expect) {
714            Ok(host) => Ok((wrap(pipe), host)),
715            Err(e) => {
716                pipe.cancel();
717                Err(e)
718            }
719        }
720    }
721
722    fn open_overlapped(endpoint: &str) -> io::Result<Pipe> {
723        use std::os::windows::fs::OpenOptionsExt;
724        use std::os::windows::io::IntoRawHandle;
725        const ERROR_FILE_NOT_FOUND: i32 = 2;
726        const ERROR_PIPE_BUSY: i32 = 231;
727        let start = std::time::Instant::now();
728        loop {
729            match std::fs::OpenOptions::new()
730                .read(true)
731                .write(true)
732                .custom_flags(FILE_FLAG_OVERLAPPED_BITS)
733                .security_qos_flags(SECURITY_SQOS_PRESENT_BITS | SECURITY_IDENTIFICATION_BITS)
734                .open(endpoint)
735            {
736                Ok(f) => return Ok(Pipe::new(HANDLE(f.into_raw_handle() as *mut _))),
737                Err(e) => {
738                    let limit = match e.raw_os_error() {
739                        Some(ERROR_PIPE_BUSY) => std::time::Duration::from_secs(3),
740                        Some(ERROR_FILE_NOT_FOUND) => std::time::Duration::from_secs(1),
741                        _ => return Err(e),
742                    };
743                    if start.elapsed() >= limit {
744                        return Err(e);
745                    }
746                    std::thread::sleep(std::time::Duration::from_millis(25));
747                }
748            }
749        }
750    }
751}
752
753pub use imp::Listener;
754pub(crate) use imp::{connect, connect_expecting, current_uid};
755
756/// Is process `pid` alive? A dead pid marks a discovery record stale and a
757/// takeover request abandoned. Hardened from HeardRight's version: pid 0 and
758/// pids that do not fit `pid_t` are never alive (they would signal process
759/// groups), and on Windows an exited process whose handle is still open is dead.
760#[cfg(unix)]
761pub fn pid_alive(pid: u32) -> bool {
762    extern "C" {
763        fn kill(pid: i32, sig: i32) -> i32;
764    }
765    if pid == 0 || pid > i32::MAX as u32 {
766        return false;
767    }
768    unsafe {
769        kill(pid as i32, 0) == 0 || io::Error::last_os_error().raw_os_error() == Some(1)
770        /* EPERM: exists */
771    }
772}
773#[cfg(windows)]
774pub fn pid_alive(pid: u32) -> bool {
775    use windows::Win32::Foundation::{CloseHandle, ERROR_ACCESS_DENIED};
776    use windows::Win32::System::Threading::{
777        GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
778    };
779    const STILL_ACTIVE: u32 = 259;
780    if pid == 0 {
781        return false;
782    }
783    unsafe {
784        match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) {
785            Ok(h) => {
786                let mut code = 0u32;
787                let ok = GetExitCodeProcess(h, &mut code).is_ok();
788                let _ = CloseHandle(h);
789                ok && code == STILL_ACTIVE
790            }
791            Err(e) => e.code() == ERROR_ACCESS_DENIED.to_hresult(),
792        }
793    }
794}
795
796/// A raw duplex byte stream to an endpoint, for callers that speak their own
797/// line protocol over the shared transport (HeardRight's v1 `EngineFrame`
798/// dialect, which has no `hello`). Same current-user-only transport as
799/// [`crate::Client`].
800pub struct Stream {
801    pub reader: Box<dyn Read + Send>,
802    pub writer: Box<dyn Write + Send>,
803    shutdown: Arc<dyn Fn() + Send + Sync>,
804}
805
806impl Stream {
807    /// Abort the stream from any thread: a blocked read/write returns, and the
808    /// host sees a disconnect (which cancels the request it is serving).
809    pub fn abort_handle(&self) -> Arc<dyn Fn() + Send + Sync> {
810        self.shutdown.clone()
811    }
812}
813
814/// Connect to `endpoint`. On Windows a busy pipe is retried up to 3 s and a
815/// not-yet-created one up to 1 s (HeardRight's behaviour).
816///
817/// The host is checked to be the current user and nothing more: whichever same-user program
818/// answers at `endpoint` receives whatever you write. Use [`open_stream_expecting`] to name the
819/// program you mean to talk to.
820pub fn open_stream(endpoint: &str) -> io::Result<Stream> {
821    let c = connect(endpoint)?;
822    Ok(Stream {
823        reader: c.reader,
824        writer: c.writer,
825        shutdown: c.shutdown,
826    })
827}
828
829/// [`open_stream`] that first verifies the host is the program `expect` names, before a byte is
830/// written (see [`Client::connect_expecting`](crate::Client::connect_expecting) for the checks and
831/// what each platform verifies). A host that is not the expected one is
832/// [`ConnectError::HostIdentity`](crate::ConnectError::HostIdentity); a host that cannot be
833/// reached is [`ConnectError::Service`](crate::ConnectError::Service) with the retryable
834/// `service_unavailable`.
835pub fn open_stream_expecting(
836    endpoint: &str,
837    expect: &HostExpectation,
838) -> Result<Stream, crate::ConnectError> {
839    match connect_expecting(endpoint, expect) {
840        Ok((c, _host)) => Ok(Stream {
841            reader: c.reader,
842            writer: c.writer,
843            shutdown: c.shutdown,
844        }),
845        Err(HostConnectError::Io(e)) => Err(crate::ConnectError::Service(
846            crate::ServiceError::retryable(
847                crate::ERROR_UNAVAILABLE,
848                format!("host unreachable: {e}"),
849            ),
850        )),
851        Err(HostConnectError::Identity(e)) => Err(crate::ConnectError::HostIdentity(e)),
852    }
853}
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858    use std::sync::atomic::{AtomicU32, Ordering};
859
860    static SEQ: AtomicU32 = AtomicU32::new(0);
861
862    fn unique(tag: &str) -> String {
863        let n = SEQ.fetch_add(1, Ordering::SeqCst);
864        let t = std::time::SystemTime::now()
865            .duration_since(std::time::UNIX_EPOCH)
866            .map(|d| d.subsec_nanos())
867            .unwrap_or(0);
868        format!("{tag}-{}-{n}-{t:x}", std::process::id())
869    }
870
871    #[cfg(unix)]
872    fn endpoint(dir: &tempfile::TempDir) -> String {
873        dir.path().join("t.sock").to_string_lossy().into_owned()
874    }
875    #[cfg(windows)]
876    fn endpoint(_: &NoDir) -> String {
877        format!(r"\\.\pipe\rightkit-transport-test-{}", unique("p"))
878    }
879    #[cfg(unix)]
880    fn tmp() -> tempfile::TempDir {
881        tempfile::Builder::new().prefix("rk").tempdir().unwrap()
882    }
883    /// Named pipes need no directory; this stands in for the Unix temporary directory.
884    #[cfg(windows)]
885    struct NoDir;
886    #[cfg(windows)]
887    fn tmp() -> NoDir {
888        NoDir
889    }
890
891    /// A same-user client and host round-trip over the real transport, with the peer
892    /// check running on both ends (accept and connect).
893    #[test]
894    fn same_user_round_trip_authenticates_both_ends() {
895        let _ = unique("x");
896        let dir = tmp();
897        let ep = endpoint(&dir);
898        let l = Listener::bind(&ep, 4).unwrap();
899        let client_ep = ep.clone();
900        let t = std::thread::spawn(move || {
901            let mut c = connect(&client_ep).expect("same-user server must authenticate");
902            c.writer.write_all(b"ping\n").unwrap();
903            c.writer.flush().unwrap();
904            let mut buf = [0u8; 5];
905            c.reader.read_exact(&mut buf).unwrap();
906            buf
907        });
908        let Accepted::Conn(mut conn) = l.accept().unwrap() else {
909            panic!("same-user peer must be accepted");
910        };
911        let mut buf = [0u8; 5];
912        conn.reader.read_exact(&mut buf).unwrap();
913        assert_eq!(&buf, b"ping\n");
914        conn.writer.write_all(b"pong\n").unwrap();
915        conn.writer.flush().unwrap();
916        assert_eq!(&t.join().unwrap(), b"pong\n");
917        l.cleanup();
918    }
919
920    /// A peer the verifier refuses is closed unread and reported as Rejected.
921    #[cfg(unix)]
922    #[test]
923    fn rejected_peer_is_closed_unread() {
924        use crate::peer::{PeerError, PeerIdentity};
925        let dir = tmp();
926        let ep = endpoint(&dir);
927        let l = Listener::bind(&ep, 4).unwrap();
928        let client_ep = ep.clone();
929        let t = std::thread::spawn(move || {
930            let mut s = std::os::unix::net::UnixStream::connect(&client_ep).unwrap();
931            let _ = s.write_all(b"{\"type\":\"hello\"}\n");
932            let mut b = [0u8; 1];
933            // The host closes without reading or answering: EOF (or a reset), never data.
934            matches!(s.read(&mut b), Ok(0) | Err(_))
935        });
936        let foreign = PeerIdentity {
937            uid: Some(crate::peer::current_euid().wrapping_add(1)),
938            ..Default::default()
939        };
940        match l
941            .accept_with(|_| Err(PeerError::Mismatch(foreign)))
942            .unwrap()
943        {
944            Accepted::Rejected(reason) => assert!(reason.contains("not the current user")),
945            _ => panic!("a foreign peer must be rejected"),
946        }
947        assert!(
948            t.join().unwrap(),
949            "rejected client must see a close, not a reply"
950        );
951        l.cleanup();
952    }
953
954    /// A platform that cannot authenticate the peer rejects instead of admitting it.
955    #[cfg(unix)]
956    #[test]
957    fn unsupported_verifier_never_admits() {
958        use crate::peer::PeerError;
959        let dir = tmp();
960        let ep = endpoint(&dir);
961        let l = Listener::bind(&ep, 4).unwrap();
962        let client_ep = ep.clone();
963        let t = std::thread::spawn(move || std::os::unix::net::UnixStream::connect(client_ep));
964        let a = l
965            .accept_with(|_| Err(PeerError::Unsupported("test")))
966            .unwrap();
967        assert!(matches!(a, Accepted::Rejected(_)));
968        let _ = t.join();
969        l.cleanup();
970    }
971
972    #[cfg(windows)]
973    #[test]
974    fn public_pipe_verifiers_accept_a_same_user_pipe() {
975        use crate::peer::{
976            owner_only_security_attributes, verify_pipe_peer_is_current_user,
977            verify_pipe_server_is_current_user,
978        };
979        use std::os::windows::fs::OpenOptionsExt;
980        use std::os::windows::io::{AsRawHandle, RawHandle};
981        use windows::core::PCWSTR;
982        use windows::Win32::Security::SECURITY_ATTRIBUTES;
983        use windows::Win32::Storage::FileSystem::PIPE_ACCESS_DUPLEX;
984        use windows::Win32::System::Pipes::{
985            ConnectNamedPipe, CreateNamedPipeW, PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE,
986            PIPE_WAIT,
987        };
988        let name = format!(r"\\.\pipe\rightkit-verify-test-{}", unique("v"));
989        let wide: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
990        let attrs = owner_only_security_attributes().unwrap();
991        let server = unsafe {
992            CreateNamedPipeW(
993                PCWSTR(wide.as_ptr()),
994                PIPE_ACCESS_DUPLEX,
995                PIPE_TYPE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS,
996                1,
997                4096,
998                4096,
999                0,
1000                Some(attrs.as_ptr() as *const SECURITY_ATTRIBUTES),
1001            )
1002        };
1003        assert!(!server.is_invalid());
1004        let client_name = name.clone();
1005        let t = std::thread::spawn(move || {
1006            std::fs::OpenOptions::new()
1007                .read(true)
1008                .write(true)
1009                .security_qos_flags(0x0010_0000 | 0x0001_0000)
1010                .open(client_name)
1011                .unwrap()
1012        });
1013        // Blocking ConnectNamedPipe returns once the client has opened the pipe.
1014        unsafe { ConnectNamedPipe(server, None) }.ok();
1015        let client = t.join().unwrap();
1016        let id = verify_pipe_peer_is_current_user(server.0 as RawHandle).unwrap();
1017        assert_eq!(id.pid, Some(std::process::id()));
1018        assert_eq!(
1019            id.sid,
1020            Some(crate::peer::current_user_sid_string().unwrap())
1021        );
1022        let id = verify_pipe_server_is_current_user(client.as_raw_handle()).unwrap();
1023        assert_eq!(id.pid, Some(std::process::id()));
1024        let _ = unsafe { windows::Win32::Foundation::CloseHandle(server) };
1025    }
1026
1027    // ---- endpoint naming (gap ledger row 167) ----
1028
1029    fn long_service() -> String {
1030        format!("{}-instance", "a".repeat(39))
1031    }
1032
1033    #[test]
1034    fn fallback_file_name_is_pinned_and_bounded() {
1035        // FNV-1a 64 of the service name; changing this string breaks client/server agreement.
1036        assert_eq!(
1037            fallback_file_name("0badc0de", &long_service()),
1038            "rk-0badc0de-3a43f919c4e992b6.sock"
1039        );
1040        assert_eq!(
1041            fallback_file_name("0badc0de", "speech"),
1042            "rk-0badc0de-c1d6288e168cb0d7.sock"
1043        );
1044        assert_eq!(fallback_file_name("0badc0de", &long_service()).len(), 33);
1045        assert_eq!(fnv1a64(""), 0xcbf2_9ce4_8422_2325);
1046    }
1047
1048    #[cfg(unix)]
1049    fn long_root() -> tempfile::TempDir {
1050        // About 60 bytes: long enough that the old `rk-<tag>-<service>.sock` (65 bytes for a
1051        // 48-byte name) never fitted, short enough for the bounded name (33 bytes).
1052        let d = tempfile::Builder::new()
1053            .prefix(&"r".repeat(49))
1054            .tempdir_in("/tmp")
1055            .unwrap();
1056        let n = d.path().as_os_str().len();
1057        assert!((50..=69).contains(&n), "root is {n} bytes");
1058        d
1059    }
1060
1061    #[cfg(unix)]
1062    #[test]
1063    fn a_48_byte_service_name_binds_and_connects_under_a_long_temp_root() {
1064        let suite = Suite::per_run_temp("ep-long").unwrap();
1065        let service = long_service();
1066        assert_eq!(service.len(), 48);
1067        let root = long_root();
1068        let ep = unix_endpoint_in(&suite, &service, root.path()).unwrap();
1069        assert!(ep.starts_with(root.path().to_str().unwrap()));
1070        assert!(
1071            ep.len() <= MAX_UNIX_SOCKET_PATH,
1072            "{ep} is {} bytes",
1073            ep.len()
1074        );
1075        // The pre-fix name for the same inputs would not have fitted.
1076        assert_eq!(legacy_unix_endpoint_in(&suite, &service, root.path()), None);
1077        let l = Listener::bind(&ep, DEFAULT_MAX_PIPE_INSTANCES).unwrap();
1078        assert!(connect(&ep).is_ok());
1079        l.cleanup();
1080        // Both ends derive the same path from the same inputs.
1081        assert_eq!(
1082            candidates_in(&suite, &service, root.path()),
1083            vec![ep.clone()]
1084        );
1085    }
1086
1087    #[cfg(unix)]
1088    #[test]
1089    fn a_new_client_still_finds_an_old_host_on_the_old_fallback_name() {
1090        let suite = Suite::per_run_temp("ep-old").unwrap();
1091        // A 20-byte name with a short temp root: the old name fitted, so an old host may hold it.
1092        let service = "legacy-fallback-name";
1093        let root = tempfile::Builder::new()
1094            .prefix("rk")
1095            .tempdir_in("/tmp")
1096            .unwrap();
1097        // Only reachable when the in-suite path is too long, as under a long $TMPDIR.
1098        if suite
1099            .service_dir(service)
1100            .join(format!("{service}.sock"))
1101            .as_os_str()
1102            .len()
1103            <= IN_SUITE_PATH_LIMIT
1104        {
1105            return;
1106        }
1107        let new = unix_endpoint_in(&suite, service, root.path()).unwrap();
1108        let old = legacy_unix_endpoint_in(&suite, service, root.path()).expect("old name fitted");
1109        assert_ne!(new, old);
1110        assert_eq!(
1111            candidates_in(&suite, service, root.path()),
1112            vec![new.clone(), old.clone()]
1113        );
1114        // An old host holds only the old name.
1115        let _old_host = std::os::unix::net::UnixListener::bind(&old).unwrap();
1116        let live = candidates_in(&suite, service, root.path())
1117            .into_iter()
1118            .find(|e| probe_endpoint(e));
1119        assert_eq!(live.as_deref(), Some(old.as_str()));
1120        // A new host on the new name wins over the old one (it is listed first).
1121        let l = Listener::bind(&new, DEFAULT_MAX_PIPE_INSTANCES).unwrap();
1122        let live = candidates_in(&suite, service, root.path())
1123            .into_iter()
1124            .find(|e| probe_endpoint(e));
1125        assert_eq!(live.as_deref(), Some(new.as_str()));
1126        l.cleanup();
1127    }
1128
1129    #[cfg(unix)]
1130    #[test]
1131    fn an_unfittable_temp_root_is_a_typed_error_naming_path_length_and_limit() {
1132        let suite = Suite::per_run_temp("ep-absurd").unwrap();
1133        let service = long_service();
1134        let temp = std::path::PathBuf::from(format!("/{}", "t".repeat(90)));
1135        let e = unix_endpoint_in(&suite, &service, &temp).unwrap_err();
1136        assert_eq!(e.limit, MAX_UNIX_SOCKET_PATH);
1137        assert_eq!(e.len, e.path.len());
1138        assert!(e.len > e.limit);
1139        assert!(e.path.starts_with(temp.to_str().unwrap()));
1140        let msg = e.to_string();
1141        assert!(msg.contains(&e.path) && msg.contains(&e.len.to_string()) && msg.contains("103"));
1142        assert!(legacy_unix_endpoint_in(&suite, &service, &temp).is_none());
1143        assert!(candidates_in(&suite, &service, &temp).is_empty());
1144    }
1145}