Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 45 additions & 31 deletions crates/openshell-sandbox/src/procfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ pub fn collect_cmdline_paths(pid: u32, stop_pid: u32, exclude: &[PathBuf]) -> Ve
/// Checks both IPv4 and IPv6 tables because some clients (notably gRPC C-core)
/// use `AF_INET6` sockets with IPv4-mapped addresses even for IPv4 connections.
///
/// Falls back to `/proc/net/tcp{,6}` (the init-namespace global view) when the
/// per-PID tables do not contain the connection. This handles container runtimes
/// (notably Docker Desktop on WSL2) where iptables REDIRECT/DNAT from the
/// sandbox network namespace creates connections that are visible in the global
/// table but not in `/proc/<pid>/net/tcp`.
///
/// Format of `/proc/net/tcp`:
/// ```text
/// sl local_address rem_address st tx_queue:rx_queue ... inode
Expand All @@ -173,48 +179,56 @@ pub fn collect_cmdline_paths(pid: u32, stop_pid: u32, exclude: &[PathBuf]) -> Ve
/// - Inode is field index 9 (0-indexed)
#[cfg(target_os = "linux")]
fn parse_proc_net_tcp(pid: u32, peer_port: u16) -> Result<u64> {
// Check IPv4 first (most common), then IPv6.
for suffix in &["tcp", "tcp6"] {
let path = format!("/proc/{pid}/net/{suffix}");
let Ok(content) = std::fs::read_to_string(&path) else {
continue;
};

for line in content.lines().skip(1) {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 10 {
// Try per-PID first (most accurate), then fall back to global.
let pid_prefixes: &[String] = &[
format!("/proc/{pid}"),
"/proc".to_string(),
];

for prefix in pid_prefixes {
for suffix in &["net/tcp", "net/tcp6"] {
let path = format!("{prefix}/{suffix}");
let Ok(content) = std::fs::read_to_string(&path) else {
continue;
}

// Parse local_address to extract port.
// IPv4 format: AABBCCDD:PORT
// IPv6 format: 00000000000000000000000000000000:PORT
let local_addr = fields[1];
let local_port = match local_addr.rsplit_once(':') {
Some((_, port_hex)) => u16::from_str_radix(port_hex, 16).unwrap_or(0),
None => continue,
};

// Check state is ESTABLISHED (01)
let state = fields[3];
if state != "01" {
continue;
}
for line in content.lines().skip(1) {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 10 {
continue;
}

if local_port == peer_port {
let inode: u64 = fields[9]
.parse()
.map_err(|_| miette::miette!("Failed to parse inode from {}", fields[9]))?;
if inode == 0 {
// Parse local_address to extract port.
// IPv4 format: AABBCCDD:PORT
// IPv6 format: 00000000000000000000000000000000:PORT
let local_addr = fields[1];
let local_port = match local_addr.rsplit_once(':') {
Some((_, port_hex)) => u16::from_str_radix(port_hex, 16).unwrap_or(0),
None => continue,
};

// Check state is ESTABLISHED (01)
let state = fields[3];
if state != "01" {
continue;
}
return Ok(inode);

if local_port == peer_port {
let inode: u64 = fields[9]
.parse()
.map_err(|_| miette::miette!("Failed to parse inode from {}", fields[9]))?;
if inode == 0 {
continue;
}
return Ok(inode);
}
}
}
}

Err(miette::miette!(
"No ESTABLISHED TCP connection found for port {} in /proc/{}/net/tcp{{,6}}",
"No ESTABLISHED TCP connection found for port {} in /proc/{}/net/tcp{{,6}} \
or /proc/net/tcp{{,6}}",
peer_port,
pid
))
Expand Down
Loading