Skip to main content

rust_supervisor/ipc/security/
peer_identity.rs

1//! Peer identity verification (C1, C2).
2//!
3//! C1: socket owner verification — validates socket file ownership
4//!      before bind, rejects symlinks.
5//! C2: peer credentials verification — validates connecting process
6//!      identity via SO_PEERCRED / LOCAL_PEERCRED.
7
8use crate::config::ipc_security::PeerIdentityConfig;
9use crate::dashboard::error::DashboardError;
10use std::os::unix::fs::FileTypeExt;
11use std::os::unix::net::UnixStream as StdUnixStream;
12
13// ---------------------------------------------------------------------------
14// Peer identity record
15// ---------------------------------------------------------------------------
16
17/// Record of peer identity taken from a connected Unix socket.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct PeerIdentity {
20    /// Process identifier of the peer.
21    pub pid: u32,
22    /// User identifier of the peer.
23    pub uid: u32,
24    /// Group identifier of the peer.
25    pub gid: u32,
26}
27
28/// Extracts peer identity from a connected std Unix stream.
29///
30/// Linux: uses `SO_PEERCRED` → `libc::ucred`.
31/// macOS / FreeBSD: uses `LOCAL_PEERCRED` → `libc::xucred`
32/// (provides uid only; gid set to 0).
33///
34/// # Arguments
35///
36/// - `stream`: A connected `std::os::unix::net::UnixStream`.
37///
38/// # Returns
39///
40/// Returns `PeerIdentity` on success, or `DashboardError` if the kernel
41/// does not support peer credentials on this platform.
42pub fn extract_peer_identity(stream: &StdUnixStream) -> Result<PeerIdentity, DashboardError> {
43    #[cfg(target_os = "linux")]
44    {
45        extract_peer_identity_linux(stream)
46    }
47    #[cfg(any(target_os = "macos", target_os = "freebsd"))]
48    {
49        extract_peer_identity_macos(stream)
50    }
51    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
52    {
53        let _ = stream;
54        Err(DashboardError::peer_cred_unavailable(
55            "peer credentials not supported on this platform",
56        ))
57    }
58}
59
60#[cfg(target_os = "linux")]
61/// Extracts peer identity via Linux SO_PEERCRED.
62fn extract_peer_identity_linux(stream: &StdUnixStream) -> Result<PeerIdentity, DashboardError> {
63    use std::os::unix::io::AsRawFd;
64
65    let fd = stream.as_raw_fd();
66    let mut cred: libc::ucred = unsafe { std::mem::zeroed() };
67    let mut cred_len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
68
69    let ret = unsafe {
70        libc::getsockopt(
71            fd,
72            libc::SOL_SOCKET,
73            libc::SO_PEERCRED,
74            &mut cred as *mut _ as *mut libc::c_void,
75            &mut cred_len,
76        )
77    };
78
79    if ret != 0 {
80        return Err(DashboardError::peer_cred_unavailable(format!(
81            "getsockopt SO_PEERCRED failed: {}",
82            std::io::Error::last_os_error()
83        )));
84    }
85
86    Ok(PeerIdentity {
87        pid: cred.pid as u32,
88        uid: cred.uid,
89        gid: cred.gid,
90    })
91}
92
93#[cfg(any(target_os = "macos", target_os = "freebsd"))]
94/// Extracts peer identity via `LOCAL_PEERCRED` (macOS / FreeBSD `xucred`).
95///
96/// On macOS the credential structure provides `cr_uid` and `cr_groups[]`
97/// but no single `cr_gid`. This function uses `cr_uid` for identity and
98/// sets `gid` to 0 (gid checks are opt-in).
99///
100/// # Arguments
101///
102/// - `stream`: Connected Unix-domain stream socket.
103///
104/// # Returns
105///
106/// Returns [`PeerIdentity`] on success, or a `DashboardError` when
107/// `getsockopt` fails.
108fn extract_peer_identity_macos(stream: &StdUnixStream) -> Result<PeerIdentity, DashboardError> {
109    use std::os::unix::io::AsRawFd;
110
111    let fd = stream.as_raw_fd();
112    let mut cred: libc::xucred = unsafe { std::mem::zeroed() };
113    let mut cred_len = std::mem::size_of::<libc::xucred>() as libc::socklen_t;
114
115    let ret = unsafe {
116        libc::getsockopt(
117            fd,
118            0, // SOL_LOCAL
119            libc::LOCAL_PEERCRED,
120            &mut cred as *mut _ as *mut libc::c_void,
121            &mut cred_len,
122        )
123    };
124
125    if ret != 0 {
126        return Err(DashboardError::peer_cred_unavailable(format!(
127            "getsockopt LOCAL_PEERCRED failed: {}",
128            std::io::Error::last_os_error()
129        )));
130    }
131
132    // macOS xucred provides cr_uid and cr_groups[] but no single cr_gid.
133    // Use cr_uid for identity; gid is set to 0 (gid checks are opt-in).
134    let gid = if cred.cr_ngroups > 0 {
135        cred.cr_groups[0]
136    } else {
137        0
138    };
139
140    Ok(PeerIdentity {
141        pid: 0, // macOS LOCAL_PEERCRED does not provide pid
142        uid: cred.cr_uid,
143        gid,
144    })
145}
146
147/// Verifies peer identity against the configured identity expectations.
148///
149/// # Arguments
150///
151/// - `peer`: Extracted peer identity record.
152/// - `config`: Peer identity verification configuration.
153///
154/// # Returns
155///
156/// Returns `Ok(())` when the peer passes all checks, or `Err(DashboardError)`
157/// with the first failing check's error code.
158pub fn verify_peer_identity(
159    peer: &PeerIdentity,
160    config: &PeerIdentityConfig,
161) -> Result<(), DashboardError> {
162    if !config.enabled {
163        return Ok(());
164    }
165
166    // C2: uid match
167    if config.require_uid_match {
168        let my_uid = unsafe { libc::getuid() };
169        if peer.uid != my_uid {
170            return Err(DashboardError::peer_cred_uid_mismatch(my_uid, peer.uid));
171        }
172    }
173
174    // C2: gid whitelist
175    if !config.allowed_gids.is_empty() && !config.allowed_gids.contains(&peer.gid) {
176        return Err(DashboardError::peer_cred_gid_not_allowed(peer.gid));
177    }
178
179    // C2: pid whitelist
180    if !config.allowed_pids.is_empty() && !config.allowed_pids.contains(&peer.pid) {
181        return Err(DashboardError::peer_cred_pid_not_allowed(peer.pid));
182    }
183
184    Ok(())
185}
186
187// ---------------------------------------------------------------------------
188// C1: Socket owner & symlink checks for bind preparation
189// ---------------------------------------------------------------------------
190
191/// Prepares a socket path for binding (C1).
192///
193/// Rejects symlinks and validates socket file ownership before allowing
194/// a replacement bind. This is called before `tokio::net::UnixListener::bind`.
195///
196/// # Arguments
197///
198/// - `path`: The configured socket file path.
199///
200/// # Returns
201///
202/// Returns `Ok(())` when binding may proceed. Returns `Err(DashboardError)`
203/// with `ipc_symlink_rejected` or `ipc_socket_owner_mismatch` on failure.
204pub fn prepare_socket_path_for_bind(path: &std::path::Path) -> Result<(), DashboardError> {
205    let metadata = match std::fs::symlink_metadata(path) {
206        Ok(m) => m,
207        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
208        Err(e) => {
209            return Err(DashboardError::new(
210                "ipc_bind",
211                "ipc_bind",
212                None,
213                format!("failed to stat socket path: {e}"),
214                false,
215            ));
216        }
217    };
218
219    // Reject symlinks
220    if metadata.file_type().is_symlink() {
221        return Err(DashboardError::new(
222            "ipc_symlink_rejected",
223            "ipc_bind",
224            None,
225            "IPC path is a symlink — rejected for security",
226            false,
227        ));
228    }
229
230    // If path exists and is a socket, check ownership
231    if metadata.file_type().is_socket() {
232        #[cfg(unix)]
233        {
234            use std::os::unix::fs::MetadataExt;
235            let owner_uid = metadata.uid();
236            let my_uid = unsafe { libc::getuid() };
237            if owner_uid != my_uid {
238                return Err(DashboardError::ipc_socket_owner_mismatch(format!(
239                    "socket owner uid {owner_uid} != process uid {my_uid}"
240                )));
241            }
242        }
243    }
244
245    Ok(())
246}