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