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