use std::collections::HashMap;
use std::fs;
use std::io::Read;
use tokio::net::UnixStream;
const LINEAGE_CAP: usize = 8;
#[derive(Debug, Clone)]
pub struct PeerInfo {
pub pid: Option<u32>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub executable_path: Option<String>,
pub attestation_class: String,
pub subject_id: Option<String>,
pub signature_valid: bool,
pub display_label: Option<String>,
}
impl Default for PeerInfo {
fn default() -> Self {
Self {
pid: None,
uid: None,
gid: None,
executable_path: None,
attestation_class: "Unknown".into(),
subject_id: None,
signature_valid: false,
display_label: None,
}
}
}
impl PeerInfo {
#[must_use]
pub fn from_stream(stream: &UnixStream) -> Self {
stream.peer_cred().map_or_else(
|_| Self::default(),
|cred| {
#[cfg_attr(not(target_os = "linux"), allow(unused_mut))]
let mut info = Self {
pid: cred.pid().map(i32::cast_unsigned),
uid: Some(cred.uid()),
gid: Some(cred.gid()),
..Default::default()
};
#[cfg(target_os = "linux")]
enrich_linux(&mut info);
info
},
)
}
#[must_use]
pub fn from_unix_cred(pid: Option<u32>, uid: u32, gid: u32) -> Self {
#[cfg_attr(not(target_os = "linux"), allow(unused_mut))]
let mut info = Self {
pid,
uid: Some(uid),
gid: Some(gid),
..Default::default()
};
#[cfg(target_os = "linux")]
enrich_linux(&mut info);
info
}
}
#[cfg(target_os = "linux")]
fn enrich_linux(info: &mut PeerInfo) {
if let Some(pid) = info.pid {
let exe = fs::read_link(format!("/proc/{pid}/exe"))
.ok()
.map(|p| p.to_string_lossy().into_owned());
info.executable_path.clone_from(&exe);
info.display_label.clone_from(&exe);
if exe.is_some() {
info.attestation_class = "PeerCred".into();
}
}
}
#[must_use]
pub fn read_proc_environ(pid: u32) -> HashMap<String, String> {
let mut map = HashMap::new();
if let Ok(mut f) = fs::File::open(format!("/proc/{pid}/environ")) {
let mut buf = Vec::new();
if f.read_to_end(&mut buf).is_ok() {
for part in buf.split(|&b| b == 0) {
if let Ok(s) = std::str::from_utf8(part)
&& let Some((k, v)) = s.split_once('=')
{
map.insert(k.to_string(), v.to_string());
}
}
}
}
map
}
#[must_use]
pub fn lineage_pids(pid: u32) -> Vec<u32> {
let mut out = vec![pid];
let mut current = pid;
for _ in 0..LINEAGE_CAP {
let Ok(status) = fs::read_to_string(format!("/proc/{current}/status")) else {
break;
};
let parent = status
.lines()
.find(|l| l.starts_with("PPid:"))
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if parent == 0 || out.contains(&parent) {
break;
}
out.push(parent);
current = parent;
}
out
}
#[must_use]
pub fn detect_ssh_session(lineage: &[u32]) -> Option<(String, String, u32)> {
for &pid in lineage {
let Ok(exe) = fs::read_link(format!("/proc/{pid}/exe")) else {
continue;
};
if !exe.to_string_lossy().contains("sshd") {
continue;
}
let env = read_proc_environ(pid);
let host = remote_host_from_ssh_connection(env.get("SSH_CONNECTION").map(String::as_str));
let user = env.get("USER").cloned().unwrap_or_default();
return Some((user, host, pid));
}
None
}
fn remote_host_from_ssh_connection(conn: Option<&str>) -> String {
conn.unwrap_or_default()
.split_whitespace()
.nth(2)
.unwrap_or_default()
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ssh_connection_host_is_the_server_side_address() {
assert_eq!(
remote_host_from_ssh_connection(Some("10.0.0.5 51000 10.0.0.1 22")),
"10.0.0.1"
);
}
#[test]
fn ssh_connection_missing_or_malformed_is_empty() {
assert_eq!(remote_host_from_ssh_connection(None), "");
assert_eq!(remote_host_from_ssh_connection(Some("")), "");
assert_eq!(remote_host_from_ssh_connection(Some("only two")), "");
}
#[test]
fn default_peer_is_unknown_and_unsigned() {
let p = PeerInfo::default();
assert_eq!(p.attestation_class, "Unknown");
assert!(!p.signature_valid);
assert!(p.subject_id.is_none());
}
}