Skip to main content

running_process_platform_internal/platform/
ipc.rs

1//! Local endpoint, listener, connection, peer, handoff, and security primitives.
2//!
3//! Endpoint strings and protocol policy remain with callers. These opaque
4//! values own the selected host transport so callers never name Unix sockets,
5//! Windows named pipes, `interprocess` types, file descriptors, or handles.
6
7#[cfg(feature = "ipc")]
8pub use crate::{
9    ipc_current_user_id as current_user_id, IpcEndpoint as Endpoint,
10    IpcInheritedListener as InheritedListener, IpcListener as Listener,
11    IpcListenerNonblockingMode as ListenerNonblockingMode, IpcPeerIdentity as PeerIdentity,
12    IpcPeerIdentitySource as PeerIdentitySource, IpcStream as Stream,
13};
14
15/// Opaque platform attachment created while transferring an accepted IPC
16/// connection to a backend process.
17///
18/// On Windows this owns the handle-table value that must be carried by the
19/// caller's existing protocol. On Unix the descriptor travels out-of-band via
20/// `SCM_RIGHTS`. Native handle and descriptor values never cross the facade.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct HandoffAttachment {
23    protocol_value: u64,
24    backend_may_adopt_before_offer: bool,
25}
26
27/// Host-neutral candidates for one endpoint address.
28///
29/// Product naming policy may derive both a kernel-namespace name and a
30/// filesystem path. The selected transport chooses the applicable standard
31/// library value without exposing that host choice to the caller.
32#[derive(Clone, Debug, Default, PartialEq, Eq)]
33#[cfg(feature = "ipc")]
34pub struct EndpointAddressCandidates {
35    kernel_namespace: Option<String>,
36    filesystem: Option<std::path::PathBuf>,
37}
38
39#[cfg(feature = "ipc")]
40impl EndpointAddressCandidates {
41    pub fn new(kernel_namespace: Option<String>, filesystem: Option<std::path::PathBuf>) -> Self {
42        Self {
43            kernel_namespace,
44            filesystem,
45        }
46    }
47
48    /// Select the address used by the active local IPC transport.
49    pub fn select(self) -> Option<String> {
50        crate::ipc_select_endpoint_address(self.kernel_namespace, self.filesystem)
51    }
52}
53
54impl HandoffAttachment {
55    pub(crate) fn new(protocol_value: u64, backend_may_adopt_before_offer: bool) -> Self {
56        Self {
57            protocol_value,
58            backend_may_adopt_before_offer,
59        }
60    }
61
62    /// Append this attachment's opaque value as an unsigned protobuf varint.
63    ///
64    /// The caller owns the wire envelope while this facade retains ownership
65    /// of the native value and its representation.
66    pub fn append_unsigned_varint(self, output: &mut Vec<u8>) {
67        let mut value = self.protocol_value;
68        while value >= 0x80 {
69            output.push((value as u8 & 0x7f) | 0x80);
70            value >>= 7;
71        }
72        output.push(value as u8);
73    }
74
75    /// Whether the backend may adopt the connection before its offer arrives.
76    ///
77    /// Unix transfers the descriptor and token together in the sideband
78    /// message, while Windows requires the later offer to identify the
79    /// duplicated handle. Callers use this transport fact to make their own
80    /// proxy-fallback ownership decision without selecting a host.
81    pub fn backend_may_adopt_before_offer(self) -> bool {
82        self.backend_may_adopt_before_offer
83    }
84}
85
86/// Result of enforcing owner-private permissions on a local IPC directory.
87#[cfg(feature = "ipc")]
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum OwnerPrivateDirectoryOutcome {
90    /// The existing directory already had the complete host policy.
91    AlreadyPrivate,
92    /// Permissions were applied or repaired.
93    Hardened,
94}
95
96/// Create a directory and enforce the selected host's owner-private policy.
97#[cfg(feature = "ipc")]
98pub fn ensure_owner_private_directory(
99    path: &std::path::Path,
100) -> std::io::Result<OwnerPrivateDirectoryOutcome> {
101    crate::ipc_ensure_owner_private_directory(path)
102}
103
104/// Return whether a directory has the selected host's owner-private policy.
105#[cfg(feature = "ipc")]
106pub fn owner_private_directory(path: &std::path::Path) -> std::io::Result<bool> {
107    crate::ipc_owner_private_directory(path)
108}
109
110/// Whether an empty nonblocking read means "not ready yet" for this host's
111/// local IPC transport rather than end-of-stream.
112#[cfg(feature = "ipc")]
113pub fn nonblocking_zero_read_is_pending() -> bool {
114    crate::ipc_nonblocking_zero_read_is_pending()
115}
116
117/// Whether the selected local IPC transport uses filesystem endpoint names.
118#[cfg(feature = "ipc")]
119pub fn endpoint_is_filesystem_backed() -> bool {
120    crate::ipc_endpoint_is_filesystem_backed()
121}
122
123/// Host-neutral classification of a failed connection-transfer primitive.
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125pub enum HandoffTransferErrorKind {
126    Unsupported,
127    PermissionDenied,
128    BackendUnavailable,
129    WouldBlock,
130    Failed,
131}
132
133/// Failure from the platform-owned connection-transfer primitive.
134#[derive(Clone, Debug, PartialEq, Eq)]
135pub struct HandoffTransferError {
136    kind: HandoffTransferErrorKind,
137    may_have_reached_backend: bool,
138    detail: String,
139}
140
141impl HandoffTransferError {
142    pub(crate) fn new(
143        kind: HandoffTransferErrorKind,
144        may_have_reached_backend: bool,
145        detail: impl Into<String>,
146    ) -> Self {
147        Self {
148            kind,
149            may_have_reached_backend,
150            detail: detail.into(),
151        }
152    }
153
154    /// Return the policy-neutral failure category.
155    pub fn kind(&self) -> HandoffTransferErrorKind {
156        self.kind
157    }
158
159    /// Whether the backend may already own a duplicated connection.
160    pub fn may_have_reached_backend(&self) -> bool {
161        self.may_have_reached_backend
162    }
163}
164
165impl std::fmt::Display for HandoffTransferError {
166    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        formatter.write_str(&self.detail)
168    }
169}
170
171impl std::error::Error for HandoffTransferError {}
172
173/// Resolve a broker endpoint name using selected-host path and pipe rules.
174#[cfg(feature = "ipc")]
175pub fn broker_endpoint_name(bare_name: &str, path_scoped: bool) -> std::io::Result<String> {
176    crate::IpcBrokerEndpointName(bare_name, path_scoped)
177}
178
179/// The selected host's limit on a local IPC endpoint name.
180///
181/// Unix transports are bounded by the `sun_path` field of `sockaddr_un`;
182/// Windows named pipes are bounded by `MAX_PATH` unless the long-path
183/// prefix is in use. Callers use this to report a budget without naming
184/// which host they are on.
185#[derive(Clone, Copy, Debug, PartialEq, Eq)]
186pub struct EndpointNameLimit {
187    /// Largest endpoint name this host accepts, in bytes.
188    pub max_bytes: usize,
189    /// Operator-facing name of the limit, e.g. `"macOS sun_path"`.
190    pub label: &'static str,
191}
192
193/// Report the selected host's endpoint-name budget.
194#[cfg(feature = "ipc")]
195pub fn endpoint_name_limit() -> EndpointNameLimit {
196    crate::ipc_endpoint_name_limit()
197}
198
199/// A derived endpoint name that does not fit this host's budget.
200#[derive(Clone, Copy, Debug, PartialEq, Eq)]
201pub struct EndpointNameTooLong {
202    /// Length of the name that was derived.
203    pub len: usize,
204    /// Largest length that would have been accepted.
205    pub max: usize,
206    /// Operator-facing name of the limit that rejected it.
207    pub limit_label: &'static str,
208}
209
210/// Last-resort per-user root when the host's runtime/temp variable is unset.
211///
212/// Deliberately not `/tmp`: a per-user directory keeps two accounts on one
213/// host from colliding without naming a uid. Only reached when the platform's
214/// runtime variable is missing -- cron and sessionless ssh being the realistic
215/// cases.
216#[cfg(feature = "ipc")]
217pub(crate) fn per_user_runtime_fallback() -> std::path::PathBuf {
218    dirs::cache_dir()
219        .or_else(dirs::data_local_dir)
220        .or_else(dirs::home_dir)
221        .unwrap_or_else(std::env::temp_dir)
222        .join("running-process")
223        .join("broker-v2")
224}
225
226/// Canonical byte spelling of `path` for endpoint-scope identity.
227///
228/// Two callers naming the same installed file must hash to the same scope, so
229/// the selected host decides which spelling differences are meaningless. On a
230/// case-insensitive host that means folding case and separator style; on a
231/// host whose paths are opaque byte strings it means the bytes as they are.
232/// Callers own the hash itself, its domain separator, and its encoding.
233#[cfg(feature = "ipc")]
234pub fn endpoint_scope_bytes(path: &std::path::Path) -> Vec<u8> {
235    crate::ipc_endpoint_scope_bytes(path)
236}
237
238/// The selected host's directory for broker-v2 runtime files and sockets.
239///
240/// Every host lands inside a location the OS already scopes to a single user,
241/// so two accounts stay apart without a uid spelled into the path. The leaf is
242/// chosen per host rather than shared: on macOS the broker's sockets live under
243/// this root, and `sun_path` leaves no budget for a long one.
244///
245/// The directory is not created here. A caller that writes into it creates it
246/// owner-only at that point; a caller that only reads treats an absent
247/// directory as "nothing published", which is a normal state.
248#[cfg(feature = "ipc")]
249pub fn broker_v2_runtime_dir() -> std::path::PathBuf {
250    crate::ipc_broker_v2_runtime_dir()
251}
252
253/// Derive the v1 broker endpoint address for `bare_name`.
254///
255/// The selected host owns directory placement, the leaf spelling, and the
256/// length check. Callers own which bare name to ask for. The returned string
257/// is the address the caller passes back to [`Endpoint::new`]; it is a
258/// filesystem path where [`endpoint_is_filesystem_backed`] reports `true` and
259/// a kernel-namespace name otherwise.
260#[cfg(feature = "ipc")]
261pub fn broker_v1_endpoint_path(bare_name: &str) -> Result<String, EndpointNameTooLong> {
262    crate::ipc_broker_v1_endpoint_path(bare_name)
263}
264
265#[cfg(feature = "ipc-async")]
266pub use crate::{
267    IpcAsyncListener as AsyncListener, IpcAsyncStream as AsyncStream,
268    IpcIntoAsyncListener as IntoAsyncListener, IpcIntoAsyncStream as IntoAsyncStream,
269};
270
271#[cfg(all(test, feature = "ipc"))]
272mod tests {
273    use std::io::{Read, Write};
274
275    use super::{
276        current_user_id, ensure_owner_private_directory, owner_private_directory, Endpoint,
277        HandoffAttachment, Listener, Stream,
278    };
279
280    #[test]
281    fn ensure_private_dir_passes_private_check() {
282        let temporary = tempfile::tempdir().expect("temporary directory");
283        let path = temporary.path().join("private");
284        ensure_owner_private_directory(&path).expect("harden directory");
285        assert!(owner_private_directory(&path).expect("inspect directory"));
286    }
287
288    #[test]
289    fn handoff_attachment_can_be_encoded_without_exposing_its_value() {
290        let mut encoded = Vec::new();
291        HandoffAttachment::new(300, false).append_unsigned_varint(&mut encoded);
292        assert_eq!(encoded, [0xac, 0x02]);
293    }
294
295    #[test]
296    fn handoff_attachment_reports_pre_offer_adoption_semantics() {
297        assert!(HandoffAttachment::new(0, true).backend_may_adopt_before_offer());
298        assert!(!HandoffAttachment::new(0, false).backend_may_adopt_before_offer());
299    }
300
301    #[test]
302    fn endpoint_lifecycle_mechanics_are_facade_owned() {
303        let endpoint = Endpoint::test("lifecycle").expect("test endpoint");
304        endpoint.retire().expect("retire absent endpoint");
305
306        let listener = Listener::bind(&endpoint).expect("bind endpoint");
307
308        drop(listener);
309        endpoint.retire().expect("retire endpoint");
310    }
311
312    #[test]
313    fn sync_bind_accept_connect_and_peer_identity_round_trip() {
314        let endpoint = Endpoint::test("sync-roundtrip").expect("test endpoint");
315        let listener = Listener::bind(&endpoint).expect("bind");
316        let expected_user = current_user_id().expect("current user identity");
317        let server = std::thread::spawn(move || {
318            let mut stream = listener.accept().expect("accept");
319            let peer = stream.peer_identity().expect("peer identity");
320            assert_eq!(peer.user_id, expected_user);
321            let mut request = [0_u8; 4];
322            stream.read_exact(&mut request).expect("read request");
323            assert_eq!(&request, b"ping");
324            stream.write_all(b"pong").expect("write response");
325        });
326
327        let mut client = Stream::connect(&endpoint).expect("connect");
328        client.write_all(b"ping").expect("write request");
329        let mut response = [0_u8; 4];
330        client.read_exact(&mut response).expect("read response");
331        assert_eq!(&response, b"pong");
332        server.join().expect("server thread");
333    }
334
335    #[cfg(feature = "ipc-async")]
336    #[tokio::test]
337    async fn async_bind_accept_connect_and_peer_identity_round_trip() {
338        use super::{AsyncListener, AsyncStream};
339        use tokio::io::{AsyncReadExt, AsyncWriteExt};
340
341        let endpoint = Endpoint::test("async-roundtrip").expect("test endpoint");
342        let listener = AsyncListener::bind(&endpoint).expect("bind");
343        let expected_user = current_user_id().expect("current user identity");
344        let server = tokio::spawn(async move {
345            let mut stream = listener.accept().await.expect("accept");
346            let peer = stream.peer_identity().expect("peer identity");
347            assert_eq!(peer.user_id, expected_user);
348            let mut request = [0_u8; 4];
349            stream.read_exact(&mut request).await.expect("read request");
350            assert_eq!(&request, b"ping");
351            stream.write_all(b"pong").await.expect("write response");
352        });
353
354        let mut client = AsyncStream::connect(&endpoint).await.expect("connect");
355        client.write_all(b"ping").await.expect("write request");
356        let mut response = [0_u8; 4];
357        client
358            .read_exact(&mut response)
359            .await
360            .expect("read response");
361        assert_eq!(&response, b"pong");
362        server.await.expect("server task");
363    }
364}