diff --git a/CHANGELOG.md b/CHANGELOG.md index d62286c..aad2f18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All releases with the relative changes are documented in this file. +## [Unreleased] +### Added +- Added `state` field to `Listener` struct, exposing the socket's TCP connection state ([#49](https://github.com/GyulyVGC/listeners/pull/49)) +- New `SocketState` enum ([#49](https://github.com/GyulyVGC/listeners/pull/49)) + ## [0.5.1] - 2026-03-02 ### Fixed - Include build.rs in the published crate diff --git a/README.md b/README.md index 9fb7bd3..55ec3ae 100644 --- a/README.md +++ b/README.md @@ -58,19 +58,19 @@ if let Ok(listeners) = listeners::get_all() { Output: ``` text -PID: 440 Process name: ControlCenter Socket: 0.0.0.0:0 Protocol: UDP -PID: 456 Process name: rapportd Socket: [::]:49158 Protocol: TCP -PID: 456 Process name: rapportd Socket: 0.0.0.0:49158 Protocol: TCP -PID: 456 Process name: rapportd Socket: 0.0.0.0:0 Protocol: UDP -PID: 485 Process name: sharingd Socket: 0.0.0.0:0 Protocol: UDP -PID: 516 Process name: WiFiAgent Socket: 0.0.0.0:0 Protocol: UDP -PID: 1480 Process name: rustrover Socket: [::7f00:1]:63342 Protocol: TCP -PID: 2123 Process name: Telegram Socket: 192.168.1.102:49659 Protocol: TCP -PID: 2123 Process name: Telegram Socket: 192.168.1.102:49656 Protocol: TCP -PID: 2156 Process name: Google Chrome Socket: 0.0.0.0:0 Protocol: UDP -PID: 2167 Process name: Google Chrome Helper Socket: 192.168.1.102:60834 Protocol: UDP -PID: 2167 Process name: Google Chrome Helper Socket: 192.168.1.102:53220 Protocol: UDP -PID: 2167 Process name: Google Chrome Helper Socket: 192.168.1.102:59216 Protocol: UDP +PID: 440 Process name: ControlCenter Socket: 0.0.0.0:0 Protocol: UDP State: UNKNOWN +PID: 456 Process name: rapportd Socket: [::]:49158 Protocol: TCP State: LISTEN +PID: 456 Process name: rapportd Socket: 0.0.0.0:49158 Protocol: TCP State: LISTEN +PID: 456 Process name: rapportd Socket: 0.0.0.0:0 Protocol: UDP State: UNKNOWN +PID: 485 Process name: sharingd Socket: 0.0.0.0:0 Protocol: UDP State: UNKNOWN +PID: 516 Process name: WiFiAgent Socket: 0.0.0.0:0 Protocol: UDP State: UNKNOWN +PID: 1480 Process name: rustrover Socket: [::7f00:1]:63342 Protocol: TCP State: ESTABLISHED +PID: 2123 Process name: Telegram Socket: 192.168.1.102:49659 Protocol: TCP State: ESTABLISHED +PID: 2123 Process name: Telegram Socket: 192.168.1.102:49656 Protocol: TCP State: ESTABLISHED +PID: 2156 Process name: Google Chrome Socket: 0.0.0.0:0 Protocol: UDP State: UNKNOWN +PID: 2167 Process name: Google Chrome Helper Socket: 192.168.1.102:60834 Protocol: UDP State: UNKNOWN +PID: 2167 Process name: Google Chrome Helper Socket: 192.168.1.102:53220 Protocol: UDP State: UNKNOWN +PID: 2167 Process name: Google Chrome Helper Socket: 192.168.1.102:59216 Protocol: UDP State: UNKNOWN ``` For more examples of usage, including how to get listening processes in a more granular way, diff --git a/examples/print_paths.rs b/examples/print_paths.rs index 4771b31..f90ba92 100644 --- a/examples/print_paths.rs +++ b/examples/print_paths.rs @@ -2,10 +2,7 @@ fn main() { // Retrieve all listeners and print their process paths if let Ok(listeners) = listeners::get_all() { for l in listeners { - println!( - "PID: {:<10} Process path: {}", - l.process.pid, l.process.path - ); + println!("PID: {:<7} Process path: {}", l.process.pid, l.process.path); } } } diff --git a/src/lib.rs b/src/lib.rs index b622979..6e59cb7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,8 @@ pub struct Listener { pub socket: SocketAddr, /// The protocol used. pub protocol: Protocol, + /// The state of the socket connection. + pub state: SocketState, } /// An active process. @@ -51,6 +53,96 @@ pub enum Protocol { UDP, } +/// The state of a socket connection. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SocketState { + /// Connection is open and exchanging data. + Established, + /// Initiating a connection. + SynSent, + /// Received a connection request. + SynReceived, + /// Sent a FIN, waiting for its ACK or the peer's FIN. + FinWait1, + /// FIN acknowledged, waiting for the peer's FIN. + FinWait2, + /// Waiting for remaining packets after close. + TimeWait, + /// Socket is not connected. + Closed, + /// Received a FIN, waiting to send FIN. + CloseWait, + /// Sent FIN, waiting for ACK. + LastAck, + /// Listening for incoming connections. + Listen, + /// Both sides sent FIN simultaneously. + Closing, + /// State is unknown or not applicable (e.g. UDP). + Unknown, +} + +impl SocketState { + #[cfg(target_os = "linux")] + pub(crate) fn from_linux(state_hex: &str) -> Self { + match u8::from_str_radix(state_hex, 16) { + Ok(0x01) => Self::Established, + Ok(0x02) => Self::SynSent, + Ok(0x03) => Self::SynReceived, + Ok(0x04) => Self::FinWait1, + Ok(0x05) => Self::FinWait2, + Ok(0x06) => Self::TimeWait, + Ok(0x07) => Self::Closed, + Ok(0x08) => Self::CloseWait, + Ok(0x09) => Self::LastAck, + Ok(0x0A) => Self::Listen, + Ok(0x0B) => Self::Closing, + _ => Self::Unknown, + } + } + + #[cfg(target_os = "windows")] + pub(crate) fn from_windows(raw: u32) -> Self { + match raw { + 1 => Self::Closed, + 2 => Self::Listen, + 3 => Self::SynSent, + 4 => Self::SynReceived, + 5 => Self::Established, + 6 => Self::FinWait1, + 7 => Self::FinWait2, + 8 => Self::CloseWait, + 9 => Self::Closing, + 10 => Self::LastAck, + 11 => Self::TimeWait, + _ => Self::Unknown, + } + } + + #[cfg(any( + target_os = "macos", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub(crate) fn from_bsd(raw: i32) -> Self { + match raw { + 0 => Self::Closed, + 1 => Self::Listen, + 2 => Self::SynSent, + 3 => Self::SynReceived, + 4 => Self::Established, + 5 => Self::CloseWait, + 6 => Self::FinWait1, + 7 => Self::Closing, + 8 => Self::LastAck, + 9 => Self::FinWait2, + 10 => Self::TimeWait, + _ => Self::Unknown, + } + } +} + /// Returns all the [Listener]s. /// /// # Errors @@ -65,19 +157,19 @@ pub enum Protocol { /// /// Output: /// ``` text -/// PID: 440 Process name: ControlCenter Socket: 0.0.0.0:0 Protocol: UDP -/// PID: 456 Process name: rapportd Socket: [::]:49158 Protocol: TCP -/// PID: 456 Process name: rapportd Socket: 0.0.0.0:49158 Protocol: TCP -/// PID: 456 Process name: rapportd Socket: 0.0.0.0:0 Protocol: UDP -/// PID: 485 Process name: sharingd Socket: 0.0.0.0:0 Protocol: UDP -/// PID: 516 Process name: WiFiAgent Socket: 0.0.0.0:0 Protocol: UDP -/// PID: 1480 Process name: rustrover Socket: [::7f00:1]:63342 Protocol: TCP -/// PID: 2123 Process name: Telegram Socket: 192.168.1.102:49659 Protocol: TCP -/// PID: 2123 Process name: Telegram Socket: 192.168.1.102:49656 Protocol: TCP -/// PID: 2156 Process name: Google Chrome Socket: 0.0.0.0:0 Protocol: UDP -/// PID: 2167 Process name: Google Chrome Helper Socket: 192.168.1.102:60834 Protocol: UDP -/// PID: 2167 Process name: Google Chrome Helper Socket: 192.168.1.102:53220 Protocol: UDP -/// PID: 2167 Process name: Google Chrome Helper Socket: 192.168.1.102:59216 Protocol: UDP +/// PID: 440 Process name: ControlCenter Socket: 0.0.0.0:0 Protocol: UDP State: UNKNOWN +/// PID: 456 Process name: rapportd Socket: [::]:49158 Protocol: TCP State: LISTEN +/// PID: 456 Process name: rapportd Socket: 0.0.0.0:49158 Protocol: TCP State: LISTEN +/// PID: 456 Process name: rapportd Socket: 0.0.0.0:0 Protocol: UDP State: UNKNOWN +/// PID: 485 Process name: sharingd Socket: 0.0.0.0:0 Protocol: UDP State: UNKNOWN +/// PID: 516 Process name: WiFiAgent Socket: 0.0.0.0:0 Protocol: UDP State: UNKNOWN +/// PID: 1480 Process name: rustrover Socket: [::7f00:1]:63342 Protocol: TCP State: ESTABLISHED +/// PID: 2123 Process name: Telegram Socket: 192.168.1.102:49659 Protocol: TCP State: ESTABLISHED +/// PID: 2123 Process name: Telegram Socket: 192.168.1.102:49656 Protocol: TCP State: ESTABLISHED +/// PID: 2156 Process name: Google Chrome Socket: 0.0.0.0:0 Protocol: UDP State: UNKNOWN +/// PID: 2167 Process name: Google Chrome Helper Socket: 192.168.1.102:60834 Protocol: UDP State: UNKNOWN +/// PID: 2167 Process name: Google Chrome Helper Socket: 192.168.1.102:53220 Protocol: UDP State: UNKNOWN +/// PID: 2167 Process name: Google Chrome Helper Socket: 192.168.1.102:59216 Protocol: UDP State: UNKNOWN /// ``` pub fn get_all() -> Result> { platform::get_all() @@ -102,7 +194,7 @@ pub fn get_all() -> Result> { /// /// Output: /// ``` text -/// PID: 2123 Process name: Telegram +/// PID: 2123 Process name: Telegram /// ``` pub fn get_process_by_port(port: u16, protocol: Protocol) -> Result { if port == 0 { @@ -113,12 +205,20 @@ pub fn get_process_by_port(port: u16, protocol: Protocol) -> Result { } impl Listener { - fn new(pid: u32, name: String, path: String, socket: SocketAddr, protocol: Protocol) -> Self { + fn new( + pid: u32, + name: String, + path: String, + socket: SocketAddr, + protocol: Protocol, + state: SocketState, + ) -> Self { let process = Process::new(pid, name, path); Self { process, socket, protocol, + state, } } } @@ -135,16 +235,40 @@ impl Display for Listener { process, socket, protocol, + state, } = self; let process = process.to_string(); - write!(f, "{process:<55} Socket: {socket:<30} Protocol: {protocol}",) + let protocol = protocol.to_string(); + write!( + f, + "{process:<52} Socket: {socket:<30} Protocol: {protocol:<7} State: {state}" + ) + } +} + +impl Display for SocketState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SocketState::Established => write!(f, "ESTABLISHED"), + SocketState::SynSent => write!(f, "SYN_SENT"), + SocketState::SynReceived => write!(f, "SYN_RECEIVED"), + SocketState::FinWait1 => write!(f, "FIN_WAIT_1"), + SocketState::FinWait2 => write!(f, "FIN_WAIT_2"), + SocketState::TimeWait => write!(f, "TIME_WAIT"), + SocketState::Closed => write!(f, "CLOSED"), + SocketState::CloseWait => write!(f, "CLOSE_WAIT"), + SocketState::LastAck => write!(f, "LAST_ACK"), + SocketState::Listen => write!(f, "LISTEN"), + SocketState::Closing => write!(f, "CLOSING"), + SocketState::Unknown => write!(f, "UNKNOWN"), + } } } impl Display for Process { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Process { pid, name, .. } = self; - write!(f, "PID: {pid:<10} Process name: {name}") + write!(f, "PID: {pid:<7} Process name: {name}") } } @@ -161,7 +285,7 @@ impl Display for Protocol { mod tests { use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - use crate::{Listener, Process, Protocol}; + use crate::{Listener, Process, Protocol, SocketState}; #[test] fn test_v4_listener_to_string() { @@ -171,10 +295,11 @@ mod tests { "path/to/rapportd".to_string(), SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 51189), Protocol::TCP, + SocketState::Listen, ); assert_eq!( listener.to_string(), - "PID: 455 Process name: rapportd Socket: 0.0.0.0:51189 Protocol: TCP" + "PID: 455 Process name: rapportd Socket: 0.0.0.0:51189 Protocol: TCP State: LISTEN" ); } @@ -186,10 +311,11 @@ mod tests { "path/to/mysqld".to_string(), SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 3306), Protocol::UDP, + SocketState::Unknown, ); assert_eq!( listener.to_string(), - "PID: 160 Process name: mysqld Socket: [::]:3306 Protocol: UDP" + "PID: 160 Process name: mysqld Socket: [::]:3306 Protocol: UDP State: UNKNOWN" ); } @@ -202,7 +328,70 @@ mod tests { ); assert_eq!( process.to_string(), - "PID: 611 Process name: Microsoft SharePoint" + "PID: 611 Process name: Microsoft SharePoint" ); } + + #[cfg(target_os = "linux")] + #[test] + fn test_socket_state_from_linux() { + assert_eq!(SocketState::from_linux("01"), SocketState::Established); + assert_eq!(SocketState::from_linux("02"), SocketState::SynSent); + assert_eq!(SocketState::from_linux("03"), SocketState::SynReceived); + assert_eq!(SocketState::from_linux("04"), SocketState::FinWait1); + assert_eq!(SocketState::from_linux("05"), SocketState::FinWait2); + assert_eq!(SocketState::from_linux("06"), SocketState::TimeWait); + assert_eq!(SocketState::from_linux("07"), SocketState::Closed); + assert_eq!(SocketState::from_linux("08"), SocketState::CloseWait); + assert_eq!(SocketState::from_linux("09"), SocketState::LastAck); + assert_eq!(SocketState::from_linux("0A"), SocketState::Listen); + assert_eq!(SocketState::from_linux("0B"), SocketState::Closing); + // unmapped code, non-hex, and out-of-range values all fall back to Unknown + assert_eq!(SocketState::from_linux("0C"), SocketState::Unknown); + assert_eq!(SocketState::from_linux("zz"), SocketState::Unknown); + assert_eq!(SocketState::from_linux("100"), SocketState::Unknown); + } + + #[cfg(target_os = "windows")] + #[test] + fn test_socket_state_from_windows() { + assert_eq!(SocketState::from_windows(1), SocketState::Closed); + assert_eq!(SocketState::from_windows(2), SocketState::Listen); + assert_eq!(SocketState::from_windows(3), SocketState::SynSent); + assert_eq!(SocketState::from_windows(4), SocketState::SynReceived); + assert_eq!(SocketState::from_windows(5), SocketState::Established); + assert_eq!(SocketState::from_windows(6), SocketState::FinWait1); + assert_eq!(SocketState::from_windows(7), SocketState::FinWait2); + assert_eq!(SocketState::from_windows(8), SocketState::CloseWait); + assert_eq!(SocketState::from_windows(9), SocketState::Closing); + assert_eq!(SocketState::from_windows(10), SocketState::LastAck); + assert_eq!(SocketState::from_windows(11), SocketState::TimeWait); + // 0 and MIB_TCP_STATE_DELETE_TCB (12) and beyond fall back to Unknown + assert_eq!(SocketState::from_windows(0), SocketState::Unknown); + assert_eq!(SocketState::from_windows(12), SocketState::Unknown); + } + + #[cfg(any( + target_os = "macos", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + #[test] + fn test_socket_state_from_bsd() { + assert_eq!(SocketState::from_bsd(0), SocketState::Closed); + assert_eq!(SocketState::from_bsd(1), SocketState::Listen); + assert_eq!(SocketState::from_bsd(2), SocketState::SynSent); + assert_eq!(SocketState::from_bsd(3), SocketState::SynReceived); + assert_eq!(SocketState::from_bsd(4), SocketState::Established); + assert_eq!(SocketState::from_bsd(5), SocketState::CloseWait); + assert_eq!(SocketState::from_bsd(6), SocketState::FinWait1); + assert_eq!(SocketState::from_bsd(7), SocketState::Closing); + assert_eq!(SocketState::from_bsd(8), SocketState::LastAck); + assert_eq!(SocketState::from_bsd(9), SocketState::FinWait2); + assert_eq!(SocketState::from_bsd(10), SocketState::TimeWait); + // the -1 UDP sentinel and any out-of-range value fall back to Unknown + assert_eq!(SocketState::from_bsd(-1), SocketState::Unknown); + assert_eq!(SocketState::from_bsd(11), SocketState::Unknown); + } } diff --git a/src/platform/bsd/ffi/mod.rs b/src/platform/bsd/ffi/mod.rs index 351338f..d32fc9f 100644 --- a/src/platform/bsd/ffi/mod.rs +++ b/src/platform/bsd/ffi/mod.rs @@ -1,6 +1,6 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; -use crate::Protocol; +use crate::{Protocol, SocketState}; #[cfg(target_os = "freebsd")] type KvAddr = usize; @@ -18,6 +18,7 @@ pub(super) mod openbsd; pub(super) struct SocketInfo { pub(super) address: SocketAddr, pub(super) protocol: Protocol, + pub(super) state: SocketState, #[cfg(any(target_os = "freebsd", target_os = "netbsd"))] pub(super) kvaddr: KvAddr, } @@ -41,6 +42,7 @@ pub(super) struct CSocketInfo { kvaddr: KvAddr, protocol: i32, port: u16, + state: i32, } impl From<&CSocketInfo> for SocketInfo { @@ -62,6 +64,7 @@ impl From<&CSocketInfo> for SocketInfo { libc::IPPROTO_TCP => Protocol::TCP, _ => Protocol::UDP, }, + state: SocketState::from_bsd(value.state), } } } diff --git a/src/platform/bsd/freebsd.rs b/src/platform/bsd/freebsd.rs index 2e335db..eb609e2 100644 --- a/src/platform/bsd/freebsd.rs +++ b/src/platform/bsd/freebsd.rs @@ -24,6 +24,7 @@ pub(crate) fn get_all() -> crate::Result> { path, socket.address, socket.protocol, + socket.state, )); } } diff --git a/src/platform/bsd/native/common.h b/src/platform/bsd/native/common.h index 58d23b1..d00e3e2 100644 --- a/src/platform/bsd/native/common.h +++ b/src/platform/bsd/native/common.h @@ -53,6 +53,7 @@ extern "C" #endif int32_t protocol; uint16_t port; + int32_t state; }; #if defined(__FreeBSD__) || defined(__NetBSD__) diff --git a/src/platform/bsd/native/freebsd.c b/src/platform/bsd/native/freebsd.c index b281298..56f2c86 100644 --- a/src/platform/bsd/native/freebsd.c +++ b/src/platform/bsd/native/freebsd.c @@ -39,6 +39,7 @@ static void fillsock_tcp(struct socket_info_t *sock, struct xtcpcb *xtp) sock->port = ntohs(xtp->xt_inp.inp_lport); sock->protocol = IPPROTO_TCP; sock->kvaddr = xtp->xt_inp.xi_socket.xso_so; + sock->state = xtp->t_state; if (xtp->xt_inp.inp_vflag & INP_IPV6) { @@ -66,6 +67,7 @@ static void fillsock_udp(struct socket_info_t *sock, struct xinpcb *xip) sock->port = ntohs(xip->inp_lport); sock->protocol = IPPROTO_UDP; sock->kvaddr = xip->xi_socket.xso_so; + sock->state = -1; if (xip->inp_vflag & INP_IPV6) { diff --git a/src/platform/bsd/native/netbsd.c b/src/platform/bsd/native/netbsd.c index 49c0b80..68030a1 100644 --- a/src/platform/bsd/native/netbsd.c +++ b/src/platform/bsd/native/netbsd.c @@ -111,6 +111,7 @@ static int fetch_sockets_common(struct socket_info_t **list, size_t *nentries, i (*list)[idx].protocol = sockets[i].ki_protocol; (*list)[idx].address.family = sockets[i].ki_family; (*list)[idx].kvaddr = sockets[i].ki_sockaddr; + (*list)[idx].state = (sockets[i].ki_protocol == IPPROTO_TCP) ? (int32_t)sockets[i].ki_tstate : -1; if (sockets[i].ki_family == AF_INET) { diff --git a/src/platform/bsd/native/openbsd.c b/src/platform/bsd/native/openbsd.c index 4a32418..9c4a55c 100644 --- a/src/platform/bsd/native/openbsd.c +++ b/src/platform/bsd/native/openbsd.c @@ -101,6 +101,7 @@ int openbsd_fetch_sockets_by_pid(pid_t pid, struct socket_info_t **list, size_t (*list)[idx].port = ntohs(files[i].inp_lport); (*list)[idx].address.family = files[i].so_family; memcpy(&((*list)[idx].address.addr), files[i].inp_laddru, sizeof(files[i].inp_laddru)); + (*list)[idx].state = (files[i].so_protocol == IPPROTO_TCP) ? (int32_t)files[i].t_state : -1; ++idx; } diff --git a/src/platform/bsd/netbsd.rs b/src/platform/bsd/netbsd.rs index dd7e4bf..f61e5ad 100644 --- a/src/platform/bsd/netbsd.rs +++ b/src/platform/bsd/netbsd.rs @@ -27,6 +27,7 @@ pub(crate) fn get_all() -> crate::Result> { path, socket.address, socket.protocol, + socket.state, )); } } diff --git a/src/platform/bsd/openbsd.rs b/src/platform/bsd/openbsd.rs index 5b33446..dc2e0db 100644 --- a/src/platform/bsd/openbsd.rs +++ b/src/platform/bsd/openbsd.rs @@ -18,6 +18,7 @@ pub(crate) fn get_all() -> crate::Result> { String::new(), socket.address, socket.protocol, + socket.state, )); } } diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 5897582..567500e 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -24,6 +24,7 @@ pub(crate) fn get_all() -> crate::Result> { p.path(), proto_listener.local_addr(), proto_listener.protocol(), + proto_listener.state(), ); listeners.insert(listener); } diff --git a/src/platform/linux/proto_listener.rs b/src/platform/linux/proto_listener.rs index 97badfd..655282a 100644 --- a/src/platform/linux/proto_listener.rs +++ b/src/platform/linux/proto_listener.rs @@ -1,4 +1,4 @@ -use crate::Protocol; +use crate::{Protocol, SocketState}; use std::fs::File; use std::io::{BufRead, BufReader}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; @@ -9,6 +9,7 @@ pub(super) struct ProtoListener { local_addr: SocketAddr, inode: u64, protocol: Protocol, + state: SocketState, } impl ProtoListener { @@ -24,6 +25,10 @@ impl ProtoListener { self.protocol } + pub(super) fn state(&self) -> SocketState { + self.state + } + pub(super) fn get_all() -> crate::Result> { let mut table = Vec::new(); @@ -117,8 +122,11 @@ impl ProtoListener { let mut s = line.split_whitespace(); let local_addr_hex = s.nth(1).ok_or("Failed to get local address")?; - // consider all states - let _ = s.nth(1).ok_or("Failed to get state")?; + let state_hex = s.nth(1).ok_or("Failed to get state")?; + let state = match protocol { + Protocol::TCP => SocketState::from_linux(state_hex), + Protocol::UDP => SocketState::Unknown, + }; let local_ip_port = local_addr_hex .split(':') @@ -138,6 +146,7 @@ impl ProtoListener { local_addr, inode, protocol, + state, }) } @@ -150,8 +159,11 @@ impl ProtoListener { let mut s = line.split_whitespace(); let local_addr_hex = s.nth(1).ok_or("Failed to get local address")?; - // consider all states - let _ = s.nth(1).ok_or("Failed to get state")?; + let state_hex = s.nth(1).ok_or("Failed to get state")?; + let state = match protocol { + Protocol::TCP => SocketState::from_linux(state_hex), + Protocol::UDP => SocketState::Unknown, + }; let mut local_ip_port = local_addr_hex.split(':'); @@ -190,6 +202,7 @@ impl ProtoListener { local_addr, inode, protocol, + state, }) } } diff --git a/src/platform/macos/c_socket_fd_info.rs b/src/platform/macos/c_socket_fd_info.rs index bb7276f..2eb95ce 100644 --- a/src/platform/macos/c_socket_fd_info.rs +++ b/src/platform/macos/c_socket_fd_info.rs @@ -3,8 +3,8 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use byteorder::{ByteOrder, NetworkEndian}; -use crate::Protocol; use crate::platform::macos::proto_listener::ProtoListener; +use crate::{Protocol, SocketState}; use super::statics::{IPPROTO_TCP, IPPROTO_UDP}; @@ -20,10 +20,16 @@ impl CSocketFdInfo { let family = sock_info.soi_family; let transport_protocol = sock_info.soi_protocol; - let general_sock_info = unsafe { + let (general_sock_info, state) = unsafe { match transport_protocol { - IPPROTO_TCP => sock_info.soi_proto.pri_tcp.tcpsi_ini, - IPPROTO_UDP => sock_info.soi_proto.pri_in, + IPPROTO_TCP => { + let tcp_info = sock_info.soi_proto.pri_tcp; + ( + tcp_info.tcpsi_ini, + SocketState::from_bsd(tcp_info.tcpsi_state), + ) + } + IPPROTO_UDP => (sock_info.soi_proto.pri_in, SocketState::Unknown), _ => return Err("Unsupported protocol".into()), } }; @@ -36,6 +42,7 @@ impl CSocketFdInfo { local_address, NetworkEndian::read_u16(&lport_bytes), protocol, + state, ); Ok(socket_info) diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index 8c43143..b6625ac 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -41,6 +41,7 @@ pub(crate) fn get_all() -> crate::Result> { path, proto_listener.socket_addr(), proto_listener.protocol(), + proto_listener.state(), ); listeners.insert(listener); } diff --git a/src/platform/macos/proto_listener.rs b/src/platform/macos/proto_listener.rs index 48f89e3..a9de520 100644 --- a/src/platform/macos/proto_listener.rs +++ b/src/platform/macos/proto_listener.rs @@ -9,19 +9,21 @@ use crate::platform::macos::proc_pid::ProcPid; use crate::platform::macos::socket_fd::SocketFd; use crate::platform::macos::statics::PROC_PID_FD_SOCKET_INFO; -use crate::Protocol; +use crate::{Protocol, SocketState}; #[derive(Debug)] pub(super) struct ProtoListener { local_addr: SocketAddr, protocol: Protocol, + state: SocketState, } impl ProtoListener { - pub(super) fn new(addr: IpAddr, port: u16, protocol: Protocol) -> Self { + pub(super) fn new(addr: IpAddr, port: u16, protocol: Protocol, state: SocketState) -> Self { ProtoListener { local_addr: SocketAddr::new(addr, port), protocol, + state, } } @@ -33,6 +35,10 @@ impl ProtoListener { self.protocol } + pub(super) fn state(&self) -> SocketState { + self.state + } + pub(super) fn from_pid_fd(pid: ProcPid, fd: &SocketFd) -> crate::Result { let mut sinfo: MaybeUninit = MaybeUninit::uninit(); diff --git a/src/platform/windows/proto_listener.rs b/src/platform/windows/proto_listener.rs index 739609a..97f1c9e 100644 --- a/src/platform/windows/proto_listener.rs +++ b/src/platform/windows/proto_listener.rs @@ -1,5 +1,6 @@ use crate::Listener; use crate::Protocol; +use crate::SocketState; use crate::platform::windows::socket_table::SocketTable; use crate::platform::windows::tcp_table::TcpTable; use crate::platform::windows::tcp6_table::Tcp6Table; @@ -29,6 +30,7 @@ pub(super) struct ProtoListener { local_port: u16, pub(super) pid: u32, protocol: Protocol, + state: SocketState, } impl ProtoListener { @@ -74,12 +76,19 @@ impl ProtoListener { Err("No listener found on port".into()) } - pub(super) fn new(local_addr: IpAddr, local_port: u16, pid: u32, protocol: Protocol) -> Self { + pub(super) fn new( + local_addr: IpAddr, + local_port: u16, + pid: u32, + protocol: Protocol, + state: SocketState, + ) -> Self { Self { local_addr, local_port, pid, protocol, + state, } } } @@ -128,7 +137,14 @@ impl PidNamePathCache { .flatten() .map(|(pname, ppath)| { let socket = SocketAddr::new(proto_listener.local_addr, proto_listener.local_port); - Listener::new(pid, pname, ppath, socket, proto_listener.protocol) + Listener::new( + pid, + pname, + ppath, + socket, + proto_listener.protocol, + proto_listener.state, + ) }) } } diff --git a/src/platform/windows/socket_table.rs b/src/platform/windows/socket_table.rs index 5fd8a0f..775413d 100644 --- a/src/platform/windows/socket_table.rs +++ b/src/platform/windows/socket_table.rs @@ -3,6 +3,7 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use super::statics::UDP_TABLE_OWNER_PID; use crate::Protocol; +use crate::SocketState; use crate::platform::target_os::proto_listener::ProtoListener; use crate::platform::windows::statics::{ AF_INET, AF_INET6, ERROR_INSUFFICIENT_BUFFER, NO_ERROR, TCP_TABLE_OWNER_PID_ALL, @@ -56,6 +57,7 @@ impl SocketTable for TcpTable { port, row.owning_pid, Protocol::TCP, + SocketState::from_windows(row.state), )) } } @@ -93,6 +95,7 @@ impl SocketTable for Tcp6Table { port, row.owning_pid, Protocol::TCP, + SocketState::from_windows(row.state), )) } } @@ -130,6 +133,7 @@ impl SocketTable for UdpTable { port, row.owning_pid, Protocol::UDP, + SocketState::Unknown, )) } } @@ -167,6 +171,7 @@ impl SocketTable for Udp6Table { port, row.owning_pid, Protocol::UDP, + SocketState::Unknown, )) } } diff --git a/tests/integration.rs b/tests/integration.rs index ea73ed9..088407c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,5 +1,5 @@ use http_test_server::TestServer; -use listeners::{Listener, Process, Protocol, get_process_by_port}; +use listeners::{Listener, Process, Protocol, SocketState, get_process_by_port}; use rand::prelude::IteratorRandom; use serial_test::serial; use std::collections::HashSet; @@ -98,7 +98,8 @@ fn test_http_server() { path: http_server_path }, socket: SocketAddr::from_str(&format!("127.0.0.1:{http_server_port}")).unwrap(), - protocol: Protocol::TCP + protocol: Protocol::TCP, + state: SocketState::Listen, } ); } @@ -255,3 +256,116 @@ fn test_udp6() { assert!(all_found); } + +#[test] +#[serial] +fn test_tcp_listen_state() { + let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + let socket = TcpListener::bind(SocketAddr::new(ip, 0)).unwrap(); + let port = socket.local_addr().unwrap().port(); + + let all = listeners::get_all().unwrap(); + let listener = all + .iter() + .find(|l| l.socket.port() == port && l.protocol == Protocol::TCP) + .unwrap(); + assert_eq!(listener.state, SocketState::Listen); +} + +#[test] +#[serial] +fn test_tcp_established_state() { + let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + let server = TcpListener::bind(SocketAddr::new(ip, 0)).unwrap(); + let port = server.local_addr().unwrap().port(); + + let _client = std::net::TcpStream::connect(server.local_addr().unwrap()).unwrap(); + let (_accepted, _) = server.accept().unwrap(); + + let all = listeners::get_all().unwrap(); + let has_established = all + .iter() + .any(|l| l.socket.port() == port && l.state == SocketState::Established); + assert!( + has_established, + "Expected an established TCP connection on port {port}" + ); +} + +#[test] +#[serial] +fn test_tcp_listen_state_ipv6() { + let ip = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)); + let socket = TcpListener::bind(SocketAddr::new(ip, 0)).unwrap(); + let port = socket.local_addr().unwrap().port(); + + let all = listeners::get_all().unwrap(); + let listener = all + .iter() + .find(|l| l.socket.port() == port && l.protocol == Protocol::TCP) + .unwrap(); + assert_eq!(listener.state, SocketState::Listen); +} + +#[test] +#[serial] +fn test_tcp_established_state_ipv6() { + let ip = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)); + let server = TcpListener::bind(SocketAddr::new(ip, 0)).unwrap(); + let port = server.local_addr().unwrap().port(); + + let _client = std::net::TcpStream::connect(server.local_addr().unwrap()).unwrap(); + let (_accepted, _) = server.accept().unwrap(); + + let all = listeners::get_all().unwrap(); + let has_established = all + .iter() + .any(|l| l.socket.port() == port && l.state == SocketState::Established); + assert!( + has_established, + "Expected an established IPv6 TCP connection on port {port}" + ); +} + +#[test] +#[serial] +fn test_tcp_close_wait_state() { + use std::io::Read; + + let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + let server = TcpListener::bind(SocketAddr::new(ip, 0)).unwrap(); + let port = server.local_addr().unwrap().port(); + + let client = std::net::TcpStream::connect(server.local_addr().unwrap()).unwrap(); + let (mut accepted, _) = server.accept().unwrap(); + + // Drop client to send FIN; read until EOF to confirm the FIN has been received + // before sampling state, ensuring the kernel has moved accepted into CloseWait. + drop(client); + let mut buf = [0u8; 1]; + let _ = accepted.read(&mut buf); + + let all = listeners::get_all().unwrap(); + let has_close_wait = all + .iter() + .any(|l| l.socket.port() == port && l.state == SocketState::CloseWait); + assert!( + has_close_wait, + "Expected a CloseWait TCP connection on port {port}" + ); +} + +#[test] +#[serial] +fn test_udp_state_is_unknown() { + let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + let socket = UdpSocket::bind(SocketAddr::new(ip, 0)).unwrap(); + let port = socket.local_addr().unwrap().port(); + + let all = listeners::get_all().unwrap(); + let listener = all + .iter() + .find(|l| l.socket.port() == port && l.protocol == Protocol::UDP) + .unwrap(); + assert_eq!(listener.state, SocketState::Unknown); +}