├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── get_all.rs ├── get_ports_by_pid.rs ├── get_ports_by_process_name.rs └── get_processes_by_port.rs ├── src ├── lib.rs └── platform │ ├── bsd │ └── mod.rs │ ├── linux │ ├── helpers.rs │ ├── mod.rs │ ├── proc_fd.rs │ ├── proc_info.rs │ ├── statics.rs │ └── tcp_listener.rs │ ├── macos │ ├── c_libproc.rs │ ├── c_proc_fd_info.rs │ ├── c_socket_fd_info.rs │ ├── mod.rs │ ├── proc_name.rs │ ├── proc_pid.rs │ ├── socket_fd.rs │ ├── statics.rs │ └── tcp_listener.rs │ ├── mod.rs │ └── windows │ ├── c_iphlpapi.rs │ ├── mod.rs │ ├── socket_table.rs │ ├── statics.rs │ ├── tcp6_table.rs │ ├── tcp_listener.rs │ └── tcp_table.rs └── tests └── integration.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | # Maintain dependencies for GitHub Actions 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | 15 | # Maintain dependencies for cargo 16 | - package-ecosystem: cargo 17 | directory: "/" 18 | schedule: 19 | interval: "daily" 20 | target-branch: "main" 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | # Linters inspired from here: https://github.com/actions-rs/meta/blob/master/recipes/quickstart.md 15 | jobs: 16 | 17 | rust: 18 | name: ${{ matrix.os }}-latest 19 | runs-on: ${{ matrix.os }}-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | include: 24 | - os: ubuntu 25 | - os: macos 26 | - os: windows 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: dtolnay/rust-toolchain@stable 31 | with: 32 | components: rustfmt, clippy 33 | 34 | - name: fmt 35 | run: cargo fmt --all -- --check 36 | - name: build 37 | run: cargo build --verbose 38 | - name: clippy 39 | run: cargo clippy -- -D warnings 40 | - name: test 41 | run: cargo test --verbose -- --nocapture 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | .DS_Store 4 | Cargo.lock -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All releases with the relative changes are documented in this file. 4 | 5 | ## [0.2.1] - 2024-07-12 6 | ### Fixed 7 | - Linux permission denied issue ([#10](https://github.com/GyulyVGC/listeners/pull/10) — fixes [#9](https://github.com/GyulyVGC/listeners/issues/9)) 8 | 9 | ## [0.2.0] - 2024-03-27 10 | ### Added 11 | - New APIs to get the listening processes in a more granular way 12 | - `get_ports_by_pid` 13 | - `get_ports_by_process_name` 14 | - `get_processes_by_port` 15 | - New `Process` struct to represent a process identified by its PID and name 16 | ### Changed 17 | - `Listener` struct now has a `process` field of type `Process`, which takes place of the old fields `pid` and `name` 18 | 19 | ## [0.1.0] - 2024-03-14 20 | ### Added 21 | - Support for Windows, Linux and macOS 22 | - `get_all` API to get all the listening processes 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "listeners" 3 | version = "0.2.1" 4 | edition = "2021" 5 | authors = ["Giuliano Bellini "] 6 | description = "Get processes listening on a TCP port in a cross-platform way" 7 | readme = "README.md" 8 | repository = "https://github.com/GyulyVGC/listeners" 9 | license = "MIT" 10 | keywords = ["tcp", "listen", "port", "process"] 11 | categories = ["network-programming"] 12 | include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md", "examples/**/*"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [target.'cfg(target_os = "windows")'.dependencies] 17 | windows = { version = "0.61.1", features = ["Win32_Foundation", "Win32_System_Diagnostics_ToolHelp"] } 18 | 19 | [target.'cfg(target_os = "macos")'.dependencies] 20 | byteorder = "1.5.0" 21 | 22 | [target.'cfg(target_os = "linux")'.dependencies] 23 | once_cell = "1.20.2" 24 | rustix = {version = "1.0.7", features = ["fs"]} 25 | 26 | #[target.'cfg(all(not(target_os = "linux"), not(target_os = "macos"), not(target_os = "windows")))'.dependencies] 27 | #bsd-kvm = "0.1.5" 28 | #sysctl = "0.5.5" 29 | 30 | [dev-dependencies] 31 | http-test-server = "2.1.1" 32 | serial_test = "3.2.0" 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Giuliano Bellini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Listene*rs* 2 | 3 | [![Crates](https://img.shields.io/crates/v/listeners?&logo=rust)](https://crates.io/crates/listeners) 4 | [![Downloads](https://img.shields.io/crates/d/listeners.svg)](https://crates.io/crates/listeners) 5 | [![Docs](https://docs.rs/listeners/badge.svg)](https://docs.rs/listeners/latest/) 6 | [![CI](https://github.com/gyulyvgc/listeners/workflows/CI/badge.svg)](https://github.com/GyulyVGC/listeners/actions/) 7 | 8 | **Rust library to get processes listening on a TCP port in a cross-platform way.** 9 | 10 | ## Motivation 11 | 12 | Despite some Rust libraries to get process information already exist, 13 | none of them satisfies the need to get process ID and name of TCP listeners in a cross-platform way. 14 | 15 | Some examples of existing libraries: 16 | - [netstat2](https://crates.io/crates/netstat2): doesn't provide the process name (and it's unmaintained) 17 | - [libproc](https://crates.io/crates/libproc): only for Linux and macOS 18 | - [sysinfo](https://crates.io/crates/sysinfo): doesn't expose the sockets used by each process 19 | 20 | This library wants to fill this gap, and it aims to be: 21 | - **Cross-platform**: it currently supports Windows, Linux and macOS 22 | - **Performant**: it internally uses low-level system APIs 23 | - **Simple**: it exposes intuitive APIs to get details about the listening processes 24 | - **Lightweight**: it has only the strictly necessary dependencies 25 | 26 | ## Roadmap 27 | 28 | - [x] Windows 29 | - [x] Linux 30 | - [x] macOS 31 | - [ ] BSD 32 | - [ ] iOS 33 | - [ ] Android 34 | 35 | ## Usage 36 | 37 | Add this to your `Cargo.toml`: 38 | 39 | ``` toml 40 | [dependencies] 41 | 42 | listeners = "0.2" 43 | ``` 44 | 45 | Get all the listening processes: 46 | 47 | ``` rust 48 | if let Ok(listeners) = listeners::get_all() { 49 | for l in listeners { 50 | println!("{l}"); 51 | } 52 | } 53 | ``` 54 | 55 | Output: 56 | 57 | ``` text 58 | PID: 1088 Process name: rustrover Socket: [::7f00:1]:63342 59 | PID: 609 Process name: Microsoft SharePoint Socket: [::1]:42050 60 | PID: 160 Process name: mysqld Socket: [::]:33060 61 | PID: 160 Process name: mysqld Socket: [::]:3306 62 | PID: 460 Process name: rapportd Socket: 0.0.0.0:50928 63 | PID: 460 Process name: rapportd Socket: [::]:50928 64 | ``` 65 | 66 | For more examples of usage, including how to get listening processes in a more granular way, 67 | check the [`examples`](https://github.com/GyulyVGC/listeners/tree/main/examples) folder. 68 | -------------------------------------------------------------------------------- /examples/get_all.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // Retrieve all listeners 3 | if let Ok(listeners) = listeners::get_all() { 4 | for l in listeners { 5 | println!("{l}"); 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/get_ports_by_pid.rs: -------------------------------------------------------------------------------- 1 | use std::env::args; 2 | 3 | fn main() { 4 | let pid = args() 5 | .nth(1) 6 | .expect("Expected CLI argument: PID") 7 | .parse() 8 | .expect("PID must be an unsigned integer on at most 32 bits"); 9 | 10 | // Retrieve ports listened to by a process given its PID 11 | if let Ok(ports) = listeners::get_ports_by_pid(pid) { 12 | for p in ports { 13 | println!("{p}"); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/get_ports_by_process_name.rs: -------------------------------------------------------------------------------- 1 | use std::env::args; 2 | 3 | fn main() { 4 | let process_name = args().nth(1).expect("Expected CLI argument: process name"); 5 | 6 | // Retrieve ports listened to by a process given its name 7 | if let Ok(ports) = listeners::get_ports_by_process_name(&process_name) { 8 | for p in ports { 9 | println!("{p}"); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/get_processes_by_port.rs: -------------------------------------------------------------------------------- 1 | use std::env::args; 2 | 3 | fn main() { 4 | let port = args() 5 | .nth(1) 6 | .expect("Expected CLI argument: port") 7 | .parse() 8 | .expect("Port must be an unsigned integer on at most 16 bits"); 9 | 10 | // Retrieve PID and name of processes listening on a given port 11 | if let Ok(processes) = listeners::get_processes_by_port(port) { 12 | for p in processes { 13 | println!("{p}"); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | use std::collections::HashSet; 4 | use std::fmt::Display; 5 | use std::net::SocketAddr; 6 | 7 | mod platform; 8 | 9 | type Result = std::result::Result>; 10 | 11 | /// A process listening on a TCP socket. 12 | #[derive(Eq, PartialEq, Hash, Debug)] 13 | pub struct Listener { 14 | /// The listening process. 15 | pub process: Process, 16 | /// The TCP socket this listener is listening on. 17 | pub socket: SocketAddr, 18 | } 19 | 20 | /// A process, characterized by its PID and name. 21 | #[derive(Eq, PartialEq, Hash, Debug)] 22 | pub struct Process { 23 | /// Process ID. 24 | pub pid: u32, 25 | /// Process name. 26 | pub name: String, 27 | } 28 | 29 | /// Returns all the [Listener]s. 30 | /// 31 | /// # Errors 32 | /// 33 | /// This function returns an error if it fails to retrieve listeners for the current platform. 34 | /// 35 | /// # Example 36 | /// 37 | /// ``` 38 | #[doc = include_str!("../examples/get_all.rs")] 39 | /// ``` 40 | /// 41 | /// Output: 42 | /// ``` text 43 | /// PID: 1088 Process name: rustrover Socket: [::7f00:1]:63342 44 | /// PID: 609 Process name: Microsoft SharePoint Socket: [::1]:42050 45 | /// PID: 160 Process name: mysqld Socket: [::]:33060 46 | /// PID: 160 Process name: mysqld Socket: [::]:3306 47 | /// PID: 460 Process name: rapportd Socket: 0.0.0.0:50928 48 | /// PID: 460 Process name: rapportd Socket: [::]:50928 49 | /// ``` 50 | pub fn get_all() -> Result> { 51 | platform::get_all() 52 | } 53 | 54 | /// Returns the list of [Process]es listening on a given TCP port. 55 | /// 56 | /// # Arguments 57 | /// 58 | /// * `port` - The TCP port to look for. 59 | /// 60 | /// # Errors 61 | /// 62 | /// This function returns an error if it fails to retrieve listeners for the current platform. 63 | /// 64 | /// # Example 65 | /// 66 | /// ``` no_run 67 | #[doc = include_str!("../examples/get_processes_by_port.rs")] 68 | /// ``` 69 | /// 70 | /// Output: 71 | /// ``` text 72 | /// PID: 160 Process name: mysqld 73 | /// ``` 74 | pub fn get_processes_by_port(port: u16) -> Result> { 75 | platform::get_all().map(|listeners| { 76 | listeners 77 | .into_iter() 78 | .filter(|listener| listener.socket.port() == port) 79 | .map(|listener| listener.process) 80 | .collect() 81 | }) 82 | } 83 | 84 | /// Returns the list of ports listened to by a process given its PID. 85 | /// 86 | /// # Arguments 87 | /// 88 | /// * `pid` - The PID of the process. 89 | /// 90 | /// # Errors 91 | /// 92 | /// This function returns an error if it fails to retrieve listeners for the current platform. 93 | /// 94 | /// # Example 95 | /// 96 | /// ``` no_run 97 | #[doc = include_str!("../examples/get_ports_by_pid.rs")] 98 | /// ``` 99 | /// 100 | /// Output: 101 | /// ``` text 102 | /// 3306 103 | /// 33060 104 | /// ``` 105 | pub fn get_ports_by_pid(pid: u32) -> Result> { 106 | platform::get_all().map(|listeners| { 107 | listeners 108 | .into_iter() 109 | .filter(|listener| listener.process.pid == pid) 110 | .map(|listener| listener.socket.port()) 111 | .collect() 112 | }) 113 | } 114 | 115 | /// Returns the list of ports listened to by a process given its name. 116 | /// 117 | /// # Arguments 118 | /// 119 | /// * `name` - The name of the process. 120 | /// 121 | /// # Errors 122 | /// 123 | /// This function returns an error if it fails to retrieve listeners for the current platform. 124 | /// 125 | /// # Example 126 | /// 127 | /// ``` no_run 128 | #[doc = include_str!("../examples/get_ports_by_process_name.rs")] 129 | /// ``` 130 | /// 131 | /// Output: 132 | /// ``` text 133 | /// 3306 134 | /// 33060 135 | /// ``` 136 | pub fn get_ports_by_process_name(name: &str) -> Result> { 137 | platform::get_all().map(|listeners| { 138 | listeners 139 | .into_iter() 140 | .filter(|listener| listener.process.name == name) 141 | .map(|listener| listener.socket.port()) 142 | .collect() 143 | }) 144 | } 145 | 146 | impl Listener { 147 | fn new(pid: u32, name: String, socket: SocketAddr) -> Self { 148 | let process = Process::new(pid, name); 149 | Self { process, socket } 150 | } 151 | } 152 | 153 | impl Process { 154 | fn new(pid: u32, name: String) -> Self { 155 | Self { pid, name } 156 | } 157 | } 158 | 159 | impl Display for Listener { 160 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 161 | let Listener { process, socket } = self; 162 | let process = process.to_string(); 163 | write!(f, "{process:<55} Socket: {socket}",) 164 | } 165 | } 166 | 167 | impl Display for Process { 168 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 169 | let Process { pid, name } = self; 170 | write!(f, "PID: {pid:<10} Process name: {name}") 171 | } 172 | } 173 | 174 | #[cfg(test)] 175 | mod tests { 176 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; 177 | 178 | use crate::{Listener, Process}; 179 | 180 | #[test] 181 | fn test_v4_listener_to_string() { 182 | let listener = Listener::new( 183 | 455, 184 | "rapportd".to_string(), 185 | SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 51189), 186 | ); 187 | assert_eq!( 188 | listener.to_string(), 189 | "PID: 455 Process name: rapportd Socket: 0.0.0.0:51189" 190 | ); 191 | } 192 | 193 | #[test] 194 | fn test_v6_listener_to_string() { 195 | let listener = Listener::new( 196 | 160, 197 | "mysqld".to_string(), 198 | SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 3306), 199 | ); 200 | assert_eq!( 201 | listener.to_string(), 202 | "PID: 160 Process name: mysqld Socket: [::]:3306" 203 | ); 204 | } 205 | 206 | #[test] 207 | fn test_process_to_string() { 208 | let process = Process::new(611, "Microsoft SharePoint".to_string()); 209 | assert_eq!( 210 | process.to_string(), 211 | "PID: 611 Process name: Microsoft SharePoint" 212 | ); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/platform/bsd/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | // use bsd_kvm::{Access, KernProc, Kvm}; 4 | use crate::Listener; 5 | 6 | pub(crate) fn get_all() -> crate::Result> { 7 | Err("Not implemented yet".into()) 8 | // let mut kvm = Kvm::open::<&str>(None, None, Access::ReadOnly).unwrap(); 9 | // let procs = kvm.get_process(KernProc::All, 0); 10 | // for p in procs { 11 | // let name = String::from_utf8( 12 | // p.info 13 | // .comm 14 | // .iter() 15 | // .filter(|c| **c > 0) 16 | // .map(|c| *c as u8) 17 | // .collect(), 18 | // ) 19 | // .unwrap(); 20 | // println!("Name: {name:<25} PID: {:<10}", p.info.pid); 21 | // } 22 | // 23 | // println!(); 24 | // 25 | // let ctl_list = sysctl::CtlIter::root(); 26 | // for c in ctl_list { 27 | // println!("{:?}", c.unwrap().name()); 28 | // } 29 | // 30 | // println!(); 31 | // 32 | // let ctl = sysctl::Ctl::new("net.inet.tcp.pcbcount").unwrap(); 33 | // println!("Value: {:?}", ctl.value()); 34 | // 35 | // let ctl = sysctl::Ctl::new("net.inet.tcp.pcblist_n").unwrap(); // each is 524 B long (?) 36 | // let val = ctl.value().unwrap(); 37 | // let val = val.as_struct(); 38 | // println!("Value: {:?}", val); 39 | } 40 | -------------------------------------------------------------------------------- /src/platform/linux/helpers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs::File; 3 | use std::os::fd::{AsFd, BorrowedFd, RawFd}; 4 | use std::path::Path; 5 | use std::str::FromStr; 6 | 7 | use rustix::fs::{Access, AtFlags, Mode, OFlags}; 8 | 9 | use crate::platform::linux::proc_fd::ProcFd; 10 | use crate::platform::linux::proc_info::ProcInfo; 11 | use crate::platform::linux::statics::O_PATH_MAYBE; 12 | 13 | pub(super) fn build_inode_proc_map(proc_fds: Vec) -> crate::Result> { 14 | let mut map: HashMap = HashMap::new(); 15 | 16 | for proc_fd in proc_fds { 17 | let dirfd = proc_fd.as_fd(); 18 | let path = "fd"; 19 | if rustix::fs::accessat(dirfd, path, Access::READ_OK, AtFlags::empty()).is_err() { 20 | continue; 21 | } 22 | let dir_fd = rustix::fs::openat( 23 | dirfd, 24 | path, 25 | OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC, 26 | Mode::empty(), 27 | )?; 28 | let mut dir = rustix::fs::Dir::read_from(&dir_fd)?; 29 | dir.rewind(); 30 | 31 | let mut socket_inodes = Vec::new(); 32 | while let Some(Ok(entry)) = dir.next() { 33 | let name = entry.file_name().to_string_lossy(); 34 | if RawFd::from_str(&name).is_ok() { 35 | if let Ok(socket_inode) = get_socket_inode(dir_fd.as_fd(), name.as_ref()) { 36 | socket_inodes.push(socket_inode); 37 | } 38 | } 39 | } 40 | 41 | let stat = rustix::fs::openat( 42 | proc_fd.as_fd(), 43 | "stat", 44 | OFlags::RDONLY | OFlags::CLOEXEC, 45 | Mode::empty(), 46 | )?; 47 | 48 | if let Ok(proc_info) = ProcInfo::from_file(File::from(stat)) { 49 | for inode in socket_inodes { 50 | map.insert(inode, proc_info.clone()); 51 | } 52 | } 53 | } 54 | 55 | Ok(map) 56 | } 57 | 58 | fn get_socket_inode>(dir_fd: BorrowedFd, path: P) -> crate::Result { 59 | let p = path.as_ref(); 60 | 61 | let flags = OFlags::NOFOLLOW | OFlags::CLOEXEC | *O_PATH_MAYBE; 62 | let file = rustix::fs::openat(dir_fd, p, flags, Mode::empty())?; 63 | let link = rustix::fs::readlinkat(&file, "", Vec::new())?; 64 | 65 | let link_os = link.to_string_lossy(); 66 | 67 | if !link_os.starts_with('/') && link_os.contains(':') { 68 | let mut s = link_os.split(':'); 69 | let fd_type = s.next().ok_or("Failed to get fd type")?; 70 | if fd_type == "socket" { 71 | let mut inode_str = s.next().ok_or("Failed to get inode")?; 72 | inode_str = inode_str.strip_prefix('[').ok_or("Failed to get inode")?; 73 | inode_str = inode_str.strip_suffix(']').ok_or("Failed to get inode")?; 74 | let inode = u64::from_str(inode_str)?; 75 | return Ok(inode); 76 | } 77 | } 78 | 79 | Err("Not a socket inode".into()) 80 | } 81 | -------------------------------------------------------------------------------- /src/platform/linux/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use helpers::build_inode_proc_map; 4 | use proc_fd::ProcFd; 5 | use tcp_listener::TcpListener; 6 | 7 | use crate::Listener; 8 | 9 | mod helpers; 10 | mod proc_fd; 11 | mod proc_info; 12 | mod statics; 13 | mod tcp_listener; 14 | 15 | pub(crate) fn get_all() -> crate::Result> { 16 | let mut listeners = HashSet::new(); 17 | 18 | let inode_proc_map = build_inode_proc_map(ProcFd::get_all()?)?; 19 | 20 | for tcp_listener in TcpListener::get_all()? { 21 | if let Some(p) = inode_proc_map.get(&tcp_listener.inode()) { 22 | let listener = Listener::new(p.pid(), p.name(), tcp_listener.local_addr()); 23 | listeners.insert(listener); 24 | } 25 | } 26 | 27 | Ok(listeners) 28 | } 29 | 30 | // #[cfg(test)] 31 | // mod tests { 32 | // #[test] 33 | // fn test_get_all() { 34 | // let listeners = crate::get_all().unwrap(); 35 | // assert!(!listeners.is_empty()); 36 | // 37 | // // let out = std::process::Command::new("netstat") 38 | // // .args(["-plnt"]) 39 | // // .output() 40 | // // .unwrap(); 41 | // // for l in String::from_utf8(out.stdout).unwrap().lines() { 42 | // // println!("{}", l); 43 | // // } 44 | // } 45 | // } 46 | -------------------------------------------------------------------------------- /src/platform/linux/proc_fd.rs: -------------------------------------------------------------------------------- 1 | use std::os::fd::OwnedFd; 2 | use std::path::{Path, PathBuf}; 3 | use std::str::FromStr; 4 | 5 | use rustix::fs::{Mode, OFlags}; 6 | 7 | use crate::platform::linux::statics::{O_PATH_MAYBE, ROOT}; 8 | 9 | #[derive(Debug)] 10 | pub(super) struct ProcFd(OwnedFd); 11 | 12 | impl ProcFd { 13 | fn new(fd: OwnedFd) -> Self { 14 | ProcFd(fd) 15 | } 16 | 17 | pub(super) fn as_fd(&self) -> &OwnedFd { 18 | &self.0 19 | } 20 | 21 | pub(super) fn get_all() -> crate::Result> { 22 | let root = Path::new(ROOT); 23 | let dir = rustix::fs::openat( 24 | rustix::fs::CWD, 25 | root, 26 | OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC, 27 | Mode::empty(), 28 | )?; 29 | let dir = rustix::fs::Dir::read_from(dir)?; 30 | 31 | let mut proc_fds: Vec = vec![]; 32 | for entry in dir.flatten() { 33 | if let Ok(pid) = i32::from_str(&entry.file_name().to_string_lossy()) { 34 | let proc_root = PathBuf::from(root).join(pid.to_string()); 35 | 36 | let flags = OFlags::DIRECTORY | OFlags::CLOEXEC | *O_PATH_MAYBE; 37 | let file = rustix::fs::openat(rustix::fs::CWD, &proc_root, flags, Mode::empty())?; 38 | 39 | proc_fds.push(ProcFd::new(file)); 40 | } 41 | } 42 | Ok(proc_fds) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/platform/linux/proc_info.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::Read; 3 | use std::str::FromStr; 4 | 5 | #[derive(Clone, Debug)] 6 | pub(super) struct ProcInfo { 7 | pid: u32, 8 | name: String, 9 | } 10 | 11 | impl ProcInfo { 12 | fn new(pid: u32, name: String) -> Self { 13 | ProcInfo { pid, name } 14 | } 15 | 16 | pub(super) fn pid(&self) -> u32 { 17 | self.pid 18 | } 19 | 20 | pub(super) fn name(&self) -> String { 21 | self.name.clone() 22 | } 23 | 24 | pub(super) fn from_file(mut file: File) -> crate::Result { 25 | // read in entire thing, this is only going to be 1 line 26 | let mut buf = Vec::new(); 27 | file.read_to_end(&mut buf)?; 28 | 29 | let line = String::from_utf8_lossy(&buf); 30 | let buf = line.trim(); 31 | 32 | // find the first opening paren, and split off the first part (pid) 33 | let start_paren = buf.find('(').ok_or("Failed to find opening paren")?; 34 | let end_paren = buf.rfind(')').ok_or("Failed to find closing paren")?; 35 | let pid_s = &buf[..start_paren - 1]; 36 | let name = buf[start_paren + 1..end_paren].to_string(); 37 | 38 | let pid = FromStr::from_str(pid_s)?; 39 | 40 | Ok(ProcInfo::new(pid, name)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/platform/linux/statics.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use rustix::fs::OFlags; 3 | 4 | pub(super) const ROOT: &str = "/proc"; 5 | 6 | pub(super) static O_PATH_MAYBE: Lazy = Lazy::new(|| { 7 | let kernel = std::fs::read_to_string("/proc/sys/kernel/osrelease") 8 | .map(|s| s.trim().to_owned()) 9 | .ok(); 10 | 11 | // for 2.6.39 <= kernel < 3.6 fstat doesn't support O_PATH see https://github.com/eminence/procfs/issues/265 12 | match kernel { 13 | Some(v) if v.as_str() < "3.6.0" => OFlags::empty(), 14 | Some(_) | None => OFlags::PATH, 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/platform/linux/tcp_listener.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{BufRead, BufReader}; 3 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; 4 | use std::str::FromStr; 5 | 6 | #[derive(Debug)] 7 | pub(super) struct TcpListener { 8 | local_addr: SocketAddr, 9 | inode: u64, 10 | } 11 | 12 | impl TcpListener { 13 | const LISTEN_STATE: &'static str = "0A"; 14 | 15 | pub(super) fn local_addr(&self) -> SocketAddr { 16 | self.local_addr 17 | } 18 | 19 | pub(super) fn inode(&self) -> u64 { 20 | self.inode 21 | } 22 | 23 | pub(super) fn get_all() -> crate::Result> { 24 | let mut table = Vec::new(); 25 | let tcp_table = File::open("/proc/net/tcp")?; 26 | for line in BufReader::new(tcp_table).lines().map_while(Result::ok) { 27 | if let Ok(l) = TcpListener::from_tcp_table_entry(&line) { 28 | table.push(l); 29 | } 30 | } 31 | let tcp6_table = File::open("/proc/net/tcp6")?; 32 | for line in BufReader::new(tcp6_table).lines().map_while(Result::ok) { 33 | if let Ok(l) = TcpListener::from_tcp6_table_entry(&line) { 34 | table.push(l); 35 | } 36 | } 37 | Ok(table) 38 | } 39 | 40 | fn from_tcp_table_entry(line: &str) -> crate::Result { 41 | let mut s = line.split_whitespace(); 42 | 43 | let local_addr_hex = s.nth(1).ok_or("Failed to get local address")?; 44 | let Some(Self::LISTEN_STATE) = s.nth(1) else { 45 | return Err("Not a listening socket".into()); 46 | }; 47 | 48 | let local_ip_port = local_addr_hex 49 | .split(':') 50 | .flat_map(|s| u32::from_str_radix(s, 16)) 51 | .collect::>(); 52 | 53 | let ip_n = local_ip_port.first().ok_or("Failed to get IP")?; 54 | let port_n = local_ip_port.get(1).ok_or("Failed to get port")?; 55 | let ip = Ipv4Addr::from(u32::from_be(*ip_n)); 56 | let port = u16::try_from(*port_n)?; 57 | let local_addr = SocketAddr::new(IpAddr::V4(ip), port); 58 | 59 | let inode_n = s.nth(5).ok_or("Failed to get inode")?; 60 | let inode = u64::from_str(inode_n)?; 61 | 62 | Ok(Self { local_addr, inode }) 63 | } 64 | 65 | fn from_tcp6_table_entry(line: &str) -> crate::Result { 66 | #[cfg(target_endian = "little")] 67 | let read_endian = u32::from_le_bytes; 68 | #[cfg(target_endian = "big")] 69 | let read_endian = u32::from_be_bytes; 70 | 71 | let mut s = line.split_whitespace(); 72 | 73 | let local_addr_hex = s.nth(1).ok_or("Failed to get local address")?; 74 | let Some(Self::LISTEN_STATE) = s.nth(1) else { 75 | return Err("Not a listening socket".into()); 76 | }; 77 | 78 | let mut local_ip_port = local_addr_hex.split(':'); 79 | 80 | let ip_str = local_ip_port.next().ok_or("Failed to get IP")?; 81 | let port_str = local_ip_port.next().ok_or("Failed to get port")?; 82 | 83 | if ip_str.len() % 2 != 0 { 84 | return Err("Invalid IP address".into()); 85 | } 86 | let bytes = (0..ip_str.len()) 87 | .step_by(2) 88 | .flat_map(|i| u8::from_str_radix(&ip_str[i..i + 2], 16)) 89 | .collect::>(); 90 | let ip_a = read_endian(bytes[0..4].try_into()?); 91 | let ip_b = read_endian(bytes[4..8].try_into()?); 92 | let ip_c = read_endian(bytes[8..12].try_into()?); 93 | let ip_d = read_endian(bytes[12..16].try_into()?); 94 | let ip = Ipv6Addr::new( 95 | ((ip_a >> 16) & 0xffff) as u16, 96 | (ip_a & 0xffff) as u16, 97 | ((ip_b >> 16) & 0xffff) as u16, 98 | (ip_b & 0xffff) as u16, 99 | ((ip_c >> 16) & 0xffff) as u16, 100 | (ip_c & 0xffff) as u16, 101 | ((ip_d >> 16) & 0xffff) as u16, 102 | (ip_d & 0xffff) as u16, 103 | ); 104 | 105 | let port = u16::from_str_radix(port_str, 16)?; 106 | let local_addr = SocketAddr::new(IpAddr::V6(ip), port); 107 | 108 | let inode_n = s.nth(5).ok_or("Failed to get inode")?; 109 | let inode = u64::from_str(inode_n)?; 110 | 111 | Ok(Self { local_addr, inode }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/platform/macos/c_libproc.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_int, c_void}; 2 | 3 | extern "C" { 4 | pub(super) fn proc_listpids( 5 | type_: u32, 6 | typeinfo: u32, 7 | buffer: *mut c_void, 8 | buffersize: c_int, 9 | ) -> c_int; 10 | } 11 | 12 | extern "C" { 13 | pub(super) fn proc_pidinfo( 14 | pid: c_int, 15 | flavor: c_int, 16 | arg: u64, 17 | buffer: *mut c_void, 18 | buffersize: c_int, 19 | ) -> c_int; 20 | } 21 | 22 | extern "C" { 23 | pub(super) fn proc_pidfdinfo( 24 | pid: c_int, 25 | fd: c_int, 26 | flavor: c_int, 27 | buffer: *mut c_void, 28 | buffersize: c_int, 29 | ) -> c_int; 30 | } 31 | 32 | extern "C" { 33 | pub(super) fn proc_name(pid: c_int, buffer: *mut c_void, buffersize: u32) -> c_int; 34 | } 35 | -------------------------------------------------------------------------------- /src/platform/macos/c_proc_fd_info.rs: -------------------------------------------------------------------------------- 1 | #[repr(C)] 2 | #[derive(Default)] 3 | pub(super) struct CProcFdInfo { 4 | proc_fd: i32, 5 | proc_fd_type: u32, 6 | } 7 | 8 | impl CProcFdInfo { 9 | pub(super) fn fd(&self) -> i32 { 10 | self.proc_fd 11 | } 12 | 13 | pub(super) fn fd_type(&self) -> u32 { 14 | self.proc_fd_type 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/platform/macos/c_socket_fd_info.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_char, c_int, c_longlong, c_short, c_uchar, c_uint, c_ushort}; 2 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 3 | 4 | use byteorder::{ByteOrder, NetworkEndian}; 5 | 6 | use crate::platform::macos::statics::SOCKET_STATE_LISTEN; 7 | use crate::platform::macos::tcp_listener::TcpListener; 8 | 9 | #[repr(C)] 10 | pub(super) struct CSocketFdInfo { 11 | pfi: ProcFileinfo, 12 | psi: SocketInfo, 13 | } 14 | 15 | impl CSocketFdInfo { 16 | pub(super) fn to_tcp_listener(&self) -> crate::Result { 17 | let sock_info = self.psi; 18 | let family = sock_info.soi_family; 19 | 20 | let tcp_in = unsafe { sock_info.soi_proto.pri_tcp }; 21 | 22 | if tcp_in.tcpsi_state != SOCKET_STATE_LISTEN { 23 | return Err("Socket is not in listen state".into()); 24 | } 25 | 26 | let tcp_sockaddr_in = tcp_in.tcpsi_ini; 27 | let lport_bytes: [u8; 4] = i32::to_le_bytes(tcp_sockaddr_in.insi_lport); 28 | let local_address = Self::get_local_addr(family, tcp_sockaddr_in)?; 29 | 30 | let socket_info = TcpListener::new(local_address, NetworkEndian::read_u16(&lport_bytes)); 31 | 32 | Ok(socket_info) 33 | } 34 | 35 | fn get_local_addr(family: c_int, tcp_sockaddr_in: InSockinfo) -> crate::Result { 36 | match family { 37 | 2 => { 38 | // AF_INET 39 | let addr = unsafe { tcp_sockaddr_in.insi_laddr.ina_46.i46a_addr4.s_addr }; 40 | Ok(IpAddr::V4(Ipv4Addr::from(u32::from_be(addr)))) 41 | } 42 | 30 => { 43 | // AF_INET6 44 | let addr = unsafe { &tcp_sockaddr_in.insi_laddr.ina_6.__u6_addr.__u6_addr8 }; 45 | let mut ipv6_addr = [0_u16; 8]; 46 | NetworkEndian::read_u16_into(addr, &mut ipv6_addr); 47 | Ok(IpAddr::V6(Ipv6Addr::from(ipv6_addr))) 48 | } 49 | _ => Err("Unsupported socket family".into()), 50 | } 51 | } 52 | } 53 | 54 | #[repr(C)] 55 | #[allow(clippy::struct_field_names)] 56 | struct ProcFileinfo { 57 | fi_openflags: u32, 58 | fi_status: u32, 59 | fi_offset: c_longlong, 60 | fi_type: i32, 61 | fi_guardflags: u32, 62 | } 63 | 64 | #[repr(C)] 65 | #[derive(Copy, Clone)] 66 | struct SocketInfo { 67 | soi_stat: VinfoStat, 68 | soi_so: u64, 69 | soi_pcb: u64, 70 | soi_type: c_int, 71 | soi_protocol: c_int, 72 | soi_family: c_int, 73 | soi_options: c_short, 74 | soi_linger: c_short, 75 | soi_state: c_short, 76 | soi_qlen: c_short, 77 | soi_incqlen: c_short, 78 | soi_qlimit: c_short, 79 | soi_timeo: c_short, 80 | soi_error: c_ushort, 81 | soi_oobmark: u32, 82 | soi_rcv: SockbufInfo, 83 | soi_snd: SockbufInfo, 84 | soi_kind: c_int, 85 | rfu_1: u32, 86 | soi_proto: SocketInfoBindgenTy1, 87 | } 88 | 89 | #[repr(C)] 90 | #[derive(Copy, Clone)] 91 | #[allow(clippy::struct_field_names)] 92 | struct VinfoStat { 93 | vst_dev: u32, 94 | vst_mode: u16, 95 | vst_nlink: u16, 96 | vst_ino: u64, 97 | vst_uid: c_uint, 98 | vst_gid: c_uint, 99 | vst_atime: i64, 100 | vst_atimensec: i64, 101 | vst_mtime: i64, 102 | vst_mtimensec: i64, 103 | vst_ctime: i64, 104 | vst_ctimensec: i64, 105 | vst_birthtime: i64, 106 | vst_birthtimensec: i64, 107 | vst_size: c_longlong, 108 | vst_blocks: i64, 109 | vst_blksize: i32, 110 | vst_flags: u32, 111 | vst_gen: u32, 112 | vst_rdev: u32, 113 | vst_qspare: [i64; 2usize], 114 | } 115 | 116 | #[repr(C)] 117 | #[derive(Copy, Clone)] 118 | #[allow(clippy::struct_field_names)] 119 | struct SockbufInfo { 120 | sbi_cc: u32, 121 | sbi_hiwat: u32, 122 | sbi_mbcnt: u32, 123 | sbi_mbmax: u32, 124 | sbi_lowat: u32, 125 | sbi_flags: c_short, 126 | sbi_timeo: c_short, 127 | } 128 | 129 | #[repr(C)] 130 | #[derive(Copy, Clone)] 131 | union SocketInfoBindgenTy1 { 132 | pri_in: InSockinfo, 133 | pri_tcp: TcpSockinfo, 134 | pri_un: UnSockinfo, 135 | pri_ndrv: NdrvInfo, 136 | pri_kern_event: KernEventInfo, 137 | pri_kern_ctl: KernCtlInfo, 138 | _bindgen_union_align: [u64; 66usize], 139 | } 140 | 141 | #[repr(C)] 142 | #[derive(Copy, Clone)] 143 | struct InSockinfo { 144 | insi_fport: c_int, 145 | insi_lport: c_int, 146 | insi_gencnt: u64, 147 | insi_flags: u32, 148 | insi_flow: u32, 149 | insi_vflag: u8, 150 | insi_ip_ttl: u8, 151 | rfu_1: u32, 152 | insi_faddr: InSockinfoBindgenTy1, 153 | insi_laddr: InSockinfoBindgenTy2, 154 | insi_v4: InSockinfoBindgenTy3, 155 | insi_v6: InSockinfoBindgenTy4, 156 | } 157 | 158 | #[repr(C)] 159 | #[derive(Copy, Clone)] 160 | union InSockinfoBindgenTy1 { 161 | ina_46: In4in6Addr, 162 | ina_6: In6Addr, 163 | _bindgen_union_align: [u32; 4usize], 164 | } 165 | 166 | #[repr(C)] 167 | #[derive(Copy, Clone)] 168 | union InSockinfoBindgenTy2 { 169 | ina_46: In4in6Addr, 170 | ina_6: In6Addr, 171 | _bindgen_union_align: [u32; 4usize], 172 | } 173 | 174 | #[repr(C)] 175 | #[derive(Copy, Clone)] 176 | struct InSockinfoBindgenTy3 { 177 | in4_tos: c_uchar, 178 | } 179 | 180 | #[repr(C)] 181 | #[derive(Copy, Clone)] 182 | #[allow(clippy::struct_field_names)] 183 | struct InSockinfoBindgenTy4 { 184 | in6_hlim: u8, 185 | in6_cksum: c_int, 186 | in6_ifindex: c_ushort, 187 | in6_hops: c_short, 188 | } 189 | 190 | #[repr(C)] 191 | #[derive(Copy, Clone)] 192 | struct In4in6Addr { 193 | i46a_pad32: [c_uint; 3usize], 194 | i46a_addr4: InAddr, 195 | } 196 | 197 | #[repr(C)] 198 | #[derive(Copy, Clone)] 199 | struct InAddr { 200 | s_addr: c_uint, 201 | } 202 | 203 | #[repr(C)] 204 | #[derive(Copy, Clone)] 205 | struct In6Addr { 206 | __u6_addr: In6AddrBindgenTy1, 207 | } 208 | 209 | #[repr(C)] 210 | #[derive(Copy, Clone)] 211 | union In6AddrBindgenTy1 { 212 | __u6_addr8: [c_uchar; 16usize], 213 | __u6_addr16: [c_ushort; 8usize], 214 | __u6_addr32: [c_uint; 4usize], 215 | _bindgen_union_align: [u32; 4usize], 216 | } 217 | 218 | #[repr(C)] 219 | #[derive(Copy, Clone)] 220 | struct TcpSockinfo { 221 | tcpsi_ini: InSockinfo, 222 | tcpsi_state: c_int, 223 | tcpsi_timer: [c_int; 4usize], 224 | tcpsi_mss: c_int, 225 | tcpsi_flags: u32, 226 | rfu_1: u32, 227 | tcpsi_tp: u64, 228 | } 229 | 230 | #[repr(C)] 231 | #[derive(Copy, Clone)] 232 | #[allow(clippy::struct_field_names)] 233 | struct UnSockinfo { 234 | unsi_conn_so: u64, 235 | unsi_conn_pcb: u64, 236 | unsi_addr: UnSockinfoBindgenTy1, 237 | unsi_caddr: UnSockinfoBindgenTy2, 238 | } 239 | 240 | #[repr(C)] 241 | #[derive(Copy, Clone)] 242 | union UnSockinfoBindgenTy1 { 243 | ua_sun: SockaddrUn, 244 | ua_dummy: [c_char; 255usize], 245 | _bindgen_union_align: [u8; 255usize], 246 | } 247 | 248 | #[repr(C)] 249 | #[derive(Copy, Clone)] 250 | #[allow(clippy::struct_field_names)] 251 | struct SockaddrUn { 252 | sun_len: c_uchar, 253 | sun_family: c_uchar, 254 | sun_path: [c_char; 104usize], 255 | } 256 | 257 | #[repr(C)] 258 | #[derive(Copy, Clone)] 259 | union UnSockinfoBindgenTy2 { 260 | ua_sun: SockaddrUn, 261 | ua_dummy: [c_char; 255usize], 262 | _bindgen_union_align: [u8; 255usize], 263 | } 264 | 265 | #[repr(C)] 266 | #[derive(Copy, Clone)] 267 | #[allow(clippy::struct_field_names)] 268 | struct NdrvInfo { 269 | ndrvsi_if_family: u32, 270 | ndrvsi_if_unit: u32, 271 | ndrvsi_if_name: [c_char; 16usize], 272 | } 273 | 274 | #[repr(C)] 275 | #[derive(Copy, Clone)] 276 | #[allow(clippy::struct_field_names)] 277 | struct KernEventInfo { 278 | kesi_vendor_code_filter: u32, 279 | kesi_class_filter: u32, 280 | kesi_subclass_filter: u32, 281 | } 282 | 283 | #[repr(C)] 284 | #[derive(Copy, Clone)] 285 | #[allow(clippy::struct_field_names)] 286 | struct KernCtlInfo { 287 | kcsi_id: u32, 288 | kcsi_reg_unit: u32, 289 | kcsi_flags: u32, 290 | kcsi_recvbufsize: u32, 291 | kcsi_sendbufsize: u32, 292 | kcsi_unit: u32, 293 | kcsi_name: [c_char; 96usize], 294 | } 295 | -------------------------------------------------------------------------------- /src/platform/macos/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use proc_name::ProcName; 4 | use proc_pid::ProcPid; 5 | use socket_fd::SocketFd; 6 | use tcp_listener::TcpListener; 7 | 8 | use crate::Listener; 9 | 10 | mod c_libproc; 11 | mod c_proc_fd_info; 12 | mod c_socket_fd_info; 13 | mod proc_name; 14 | mod proc_pid; 15 | mod socket_fd; 16 | mod statics; 17 | mod tcp_listener; 18 | 19 | pub(crate) fn get_all() -> crate::Result> { 20 | let mut listeners = HashSet::new(); 21 | 22 | for pid in ProcPid::get_all().into_iter().flatten() { 23 | for fd in SocketFd::get_all_of_pid(pid).iter().flatten() { 24 | if let Ok(tcp_listener) = TcpListener::from_pid_fd(pid, fd) { 25 | if let Ok(ProcName(name)) = ProcName::from_pid(pid) { 26 | let listener = Listener::new(pid.as_u_32()?, name, tcp_listener.socket_addr()); 27 | listeners.insert(listener); 28 | } 29 | } 30 | } 31 | } 32 | 33 | Ok(listeners) 34 | } 35 | 36 | // #[cfg(test)] 37 | // mod tests { 38 | // #[test] 39 | // fn test_get_all() { 40 | // let listeners = crate::get_all().unwrap(); 41 | // assert!(!listeners.is_empty()); 42 | // 43 | // // let out = std::process::Command::new("netstat") 44 | // // .args(["-p", "tcp", "-van"]) 45 | // // .output() 46 | // // .unwrap(); 47 | // // for l in String::from_utf8(out.stdout).unwrap().lines().filter(|l| l.contains("LISTEN")) { 48 | // // println!("{}", l); 49 | // // } 50 | // } 51 | // } 52 | -------------------------------------------------------------------------------- /src/platform/macos/proc_name.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use crate::platform::macos::c_libproc::proc_name; 4 | use crate::platform::macos::proc_pid::ProcPid; 5 | use crate::platform::macos::statics::PROC_PID_PATH_INFO_MAXSIZE; 6 | 7 | #[derive(Default)] 8 | pub(super) struct ProcName(pub(super) String); 9 | 10 | impl ProcName { 11 | fn new(name: String) -> Self { 12 | ProcName(name) 13 | } 14 | 15 | pub(super) fn from_pid(pid: ProcPid) -> crate::Result { 16 | let mut buf: Vec = Vec::with_capacity(PROC_PID_PATH_INFO_MAXSIZE); 17 | let buffer_ptr = buf.as_mut_ptr().cast::(); 18 | let buffer_size = u32::try_from(buf.capacity())?; 19 | 20 | let ret; 21 | unsafe { 22 | ret = proc_name(pid.as_c_int(), buffer_ptr, buffer_size); 23 | }; 24 | 25 | if ret <= 0 { 26 | return Err("Failed to get process name".into()); 27 | } 28 | 29 | unsafe { 30 | buf.set_len(usize::try_from(ret)?); 31 | } 32 | 33 | match String::from_utf8(buf) { 34 | Ok(name) => Ok(Self::new(name)), 35 | Err(_) => Err("Invalid UTF sequence for process name".into()), 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/platform/macos/proc_pid.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_int, c_void}; 2 | use std::{mem, ptr}; 3 | 4 | use super::c_libproc::proc_listpids; 5 | use super::statics::PROC_ALL_PIDS; 6 | 7 | #[derive(Debug, Copy, Clone)] 8 | pub(super) struct ProcPid(c_int); 9 | 10 | impl ProcPid { 11 | fn new(n: c_int) -> Self { 12 | ProcPid(n) 13 | } 14 | 15 | pub(super) fn as_c_int(self) -> c_int { 16 | self.0 17 | } 18 | 19 | pub(super) fn as_u_32(self) -> crate::Result { 20 | match u32::try_from(self.0) { 21 | Ok(n) => Ok(n), 22 | Err(_) => Err("Failed to convert pid to u32".into()), 23 | } 24 | } 25 | 26 | pub(super) fn get_all() -> crate::Result> { 27 | let number_of_pids; 28 | 29 | unsafe { 30 | number_of_pids = proc_listpids(PROC_ALL_PIDS, 0, ptr::null_mut(), 0); 31 | } 32 | 33 | if number_of_pids <= 0 { 34 | return Err("Failed to list processes".into()); 35 | } 36 | 37 | let mut pids: Vec = Vec::new(); 38 | pids.resize_with(usize::try_from(number_of_pids)?, Default::default); 39 | 40 | let return_code = unsafe { 41 | proc_listpids( 42 | PROC_ALL_PIDS, 43 | 0, 44 | pids.as_mut_ptr().cast::(), 45 | c_int::try_from(pids.len() * mem::size_of::())?, 46 | ) 47 | }; 48 | 49 | if return_code <= 0 { 50 | return Err("Failed to list processes".into()); 51 | } 52 | 53 | Ok(pids 54 | .into_iter() 55 | .filter(|f| *f > 0) 56 | .map(ProcPid::new) 57 | .collect()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/platform/macos/socket_fd.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | use std::{mem, ptr}; 3 | 4 | use crate::platform::macos::c_libproc::proc_pidinfo; 5 | use crate::platform::macos::c_proc_fd_info::CProcFdInfo; 6 | use crate::platform::macos::proc_pid::ProcPid; 7 | use crate::platform::macos::statics::{FD_TYPE_SOCKET, PROC_PID_LIST_FDS}; 8 | 9 | pub(super) struct SocketFd(i32); 10 | 11 | impl SocketFd { 12 | fn new(fd: i32) -> Self { 13 | SocketFd(fd) 14 | } 15 | 16 | pub(super) fn fd(&self) -> i32 { 17 | self.0 18 | } 19 | 20 | pub(super) fn get_all_of_pid(pid: ProcPid) -> crate::Result> { 21 | let buffer_size = 22 | unsafe { proc_pidinfo(pid.as_c_int(), PROC_PID_LIST_FDS, 0, ptr::null_mut(), 0) }; 23 | 24 | if buffer_size <= 0 { 25 | return Err("Failed to list file descriptors".into()); 26 | } 27 | 28 | let number_of_fds = usize::try_from(buffer_size)? / mem::size_of::(); 29 | 30 | let mut fds: Vec = Vec::new(); 31 | fds.resize_with(number_of_fds, CProcFdInfo::default); 32 | 33 | let return_code = unsafe { 34 | proc_pidinfo( 35 | pid.as_c_int(), 36 | PROC_PID_LIST_FDS, 37 | 0, 38 | fds.as_mut_ptr().cast::(), 39 | buffer_size, 40 | ) 41 | }; 42 | 43 | if return_code <= 0 { 44 | return Err("Failed to list file descriptors".into()); 45 | } 46 | 47 | Ok(fds 48 | .iter() 49 | .filter(|fd| fd.fd_type() == FD_TYPE_SOCKET) 50 | .map(|fd| Self::new(fd.fd())) 51 | .collect()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/platform/macos/statics.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_int; 2 | 3 | pub(super) const PROC_ALL_PIDS: u32 = 1; 4 | pub(super) const PROC_PID_LIST_FDS: c_int = 1; 5 | pub(super) const PROC_PID_FD_SOCKET_INFO: c_int = 3; 6 | pub(super) const FD_TYPE_SOCKET: u32 = 2; 7 | pub(super) const SOCKET_STATE_LISTEN: c_int = 1; 8 | pub(super) const PROC_PID_PATH_INFO_MAXSIZE: usize = 4096; 9 | -------------------------------------------------------------------------------- /src/platform/macos/tcp_listener.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_int, c_void}; 2 | use std::mem; 3 | use std::mem::MaybeUninit; 4 | use std::net::{IpAddr, SocketAddr}; 5 | 6 | use crate::platform::macos::c_libproc::proc_pidfdinfo; 7 | use crate::platform::macos::c_socket_fd_info::CSocketFdInfo; 8 | use crate::platform::macos::proc_pid::ProcPid; 9 | use crate::platform::macos::socket_fd::SocketFd; 10 | use crate::platform::macos::statics::PROC_PID_FD_SOCKET_INFO; 11 | 12 | #[derive(Debug)] 13 | pub(super) struct TcpListener(SocketAddr); 14 | 15 | impl TcpListener { 16 | pub(super) fn new(addr: IpAddr, port: u16) -> Self { 17 | TcpListener(SocketAddr::new(addr, port)) 18 | } 19 | 20 | pub(super) fn socket_addr(&self) -> SocketAddr { 21 | self.0 22 | } 23 | 24 | pub(super) fn from_pid_fd(pid: ProcPid, fd: &SocketFd) -> crate::Result { 25 | let mut sinfo: MaybeUninit = MaybeUninit::uninit(); 26 | 27 | let return_code = unsafe { 28 | proc_pidfdinfo( 29 | pid.as_c_int(), 30 | fd.fd(), 31 | PROC_PID_FD_SOCKET_INFO, 32 | sinfo.as_mut_ptr().cast::(), 33 | c_int::try_from(mem::size_of::())?, 34 | ) 35 | }; 36 | 37 | if return_code < 0 { 38 | return Err("Failed to get file descriptor information".into()); 39 | } 40 | 41 | let c_socket_fd_info = unsafe { sinfo.assume_init() }; 42 | c_socket_fd_info.to_tcp_listener() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/platform/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) use target_os::get_all; 2 | 3 | /* ---------- windows ---------- */ 4 | #[cfg(target_os = "windows")] 5 | mod windows; 6 | #[cfg(target_os = "windows")] 7 | use windows as target_os; 8 | 9 | /* ----------- macos ----------- */ 10 | #[cfg(target_os = "macos")] 11 | mod macos; 12 | #[cfg(target_os = "macos")] 13 | use macos as target_os; 14 | 15 | /* ----------- linux ----------- */ 16 | #[cfg(target_os = "linux")] 17 | mod linux; 18 | #[cfg(target_os = "linux")] 19 | use linux as target_os; 20 | 21 | /* ----------- other ----------- */ 22 | #[cfg(all( 23 | not(target_os = "linux"), 24 | not(target_os = "macos"), 25 | not(target_os = "windows") 26 | ))] 27 | mod bsd; 28 | #[cfg(all( 29 | not(target_os = "linux"), 30 | not(target_os = "macos"), 31 | not(target_os = "windows") 32 | ))] 33 | use bsd as target_os; 34 | -------------------------------------------------------------------------------- /src/platform/windows/c_iphlpapi.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_int, c_ulong, c_void}; 2 | 3 | #[allow(non_snake_case)] 4 | #[link(name = "iphlpapi")] 5 | extern "system" { 6 | pub(super) fn GetExtendedTcpTable( 7 | pTcpTable: *mut c_void, 8 | pdwSize: *mut c_ulong, 9 | bOrder: c_int, 10 | ulAf: c_ulong, 11 | TableClass: c_ulong, 12 | Reserved: c_ulong, 13 | ) -> c_ulong; 14 | } 15 | -------------------------------------------------------------------------------- /src/platform/windows/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use tcp_listener::TcpListener; 4 | 5 | use crate::Listener; 6 | 7 | mod c_iphlpapi; 8 | mod socket_table; 9 | mod statics; 10 | mod tcp6_table; 11 | mod tcp_listener; 12 | mod tcp_table; 13 | 14 | pub(crate) fn get_all() -> crate::Result> { 15 | let mut listeners = HashSet::new(); 16 | 17 | for tcp_listener in TcpListener::get_all() { 18 | if let Some(listener) = tcp_listener.to_listener() { 19 | listeners.insert(listener); 20 | } 21 | } 22 | 23 | Ok(listeners) 24 | } 25 | -------------------------------------------------------------------------------- /src/platform/windows/socket_table.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_ulong, c_void}; 2 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 3 | 4 | use crate::platform::target_os::c_iphlpapi::GetExtendedTcpTable; 5 | use crate::platform::target_os::statics::FALSE; 6 | use crate::platform::target_os::tcp_listener::TcpListener; 7 | use crate::platform::windows::statics::{ 8 | AF_INET, AF_INET6, ERROR_INSUFFICIENT_BUFFER, LISTEN, NO_ERROR, TCP_TABLE_OWNER_PID_ALL, 9 | }; 10 | use crate::platform::windows::tcp6_table::Tcp6Table; 11 | use crate::platform::windows::tcp_table::TcpTable; 12 | 13 | pub(super) trait SocketTable { 14 | fn get_table() -> crate::Result>; 15 | fn get_rows_count(table: &[u8]) -> usize; 16 | fn get_tcp_listener(table: &[u8], index: usize) -> Option; 17 | } 18 | 19 | impl SocketTable for TcpTable { 20 | fn get_table() -> crate::Result> { 21 | get_tcp_table(AF_INET) 22 | } 23 | 24 | fn get_rows_count(table: &[u8]) -> usize { 25 | #[allow(clippy::cast_ptr_alignment)] 26 | let table = unsafe { &*(table.as_ptr().cast::()) }; 27 | table.rows_count as usize 28 | } 29 | 30 | fn get_tcp_listener(table: &[u8], index: usize) -> Option { 31 | #[allow(clippy::cast_ptr_alignment)] 32 | let table = unsafe { &*(table.as_ptr().cast::()) }; 33 | let rows_ptr = std::ptr::addr_of!(table.rows[0]); 34 | let row = unsafe { &*rows_ptr.add(index) }; 35 | if row.state == LISTEN { 36 | Some(TcpListener::new( 37 | IpAddr::V4(Ipv4Addr::from(u32::from_be(row.local_addr))), 38 | u16::from_be(u16::try_from(row.local_port).ok()?), 39 | row.owning_pid, 40 | )) 41 | } else { 42 | None 43 | } 44 | } 45 | } 46 | 47 | impl SocketTable for Tcp6Table { 48 | fn get_table() -> crate::Result> { 49 | get_tcp_table(AF_INET6) 50 | } 51 | 52 | fn get_rows_count(table: &[u8]) -> usize { 53 | #[allow(clippy::cast_ptr_alignment)] 54 | let table = unsafe { &*(table.as_ptr().cast::()) }; 55 | table.rows_count as usize 56 | } 57 | 58 | fn get_tcp_listener(table: &[u8], index: usize) -> Option { 59 | #[allow(clippy::cast_ptr_alignment)] 60 | let table = unsafe { &*(table.as_ptr().cast::()) }; 61 | let rows_ptr = std::ptr::addr_of!(table.rows[0]); 62 | let row = unsafe { &*rows_ptr.add(index) }; 63 | if row.state == LISTEN { 64 | Some(TcpListener::new( 65 | IpAddr::V6(Ipv6Addr::from(row.local_addr)), 66 | u16::from_be(u16::try_from(row.local_port).ok()?), 67 | row.owning_pid, 68 | )) 69 | } else { 70 | None 71 | } 72 | } 73 | } 74 | 75 | fn get_tcp_table(address_family: c_ulong) -> crate::Result> { 76 | let mut table_size: c_ulong = 0; 77 | let mut err_code = unsafe { 78 | GetExtendedTcpTable( 79 | std::ptr::null_mut(), 80 | &mut table_size, 81 | FALSE, 82 | address_family, 83 | TCP_TABLE_OWNER_PID_ALL, 84 | 0, 85 | ) 86 | }; 87 | let mut table = Vec::::new(); 88 | let mut iterations = 0; 89 | while err_code == ERROR_INSUFFICIENT_BUFFER { 90 | table = Vec::::with_capacity(table_size as usize); 91 | err_code = unsafe { 92 | GetExtendedTcpTable( 93 | table.as_mut_ptr().cast::(), 94 | &mut table_size, 95 | FALSE, 96 | address_family, 97 | TCP_TABLE_OWNER_PID_ALL, 98 | 0, 99 | ) 100 | }; 101 | iterations += 1; 102 | if iterations > 100 { 103 | return Err("Failed to allocate buffer".into()); 104 | } 105 | } 106 | if err_code == NO_ERROR { 107 | Ok(table) 108 | } else { 109 | Err("Failed to get TCP table".into()) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/platform/windows/statics.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{c_int, c_ulong}; 2 | 3 | pub(super) const TCP_TABLE_OWNER_PID_ALL: c_ulong = 5; 4 | pub(super) const FALSE: c_int = 0; 5 | pub(super) const ERROR_INSUFFICIENT_BUFFER: c_ulong = 0x7A; 6 | pub(super) const NO_ERROR: c_ulong = 0; 7 | pub(super) const AF_INET: c_ulong = 2; 8 | pub(super) const AF_INET6: c_ulong = 23; 9 | pub(super) const LISTEN: c_ulong = 2; 10 | -------------------------------------------------------------------------------- /src/platform/windows/tcp6_table.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_uchar; 2 | use std::os::raw::c_ulong; 3 | 4 | #[derive(Copy, Clone, Debug)] 5 | #[repr(C)] 6 | pub(super) struct Tcp6Table { 7 | pub(super) rows_count: c_ulong, 8 | pub(super) rows: [Tcp6Row; 1], 9 | } 10 | 11 | #[derive(Copy, Clone, Debug)] 12 | #[repr(C)] 13 | pub(super) struct Tcp6Row { 14 | pub(super) local_addr: [c_uchar; 16], 15 | local_scope_id: c_ulong, 16 | pub(super) local_port: c_ulong, 17 | remote_addr: [c_uchar; 16], 18 | remote_scope_id: c_ulong, 19 | remote_port: c_ulong, 20 | pub(super) state: c_ulong, 21 | pub(super) owning_pid: c_ulong, 22 | } 23 | -------------------------------------------------------------------------------- /src/platform/windows/tcp_listener.rs: -------------------------------------------------------------------------------- 1 | use std::mem::size_of; 2 | use std::mem::zeroed; 3 | use std::net::{IpAddr, SocketAddr}; 4 | 5 | use windows::Win32::Foundation::CloseHandle; 6 | use windows::Win32::System::Diagnostics::ToolHelp::{ 7 | CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, TH32CS_SNAPPROCESS, 8 | }; 9 | 10 | use crate::platform::windows::socket_table::SocketTable; 11 | use crate::platform::windows::tcp6_table::Tcp6Table; 12 | use crate::platform::windows::tcp_table::TcpTable; 13 | use crate::Listener; 14 | 15 | #[derive(Debug)] 16 | pub(super) struct TcpListener { 17 | local_addr: IpAddr, 18 | local_port: u16, 19 | pid: u32, 20 | } 21 | 22 | impl TcpListener { 23 | pub(super) fn get_all() -> Vec { 24 | Self::table_entries::() 25 | .into_iter() 26 | .flatten() 27 | .chain(Self::table_entries::().into_iter().flatten()) 28 | .collect() 29 | } 30 | 31 | fn table_entries() -> crate::Result> { 32 | let mut tcp_listeners = Vec::new(); 33 | let table = Table::get_table()?; 34 | for i in 0..Table::get_rows_count(&table) { 35 | if let Some(tcp_listener) = Table::get_tcp_listener(&table, i) { 36 | tcp_listeners.push(tcp_listener); 37 | } 38 | } 39 | Ok(tcp_listeners) 40 | } 41 | 42 | pub(super) fn new(local_addr: IpAddr, local_port: u16, pid: u32) -> Self { 43 | Self { 44 | local_addr, 45 | local_port, 46 | pid, 47 | } 48 | } 49 | 50 | fn pname(&self) -> Option { 51 | let pid = self.pid; 52 | 53 | let h = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).ok()? }; 54 | 55 | let mut process = unsafe { zeroed::() }; 56 | process.dwSize = u32::try_from(size_of::()).ok()?; 57 | 58 | if unsafe { Process32First(h, &mut process) }.is_ok() { 59 | loop { 60 | if unsafe { Process32Next(h, &mut process) }.is_ok() { 61 | let id: u32 = process.th32ProcessID; 62 | if id == pid { 63 | break; 64 | } 65 | } else { 66 | return None; 67 | } 68 | } 69 | } 70 | 71 | unsafe { CloseHandle(h).ok()? }; 72 | 73 | let name = process.szExeFile; 74 | let len = name.iter().position(|&x| x == 0)?; 75 | 76 | String::from_utf8(name[0..len].iter().map(|e| *e as u8).collect()).ok() 77 | } 78 | 79 | pub(super) fn to_listener(&self) -> Option { 80 | let socket = SocketAddr::new(self.local_addr, self.local_port); 81 | let pname = self.pname()?; 82 | Some(Listener::new(self.pid, pname, socket)) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/platform/windows/tcp_table.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_ulong; 2 | 3 | #[derive(Copy, Clone, Debug)] 4 | #[repr(C)] 5 | pub(super) struct TcpTable { 6 | pub(super) rows_count: c_ulong, 7 | pub(super) rows: [TcpRow; 1], 8 | } 9 | 10 | #[derive(Copy, Clone, Debug)] 11 | #[repr(C)] 12 | pub(super) struct TcpRow { 13 | pub(super) state: c_ulong, 14 | pub(super) local_addr: c_ulong, 15 | pub(super) local_port: c_ulong, 16 | remote_addr: c_ulong, 17 | remote_port: c_ulong, 18 | pub(super) owning_pid: c_ulong, 19 | } 20 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::str::FromStr; 3 | 4 | use http_test_server::TestServer; 5 | use serial_test::serial; 6 | 7 | use listeners::{Listener, Process}; 8 | 9 | #[test] 10 | #[serial] 11 | fn test_consistency() { 12 | // starts test server 13 | let _test = TestServer::new().unwrap(); 14 | 15 | // retrieve all listeners and check that the set is not empty 16 | let listeners = listeners::get_all().unwrap(); 17 | assert!(!listeners.is_empty()); 18 | 19 | // check that the listeners retrieved by the different APIs are consistent 20 | for l in listeners { 21 | println!("{l}"); 22 | 23 | let ports_by_pid = listeners::get_ports_by_pid(l.process.pid).unwrap(); 24 | assert!(ports_by_pid.contains(&l.socket.port())); 25 | 26 | let ports_by_name = listeners::get_ports_by_process_name(&l.process.name).unwrap(); 27 | assert!(ports_by_name.contains(&l.socket.port())); 28 | 29 | let processes_by_port = listeners::get_processes_by_port(l.socket.port()).unwrap(); 30 | assert!(processes_by_port.contains(&l.process)); 31 | } 32 | } 33 | 34 | #[test] 35 | #[serial] 36 | fn test_http_server() { 37 | // starts test server 38 | let http_server = TestServer::new().unwrap(); 39 | let http_server_port = http_server.port(); 40 | 41 | // get the http server process by its port 42 | let processes = listeners::get_processes_by_port(http_server_port).unwrap(); 43 | assert_eq!(processes.len(), 1); 44 | let http_server_process = processes.into_iter().next().unwrap(); 45 | 46 | let http_server_name = http_server_process.name.clone(); 47 | let http_server_pid = http_server_process.pid; 48 | 49 | // get the http server port by its process name 50 | // and check that it is the same as the one of the http server 51 | let ports = listeners::get_ports_by_process_name(&http_server_name).unwrap(); 52 | assert_eq!(ports.len(), 1); 53 | assert!(ports.contains(&http_server_port)); 54 | 55 | // get the http server port by its process id 56 | // and check that it is the same as the one of the http server 57 | let ports = listeners::get_ports_by_pid(http_server_pid).unwrap(); 58 | assert_eq!(ports.len(), 1); 59 | assert!(ports.contains(&http_server_port)); 60 | 61 | // get all listeners 62 | // and check that the http server is in the list 63 | let listeners = listeners::get_all().unwrap(); 64 | let http_server_listener = listeners 65 | .iter() 66 | .find(|l| l.process.pid == http_server_pid) 67 | .unwrap(); 68 | println!("{http_server_listener}"); 69 | assert_eq!( 70 | http_server_listener, 71 | &Listener { 72 | process: Process { 73 | pid: http_server_pid, 74 | name: http_server_name 75 | }, 76 | socket: SocketAddr::from_str(&format!("127.0.0.1:{http_server_port}")).unwrap() 77 | } 78 | ); 79 | } 80 | --------------------------------------------------------------------------------