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]