Skip to content
Open
Show file tree
Hide file tree
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
30 changes: 20 additions & 10 deletions crates/ospect/src/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ struct TcpConnectionInner<A> {
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.
Expand Down Expand Up @@ -315,6 +318,9 @@ pub struct UdpConnectionInner<A> {
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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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::<Vec<_>>();

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
)
}
}
100 changes: 83 additions & 17 deletions crates/ospect/src/net/linux/conn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HashSet<u64>> {
fn parse_socket_inode(path_str: &str) -> Option<u64> {
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<impl Iterator<Item = std::io::Result<TcpConnectionV4>>> {
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<impl Iterator<Item = std::io::Result<TcpConnectionV6>>> {
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<impl Iterator<Item = std::io::Result<UdpConnectionV4>>> {
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<impl Iterator<Item = std::io::Result<UdpConnectionV6>>> {
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`.
Expand Down Expand Up @@ -190,9 +242,7 @@ fn parse_tcp_connection<A>(
string: &str,
parse_socket_addr: fn(&str) -> Result<A, ParseSocketAddrError>,
) -> Result<TcpConnectionInner<A>, 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.
Expand All @@ -215,16 +265,27 @@ fn parse_tcp_connection<A>(
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,
})
}

Expand All @@ -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,
})
}

Expand Down Expand Up @@ -504,6 +566,7 @@ mod tests {
assert_eq!(remote_addr.port(), 0);

assert_eq!(conn.state, TcpState::Listen);
assert_eq!(conn.inode, 666333);
}

#[test]
Expand All @@ -523,6 +586,7 @@ mod tests {
assert_eq!(remote_addr.port(), 0);

assert_eq!(conn.state, TcpState::Listen);
assert_eq!(conn.inode, 666333);
}

#[test]
Expand All @@ -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]
Expand All @@ -547,6 +612,7 @@ mod tests {
let local_addr = conn.local_addr;
assert_eq!(local_addr.ip(), &"::".parse::<Ipv6Addr>().unwrap());
assert_eq!(local_addr.port(), 0x14E9);
assert_eq!(conn.inode, 66333);
}

#[test]
Expand Down
Loading