pub const ESTABLISHED: u8 = 1;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TcpSocket {
pub inode: u64,
pub state: u8,
pub bytes_acked: u64,
pub bytes_received: u64,
}
#[cfg(target_os = "linux")]
const NETLINK_SOCK_DIAG: libc::c_int = 4;
#[cfg(target_os = "linux")]
const SOCK_DIAG_BY_FAMILY: u16 = 20;
#[cfg(target_os = "linux")]
const REQUEST_AND_DUMP: u16 = 0x01 | 0x300;
#[cfg(target_os = "linux")]
const NLMSG_ERROR: u16 = 2;
#[cfg(target_os = "linux")]
const NLMSG_DONE: u16 = 3;
#[cfg(target_os = "linux")]
const INET_DIAG_INFO: u16 = 2;
#[cfg(target_os = "linux")]
const INET_DIAG_NOCOOKIE: u32 = u32::MAX;
#[cfg(target_os = "linux")]
const NETLINK_HEADER: usize = 16;
#[cfg(target_os = "linux")]
const REQUEST_BODY: usize = 56;
#[cfg(target_os = "linux")]
const MESSAGE_BODY: usize = 72;
#[cfg(target_os = "linux")]
const STATE_OFFSET: usize = 1;
#[cfg(target_os = "linux")]
const INODE_OFFSET: usize = 68;
#[cfg(target_os = "linux")]
const ATTRIBUTE_HEADER: usize = 4;
#[cfg(target_os = "linux")]
const BYTES_ACKED_OFFSET: usize = 120;
#[cfg(target_os = "linux")]
const BYTES_RECEIVED_OFFSET: usize = BYTES_ACKED_OFFSET + 8;
#[cfg(target_os = "linux")]
const BYTES_END: usize = BYTES_RECEIVED_OFFSET + 8;
#[cfg(target_os = "linux")]
const REPLY_WAIT: libc::timeval = libc::timeval { tv_sec: 2, tv_usec: 0 };
#[cfg(target_os = "linux")]
const READ_BUFFER: usize = 64 * 1024;
#[cfg(target_os = "linux")]
const FAMILIES: [libc::c_int; 2] = [libc::AF_INET, libc::AF_INET6];
#[cfg(target_os = "linux")]
fn u32_at(bytes: &[u8], at: usize) -> Option<u32> {
let window: &[u8] = bytes.get(at..at + 4)?;
return Some(u32::from_ne_bytes([window[0], window[1], window[2], window[3]]));
}
#[cfg(target_os = "linux")]
fn u16_at(bytes: &[u8], at: usize) -> Option<u16> {
let window: &[u8] = bytes.get(at..at + 2)?;
return Some(u16::from_ne_bytes([window[0], window[1]]));
}
#[cfg(target_os = "linux")]
fn u64_at(bytes: &[u8], at: usize) -> Option<u64> {
let window: &[u8] = bytes.get(at..at + 8)?;
let mut array: [u8; 8] = [0; 8];
array.copy_from_slice(window);
return Some(u64::from_ne_bytes(array));
}
#[cfg(target_os = "linux")]
fn dump_request(family: libc::c_int, sequence: u32) -> Vec<u8> {
let mut request: Vec<u8> = Vec::with_capacity(NETLINK_HEADER + REQUEST_BODY);
request.extend_from_slice(&u32::try_from(NETLINK_HEADER + REQUEST_BODY).unwrap_or(0).to_ne_bytes());
request.extend_from_slice(&SOCK_DIAG_BY_FAMILY.to_ne_bytes());
request.extend_from_slice(&REQUEST_AND_DUMP.to_ne_bytes());
request.extend_from_slice(&sequence.to_ne_bytes());
request.extend_from_slice(&0u32.to_ne_bytes());
request.push(u8::try_from(family).unwrap_or(0));
request.push(u8::try_from(libc::IPPROTO_TCP).unwrap_or(0));
request.push(1 << (INET_DIAG_INFO - 1));
request.push(0);
request.extend_from_slice(&(1u32 << ESTABLISHED).to_ne_bytes());
request.extend_from_slice(&[0; 2 + 2 + 16 + 16 + 4]);
request.extend_from_slice(&INET_DIAG_NOCOOKIE.to_ne_bytes());
request.extend_from_slice(&INET_DIAG_NOCOOKIE.to_ne_bytes());
return request;
}
#[cfg(target_os = "linux")]
fn read_reply(block: &[u8], into: &mut Vec<TcpSocket>) -> Result<bool, ()> {
let mut at: usize = 0;
while at + NETLINK_HEADER <= block.len() {
let length: usize = u32_at(block, at).ok_or(())? as usize;
let kind: u16 = u16_at(block, at + 4).ok_or(())?;
if length < NETLINK_HEADER || at + length > block.len() {
return Err(());
}
if kind == NLMSG_ERROR {
return Err(());
}
if kind == NLMSG_DONE {
return Ok(true);
}
if kind == SOCK_DIAG_BY_FAMILY {
let message: &[u8] = &block[at + NETLINK_HEADER..at + length];
if let Some(socket) = read_message(message) {
into.push(socket);
}
}
at += (length + 3) & !3;
}
return Ok(false);
}
#[cfg(target_os = "linux")]
fn read_message(message: &[u8]) -> Option<TcpSocket> {
let state: u8 = *message.get(STATE_OFFSET)?;
let inode: u64 = u64::from(u32_at(message, INODE_OFFSET)?);
let mut counters: (u64, u64) = (0, 0);
let mut at: usize = MESSAGE_BODY;
while at + ATTRIBUTE_HEADER <= message.len() {
let length: usize = u16_at(message, at)? as usize;
let kind: u16 = u16_at(message, at + 2)?;
if length < ATTRIBUTE_HEADER || at + length > message.len() {
break;
}
if kind == INET_DIAG_INFO {
let info: &[u8] = &message[at + ATTRIBUTE_HEADER..at + length];
if info.len() >= BYTES_END {
counters = (u64_at(info, BYTES_ACKED_OFFSET)?, u64_at(info, BYTES_RECEIVED_OFFSET)?);
}
}
at += (length + 3) & !3;
}
return Some(TcpSocket { inode, state, bytes_acked: counters.0, bytes_received: counters.1 });
}
#[cfg(target_os = "linux")]
fn dump_family(descriptor: libc::c_int, family: libc::c_int, sequence: u32) -> Option<Vec<TcpSocket>> {
let request: Vec<u8> = dump_request(family, sequence);
let sent: isize = unsafe { libc::send(descriptor, request.as_ptr().cast(), request.len(), 0) };
if sent < 0 {
return None;
}
let mut sockets: Vec<TcpSocket> = Vec::new();
let mut buffer: Vec<u8> = vec![0; READ_BUFFER];
loop {
let received: isize = unsafe { libc::recv(descriptor, buffer.as_mut_ptr().cast(), buffer.len(), 0) };
if received <= 0 {
return None;
}
let block: &[u8] = &buffer[..usize::try_from(received).ok()?];
if read_reply(block, &mut sockets).ok()? {
return Some(sockets);
}
}
}
#[cfg(target_os = "linux")]
pub fn established_sockets() -> Option<Vec<TcpSocket>> {
let descriptor: libc::c_int = unsafe { libc::socket(libc::AF_NETLINK, libc::SOCK_RAW | libc::SOCK_CLOEXEC, NETLINK_SOCK_DIAG) };
if descriptor < 0 {
return None;
}
let patience: libc::timeval = REPLY_WAIT;
unsafe {
libc::setsockopt(descriptor, libc::SOL_SOCKET, libc::SO_RCVTIMEO, (&raw const patience).cast(), std::mem::size_of::<libc::timeval>() as libc::socklen_t);
}
let mut all: Option<Vec<TcpSocket>> = None;
for (sequence, family) in (1u32..).zip(FAMILIES) {
if let Some(found) = dump_family(descriptor, family, sequence) {
all.get_or_insert_with(Vec::new).extend(found);
}
}
unsafe {
libc::close(descriptor);
}
return all;
}
#[cfg(not(target_os = "linux"))]
pub fn established_sockets() -> Option<Vec<TcpSocket>> {
return None;
}
#[cfg(target_os = "linux")]
pub fn socket_inodes_of(pid: i32) -> Vec<u64> {
let mut inodes: Vec<u64> = Vec::new();
let Ok(descriptors) = std::fs::read_dir(format!("/proc/{pid}/fd")) else {
return inodes;
};
for entry in descriptors.flatten() {
let Ok(link) = std::fs::read_link(entry.path()) else { continue };
let text: String = link.to_string_lossy().into_owned();
if let Some(number) = text.strip_prefix("socket:[").and_then(|rest| { return rest.strip_suffix(']'); })
&& let Ok(inode) = number.parse::<u64>()
{
inodes.push(inode);
}
}
return inodes;
}
#[cfg(not(target_os = "linux"))]
pub fn socket_inodes_of(_pid: i32) -> Vec<u64> {
return Vec::new();
}
#[cfg(test)]
#[path = "tcp_sockets_tests.rs"]
mod tests;