use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NetNamespace {
pub inode: u64,
pub name: Option<String>,
}
impl NetNamespace {
pub fn label(&self) -> String {
match &self.name {
Some(n) => n.clone(),
None => format!("ns:{}", self.inode),
}
}
}
pub fn resolve_namespace(pid: u32) -> Option<NetNamespace> {
if !cfg!(target_os = "linux") {
return None;
}
let inode = read_ns_inode(pid)?;
Some(NetNamespace { inode, name: None })
}
pub fn resolve_namespaces(pids: &[u32]) -> HashMap<u32, NetNamespace> {
let mut result = HashMap::new();
if !cfg!(target_os = "linux") {
return result;
}
let named = load_named_namespaces();
for &pid in pids {
if let Some(inode) = read_ns_inode(pid) {
let name = named.get(&inode).cloned();
result.insert(pid, NetNamespace { inode, name });
}
}
result
}
pub fn group_by_namespace(pid_ns: &HashMap<u32, NetNamespace>) -> Vec<(NetNamespace, Vec<u32>)> {
let mut by_inode: HashMap<u64, (NetNamespace, Vec<u32>)> = HashMap::new();
for (&pid, ns) in pid_ns {
by_inode
.entry(ns.inode)
.or_insert_with(|| (ns.clone(), Vec::new()))
.1
.push(pid);
}
let mut groups: Vec<_> = by_inode.into_values().collect();
groups.sort_by(|a, b| a.0.label().cmp(&b.0.label()));
for (_, pids) in &mut groups {
pids.sort();
}
groups
}
#[allow(dead_code)]
fn read_ns_inode(pid: u32) -> Option<u64> {
let link = std::fs::read_link(format!("/proc/{pid}/ns/net")).ok()?;
let s = link.to_string_lossy();
parse_ns_inode(&s)
}
fn parse_ns_inode(s: &str) -> Option<u64> {
let start = s.find('[')?;
let end = s.find(']')?;
s[start + 1..end].parse().ok()
}
#[allow(dead_code)]
fn load_named_namespaces() -> HashMap<u64, String> {
#[cfg(target_os = "linux")]
{
use std::os::unix::fs::MetadataExt;
let mut result = HashMap::new();
if let Ok(dir) = std::fs::read_dir("/run/netns") {
for entry in dir.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if let Ok(meta) = std::fs::metadata(format!("/run/netns/{name}")) {
result.insert(meta.ino(), name);
}
}
}
result
}
#[cfg(not(target_os = "linux"))]
HashMap::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_ns_inode_valid() {
assert_eq!(parse_ns_inode("net:[4026531992]"), Some(4026531992));
}
#[test]
fn parse_ns_inode_invalid() {
assert_eq!(parse_ns_inode("garbage"), None);
assert_eq!(parse_ns_inode("net:[]"), None);
assert_eq!(parse_ns_inode("net:[abc]"), None);
}
#[test]
fn namespace_label_with_name() {
let ns = NetNamespace {
inode: 123,
name: Some("myns".into()),
};
assert_eq!(ns.label(), "myns");
}
#[test]
fn namespace_label_without_name() {
let ns = NetNamespace {
inode: 4026531992,
name: None,
};
assert_eq!(ns.label(), "ns:4026531992");
}
#[test]
fn group_by_namespace_groups_correctly() {
let mut pid_ns = HashMap::new();
let ns1 = NetNamespace {
inode: 100,
name: Some("default".into()),
};
let ns2 = NetNamespace {
inode: 200,
name: Some("container".into()),
};
pid_ns.insert(1, ns1.clone());
pid_ns.insert(2, ns1.clone());
pid_ns.insert(3, ns2.clone());
let groups = group_by_namespace(&pid_ns);
assert_eq!(groups.len(), 2);
assert_eq!(groups[0].0.name, Some("container".into()));
assert_eq!(groups[0].1, vec![3]);
assert_eq!(groups[1].0.name, Some("default".into()));
assert_eq!(groups[1].1, vec![1, 2]);
}
#[test]
fn group_by_namespace_empty() {
let groups = group_by_namespace(&HashMap::new());
assert!(groups.is_empty());
}
#[test]
fn resolve_namespaces_non_linux_returns_empty() {
if !cfg!(target_os = "linux") {
let result = resolve_namespaces(&[1, 2, 3]);
assert!(result.is_empty());
}
}
}