use crate::Result;
use std::collections::HashMap;
use std::net::Ipv6Addr;
use tokio::fs as tokio_fs;
use super::ProcessInfo;
pub struct ProcfsPortManager {
pid_cache: HashMap<u32, ProcessDetails>,
last_update: std::time::Instant,
cache_ttl: std::time::Duration,
}
#[derive(Debug, Clone)]
struct ProcessDetails {
name: String,
command: String,
executable_path: String,
working_directory: String,
}
impl ProcfsPortManager {
pub fn new() -> Self {
Self {
pid_cache: HashMap::new(),
last_update: std::time::Instant::now(),
cache_ttl: std::time::Duration::from_secs(2),
}
}
pub async fn list_processes(&mut self, protocol: &str) -> Result<Vec<ProcessInfo>> {
let mut processes = Vec::new();
let tcp_processes = if protocol == "tcp" || protocol == "all" {
self.read_tcp_connections().await?
} else {
Vec::new()
};
let udp_processes = if protocol == "udp" || protocol == "all" {
self.read_udp_connections().await?
} else {
Vec::new()
};
processes.extend(tcp_processes);
processes.extend(udp_processes);
self.enrich_with_process_info(&mut processes).await?;
Ok(processes)
}
pub async fn check_port(&mut self, port: u16, protocol: &str) -> Result<Option<ProcessInfo>> {
let processes = self.list_processes(protocol).await?;
Ok(processes.into_iter().find(|p| p.port == port))
}
async fn read_tcp_connections(&self) -> Result<Vec<ProcessInfo>> {
let mut processes = Vec::new();
if let Ok(content) = tokio_fs::read_to_string("/proc/net/tcp").await {
processes.extend(self.parse_tcp_content(&content, false)?);
}
if let Ok(content) = tokio_fs::read_to_string("/proc/net/tcp6").await {
processes.extend(self.parse_tcp_content(&content, true)?);
}
processes.retain(|p| self.is_listening_connection(p));
Ok(processes)
}
async fn read_udp_connections(&self) -> Result<Vec<ProcessInfo>> {
let mut processes = Vec::new();
if let Ok(content) = tokio_fs::read_to_string("/proc/net/udp").await {
processes.extend(self.parse_udp_content(&content, false)?);
}
if let Ok(content) = tokio_fs::read_to_string("/proc/net/udp6").await {
processes.extend(self.parse_udp_content(&content, true)?);
}
Ok(processes)
}
fn parse_tcp_content(&self, content: &str, is_ipv6: bool) -> Result<Vec<ProcessInfo>> {
let mut processes = Vec::new();
for line in content.lines().skip(1) {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 10 {
continue;
}
let local_address = parts[1];
let state = parts[3];
let inode = parts[9];
if let Some((address, port)) = self.parse_address(local_address, is_ipv6) {
if state == "0A" {
if let Ok(inode_num) = inode.parse::<u64>() {
processes.push(ProcessInfo {
pid: 0, name: String::new(),
command: String::new(),
executable_path: String::new(),
working_directory: String::new(),
port,
protocol: "tcp".to_string(),
address,
inode: Some(inode_num),
});
}
}
}
}
Ok(processes)
}
fn parse_udp_content(&self, content: &str, is_ipv6: bool) -> Result<Vec<ProcessInfo>> {
let mut processes = Vec::new();
for line in content.lines().skip(1) {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 10 {
continue;
}
let local_address = parts[1];
let inode = parts[9];
if let Some((address, port)) = self.parse_address(local_address, is_ipv6) {
if let Ok(inode_num) = inode.parse::<u64>() {
processes.push(ProcessInfo {
pid: 0, name: String::new(),
command: String::new(),
executable_path: String::new(),
working_directory: String::new(),
port,
protocol: "udp".to_string(),
address,
inode: Some(inode_num),
});
}
}
}
Ok(processes)
}
fn parse_address(&self, address_port: &str, is_ipv6: bool) -> Option<(String, u16)> {
let colon_pos = address_port.rfind(':')?;
let address_hex = &address_port[..colon_pos];
let port_hex = &address_port[colon_pos + 1..];
let port = u16::from_str_radix(port_hex, 16).ok()?;
let address = if is_ipv6 {
self.parse_ipv6_address(address_hex)
} else {
self.parse_ipv4_address(address_hex)
};
Some((address, port))
}
fn parse_ipv4_address(&self, hex: &str) -> String {
if hex.len() != 8 {
return "*".to_string();
}
let bytes = (0..4)
.map(|i| u8::from_str_radix(&hex[i * 2..(i + 1) * 2], 16).unwrap_or(0))
.collect::<Vec<_>>();
if bytes == [0, 0, 0, 0] {
"*".to_string()
} else {
format!("{}.{}.{}.{}", bytes[3], bytes[2], bytes[1], bytes[0])
}
}
fn parse_ipv6_address(&self, hex: &str) -> String {
if hex.len() != 32 {
return "*".to_string();
}
if hex == "00000000000000000000000000000000" {
return "*".to_string();
}
let mut bytes = [0u8; 16];
for i in 0..16 {
bytes[i] = u8::from_str_radix(&hex[i * 2..(i + 1) * 2], 16).unwrap_or(0);
}
let addr = Ipv6Addr::from(bytes);
addr.to_string()
}
fn is_listening_connection(&self, _process: &ProcessInfo) -> bool {
true
}
async fn enrich_with_process_info(&mut self, processes: &mut Vec<ProcessInfo>) -> Result<()> {
let mut inode_to_pid: HashMap<u64, u32> = HashMap::new();
if let Ok(proc_entries) = tokio_fs::read_dir("/proc").await {
let mut entries = proc_entries;
while let Ok(Some(entry)) = entries.next_entry().await {
if let Some(filename) = entry.file_name().to_str() {
if let Ok(pid) = filename.parse::<u32>() {
self.scan_process_fds(pid, &mut inode_to_pid).await;
}
}
}
}
for process in processes.iter_mut() {
if let Some(inode) = process.inode {
if let Some(&pid) = inode_to_pid.get(&inode) {
process.pid = pid;
self.update_process_details(process).await?;
}
}
}
processes.retain(|p| p.pid != 0);
Ok(())
}
async fn scan_process_fds(&self, pid: u32, inode_to_pid: &mut HashMap<u64, u32>) {
let fd_path = format!("/proc/{pid}/fd");
if let Ok(mut fd_entries) = tokio_fs::read_dir(&fd_path).await {
while let Ok(Some(fd_entry)) = fd_entries.next_entry().await {
if let Ok(link_target) = tokio_fs::read_link(fd_entry.path()).await {
if let Some(target_str) = link_target.to_str() {
if target_str.starts_with("socket:[") && target_str.ends_with(']') {
let inode_str = &target_str[8..target_str.len() - 1];
if let Ok(inode) = inode_str.parse::<u64>() {
inode_to_pid.insert(inode, pid);
}
}
}
}
}
}
}
async fn update_process_details(&mut self, process: &mut ProcessInfo) -> Result<()> {
let now = std::time::Instant::now();
if now.duration_since(self.last_update) < self.cache_ttl {
if let Some(cached) = self.pid_cache.get(&process.pid) {
process.name = cached.name.clone();
process.command = cached.command.clone();
process.executable_path = cached.executable_path.clone();
process.working_directory = cached.working_directory.clone();
return Ok(());
}
}
let details = self.read_process_details(process.pid).await?;
process.name = details.name.clone();
process.command = details.command.clone();
process.executable_path = details.executable_path.clone();
process.working_directory = details.working_directory.clone();
self.pid_cache.insert(process.pid, details);
self.last_update = now;
Ok(())
}
async fn read_process_details(&self, pid: u32) -> Result<ProcessDetails> {
let mut details = ProcessDetails {
name: "Unknown".to_string(),
command: "Unknown".to_string(),
executable_path: "Unknown".to_string(),
working_directory: "Unknown".to_string(),
};
if let Ok(name) = tokio_fs::read_to_string(format!("/proc/{pid}/comm")).await {
details.name = name.trim().to_string();
}
if let Ok(cmdline) = tokio_fs::read(format!("/proc/{pid}/cmdline")).await {
let command = String::from_utf8_lossy(&cmdline)
.replace('\0', " ")
.trim()
.to_string();
if !command.is_empty() {
details.command = command;
if let Some(first_arg) = details.command.split_whitespace().next() {
details.executable_path = first_arg.to_string();
}
}
}
if let Ok(cwd) = tokio_fs::read_link(format!("/proc/{pid}/cwd")).await {
if let Some(cwd_str) = cwd.to_str() {
details.working_directory = cwd_str.to_string();
}
}
if let Ok(exe) = tokio_fs::read_link(format!("/proc/{pid}/exe")).await {
if let Some(exe_str) = exe.to_str() {
details.executable_path = exe_str.to_string();
}
}
Ok(details)
}
pub fn get_display_path(&self, process_info: &ProcessInfo) -> String {
if process_info.working_directory != "/" && process_info.working_directory != "Unknown" {
let is_dev_process = process_info.executable_path.contains("/node")
|| process_info.executable_path.contains("/python")
|| process_info.executable_path.contains("/ruby")
|| process_info.executable_path.contains("/java")
|| process_info.command.contains("npm")
|| process_info.command.contains("yarn")
|| process_info.command.contains("pnpm")
|| process_info.command.contains("next")
|| process_info.command.contains("serve")
|| process_info.command.contains("dev");
if is_dev_process {
return process_info.working_directory.clone();
}
}
process_info.executable_path.clone()
}
pub fn clear_cache(&mut self) {
self.pid_cache.clear();
self.last_update = std::time::Instant::now() - self.cache_ttl;
}
}
impl Default for ProcfsPortManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_procfs_port_manager_creation() {
let manager = ProcfsPortManager::new();
assert!(manager.pid_cache.is_empty());
assert_eq!(manager.cache_ttl, std::time::Duration::from_secs(2));
}
#[test]
fn test_procfs_port_manager_default() {
let manager = ProcfsPortManager::default();
assert!(manager.pid_cache.is_empty());
}
#[test]
fn test_parse_ipv4_address_all_zeros() {
let manager = ProcfsPortManager::new();
let result = manager.parse_ipv4_address("00000000");
assert_eq!(result, "*");
}
#[test]
fn test_parse_ipv4_address_localhost() {
let manager = ProcfsPortManager::new();
let result = manager.parse_ipv4_address("0100007F");
assert_eq!(result, "127.0.0.1");
}
#[test]
fn test_parse_ipv4_address_invalid_length() {
let manager = ProcfsPortManager::new();
let result = manager.parse_ipv4_address("00");
assert_eq!(result, "*");
}
#[test]
fn test_parse_ipv6_address_all_zeros() {
let manager = ProcfsPortManager::new();
let result = manager.parse_ipv6_address("00000000000000000000000000000000");
assert_eq!(result, "*");
}
#[test]
fn test_parse_ipv6_address_invalid_length() {
let manager = ProcfsPortManager::new();
let result = manager.parse_ipv6_address("0000");
assert_eq!(result, "*");
}
#[test]
fn test_parse_ipv6_address_localhost() {
let manager = ProcfsPortManager::new();
let result = manager.parse_ipv6_address("00000000000000000000000000000001");
assert_eq!(result, "::1");
}
#[test]
fn test_parse_address_ipv4() {
let manager = ProcfsPortManager::new();
let result = manager.parse_address("00000000:1F90", false);
assert!(result.is_some());
let (address, port) = result.unwrap();
assert_eq!(address, "*");
assert_eq!(port, 8080);
}
#[test]
fn test_parse_address_ipv4_localhost_port_3000() {
let manager = ProcfsPortManager::new();
let result = manager.parse_address("0100007F:0BB8", false);
assert!(result.is_some());
let (address, port) = result.unwrap();
assert_eq!(address, "127.0.0.1");
assert_eq!(port, 3000);
}
#[test]
fn test_parse_address_ipv6() {
let manager = ProcfsPortManager::new();
let result = manager.parse_address("00000000000000000000000000000000:1F90", true);
assert!(result.is_some());
let (address, port) = result.unwrap();
assert_eq!(address, "*");
assert_eq!(port, 8080);
}
#[test]
fn test_parse_address_invalid() {
let manager = ProcfsPortManager::new();
let result = manager.parse_address("00000000", false);
assert!(result.is_none());
}
#[test]
fn test_parse_tcp_content_empty() {
let manager = ProcfsPortManager::new();
let content = " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n";
let result = manager.parse_tcp_content(content, false);
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn test_parse_tcp_content_listening() {
let manager = ProcfsPortManager::new();
let content = " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n 0: 00000000:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 12345 1 0000000000000000 100 0 0 10 0";
let result = manager.parse_tcp_content(content, false);
assert!(result.is_ok());
let processes = result.unwrap();
assert_eq!(processes.len(), 1);
assert_eq!(processes[0].port, 8080);
assert_eq!(processes[0].protocol, "tcp");
assert_eq!(processes[0].address, "*");
assert_eq!(processes[0].inode, Some(12345));
}
#[test]
fn test_parse_tcp_content_established_skipped() {
let manager = ProcfsPortManager::new();
let content = " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n 0: 00000000:1F90 00000000:0000 01 00000000:00000000 00:00000000 00000000 0 0 12345 1 0000000000000000 100 0 0 10 0";
let result = manager.parse_tcp_content(content, false);
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn test_parse_udp_content() {
let manager = ProcfsPortManager::new();
let content = " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops\n 0: 00000000:0035 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 54321 2 0000000000000000 0";
let result = manager.parse_udp_content(content, false);
assert!(result.is_ok());
let processes = result.unwrap();
assert_eq!(processes.len(), 1);
assert_eq!(processes[0].port, 53); assert_eq!(processes[0].protocol, "udp");
assert_eq!(processes[0].inode, Some(54321));
}
#[test]
fn test_get_display_path_dev_process() {
let manager = ProcfsPortManager::new();
let process_info = ProcessInfo {
pid: 1234,
name: "node".to_string(),
command: "node /home/user/project/server.js".to_string(),
executable_path: "/usr/bin/node".to_string(),
working_directory: "/home/user/project".to_string(),
port: 3000,
protocol: "tcp".to_string(),
address: "*".to_string(),
inode: Some(12345),
};
let result = manager.get_display_path(&process_info);
assert_eq!(result, "/home/user/project");
}
#[test]
fn test_get_display_path_system_process() {
let manager = ProcfsPortManager::new();
let process_info = ProcessInfo {
pid: 1234,
name: "nginx".to_string(),
command: "nginx: master process".to_string(),
executable_path: "/usr/sbin/nginx".to_string(),
working_directory: "/".to_string(),
port: 80,
protocol: "tcp".to_string(),
address: "*".to_string(),
inode: Some(12345),
};
let result = manager.get_display_path(&process_info);
assert_eq!(result, "/usr/sbin/nginx");
}
#[test]
fn test_clear_cache() {
let mut manager = ProcfsPortManager::new();
manager.pid_cache.insert(
1234,
ProcessDetails {
name: "test".to_string(),
command: "test".to_string(),
executable_path: "/test".to_string(),
working_directory: "/".to_string(),
},
);
assert!(!manager.pid_cache.is_empty());
manager.clear_cache();
assert!(manager.pid_cache.is_empty());
}
#[test]
fn test_is_listening_connection() {
let manager = ProcfsPortManager::new();
let process_info = ProcessInfo {
pid: 1234,
name: "test".to_string(),
command: "test".to_string(),
executable_path: "/test".to_string(),
working_directory: "/".to_string(),
port: 3000,
protocol: "tcp".to_string(),
address: "*".to_string(),
inode: Some(12345),
};
assert!(manager.is_listening_connection(&process_info));
}
}