Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 1 addition & 4 deletions examples/print_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
231 changes: 210 additions & 21 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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<HashSet<Listener>> {
platform::get_all()
Expand All @@ -102,7 +194,7 @@ pub fn get_all() -> Result<HashSet<Listener>> {
///
/// Output:
/// ``` text
/// PID: 2123 Process name: Telegram
/// PID: 2123 Process name: Telegram
/// ```
pub fn get_process_by_port(port: u16, protocol: Protocol) -> Result<Process> {
if port == 0 {
Expand All @@ -113,12 +205,20 @@ pub fn get_process_by_port(port: u16, protocol: Protocol) -> Result<Process> {
}

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

Expand All @@ -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() {
Expand All @@ -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"
);
}

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

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