use crate::config::ipc_security::PeerIdentityConfig;
use crate::dashboard::error::DashboardError;
use std::os::unix::fs::FileTypeExt;
use std::os::unix::net::UnixStream as StdUnixStream;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerIdentity {
pub pid: u32,
pub uid: u32,
pub gid: u32,
}
pub fn extract_peer_identity(stream: &StdUnixStream) -> Result<PeerIdentity, DashboardError> {
#[cfg(target_os = "linux")]
{
extract_peer_identity_linux(stream)
}
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
{
extract_peer_identity_macos(stream)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
{
let _ = stream;
Err(DashboardError::peer_cred_unavailable(
"peer credentials not supported on this platform",
))
}
}
#[cfg(target_os = "linux")]
fn extract_peer_identity_linux(stream: &StdUnixStream) -> Result<PeerIdentity, DashboardError> {
use std::os::unix::io::AsRawFd;
let fd = stream.as_raw_fd();
let mut cred: libc::ucred = unsafe { std::mem::zeroed() };
let mut cred_len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
let ret = unsafe {
libc::getsockopt(
fd,
libc::SOL_SOCKET,
libc::SO_PEERCRED,
&mut cred as *mut _ as *mut libc::c_void,
&mut cred_len,
)
};
if ret != 0 {
return Err(DashboardError::peer_cred_unavailable(format!(
"getsockopt SO_PEERCRED failed: {}",
std::io::Error::last_os_error()
)));
}
Ok(PeerIdentity {
pid: cred.pid as u32,
uid: cred.uid,
gid: cred.gid,
})
}
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
fn extract_peer_identity_macos(stream: &StdUnixStream) -> Result<PeerIdentity, DashboardError> {
use std::os::unix::io::AsRawFd;
let fd = stream.as_raw_fd();
let mut cred: libc::xucred = unsafe { std::mem::zeroed() };
let mut cred_len = std::mem::size_of::<libc::xucred>() as libc::socklen_t;
let ret = unsafe {
libc::getsockopt(
fd,
0, libc::LOCAL_PEERCRED,
&mut cred as *mut _ as *mut libc::c_void,
&mut cred_len,
)
};
if ret != 0 {
return Err(DashboardError::peer_cred_unavailable(format!(
"getsockopt LOCAL_PEERCRED failed: {}",
std::io::Error::last_os_error()
)));
}
let gid = if cred.cr_ngroups > 0 {
cred.cr_groups[0] as u32
} else {
0
};
Ok(PeerIdentity {
pid: 0, uid: cred.cr_uid,
gid,
})
}
pub fn verify_peer_identity(
peer: &PeerIdentity,
config: &PeerIdentityConfig,
) -> Result<(), DashboardError> {
if !config.enabled {
return Ok(());
}
if config.require_uid_match {
let my_uid = unsafe { libc::getuid() };
if peer.uid != my_uid {
return Err(DashboardError::peer_cred_uid_mismatch(my_uid, peer.uid));
}
}
if !config.allowed_gids.is_empty() && !config.allowed_gids.contains(&peer.gid) {
return Err(DashboardError::peer_cred_gid_not_allowed(peer.gid));
}
if !config.allowed_pids.is_empty() && !config.allowed_pids.contains(&peer.pid) {
return Err(DashboardError::peer_cred_pid_not_allowed(peer.pid));
}
Ok(())
}
pub fn prepare_socket_path_for_bind(path: &std::path::Path) -> Result<(), DashboardError> {
let metadata = match std::fs::symlink_metadata(path) {
Ok(m) => m,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => {
return Err(DashboardError::new(
"ipc_bind",
"ipc_bind",
None,
format!("failed to stat socket path: {e}"),
false,
));
}
};
if metadata.file_type().is_symlink() {
return Err(DashboardError::new(
"ipc_symlink_rejected",
"ipc_bind",
None,
"IPC path is a symlink — rejected for security",
false,
));
}
if metadata.file_type().is_socket() {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let owner_uid = metadata.uid();
let my_uid = unsafe { libc::getuid() };
if owner_uid != my_uid {
return Err(DashboardError::ipc_socket_owner_mismatch(format!(
"socket owner uid {owner_uid} != process uid {my_uid}"
)));
}
}
}
Ok(())
}