(pid: u32, peer_port: u16)
| 289 | /// - Inode is field index 9 (0-indexed) |
| 290 | #[cfg(target_os = "linux")] |
| 291 | fn parse_proc_net_tcp(pid: u32, peer_port: u16) -> Result<u64> { |
| 292 | // Check IPv4 first (most common), then IPv6. |
| 293 | for suffix in &["tcp", "tcp6"] { |
| 294 | let path = format!("/proc/{pid}/net/{suffix}"); |
| 295 | let Ok(content) = std::fs::read_to_string(&path) else { |
| 296 | continue; |
| 297 | }; |
| 298 | |
| 299 | for line in content.lines().skip(1) { |
| 300 | let fields: Vec<&str> = line.split_whitespace().collect(); |
| 301 | if fields.len() < 10 { |
| 302 | continue; |
| 303 | } |
| 304 | |
| 305 | // Parse local_address to extract port. |
| 306 | // IPv4 format: AABBCCDD:PORT |
| 307 | // IPv6 format: 00000000000000000000000000000000:PORT |
| 308 | let local_addr = fields[1]; |
| 309 | let local_port = match local_addr.rsplit_once(':') { |
| 310 | Some((_, port_hex)) => u16::from_str_radix(port_hex, 16).unwrap_or(0), |
| 311 | None => continue, |
| 312 | }; |
| 313 | |
| 314 | // Check state is ESTABLISHED (01) |
| 315 | let state = fields[3]; |
| 316 | if state != "01" { |
| 317 | continue; |
| 318 | } |
| 319 | |
| 320 | if local_port == peer_port { |
| 321 | let inode: u64 = fields[9] |
| 322 | .parse() |
| 323 | .map_err(|_| miette::miette!("Failed to parse inode from {}", fields[9]))?; |
| 324 | if inode == 0 { |
| 325 | continue; |
| 326 | } |
| 327 | return Ok(inode); |
| 328 | } |
| 329 | } |
| 330 | } |
| 331 | |
| 332 | Err(miette::miette!( |
| 333 | "No ESTABLISHED TCP connection found for port {} in /proc/{}/net/tcp{{,6}}", |
| 334 | peer_port, |
| 335 | pid |
| 336 | )) |
| 337 | } |
| 338 | |
| 339 | /// Scan `/proc` to find every PID that owns a given socket inode. |
| 340 | /// |
no test coverage detected