prt_core/core/
namespace.rs1use std::collections::HashMap;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct NetNamespace {
14 pub inode: u64,
16 pub name: Option<String>,
18}
19
20impl NetNamespace {
21 pub fn label(&self) -> String {
23 match &self.name {
24 Some(n) => n.clone(),
25 None => format!("ns:{}", self.inode),
26 }
27 }
28}
29
30pub fn resolve_namespace(pid: u32) -> Option<NetNamespace> {
33 if !cfg!(target_os = "linux") {
34 return None;
35 }
36 let inode = read_ns_inode(pid)?;
37 Some(NetNamespace { inode, name: None })
38}
39
40pub fn resolve_namespaces(pids: &[u32]) -> HashMap<u32, NetNamespace> {
43 let mut result = HashMap::new();
44 if !cfg!(target_os = "linux") {
45 return result;
46 }
47
48 let named = load_named_namespaces();
50
51 for &pid in pids {
52 if let Some(inode) = read_ns_inode(pid) {
53 let name = named.get(&inode).cloned();
54 result.insert(pid, NetNamespace { inode, name });
55 }
56 }
57
58 result
59}
60
61pub fn group_by_namespace(pid_ns: &HashMap<u32, NetNamespace>) -> Vec<(NetNamespace, Vec<u32>)> {
64 let mut by_inode: HashMap<u64, (NetNamespace, Vec<u32>)> = HashMap::new();
65
66 for (&pid, ns) in pid_ns {
67 by_inode
68 .entry(ns.inode)
69 .or_insert_with(|| (ns.clone(), Vec::new()))
70 .1
71 .push(pid);
72 }
73
74 let mut groups: Vec<_> = by_inode.into_values().collect();
75 groups.sort_by(|a, b| a.0.label().cmp(&b.0.label()));
76 for (_, pids) in &mut groups {
77 pids.sort();
78 }
79 groups
80}
81
82#[allow(dead_code)]
84fn read_ns_inode(pid: u32) -> Option<u64> {
85 let link = std::fs::read_link(format!("/proc/{pid}/ns/net")).ok()?;
86 let s = link.to_string_lossy();
87 parse_ns_inode(&s)
89}
90
91fn parse_ns_inode(s: &str) -> Option<u64> {
93 let start = s.find('[')?;
94 let end = s.find(']')?;
95 s[start + 1..end].parse().ok()
96}
97
98#[allow(dead_code)]
103fn load_named_namespaces() -> HashMap<u64, String> {
104 #[cfg(target_os = "linux")]
105 {
106 use std::os::unix::fs::MetadataExt;
107 let mut result = HashMap::new();
108 if let Ok(dir) = std::fs::read_dir("/run/netns") {
109 for entry in dir.flatten() {
110 let name = entry.file_name().to_string_lossy().to_string();
111 if let Ok(meta) = std::fs::metadata(format!("/run/netns/{name}")) {
112 result.insert(meta.ino(), name);
113 }
114 }
115 }
116 result
117 }
118
119 #[cfg(not(target_os = "linux"))]
120 HashMap::new()
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn parse_ns_inode_valid() {
129 assert_eq!(parse_ns_inode("net:[4026531992]"), Some(4026531992));
130 }
131
132 #[test]
133 fn parse_ns_inode_invalid() {
134 assert_eq!(parse_ns_inode("garbage"), None);
135 assert_eq!(parse_ns_inode("net:[]"), None);
136 assert_eq!(parse_ns_inode("net:[abc]"), None);
137 }
138
139 #[test]
140 fn namespace_label_with_name() {
141 let ns = NetNamespace {
142 inode: 123,
143 name: Some("myns".into()),
144 };
145 assert_eq!(ns.label(), "myns");
146 }
147
148 #[test]
149 fn namespace_label_without_name() {
150 let ns = NetNamespace {
151 inode: 4026531992,
152 name: None,
153 };
154 assert_eq!(ns.label(), "ns:4026531992");
155 }
156
157 #[test]
158 fn group_by_namespace_groups_correctly() {
159 let mut pid_ns = HashMap::new();
160 let ns1 = NetNamespace {
161 inode: 100,
162 name: Some("default".into()),
163 };
164 let ns2 = NetNamespace {
165 inode: 200,
166 name: Some("container".into()),
167 };
168 pid_ns.insert(1, ns1.clone());
169 pid_ns.insert(2, ns1.clone());
170 pid_ns.insert(3, ns2.clone());
171
172 let groups = group_by_namespace(&pid_ns);
173 assert_eq!(groups.len(), 2);
174
175 assert_eq!(groups[0].0.name, Some("container".into()));
177 assert_eq!(groups[0].1, vec![3]);
178 assert_eq!(groups[1].0.name, Some("default".into()));
179 assert_eq!(groups[1].1, vec![1, 2]);
180 }
181
182 #[test]
183 fn group_by_namespace_empty() {
184 let groups = group_by_namespace(&HashMap::new());
185 assert!(groups.is_empty());
186 }
187
188 #[test]
189 fn resolve_namespaces_non_linux_returns_empty() {
190 if !cfg!(target_os = "linux") {
191 let result = resolve_namespaces(&[1, 2, 3]);
192 assert!(result.is_empty());
193 }
194 }
195}