diff --git a/crates/ospect/src/net.rs b/crates/ospect/src/net.rs index 4de8f6c0..4e58e9af 100644 --- a/crates/ospect/src/net.rs +++ b/crates/ospect/src/net.rs @@ -164,6 +164,9 @@ struct TcpConnectionInner { state: TcpState, /// An identifier of the process that owns the connection. pid: u32, + // inode identifier of the open socket file descriptor. + #[cfg(target_os = "linux")] + inode: u64, } /// Information about a TCP IPv4 connection. @@ -315,6 +318,9 @@ pub struct UdpConnectionInner { local_addr: A, /// An identifier of the process that owns the connection. pid: u32, + // inode identifier of the open socket file descriptor. + #[cfg(target_os = "linux")] + inode: u64, } /// Information about a UDP IPv4 connection. @@ -660,6 +666,7 @@ mod tests { let mut conns = tcp_v4_connections(std::process::id()) .unwrap() + .map(|x| dbg!(x)) .filter_map(Result::ok); let server_conn = conns @@ -745,22 +752,25 @@ mod tests { let udp_socket_addr = udp_socket.local_addr() .unwrap(); - let mut conns = all_connections() + let conns = all_connections() .unwrap() - .filter_map(Result::ok); + .filter_map(Result::ok) + .collect::>(); - assert! { - conns.find(|conn| { + // Check that the sockets are found and that there are no duplicates + + assert_eq!( + conns.iter().filter(|conn| { conn.pid() == std::process::id() && conn.local_addr() == tcp_server_addr - }).is_some() - } + }).count(), 1 + ); - assert! { - conns.find(|conn| { + assert_eq!( + conns.iter().filter(|conn| { conn.pid() == std::process::id() && conn.local_addr() == udp_socket_addr - }).is_some() - } + }).count(), 1 + ) } } diff --git a/crates/ospect/src/net/linux/conn.rs b/crates/ospect/src/net/linux/conn.rs index 247dc5e6..69093011 100644 --- a/crates/ospect/src/net/linux/conn.rs +++ b/crates/ospect/src/net/linux/conn.rs @@ -3,42 +3,94 @@ // Use of this source code is governed by an MIT-style license that can be found // in the LICENSE file or at https://opensource.org/licenses/MIT. +use std::collections::HashSet; + use crate::net::*; +/// Returns a set of inodes of the given process' currently open sockets, parsed from procfs. +/// The return value be used to cross-check whether a given connection "belongs" to the process. +fn get_open_socket_inodes(pid: u32) -> std::io::Result> { + fn parse_socket_inode(path_str: &str) -> Option { + let inode_num = path_str + .strip_prefix("socket:[") + .and_then(|str| str.strip_suffix(']')); + // Be lenient and treat a failure to parse an integer as the inode being absent. + // In practice, this should never happen. + inode_num.and_then(|s| s.parse().ok()) + } + + let mut results = HashSet::new(); + for entry in std::fs::read_dir(format!("/proc/{pid}/fd"))?.filter_map(Result::ok) { + // readlink can fail + let Ok(resolved_path) = dbg!(std::fs::read_link(entry.path())) else { + continue; + }; + let Some(path_str) = resolved_path.to_str() else { + // Fds can point to arbitrary disk paths which may not be UTF-8, + // but we don't care about those. + continue; + }; + if let Some(inode) = parse_socket_inode(path_str) { + results.insert(inode); + } + } + Ok(results) +} + /// Returns an iterator over IPv4 TCP connections for the specified process. pub fn tcp_v4(pid: u32) -> std::io::Result>> { - let path = format!("/proc/{pid}/net/tcp"); + let socket_inodes = get_open_socket_inodes(pid)?; Ok(TcpConnections { pid, - iter: Connections::new(path, parse_tcp_v4_connection)?, - }.map(|conn| Ok(TcpConnectionV4::from_inner(conn?)))) + iter: Connections::new("/proc/net/tcp", parse_tcp_v4_connection)?, + } + .filter(move |conn| match conn { + Ok(conn) => socket_inodes.contains(&conn.inode), + Err(_) => true, + }) + .map(|conn| Ok(TcpConnectionV4::from_inner(conn?)))) } /// Returns an iterator over IPv6 TCP connections for the specified process. pub fn tcp_v6(pid: u32) -> std::io::Result>> { - let path = format!("/proc/{pid}/net/tcp6"); + let socket_inodes = get_open_socket_inodes(pid)?; Ok(TcpConnections { pid, - iter: Connections::new(path, parse_tcp_v6_connection)?, - }.map(|conn| Ok(TcpConnectionV6::from_inner(conn?)))) + iter: Connections::new("/proc/net/tcp6", parse_tcp_v6_connection)?, + } + .filter(move |conn| match conn { + Ok(conn) => socket_inodes.contains(&conn.inode), + Err(_) => true, + }) + .map(|conn| Ok(TcpConnectionV6::from_inner(conn?)))) } /// Returns an iterator over IPv4 UDP connections for the specified process. pub fn udp_v4(pid: u32) -> std::io::Result>> { - let path = format!("/proc/{pid}/net/udp"); + let socket_inodes = get_open_socket_inodes(pid)?; Ok(UdpConnections { pid, - iter: Connections::new(path, parse_udp_v4_connection)?, - }.map(|conn| Ok(UdpConnectionV4::from_inner(conn?)))) + iter: Connections::new("/proc/net/udp", parse_udp_v4_connection)?, + } + .filter(move |conn| match conn { + Ok(conn) => socket_inodes.contains(&conn.inode), + Err(_) => true, + }) + .map(|conn| Ok(UdpConnectionV4::from_inner(conn?)))) } /// Returns an iterator over IPv6 UDP connections for the specified process. pub fn udp_v6(pid: u32) -> std::io::Result>> { - let path = format!("/proc/{pid}/net/udp6"); + let socket_inodes = get_open_socket_inodes(pid)?; Ok(UdpConnections { pid, - iter: Connections::new(path, parse_udp_v6_connection)?, - }.map(|conn| Ok(UdpConnectionV6::from_inner(conn?)))) + iter: Connections::new("/proc/net/udp6", parse_udp_v6_connection)?, + } + .filter(move |conn| match conn { + Ok(conn) => socket_inodes.contains(&conn.inode), + Err(_) => true, + }) + .map(|conn| Ok(UdpConnectionV6::from_inner(conn?)))) } // TODO(rust-lang/rust#63063): Simplify as an alias to `impl`. @@ -190,9 +242,7 @@ fn parse_tcp_connection( string: &str, parse_socket_addr: fn(&str) -> Result, ) -> Result, ParseConnectionError> { - // There can be some leading whitespace at the beginning of the line, so - // in order not to get empty parts, we also trim it. - let mut parts = string.trim_start().split(char::is_whitespace); + let mut parts = string.split_ascii_whitespace(); // `sl` column (whatever that means but it is just a line number), we don't // care about it but expect it to be there. @@ -215,16 +265,27 @@ fn parse_tcp_connection( let state = parse_tcp_state(state_str) .map_err(ParseConnectionError::InvalidState)?; + // We don't care about these fields for now. + let _queues = parts.next().ok_or(ParseConnectionError::InvalidFormat)?; + let _tr_tm = parts.next().ok_or(ParseConnectionError::InvalidFormat)?; + let _retrnsmt = parts.next().ok_or(ParseConnectionError::InvalidFormat)?; + let _uid = parts.next().ok_or(ParseConnectionError::InvalidFormat)?; + let _timeout = parts.next().ok_or(ParseConnectionError::InvalidFormat)?; + + let inode_str = parts.next().ok_or(ParseConnectionError::InvalidFormat)?; + let inode: u64 = inode_str.parse().map_err(|_| ParseConnectionError::InvalidFormat)?; + // The line afterwards may contain some ill-formed data and we could raise // an error if we detect it. However, we choose to be generous and not to do // that to keep things simple. It also makes the code slightly more resilient // to potential format changes. Ok(TcpConnectionInner { - local_addr: local_addr, - remote_addr: remote_addr, + local_addr, + remote_addr, state, pid: 0, // Set at the iterator level where PID is available. + inode, }) } @@ -251,6 +312,7 @@ where Ok(UdpConnectionInner { local_addr: conn.local_addr, pid: 0, // Set at the iterator level where PID is available. + inode: conn.inode, }) } @@ -504,6 +566,7 @@ mod tests { assert_eq!(remote_addr.port(), 0); assert_eq!(conn.state, TcpState::Listen); + assert_eq!(conn.inode, 666333); } #[test] @@ -523,6 +586,7 @@ mod tests { assert_eq!(remote_addr.port(), 0); assert_eq!(conn.state, TcpState::Listen); + assert_eq!(conn.inode, 666333); } #[test] @@ -534,6 +598,7 @@ mod tests { let local_addr = conn.local_addr; assert_eq!(local_addr.ip(), &std::net::Ipv4Addr::from([127, 0, 0, 1])); assert_eq!(local_addr.port(), 0x0035); + assert_eq!(conn.inode, 6663330); } #[test] @@ -547,6 +612,7 @@ mod tests { let local_addr = conn.local_addr; assert_eq!(local_addr.ip(), &"::".parse::().unwrap()); assert_eq!(local_addr.port(), 0x14E9); + assert_eq!(conn.inode, 66333); } #[test]