rust_supervisor/ipc/security/
peer_identity.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct PeerIdentity {
20 pub pid: u32,
22 pub uid: u32,
24 pub gid: u32,
26}
27
28pub 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")]
61fn 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"))]
94fn 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, 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 let gid = if cred.cr_ngroups > 0 {
135 cred.cr_groups[0]
136 } else {
137 0
138 };
139
140 Ok(PeerIdentity {
141 pid: 0, uid: cred.cr_uid,
143 gid,
144 })
145}
146
147pub fn verify_peer_identity(
159 peer: &PeerIdentity,
160 config: &PeerIdentityConfig,
161) -> Result<(), DashboardError> {
162 if !config.enabled {
163 return Ok(());
164 }
165
166 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 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 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
187pub 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 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 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}