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