1use std::collections::HashMap;
15use std::fs;
16use std::io::Read;
17
18use tokio::net::UnixStream;
19
20const LINEAGE_CAP: usize = 8;
21
22#[derive(Debug, Clone)]
24pub struct PeerInfo {
25 pub pid: Option<u32>,
26 pub uid: Option<u32>,
27 pub gid: Option<u32>,
28 pub executable_path: Option<String>,
29 pub attestation_class: String,
30 pub subject_id: Option<String>,
31 pub signature_valid: bool,
32 pub display_label: Option<String>,
33}
34
35impl Default for PeerInfo {
36 fn default() -> Self {
37 Self {
38 pid: None,
39 uid: None,
40 gid: None,
41 executable_path: None,
42 attestation_class: "Unknown".into(),
44 subject_id: None,
45 signature_valid: false,
47 display_label: None,
48 }
49 }
50}
51
52impl PeerInfo {
53 #[must_use]
55 pub fn from_stream(stream: &UnixStream) -> Self {
56 stream.peer_cred().map_or_else(
57 |_| Self::default(),
58 |cred| {
59 #[cfg_attr(not(target_os = "linux"), allow(unused_mut))]
62 let mut info = Self {
63 pid: cred.pid().map(i32::cast_unsigned),
64 uid: Some(cred.uid()),
65 gid: Some(cred.gid()),
66 ..Default::default()
67 };
68 #[cfg(target_os = "linux")]
69 enrich_linux(&mut info);
70 info
71 },
72 )
73 }
74
75 #[must_use]
81 pub fn from_unix_cred(pid: Option<u32>, uid: u32, gid: u32) -> Self {
82 #[cfg_attr(not(target_os = "linux"), allow(unused_mut))]
85 let mut info = Self {
86 pid,
87 uid: Some(uid),
88 gid: Some(gid),
89 ..Default::default()
90 };
91 #[cfg(target_os = "linux")]
92 enrich_linux(&mut info);
93 info
94 }
95}
96
97#[cfg(target_os = "linux")]
105fn enrich_linux(info: &mut PeerInfo) {
106 if let Some(pid) = info.pid {
107 let exe = fs::read_link(format!("/proc/{pid}/exe"))
108 .ok()
109 .map(|p| p.to_string_lossy().into_owned());
110 info.executable_path.clone_from(&exe);
111 info.display_label.clone_from(&exe);
112 if exe.is_some() {
113 info.attestation_class = "PeerCred".into();
114 }
115 }
116}
117
118#[must_use]
120pub fn read_proc_environ(pid: u32) -> HashMap<String, String> {
121 let mut map = HashMap::new();
122 if let Ok(mut f) = fs::File::open(format!("/proc/{pid}/environ")) {
123 let mut buf = Vec::new();
124 if f.read_to_end(&mut buf).is_ok() {
125 for part in buf.split(|&b| b == 0) {
126 if let Ok(s) = std::str::from_utf8(part)
127 && let Some((k, v)) = s.split_once('=')
128 {
129 map.insert(k.to_string(), v.to_string());
130 }
131 }
132 }
133 }
134 map
135}
136
137#[must_use]
139pub fn lineage_pids(pid: u32) -> Vec<u32> {
140 let mut out = vec![pid];
141 let mut current = pid;
142 for _ in 0..LINEAGE_CAP {
143 let Ok(status) = fs::read_to_string(format!("/proc/{current}/status")) else {
144 break;
145 };
146 let parent = status
147 .lines()
148 .find(|l| l.starts_with("PPid:"))
149 .and_then(|l| l.split_whitespace().nth(1))
150 .and_then(|s| s.parse().ok())
151 .unwrap_or(0);
152 if parent == 0 || out.contains(&parent) {
153 break;
154 }
155 out.push(parent);
156 current = parent;
157 }
158 out
159}
160
161#[must_use]
168pub fn detect_ssh_session(lineage: &[u32]) -> Option<(String, String, u32)> {
169 for &pid in lineage {
170 let Ok(exe) = fs::read_link(format!("/proc/{pid}/exe")) else {
171 continue;
172 };
173 if !exe.to_string_lossy().contains("sshd") {
174 continue;
175 }
176 let env = read_proc_environ(pid);
177 let host = remote_host_from_ssh_connection(env.get("SSH_CONNECTION").map(String::as_str));
178 let user = env.get("USER").cloned().unwrap_or_default();
179 return Some((user, host, pid));
180 }
181 None
182}
183
184fn remote_host_from_ssh_connection(conn: Option<&str>) -> String {
190 conn.unwrap_or_default()
191 .split_whitespace()
192 .nth(2)
193 .unwrap_or_default()
194 .to_string()
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn ssh_connection_host_is_the_server_side_address() {
203 assert_eq!(
205 remote_host_from_ssh_connection(Some("10.0.0.5 51000 10.0.0.1 22")),
206 "10.0.0.1"
207 );
208 }
209
210 #[test]
211 fn ssh_connection_missing_or_malformed_is_empty() {
212 assert_eq!(remote_host_from_ssh_connection(None), "");
213 assert_eq!(remote_host_from_ssh_connection(Some("")), "");
214 assert_eq!(remote_host_from_ssh_connection(Some("only two")), "");
215 }
216
217 #[test]
218 fn default_peer_is_unknown_and_unsigned() {
219 let p = PeerInfo::default();
220 assert_eq!(p.attestation_class, "Unknown");
221 assert!(!p.signature_valid);
222 assert!(p.subject_id.is_none());
223 }
224}