Skip to main content

endpoint_libs/libs/
peer.rs

1//! Peer identity for a connection, independent of transport.
2//!
3//! Before 2.0 a connection's peer was a bare `SocketAddr`, which only makes sense for
4//! TCP/TLS. Local transports (Unix sockets, Windows named pipes, macOS XPC) identify
5//! peers by process and by *code identity* — a codesign requirement, an executable
6//! digest, a SID — and that has to survive all the way into the request context so
7//! authorization and logging can see it.
8//!
9//! [`PeerIdentity`] is `#[non_exhaustive]`: new transports may add variants without a
10//! breaking release.
11
12use std::any::{Any, TypeId};
13use std::collections::HashMap;
14use std::net::{IpAddr, Ipv4Addr, SocketAddr};
15
16/// Who is on the other end of a connection.
17#[derive(Debug, Clone)]
18#[non_exhaustive]
19pub enum PeerIdentity {
20    /// A TCP/TLS network peer — the pre-2.0 behaviour.
21    Network(SocketAddr),
22    /// A same-machine peer reached over a local transport.
23    Local(LocalPeer),
24    /// Transport could not determine a peer (in-process tests, exotic transports).
25    Unknown,
26}
27
28/// A same-machine peer: OS-level process identity plus whatever code identity the
29/// transport was able to verify.
30#[derive(Debug, Clone)]
31pub struct LocalPeer {
32    pub pid: Option<u32>,
33    /// Effective uid. Unix only — always `None` on Windows.
34    pub uid: Option<u32>,
35    pub attestation: Attestation,
36}
37
38/// Whether — and how — the transport verified the peer's *code* identity.
39///
40/// This is deliberately separate from `pid`/`uid`: knowing which process connected is
41/// not the same as knowing it runs the binary you expect.
42#[derive(Debug, Clone)]
43#[non_exhaustive]
44pub enum Attestation {
45    /// The transport did not verify code identity. Treat the peer as untrusted.
46    None,
47    /// The OS or the transport verified code identity before the connection was
48    /// handed over.
49    Verified {
50        /// How it was verified, e.g. `"xpc-codesign-requirement"`,
51        /// `"pidfd-exe-sha256"`, `"pipe-sid-dacl"`.
52        mechanism: &'static str,
53        /// What matched: the requirement string, digest, or SID.
54        subject: String,
55    },
56}
57
58impl Attestation {
59    /// True only for [`Attestation::Verified`].
60    pub fn is_verified(&self) -> bool {
61        matches!(self, Self::Verified { .. })
62    }
63}
64
65impl PeerIdentity {
66    /// Best-effort IP for logging and for the pre-2.0 `ip_addr` field.
67    ///
68    /// Local and unknown peers report loopback — they have no IP, and loopback is
69    /// both truthful about locality and safe for code that only formats this value.
70    pub fn ip_addr(&self) -> IpAddr {
71        match self {
72            Self::Network(addr) => addr.ip(),
73            Self::Local(_) | Self::Unknown => IpAddr::V4(Ipv4Addr::LOCALHOST),
74        }
75    }
76
77    /// The socket address for network peers; a loopback placeholder otherwise.
78    ///
79    /// Provided for the deprecated `WsConnection::address` accessor. Prefer matching
80    /// on the enum.
81    pub fn socket_addr(&self) -> SocketAddr {
82        match self {
83            Self::Network(addr) => *addr,
84            Self::Local(_) | Self::Unknown => SocketAddr::from(([127, 0, 0, 1], 0)),
85        }
86    }
87
88    /// Attestation for local peers; `None` for network peers (TLS client certs are
89    /// not modelled here).
90    pub fn attestation(&self) -> Option<&Attestation> {
91        match self {
92            Self::Local(peer) => Some(&peer.attestation),
93            _ => None,
94        }
95    }
96
97    /// Compact one-line form for logs: `1.2.3.4:5678`, `local(pid=42,xpc-codesign-requirement)`.
98    pub fn display(&self) -> String {
99        match self {
100            Self::Network(addr) => addr.to_string(),
101            Self::Local(peer) => {
102                let pid = peer
103                    .pid
104                    .map_or_else(|| "?".to_owned(), |pid| pid.to_string());
105                match &peer.attestation {
106                    Attestation::Verified { mechanism, .. } => {
107                        format!("local(pid={pid},{mechanism})")
108                    }
109                    Attestation::None => format!("local(pid={pid},unattested)"),
110                }
111            }
112            Self::Unknown => "unknown".to_owned(),
113        }
114    }
115}
116
117impl std::fmt::Display for PeerIdentity {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.write_str(&self.display())
120    }
121}
122
123impl From<SocketAddr> for PeerIdentity {
124    fn from(addr: SocketAddr) -> Self {
125        Self::Network(addr)
126    }
127}
128
129// ---------------------------------------------------------------------------
130// Extensions
131// ---------------------------------------------------------------------------
132
133/// Object-safe `Any` that can also clone itself.
134///
135/// `RequestContext` derives `Clone` and consumers rely on that, so a plain
136/// `Box<dyn Any>` map cannot live inside it. Requiring `Clone` on stored values (the
137/// same trade `http::Extensions` makes) keeps the containing types cloneable.
138trait CloneAny: Any + Send + Sync {
139    fn clone_box(&self) -> Box<dyn CloneAny>;
140    fn as_any(&self) -> &dyn Any;
141    fn as_any_mut(&mut self) -> &mut dyn Any;
142    fn into_any(self: Box<Self>) -> Box<dyn Any>;
143}
144
145impl<T: Any + Send + Sync + Clone> CloneAny for T {
146    fn clone_box(&self) -> Box<dyn CloneAny> {
147        Box::new(self.clone())
148    }
149    fn as_any(&self) -> &dyn Any {
150        self
151    }
152    fn as_any_mut(&mut self) -> &mut dyn Any {
153        self
154    }
155    fn into_any(self: Box<Self>) -> Box<dyn Any> {
156        self
157    }
158}
159
160impl Clone for Box<dyn CloneAny> {
161    fn clone(&self) -> Self {
162        // `(**self)` is load-bearing. `Box<dyn CloneAny>` satisfies the blanket impl's
163        // bounds, so `self.clone_box()` would resolve to the box's own `clone_box`,
164        // which calls `clone` — infinite recursion and a stack overflow. Deref to the
165        // trait object so the inner value's `clone_box` runs instead.
166        (**self).clone_box()
167    }
168}
169
170/// A type-keyed map for attaching arbitrary data to a connection or request.
171///
172/// Modelled on `http::Extensions`, implemented here to avoid the dependency. Used to
173/// carry attestation on a connection and verified mission-token claims on a request,
174/// without either concept leaking into this crate's core types.
175///
176/// Stored values must be `Clone` so that the containing `RequestContext` stays
177/// `Clone`.
178#[derive(Default, Clone)]
179pub struct Extensions {
180    map: HashMap<TypeId, Box<dyn CloneAny>>,
181}
182
183impl Extensions {
184    pub fn new() -> Self {
185        Self::default()
186    }
187
188    /// Insert a value, returning the previous one of the same type, if any.
189    pub fn insert<T: Clone + Send + Sync + 'static>(&mut self, value: T) -> Option<T> {
190        self.map
191            .insert(TypeId::of::<T>(), Box::new(value))
192            .and_then(|prev| prev.into_any().downcast().ok().map(|boxed| *boxed))
193    }
194
195    pub fn get<T: Clone + Send + Sync + 'static>(&self) -> Option<&T> {
196        // `(**boxed)` is load-bearing: `Box<dyn CloneAny>` itself satisfies
197        // `Any + Send + Sync + Clone`, so it matches the blanket impl below and
198        // `boxed.as_any()` would resolve to the *box's* impl — yielding a `dyn Any`
199        // whose concrete type is the box, which never downcasts to `T`.
200        self.map
201            .get(&TypeId::of::<T>())
202            .and_then(|boxed| (**boxed).as_any().downcast_ref())
203    }
204
205    pub fn get_mut<T: Clone + Send + Sync + 'static>(&mut self) -> Option<&mut T> {
206        // See the note in `get` — the explicit deref is required here too.
207        self.map
208            .get_mut(&TypeId::of::<T>())
209            .and_then(|boxed| (**boxed).as_any_mut().downcast_mut())
210    }
211
212    pub fn remove<T: Clone + Send + Sync + 'static>(&mut self) -> Option<T> {
213        self.map
214            .remove(&TypeId::of::<T>())
215            .and_then(|boxed| boxed.into_any().downcast().ok().map(|b| *b))
216    }
217
218    pub fn contains<T: Clone + Send + Sync + 'static>(&self) -> bool {
219        self.map.contains_key(&TypeId::of::<T>())
220    }
221
222    pub fn is_empty(&self) -> bool {
223        self.map.is_empty()
224    }
225
226    pub fn len(&self) -> usize {
227        self.map.len()
228    }
229}
230
231impl std::fmt::Debug for Extensions {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        // Values are `dyn Any` and cannot be formatted; report the count so logs
234        // still show whether anything was attached.
235        f.debug_struct("Extensions")
236            .field("len", &self.map.len())
237            .finish()
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn network_peer_reports_its_own_ip() {
247        let addr: SocketAddr = "203.0.113.7:9000".parse().unwrap();
248        let peer = PeerIdentity::Network(addr);
249        assert_eq!(peer.ip_addr(), addr.ip());
250        assert_eq!(peer.socket_addr(), addr);
251        assert_eq!(peer.display(), "203.0.113.7:9000");
252        assert!(peer.attestation().is_none());
253    }
254
255    #[test]
256    fn local_peer_reports_loopback_and_keeps_attestation() {
257        let peer = PeerIdentity::Local(LocalPeer {
258            pid: Some(42),
259            uid: Some(501),
260            attestation: Attestation::Verified {
261                mechanism: "xpc-codesign-requirement",
262                subject: "identifier agentone".to_owned(),
263            },
264        });
265        assert_eq!(peer.ip_addr(), IpAddr::V4(Ipv4Addr::LOCALHOST));
266        assert!(peer.attestation().unwrap().is_verified());
267        assert_eq!(peer.display(), "local(pid=42,xpc-codesign-requirement)");
268    }
269
270    #[test]
271    fn unattested_local_peer_is_visible_as_such() {
272        let peer = PeerIdentity::Local(LocalPeer {
273            pid: None,
274            uid: None,
275            attestation: Attestation::None,
276        });
277        assert!(!peer.attestation().unwrap().is_verified());
278        assert_eq!(peer.display(), "local(pid=?,unattested)");
279    }
280
281    #[test]
282    fn extensions_survive_a_clone() {
283        #[derive(Debug, Clone, PartialEq)]
284        struct Claims(&'static str);
285
286        let mut ext = Extensions::new();
287        ext.insert(Claims("mission-1"));
288        // RequestContext derives Clone; extensions must come along intact.
289        let copy = ext.clone();
290        assert_eq!(copy.get::<Claims>(), Some(&Claims("mission-1")));
291    }
292
293    #[test]
294    fn extensions_round_trip_by_type() {
295        #[derive(Debug, Clone, PartialEq)]
296        struct Claims(&'static str);
297
298        let mut ext = Extensions::new();
299        assert!(ext.is_empty());
300        assert!(ext.get::<Claims>().is_none());
301
302        assert!(ext.insert(Claims("mission-1")).is_none());
303        assert_eq!(ext.get::<Claims>(), Some(&Claims("mission-1")));
304        assert!(ext.contains::<Claims>());
305
306        // Re-inserting returns the previous value rather than silently dropping it.
307        let prev = ext.insert(Claims("mission-2"));
308        assert_eq!(prev, Some(Claims("mission-1")));
309        assert_eq!(ext.get::<Claims>(), Some(&Claims("mission-2")));
310
311        assert_eq!(ext.remove::<Claims>(), Some(Claims("mission-2")));
312        assert!(ext.is_empty());
313    }
314}