crdhcpc 0.1.1

Standalone DHCP Client for Linux with DHCPv4, DHCPv6, PXE, and Dynamic DNS support
Documentation
/// Peer credential verification for Unix sockets
///
/// Provides SO_PEERCRED support to verify the identity of connecting processes
use nix::sys::socket::{getsockopt, sockopt::PeerCredentials as NixPeerCred};
use std::os::fd::AsFd;
use super::error::{ProtocolError, ProtocolResult};

#[cfg(test)]
use nix::unistd::{getuid, getgid};

/// Peer credentials from Unix socket
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PeerCredentials {
    /// Process ID
    pub pid: i32,
    /// User ID
    pub uid: u32,
    /// Group ID
    pub gid: u32,
}

impl PeerCredentials {
    /// Get peer credentials from a Unix socket
    pub fn from_socket<F: AsFd>(socket: &F) -> ProtocolResult<Self> {
        let creds = getsockopt(socket, NixPeerCred)
            .map_err(|e| ProtocolError::Internal(format!("Failed to get peer credentials: {}", e)))?;

        Ok(Self {
            pid: creds.pid(),
            uid: creds.uid(),
            gid: creds.gid(),
        })
    }

    /// Check if peer is running as root
    pub fn is_root(&self) -> bool {
        self.uid == 0
    }

    /// Check if peer has specific UID
    pub fn has_uid(&self, uid: u32) -> bool {
        self.uid == uid
    }

    /// Check if peer has specific GID
    pub fn has_gid(&self, gid: u32) -> bool {
        self.gid == gid
    }

    /// Get process info (if available)
    #[cfg(target_os = "linux")]
    pub fn process_info(&self) -> Option<ProcessInfo> {
        ProcessInfo::from_pid(self.pid)
    }
}

/// Process information from /proc
#[cfg(target_os = "linux")]
#[derive(Debug, Clone)]
pub struct ProcessInfo {
    pub pid: i32,
    pub name: String,
    pub exe_path: Option<String>,
}

#[cfg(target_os = "linux")]
impl ProcessInfo {
    /// Read process info from /proc
    pub fn from_pid(pid: i32) -> Option<Self> {
        use std::fs;

        // Read process name from /proc/[pid]/comm
        let comm_path = format!("/proc/{}/comm", pid);
        let name = fs::read_to_string(comm_path).ok()?.trim().to_string();

        // Read executable path from /proc/[pid]/exe
        let exe_path = fs::read_link(format!("/proc/{}/exe", pid))
            .ok()
            .and_then(|p| p.to_str().map(String::from));

        Some(Self {
            pid,
            name,
            exe_path,
        })
    }
}

/// Verify peer credentials against expected values
pub fn verify_peer_credentials<F: AsFd>(
    socket: &F,
    expected_uid: Option<u32>,
    expected_gid: Option<u32>,
) -> ProtocolResult<PeerCredentials> {
    let creds = PeerCredentials::from_socket(socket)?;

    // Verify UID if specified
    if let Some(uid) = expected_uid {
        if creds.uid != uid {
            return Err(ProtocolError::PermissionDenied(format!(
                "Expected UID {}, got {}",
                uid, creds.uid
            )));
        }
    }

    // Verify GID if specified
    if let Some(gid) = expected_gid {
        if creds.gid != gid {
            return Err(ProtocolError::PermissionDenied(format!(
                "Expected GID {}, got {}",
                gid, creds.gid
            )));
        }
    }

    Ok(creds)
}

/// Require peer to be running as root
pub fn require_root<F: AsFd>(socket: &F) -> ProtocolResult<PeerCredentials> {
    let creds = PeerCredentials::from_socket(socket)?;

    if !creds.is_root() {
        return Err(ProtocolError::PermissionDenied(format!(
            "Root access required (peer UID: {})",
            creds.uid
        )));
    }

    Ok(creds)
}

/// Require peer to be a specific user
pub fn require_user<F: AsFd>(socket: &F, uid: u32) -> ProtocolResult<PeerCredentials> {
    verify_peer_credentials(socket, Some(uid), None)
}

/// Allow only specific UIDs
pub fn allow_uids<F: AsFd>(socket: &F, allowed_uids: &[u32]) -> ProtocolResult<PeerCredentials> {
    let creds = PeerCredentials::from_socket(socket)?;

    if !allowed_uids.contains(&creds.uid) {
        return Err(ProtocolError::PermissionDenied(format!(
            "UID {} not in allowed list: {:?}",
            creds.uid, allowed_uids
        )));
    }

    Ok(creds)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::os::unix::net::UnixStream;

    #[test]
    fn test_peer_credentials() {
        // Create a Unix socket pair for testing
        let (sock1, sock2) = UnixStream::pair().unwrap();

        // Get credentials from one end
        let creds = PeerCredentials::from_socket(&sock1).unwrap();

        // Should be our own process
        assert_eq!(creds.pid, std::process::id() as i32);
        assert_eq!(creds.uid, getuid().as_raw());
        assert_eq!(creds.gid, getgid().as_raw());

        // Verify from other end
        let creds2 = PeerCredentials::from_socket(&sock2).unwrap();
        assert_eq!(creds, creds2);
    }

    #[test]
    fn test_is_root() {
        let (sock, _) = UnixStream::pair().unwrap();
        let creds = PeerCredentials::from_socket(&sock).unwrap();

        // Test should not run as root
        assert!(!creds.is_root());
    }

    #[test]
    fn test_verify_credentials() {
        let (sock, _) = UnixStream::pair().unwrap();
        let my_uid = getuid().as_raw();

        // Should succeed with correct UID
        let result = verify_peer_credentials(&sock, Some(my_uid), None);
        assert!(result.is_ok());

        // Should fail with wrong UID
        let result = verify_peer_credentials(&sock, Some(0), None);
        assert!(result.is_err());
    }

    #[test]
    fn test_allow_uids() {
        let (sock, _) = UnixStream::pair().unwrap();
        let my_uid = getuid().as_raw();

        // Should succeed if our UID is in the list
        let result = allow_uids(&sock, &[my_uid, 1000, 1001]);
        assert!(result.is_ok());

        // Should fail if our UID is not in the list
        // Use UIDs that are definitely not our current UID
        let other_uids: Vec<u32> = (0..3).map(|i| my_uid + 1000 + i).collect();
        let result = allow_uids(&sock, &other_uids);
        assert!(result.is_err());
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn test_process_info() {
        let (sock, _) = UnixStream::pair().unwrap();
        let creds = PeerCredentials::from_socket(&sock).unwrap();

        if let Some(info) = creds.process_info() {
            assert_eq!(info.pid, creds.pid);
            assert!(!info.name.is_empty());
            println!("Process: {} (PID: {})", info.name, info.pid);
        }
    }
}