From 0b126c20402a0dcbbeecbbaf9233981809df8400 Mon Sep 17 00:00:00 2001 From: Marco Cadetg Date: Mon, 28 Apr 2025 10:28:09 +0200 Subject: [PATCH] make it work on mac --- src/app.rs | 70 +++-- src/network/macos.rs | 638 +++++++++++++++++++++++++++++++++---------- src/network/mod.rs | 450 +++++++++++++++++++++++++----- src/ui.rs | 224 ++++++++------- 4 files changed, 1020 insertions(+), 362 deletions(-) diff --git a/src/app.rs b/src/app.rs index eae032e..693640c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -70,16 +70,11 @@ impl App { let interface = self.config.interface.clone(); let mut monitor = NetworkMonitor::new(interface)?; - // Get initial connections - self.connections = monitor.get_connections()?; + // Disable process information collection by default for better performance + monitor.set_collect_process_info(false); - // Get processes for connections - for conn in &self.connections { - // Use the platform-specific method - if let Some(process) = monitor.get_platform_process_for_connection(conn) { - self.processes.insert(process.pid, process); - } - } + // Get initial connections without process info + self.connections = monitor.get_connections()?; // Start monitoring in background thread let monitor = Arc::new(Mutex::new(monitor)); @@ -203,14 +198,6 @@ impl App { if let Some(monitor_arc) = &self.network_monitor { let mut monitor = monitor_arc.lock().unwrap(); // Lock the mutex self.connections = monitor.get_connections()?; - - // Update processes - for conn in &self.connections { - // Use the platform-specific method - if let Some(process) = monitor.get_platform_process_for_connection(conn) { - self.processes.insert(process.pid, process); - } - } } Ok(()) @@ -221,17 +208,48 @@ impl App { if let Some(monitor_arc) = &self.network_monitor { let mut monitor = monitor_arc.lock().unwrap(); // Lock the mutex self.connections = monitor.get_connections()?; - - // Clear and update processes - self.processes.clear(); - for conn in &self.connections { - // Use the platform-specific method - if let Some(process) = monitor.get_platform_process_for_connection(conn) { - self.processes.insert(process.pid, process); - } - } } Ok(()) } + + /// Get process info for selected connection + pub fn get_process_for_selected_connection(&mut self) -> Option { + if self.connections.is_empty() || self.selected_connection_idx >= self.connections.len() { + return None; + } + + // Get the selected connection + let connection = &mut self.connections[self.selected_connection_idx].clone(); + + // Check if we already have process info in our local cache + if let Some(pid) = connection.pid { + if let Some(process) = self.processes.get(&pid) { + return Some(process.clone()); + } + } + + // Otherwise, look it up on demand + if let Some(monitor_arc) = &self.network_monitor { + let monitor = monitor_arc.lock().unwrap(); + + // Look up the process info for this specific connection + if let Some(process) = monitor.get_platform_process_for_connection(connection) { + // Update our local cache + let pid = process.pid; + self.processes.insert(pid, process.clone()); + + // Update the connection in our list + if self.selected_connection_idx < self.connections.len() { + self.connections[self.selected_connection_idx].pid = Some(pid); + self.connections[self.selected_connection_idx].process_name = + Some(self.processes[&pid].name.clone()); + } + + return Some(process); + } + } + + None + } } diff --git a/src/network/macos.rs b/src/network/macos.rs index d25ffe1..903816f 100644 --- a/src/network/macos.rs +++ b/src/network/macos.rs @@ -1,68 +1,159 @@ use anyhow::Result; -use std::net::SocketAddr; +use log::{debug, info, warn}; +use std::collections::HashSet; +use std::net::IpAddr; use std::process::Command; use super::{Connection, ConnectionState, NetworkMonitor, Process, Protocol}; -impl NetworkMonitor { - /// Get connections using platform-specific methods - pub(super) fn get_platform_connections(&self, connections: &mut Vec) -> Result<()> { - // Use lsof on macOS - self.get_connections_from_lsof(connections)?; +/// Get IP addresses associated with an interface +pub fn get_interface_addresses(interface: &str) -> Result> { + let mut addresses = Vec::new(); - // Fall back to netstat if needed - if connections.is_empty() { - self.get_connections_from_netstat(connections)?; - } + // Use ifconfig to get interface IP addresses on macOS + let output = Command::new("ifconfig").arg(interface).output()?; - Ok(()) - } + if output.status.success() { + let text = String::from_utf8_lossy(&output.stdout); + debug!("ifconfig output for {}: {}", interface, text); - /// Get platform-specific process for a connection - pub(super) fn get_platform_process_for_connection( - &self, - connection: &Connection, - ) -> Option { - // Try lsof first (more detailed) - if let Some(process) = try_lsof_command(connection) { - return Some(process); - } - - // Fall back to netstat - try_netstat_command(connection) - } - - /// Get process information by PID - pub(super) fn get_process_by_pid(&self, pid: u32) -> Option { - // Use ps to get process info - if let Ok(output) = Command::new("ps") - .args(["-p", &pid.to_string(), "-o", "comm=,user="]) - .output() - { - let text = String::from_utf8_lossy(&output.stdout); - let line = text.trim(); - - let parts: Vec<&str> = line.split_whitespace().collect(); - if !parts.is_empty() { - let name = parts[0].to_string(); - let user = parts.get(1).map(|s| s.to_string()); - - return Some(Process { - pid, - name, - command_line: None, - user, - cpu_usage: None, - memory_usage: None, - }); + // Parse IPv4 addresses + for line in text.lines() { + if line.contains("inet ") && !line.contains("127.0.0.1") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + if let Ok(ip) = parts[1].parse() { + debug!("Found IPv4 address: {}", ip); + addresses.push(ip); + } + } + } + // Parse IPv6 addresses + else if line.contains("inet6 ") && !line.contains("::1") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + if let Ok(ip) = parts[1].parse() { + debug!("Found IPv6 address: {}", ip); + addresses.push(ip); + } + } + } + } + } else { + warn!("ifconfig command failed for interface {}", interface); + // Try fallback with ipconfig getifaddr + if let Ok(output) = Command::new("ipconfig") + .args(["getifaddr", interface]) + .output() + { + if output.status.success() { + let ip_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if let Ok(ip) = ip_str.parse() { + addresses.push(ip); + } } } - - None } + // Add loopback addresses for completeness + addresses.push("127.0.0.1".parse().unwrap()); + addresses.push("::1".parse().unwrap()); + + Ok(addresses) +} + +/// Get platform-specific connections for macOS +pub fn get_platform_connections( + connections: &mut Vec, + interface: &Option, +) -> Result<()> { + // Create a temporary NetworkMonitor to use its methods + // We only need this to access network-related methods + let monitor = NetworkMonitor::new(interface.clone())?; + + // Try different commands to maximize connection detection + // First try netstat - more reliable on macOS than lsof in some cases + monitor.get_connections_from_netstat(connections)?; + debug!("Found {} connections from netstat", connections.len()); + + // Then try lsof for additional connections + let before_count = connections.len(); + monitor.get_connections_from_lsof(connections)?; + debug!( + "Found {} additional connections from lsof", + connections.len() - before_count + ); + + // Filter by interface if specified + if let Some(iface) = interface { + debug!("Filtering connections for interface: {}", iface); + let connection_count_before = connections.len(); + + // Get interface addresses + let interface_addresses = match get_interface_addresses(iface) { + Ok(addrs) => { + debug!( + "Interface {} has {} addresses: {:?}", + iface, + addrs.len(), + addrs + ); + addrs + } + Err(e) => { + warn!("Failed to get addresses for interface {}: {}", iface, e); + Vec::new() + } + }; + + if !interface_addresses.is_empty() { + // Filter connections only if we found interface addresses + connections.retain(|conn| { + let local_ip = conn.local_addr.ip(); + let is_interface_match = interface_addresses.iter().any(|&addr| local_ip == addr); + let is_unspecified = local_ip.is_unspecified(); + + is_interface_match || is_unspecified + }); + + info!( + "Interface filtering: {} -> {} connections for interface {}", + connection_count_before, + connections.len(), + iface + ); + } else { + // If we couldn't get interface addresses, don't filter + info!( + "Could not determine IP addresses for interface {}, showing all connections", + iface + ); + } + } + + // If still no connections, try using ss command with less filtering + if connections.is_empty() { + debug!("No connections found with standard methods, trying alternative approaches"); + monitor.get_connections_from_ss(connections)?; + } + + Ok(()) +} + +impl NetworkMonitor { /// Get connections from lsof command fn get_connections_from_lsof(&self, connections: &mut Vec) -> Result<()> { + // Track unique connections to avoid duplicates + let mut seen_connections = HashSet::new(); + for conn in connections.iter() { + let key = format!( + "{:?}:{}-{:?}:{}", + conn.protocol, conn.local_addr, conn.protocol, conn.remote_addr + ); + seen_connections.insert(key); + } + + // Use more aggressive lsof command with less filtering let output = Command::new("lsof").args(["-i", "-n", "-P"]).output()?; if output.status.success() { @@ -71,7 +162,7 @@ impl NetworkMonitor { for line in text.lines().skip(1) { // Skip header let fields: Vec<&str> = line.split_whitespace().collect(); - if fields.len() < 9 { + if fields.len() < 8 { continue; } @@ -79,64 +170,89 @@ impl NetworkMonitor { let process_name = fields[0].to_string(); let pid = fields[1].parse::().unwrap_or(0); - // Parse protocol and addresses - let proto_addr = fields[8]; - if let Some(proto_end) = proto_addr.find(' ') { - let proto_str = &proto_addr[..proto_end]; - let protocol = match proto_str.to_lowercase().as_str() { - "tcp" | "tcp6" | "tcp4" => Protocol::TCP, - "udp" | "udp6" | "udp4" => Protocol::UDP, - _ => continue, - }; + // Find the field with connection info - format usually has (LISTEN), (ESTABLISHED) etc. + let proto_addr_idx = 8; + if fields.len() <= proto_addr_idx { + continue; + } - // Parse connection state - let state = match fields.get(9) { - Some(&"(ESTABLISHED)") => ConnectionState::Established, - Some(&"(LISTEN)") => ConnectionState::Listen, - Some(&"(TIME_WAIT)") => ConnectionState::TimeWait, - Some(&"(CLOSE_WAIT)") => ConnectionState::CloseWait, - Some(&"(SYN_SENT)") => ConnectionState::SynSent, - Some(&"(SYN_RECEIVED)") | Some(&"(SYN_RECV)") => { - ConnectionState::SynReceived - } - Some(&"(FIN_WAIT_1)") => ConnectionState::FinWait1, - Some(&"(FIN_WAIT_2)") => ConnectionState::FinWait2, - Some(&"(LAST_ACK)") => ConnectionState::LastAck, - Some(&"(CLOSING)") => ConnectionState::Closing, + let proto_addr = fields[proto_addr_idx]; + let proto_end = match proto_addr.find(' ') { + Some(pos) => pos, + None => continue, + }; + + let proto_str = &proto_addr[..proto_end].to_lowercase(); + let protocol = if proto_str == "tcp" || proto_str == "tcp4" || proto_str == "tcp6" { + Protocol::TCP + } else if proto_str == "udp" || proto_str == "udp4" || proto_str == "udp6" { + Protocol::UDP + } else { + continue; + }; + + // Parse connection state + let state = if fields.len() > proto_addr_idx + 1 { + match fields[proto_addr_idx + 1] { + "(ESTABLISHED)" => ConnectionState::Established, + "(LISTEN)" => ConnectionState::Listen, + "(TIME_WAIT)" => ConnectionState::TimeWait, + "(CLOSE_WAIT)" => ConnectionState::CloseWait, + "(SYN_SENT)" => ConnectionState::SynSent, + "(SYN_RECEIVED)" | "(SYN_RECV)" => ConnectionState::SynReceived, + "(FIN_WAIT_1)" => ConnectionState::FinWait1, + "(FIN_WAIT_2)" => ConnectionState::FinWait2, + "(LAST_ACK)" => ConnectionState::LastAck, + "(CLOSING)" => ConnectionState::Closing, _ => ConnectionState::Unknown, - }; + } + } else { + ConnectionState::Unknown + }; - // Parse addresses - if let Some(addr_part) = proto_addr.find("->") { - // Has local and remote address - let addr_str = &proto_addr[proto_end + 1..]; - let parts: Vec<&str> = addr_str.split("->").collect(); - if parts.len() == 2 { - if let (Some(local), Some(remote)) = - (self.parse_addr(parts[0]), self.parse_addr(parts[1])) - { + // Parse addresses + if proto_addr.find("->").is_some() { + // Has local and remote address (ESTABLISHED connection) + let addr_str = &proto_addr[proto_end + 1..]; + let parts: Vec<&str> = addr_str.split("->").collect(); + if parts.len() == 2 { + if let (Some(local), Some(remote)) = + (self.parse_addr(parts[0]), self.parse_addr(parts[1])) + { + // Check if this connection is already in our list + let conn_key = + format!("{:?}:{}-{:?}:{}", protocol, local, protocol, remote); + + if !seen_connections.contains(&conn_key) { let mut conn = Connection::new(protocol, local, remote, state); conn.pid = Some(pid); conn.process_name = Some(process_name); connections.push(conn); + seen_connections.insert(conn_key); } } - } else { - // Only local address (likely LISTEN) - let addr_str = &proto_addr[proto_end + 1..]; - if let Some(local) = self.parse_addr(addr_str) { - // Use 0.0.0.0:0 as remote for listening sockets - let remote = if local.ip().is_ipv4() { - "0.0.0.0:0".parse().unwrap() - } else { - "[::]:0".parse().unwrap() - }; + } + } else { + // Only local address (likely LISTEN) + let addr_str = &proto_addr[proto_end + 1..]; + if let Some(local) = self.parse_addr(addr_str) { + // Use 0.0.0.0:0 as remote for listening sockets + let remote = if local.ip().is_ipv4() { + "0.0.0.0:0".parse().unwrap() + } else { + "[::]:0".parse().unwrap() + }; - let mut conn = - Connection::new(protocol, local, remote, ConnectionState::Listen); + // Check if this connection is already in our list + let conn_key = + format!("{:?}:{}-{:?}:{}", protocol, local, protocol, remote); + + if !seen_connections.contains(&conn_key) { + let mut conn = Connection::new(protocol, local, remote, state); conn.pid = Some(pid); conn.process_name = Some(process_name); connections.push(conn); + seen_connections.insert(conn_key); } } } @@ -148,7 +264,13 @@ impl NetworkMonitor { /// Get connections from netstat command fn get_connections_from_netstat(&self, connections: &mut Vec) -> Result<()> { - let output = Command::new("netstat").args(["-p", "tcp", "-n"]).output()?; + // Track unique connections to avoid duplicates + let mut seen_connections = HashSet::new(); + + // Get TCP connections + let output = Command::new("netstat") + .args(["-anv", "-p", "tcp"]) + .output()?; if output.status.success() { let text = String::from_utf8_lossy(&output.stdout); @@ -164,9 +286,9 @@ impl NetworkMonitor { let protocol = Protocol::TCP; // Parse state - let state_pos = 5; - let state = if fields.len() > state_pos { - match fields[state_pos] { + let state_idx = 5; // Index where state info is typically found + let state = if fields.len() > state_idx { + match fields[state_idx] { "ESTABLISHED" => ConnectionState::Established, "LISTEN" => ConnectionState::Listen, "TIME_WAIT" => ConnectionState::TimeWait, @@ -187,17 +309,29 @@ impl NetworkMonitor { let local_idx = 3; let remote_idx = 4; + if fields.len() <= local_idx || fields.len() <= remote_idx { + continue; + } + if let (Some(local), Some(remote)) = ( self.parse_addr(fields[local_idx]), self.parse_addr(fields[remote_idx]), ) { - connections.push(Connection::new(protocol, local, remote, state)); + // Check if this connection is already in our list + let conn_key = format!("{:?}:{}-{:?}:{}", protocol, local, protocol, remote); + + if !seen_connections.contains(&conn_key) { + connections.push(Connection::new(protocol, local, remote, state)); + seen_connections.insert(conn_key); + } } } } - // Also get UDP connections - let output = Command::new("netstat").args(["-p", "udp", "-n"]).output()?; + // Get UDP connections + let output = Command::new("netstat") + .args(["-anv", "-p", "udp"]) + .output()?; if output.status.success() { let text = String::from_utf8_lossy(&output.stdout); @@ -215,6 +349,10 @@ impl NetworkMonitor { // Parse local address let local_idx = 3; + if fields.len() <= local_idx { + continue; + } + if let Some(local) = self.parse_addr(fields[local_idx]) { // Use 0.0.0.0:0 as remote for UDP let remote = if local.ip().is_ipv4() { @@ -223,12 +361,137 @@ impl NetworkMonitor { "[::]:0".parse().unwrap() }; - connections.push(Connection::new( - protocol, - local, - remote, - ConnectionState::Unknown, - )); + // Check if this connection is already in our list + let conn_key = format!("{:?}:{}-{:?}:{}", protocol, local, protocol, remote); + + if !seen_connections.contains(&conn_key) { + connections.push(Connection::new( + protocol, + local, + remote, + ConnectionState::Unknown, + )); + seen_connections.insert(conn_key); + } + } + } + } + + Ok(()) + } + + /// Try ss command as an alternative (if available on system) + fn get_connections_from_ss(&self, connections: &mut Vec) -> Result<()> { + // Check if ss command is available + let ss_check = Command::new("which").arg("ss").output(); + if ss_check.is_err() || !ss_check.unwrap().status.success() { + debug!("ss command not available"); + return Ok(()); + } + + let mut seen_connections = HashSet::new(); + for conn in connections.iter() { + let key = format!( + "{:?}:{}-{:?}:{}", + conn.protocol, conn.local_addr, conn.protocol, conn.remote_addr + ); + seen_connections.insert(key); + } + + // Try ss command for TCP + if let Ok(output) = Command::new("ss").args(["-tn"]).output() { + if output.status.success() { + let text = String::from_utf8_lossy(&output.stdout); + for line in text.lines().skip(1) { + // Skip header + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() < 5 { + continue; + } + + // Extract state, local and remote addresses + let state_str = fields[0]; + let local_addr_str = fields[3]; + let remote_addr_str = fields[4]; + + if let (Some(local), Some(remote)) = ( + self.parse_addr(local_addr_str), + self.parse_addr(remote_addr_str), + ) { + // Determine connection state + let state = match state_str { + "ESTAB" => ConnectionState::Established, + "LISTEN" => ConnectionState::Listen, + "TIME-WAIT" => ConnectionState::TimeWait, + "CLOSE-WAIT" => ConnectionState::CloseWait, + "SYN-SENT" => ConnectionState::SynSent, + "SYN-RECV" => ConnectionState::SynReceived, + "FIN-WAIT-1" => ConnectionState::FinWait1, + "FIN-WAIT-2" => ConnectionState::FinWait2, + "LAST-ACK" => ConnectionState::LastAck, + "CLOSING" => ConnectionState::Closing, + _ => ConnectionState::Unknown, + }; + + // Add connection if not already seen + let conn_key = format!( + "{:?}:{}-{:?}:{}", + Protocol::TCP, + local, + Protocol::TCP, + remote + ); + + if !seen_connections.contains(&conn_key) { + connections.push(Connection::new(Protocol::TCP, local, remote, state)); + seen_connections.insert(conn_key); + } + } + } + } + } + + // Try ss command for UDP + if let Ok(output) = Command::new("ss").args(["-un"]).output() { + if output.status.success() { + let text = String::from_utf8_lossy(&output.stdout); + for line in text.lines().skip(1) { + // Skip header + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() < 4 { + continue; + } + + // Extract local address + let local_addr_str = fields[3]; + + if let Some(local) = self.parse_addr(local_addr_str) { + // Use 0.0.0.0:0 as remote for UDP + let remote = if local.ip().is_ipv4() { + "0.0.0.0:0".parse().unwrap() + } else { + "[::]:0".parse().unwrap() + }; + + // Add connection if not already seen + let conn_key = format!( + "{:?}:{}-{:?}:{}", + Protocol::UDP, + local, + Protocol::UDP, + remote + ); + + if !seen_connections.contains(&conn_key) { + connections.push(Connection::new( + Protocol::UDP, + local, + remote, + ConnectionState::Unknown, + )); + seen_connections.insert(conn_key); + } + } } } } @@ -238,9 +501,79 @@ impl NetworkMonitor { } /// Get process information using lsof command -fn try_lsof_command(connection: &Connection) -> Option { - let output = Command::new("lsof") - .args(["-i", "-n", "-P"]) +pub(super) fn try_lsof_command(connection: &Connection) -> Option { + // Build lsof command with specific filters + let local_port = connection.local_addr.port(); + let is_listening = connection.state == ConnectionState::Listen; + + // Different command based on whether it's LISTEN or ESTABLISHED + let args; + let port_spec; + + if is_listening { + port_spec = format!(":{}", local_port); + args = vec!["-i", &port_spec, "-n", "-P"]; + } else { + let remote_port = connection.remote_addr.port(); + if remote_port == 0 { + port_spec = format!(":{}", local_port); + args = vec!["-i", &port_spec, "-n", "-P"]; + } else { + port_spec = format!(":{}->{}", local_port, remote_port); + args = vec!["-i", &port_spec, "-n", "-P"]; + } + } + + let output = Command::new("lsof").args(&args).output().ok()?; + + if output.status.success() { + let text = String::from_utf8_lossy(&output.stdout); + for line in text.lines().skip(1) { + // Skip header + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() < 2 { + continue; + } + + // Get process name and PID + let process_name = fields[0].to_string(); + if let Ok(pid) = fields[1].parse::() { + // Try to get user + let user = if fields.len() > 2 { + Some(fields[2].to_string()) + } else { + None + }; + + return Some(Process { + pid, + name: process_name, + command_line: None, + user, + cpu_usage: None, + memory_usage: None, + }); + } + } + } + + // If we couldn't find it with lsof, try alternate methods + None +} + +/// Get process information using netstat command +pub(super) fn try_netstat_command(connection: &Connection) -> Option { + // macOS netstat doesn't show process info directly + // We need to use a combination with ps + + // First try lsof as that works best + if let Some(process) = try_lsof_command(connection) { + return Some(process); + } + + // If lsof failed, try netstat -p tcp -v which shows PIDs on newer macOS + let output = Command::new("netstat") + .args(["-p", "tcp", "-v"]) .output() .ok()?; @@ -249,34 +582,36 @@ fn try_lsof_command(connection: &Connection) -> Option { let local_port = connection.local_addr.port(); let remote_port = connection.remote_addr.port(); - for line in text.lines().skip(1) { - // Skip header - if line.contains(&format!(":{}", local_port)) - && (remote_port == 0 || line.contains(&format!(":{}", remote_port))) - { - let fields: Vec<&str> = line.split_whitespace().collect(); - if fields.len() < 2 { - continue; - } + for line in text.lines().skip(2) { + // Skip headers + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() < 9 { + // Need at least 9 fields for PID + continue; + } - // Get process name and PID - let process_name = fields[0].to_string(); - if let Ok(pid) = fields[1].parse::() { - // Try to get user - let user = if fields.len() > 2 { - Some(fields[2].to_string()) - } else { - None - }; - - return Some(Process { - pid, - name: process_name, - command_line: None, - user, - cpu_usage: None, - memory_usage: None, - }); + // Check if local port matches + if let Some(local_addr) = fields.get(3) { + if local_addr.contains(&format!(":{}", local_port)) + && (remote_port == 0 + || fields + .get(4) + .map_or(false, |addr| addr.contains(&format!(":{}", remote_port)))) + { + // Try to get PID from the field where it's usually stored + if let Some(pid_str) = fields.get(8) { + if let Ok(pid) = pid_str.parse::() { + // Now get process name using ps + return get_process_name_by_pid(pid).map(|name| Process { + pid, + name, + command_line: None, + user: None, + cpu_usage: None, + memory_usage: None, + }); + } + } } } } @@ -285,22 +620,23 @@ fn try_lsof_command(connection: &Connection) -> Option { None } -/// Get process information using netstat command -fn try_netstat_command(connection: &Connection) -> Option { - // macOS netstat doesn't show process info, so we need to combine with ps - // This is a limited implementation since macOS netstat doesn't show PIDs - - // Use lsof as the main tool for macOS - try_lsof_command(connection) -} - /// Get process name by PID -fn get_process_name_by_pid(pid: u32) -> Option { +#[allow(dead_code)] +pub(super) fn get_process_name_by_pid(pid: u32) -> Option { let output = Command::new("ps") .args(["-p", &pid.to_string(), "-o", "comm="]) .output() .ok()?; + if !output.status.success() { + return None; + } + let text = String::from_utf8_lossy(&output.stdout); - Some(text.trim().to_string()) + let name = text.trim(); + if name.is_empty() { + None + } else { + Some(name.to_string()) + } } diff --git a/src/network/mod.rs b/src/network/mod.rs index 9323b25..9d63e4b 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -1,10 +1,10 @@ use anyhow::{anyhow, Result}; -use log::{debug, error, info, trace, warn}; +use log::{debug, error, info}; use maxminddb::geoip2; -use pcap::{Capture, Device}; +use pcap::{Capture, Device, Packet}; use std::collections::HashMap; use std::net::{IpAddr, SocketAddr}; -use std::time::{Duration, SystemTime}; +use std::time::{Duration, Instant, SystemTime}; #[cfg(target_os = "linux")] mod linux; @@ -17,7 +17,7 @@ use windows::*; #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "macos")] -use macos::*; +use macos::get_interface_addresses; /// Connection protocol #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -53,6 +53,7 @@ pub enum ConnectionState { LastAck, Listen, Closing, + Reset, // Added Reset variant Unknown, } @@ -70,6 +71,7 @@ impl std::fmt::Display for ConnectionState { ConnectionState::LastAck => write!(f, "LAST_ACK"), ConnectionState::Listen => write!(f, "LISTEN"), ConnectionState::Closing => write!(f, "CLOSING"), + ConnectionState::Reset => write!(f, "RESET"), ConnectionState::Unknown => write!(f, "UNKNOWN"), } } @@ -135,6 +137,27 @@ impl Connection { pub fn is_active(&self) -> bool { self.idle_time() < Duration::from_secs(60) } + + /// Update the connection with packet data + pub fn update_with_packet(&mut self, is_outgoing: bool, packet_size: usize) { + self.last_activity = SystemTime::now(); + + if is_outgoing { + self.packets_sent += 1; + self.bytes_sent += packet_size as u64; + } else { + self.packets_received += 1; + self.bytes_received += packet_size as u64; + } + } + + /// Get connection key for HashMap lookups + pub fn get_key(&self) -> String { + format!( + "{:?}:{}:{:?}:{}", + self.protocol, self.local_addr, self.protocol, self.remote_addr + ) + } } /// Process information @@ -165,6 +188,8 @@ pub struct NetworkMonitor { capture: Option>, connections: HashMap, geo_db: Option>>, + collect_process_info: bool, + last_packet_check: Instant, } impl NetworkMonitor { @@ -178,27 +203,27 @@ impl NetworkMonitor { .ok_or_else(|| anyhow!("Interface not found: {}", iface))?; info!("Opening capture on interface: {}", iface); - Some( - Capture::from_device(device)? - .immediate_mode(true) - .timeout(500) - .snaplen(65535) - .promisc(true) - .open()?, - ) + let cap = Capture::from_device(device)? + .immediate_mode(true) + .timeout(100) + .snaplen(65535) + .promisc(true) + .open()?; + + Some(cap) } else { // Get default interface if none specified let device = Device::lookup()?.ok_or_else(|| anyhow!("No default device found"))?; info!("Opening capture on default interface: {}", device.name); - Some( - Capture::from_device(device)? - .immediate_mode(true) - .timeout(500) - .snaplen(65535) - .promisc(true) - .open()?, - ) + let cap = Capture::from_device(device)? + .immediate_mode(true) + .timeout(100) + .snaplen(65535) + .promisc(true) + .open()?; + + Some(cap) }; // Set BPF filter to capture all TCP and UDP traffic @@ -214,7 +239,7 @@ impl NetworkMonitor { .ok() .map(|data| maxminddb::Reader::from_source(data).ok()) .flatten(); - + if geo_db.is_some() { info!("Loaded MaxMind GeoIP database"); } else { @@ -226,9 +251,16 @@ impl NetworkMonitor { capture, connections: HashMap::new(), geo_db, + collect_process_info: false, + last_packet_check: Instant::now(), }) } + /// Set whether to collect process information for connections + pub fn set_collect_process_info(&mut self, collect: bool) { + self.collect_process_info = collect; + } + /// Get network device list pub fn get_devices() -> Result> { let devices = Device::list()?; @@ -237,54 +269,266 @@ impl NetworkMonitor { /// Get active connections pub fn get_connections(&mut self) -> Result> { - // Get connections from system + // Process packets from capture + self.process_packets()?; + + // Get connections from system methods let mut connections = Vec::new(); // Use platform-specific code to get connections self.get_platform_connections(&mut connections)?; - // Update with processes - for conn in &mut connections { - if conn.pid.is_none() { - // Use the platform-specific method - if let Some(process) = self.get_platform_process_for_connection(conn) { - conn.pid = Some(process.pid); - conn.process_name = Some(process.name.clone()); + // Add connections from packet capture + for (_, conn) in &self.connections { + // Check if this connection exists in the list already + let exists = connections.iter().any(|c| { + c.protocol == conn.protocol + && c.local_addr == conn.local_addr + && c.remote_addr == conn.remote_addr + }); + + if !exists && conn.is_active() { + connections.push(conn.clone()); + } + } + + // Update with processes only if flag is set + if self.collect_process_info { + for conn in &mut connections { + if conn.pid.is_none() { + // Use the platform-specific method + if let Some(process) = self.get_platform_process_for_connection(conn) { + conn.pid = Some(process.pid); + conn.process_name = Some(process.name.clone()); + } } } } + // Sort connections by last activity + connections.sort_by(|a, b| b.last_activity.cmp(&a.last_activity)); + Ok(connections) } - /// Parse socket address from string - fn parse_addr(&self, addr_str: &str) -> Option { - // Handle different address formats - let addr_str = addr_str.trim_end_matches('.'); - - if addr_str == "*" || addr_str == "*:*" { - // Default to 0.0.0.0:0 for wildcard - return Some(SocketAddr::from(([0, 0, 0, 0], 0))); + /// Process packets from capture + fn process_packets(&mut self) -> Result<()> { + // Only check packets every 100ms to avoid too frequent checks + if self.last_packet_check.elapsed() < Duration::from_millis(100) { + return Ok(()); } + self.last_packet_check = Instant::now(); - // Try to parse directly - if let Ok(addr) = addr_str.parse::() { - return Some(addr); - } + // Define a helper function to process a single packet + // This avoids the borrowing issues + let process_single_packet = + |data: &[u8], + connections: &mut HashMap, + interface: &Option| { + // Check if it's an ethernet frame + if data.len() < 14 { + return; // Too short for Ethernet + } - // Try to parse IPv4:port format - if let Some(colon_pos) = addr_str.rfind(':') { - let ip_part = &addr_str[..colon_pos]; - let port_part = &addr_str[colon_pos + 1..]; + // Skip Ethernet header (14 bytes) to get to IP header + let ip_data = &data[14..]; - if let (Ok(ip), Ok(port)) = (ip_part.parse::(), port_part.parse::()) { - return Some(SocketAddr::new(ip, port)); + // Make sure we have enough data for an IP header + if ip_data.len() < 20 { + return; // Too short for IP + } + + // Check if it's IPv4 + let version_ihl = ip_data[0]; + let version = version_ihl >> 4; + if version != 4 { + return; // Not IPv4 + } + + // Extract protocol (TCP=6, UDP=17) + let protocol = ip_data[9]; + + // Extract source and destination IP + let src_ip = IpAddr::from([ip_data[12], ip_data[13], ip_data[14], ip_data[15]]); + let dst_ip = IpAddr::from([ip_data[16], ip_data[17], ip_data[18], ip_data[19]]); + + // Calculate IP header length + let ihl = version_ihl & 0x0F; + let ip_header_len = (ihl as usize) * 4; + + // Skip to TCP/UDP header + let transport_data = &ip_data[ip_header_len..]; + if transport_data.len() < 8 { + return; // Too short for TCP/UDP + } + + // Determine if packet is outgoing based on IP address + // For now using a simple heuristic - consider private IPs as local + let is_outgoing = match src_ip { + IpAddr::V4(ipv4) => { + let octets = ipv4.octets(); + // 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8 + octets[0] == 10 + || (octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31) + || (octets[0] == 192 && octets[1] == 168) + || octets[0] == 127 + } + IpAddr::V6(_) => false, // Simplification + }; + + match protocol { + 6 => { + // TCP + if transport_data.len() < 20 { + return; // Too short for TCP + } + + // Extract ports + let src_port = ((transport_data[0] as u16) << 8) | transport_data[1] as u16; + let dst_port = ((transport_data[2] as u16) << 8) | transport_data[3] as u16; + + // Extract TCP flags + let flags = transport_data[13]; + + // Determine connection state from flags + let state = match flags { + 0x02 => ConnectionState::SynSent, // SYN + 0x12 => ConnectionState::SynReceived, // SYN+ACK + 0x10 => ConnectionState::Established, // ACK + 0x01 => ConnectionState::FinWait1, // FIN + 0x11 => ConnectionState::FinWait2, // FIN+ACK + 0x04 => ConnectionState::Reset, // RST + 0x14 => ConnectionState::Closing, // RST+ACK + _ => ConnectionState::Established, // Default to established + }; + + // Determine local and remote addresses + let (local_addr, remote_addr) = if is_outgoing { + ( + SocketAddr::new(src_ip, src_port), + SocketAddr::new(dst_ip, dst_port), + ) + } else { + ( + SocketAddr::new(dst_ip, dst_port), + SocketAddr::new(src_ip, src_port), + ) + }; + + // Create or update connection + let conn_key = format!( + "{:?}:{}-{:?}:{}", + Protocol::TCP, + local_addr, + Protocol::TCP, + remote_addr + ); + + if let Some(conn) = connections.get_mut(&conn_key) { + conn.last_activity = SystemTime::now(); + if is_outgoing { + conn.packets_sent += 1; + conn.bytes_sent += data.len() as u64; + } else { + conn.packets_received += 1; + conn.bytes_received += data.len() as u64; + } + conn.state = state; + } else { + let mut conn = + Connection::new(Protocol::TCP, local_addr, remote_addr, state); + conn.last_activity = SystemTime::now(); + if is_outgoing { + conn.packets_sent += 1; + conn.bytes_sent += data.len() as u64; + } else { + conn.packets_received += 1; + conn.bytes_received += data.len() as u64; + } + connections.insert(conn_key, conn); + } + } + 17 => { + // UDP + // Extract ports + let src_port = ((transport_data[0] as u16) << 8) | transport_data[1] as u16; + let dst_port = ((transport_data[2] as u16) << 8) | transport_data[3] as u16; + + // Determine local and remote addresses + let (local_addr, remote_addr) = if is_outgoing { + ( + SocketAddr::new(src_ip, src_port), + SocketAddr::new(dst_ip, dst_port), + ) + } else { + ( + SocketAddr::new(dst_ip, dst_port), + SocketAddr::new(src_ip, src_port), + ) + }; + + // Create or update connection + let conn_key = format!( + "{:?}:{}-{:?}:{}", + Protocol::UDP, + local_addr, + Protocol::UDP, + remote_addr + ); + + if let Some(conn) = connections.get_mut(&conn_key) { + conn.last_activity = SystemTime::now(); + if is_outgoing { + conn.packets_sent += 1; + conn.bytes_sent += data.len() as u64; + } else { + conn.packets_received += 1; + conn.bytes_received += data.len() as u64; + } + } else { + let mut conn = Connection::new( + Protocol::UDP, + local_addr, + remote_addr, + ConnectionState::Unknown, + ); + conn.last_activity = SystemTime::now(); + if is_outgoing { + conn.packets_sent += 1; + conn.bytes_sent += data.len() as u64; + } else { + conn.packets_received += 1; + conn.bytes_received += data.len() as u64; + } + connections.insert(conn_key, conn); + } + } + _ => {} // Ignore other protocols + } + }; + + // Get packets from the capture + if let Some(ref mut cap) = self.capture { + // Process up to 100 packets + for _ in 0..100 { + match cap.next_packet() { + Ok(packet) => { + // Use the local helper function to avoid borrowing issues + process_single_packet(packet.data, &mut self.connections, &self.interface); + } + Err(_) => { + break; // No more packets or error + } + } } } - None + Ok(()) } + /// We don't need this method anymore since packet processing is done inline + // fn process_packet(&mut self, packet: Packet) { ... } + /// Get platform-specific process for a connection pub fn get_platform_process_for_connection(&self, connection: &Connection) -> Option { #[cfg(target_os = "linux")] @@ -315,23 +559,67 @@ impl NetworkMonitor { } } + /// Get platform-specific connections + fn get_platform_connections(&mut self, connections: &mut Vec) -> Result<()> { + #[cfg(target_os = "linux")] + { + // Use Linux-specific implementation + linux::get_platform_connections(connections, &self.interface)?; + } + #[cfg(target_os = "macos")] + { + // Use macOS-specific implementation + macos::get_platform_connections(connections, &self.interface)?; + } + #[cfg(target_os = "windows")] + { + // Use Windows-specific implementation + windows::get_platform_connections(connections, &self.interface)?; + } + + Ok(()) + } + + /// Get IP addresses associated with an interface + fn get_interface_addresses(&self, interface: &str) -> Result> { + #[cfg(target_os = "linux")] + { + // Linux implementation + unimplemented!() + } + #[cfg(target_os = "macos")] + { + // Use macOS implementation + macos::get_interface_addresses(interface) + } + #[cfg(target_os = "windows")] + { + // Windows implementation + unimplemented!() + } + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + { + Ok(Vec::new()) + } + } + /// Get location information for an IP address pub fn get_ip_location(&self, ip: IpAddr) -> Option { if let Some(ref reader) = self.geo_db { // Access fields directly on the lookup result (geoip2::City) if let Ok(lookup_result) = reader.lookup::(ip) { - let country = lookup_result.country.as_ref().and_then(|c| { - let code = c.iso_code.map(String::from); - let name = c - .names - .as_ref() - .and_then(|n| n.get("en").map(|s| s.to_string())); - if code.is_some() || name.is_some() { - Some((code, name)) - } else { - None - } - }); + let country_code = lookup_result + .country + .as_ref() + .and_then(|c| c.iso_code) + .map(|s| s.to_string()); + + let country_name = lookup_result + .country + .as_ref() + .and_then(|c| c.names.as_ref()) + .and_then(|n| n.get("en")) + .map(|s| s.to_string()); let city_name = lookup_result .city @@ -340,17 +628,16 @@ impl NetworkMonitor { .and_then(|n| n.get("en")) .map(|s| s.to_string()); - let location = lookup_result - .location - .as_ref() - .map(|l| (l.latitude, l.longitude)); + let latitude = lookup_result.location.as_ref().and_then(|l| l.latitude); + + let longitude = lookup_result.location.as_ref().and_then(|l| l.longitude); return Some(IpLocation { - country_code: country.as_ref().and_then(|(code, _)| code.clone()), - country_name: country.as_ref().and_then(|(_, name)| name.clone()), + country_code, + country_name, city_name, - latitude: location.and_then(|(lat, _)| lat), - longitude: location.and_then(|(_, lon)| lon), + latitude, + longitude, isp: None, // Not available in GeoLite2-City }); } @@ -358,4 +645,31 @@ impl NetworkMonitor { None } + + /// Parse an address string into a SocketAddr + fn parse_addr(&self, addr_str: &str) -> Option { + // Handle IPv6 address format [addr]:port + let addr_str = addr_str.trim(); + + // Direct parse attempt + if let Ok(addr) = addr_str.parse() { + return Some(addr); + } + + // Handle common formats + if addr_str.contains(':') { + // Try parsing as "addr:port" + return addr_str.parse().ok(); + } else { + // If only port is provided, assume 127.0.0.1:port + if let Ok(port) = addr_str.parse::() { + return Some(std::net::SocketAddr::new( + std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), + port, + )); + } + } + + None + } } diff --git a/src/ui.rs b/src/ui.rs index 8ebd251..f2426e7 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -440,7 +440,7 @@ fn draw_connection_details(f: &mut Frame, app: &App, area: Rect) -> Result<()> { } /// Draw process details view -fn draw_process_details(f: &mut Frame, app: &App, area: Rect) -> Result<()> { +fn draw_process_details(f: &mut Frame, app: &mut App, area: Rect) -> Result<()> { if app.connections.is_empty() { let text = Paragraph::new(app.i18n.get("no_processes")) .block( @@ -454,131 +454,121 @@ fn draw_process_details(f: &mut Frame, app: &App, area: Rect) -> Result<()> { return Ok(()); } - let conn = &app.connections[app.selected_connection_idx]; + // Look up process info on demand for the selected connection + // This now returns an owned Process, not a reference + if let Some(process) = app.get_process_for_selected_connection() { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(10), // Process details + Constraint::Min(0), // Process connections + ]) + .split(area); - if let Some(pid) = conn.pid { - if let Some(process) = app.processes.get(&pid) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(10), // Process details - Constraint::Min(0), // Process connections - ]) - .split(area); + let mut details_text: Vec = Vec::new(); + details_text.push(Line::from(vec![ + Span::styled( + format!("{}: ", app.i18n.get("process_name")), + Style::default().fg(Color::Yellow), + ), + Span::raw(&process.name), + ])); - let mut details_text: Vec = Vec::new(); + details_text.push(Line::from(vec![ + Span::styled( + format!("{}: ", app.i18n.get("pid")), + Style::default().fg(Color::Yellow), + ), + Span::raw(process.pid.to_string()), + ])); + + if let Some(ref cmd) = process.command_line { details_text.push(Line::from(vec![ Span::styled( - format!("{}: ", app.i18n.get("process_name")), + format!("{}: ", app.i18n.get("command_line")), Style::default().fg(Color::Yellow), ), - Span::raw(&process.name), + Span::raw(cmd), ])); - - details_text.push(Line::from(vec![ - Span::styled( - format!("{}: ", app.i18n.get("pid")), - Style::default().fg(Color::Yellow), - ), - Span::raw(process.pid.to_string()), - ])); - - if let Some(ref cmd) = process.command_line { - details_text.push(Line::from(vec![ - Span::styled( - format!("{}: ", app.i18n.get("command_line")), - Style::default().fg(Color::Yellow), - ), - Span::raw(cmd), - ])); - } - - if let Some(ref user) = process.user { - details_text.push(Line::from(vec![ - Span::styled( - format!("{}: ", app.i18n.get("user")), - Style::default().fg(Color::Yellow), - ), - Span::raw(user), - ])); - } - - if let Some(cpu) = process.cpu_usage { - details_text.push(Line::from(vec![ - Span::styled( - format!("{}: ", app.i18n.get("cpu_usage")), - Style::default().fg(Color::Yellow), - ), - Span::raw(format!("{:.1}%", cpu)), - ])); - } - - if let Some(mem) = process.memory_usage { - details_text.push(Line::from(vec![ - Span::styled( - format!("{}: ", app.i18n.get("memory_usage")), - Style::default().fg(Color::Yellow), - ), - Span::raw(format_bytes(mem)), - ])); - } - - let details = Paragraph::new(details_text) - .block( - Block::default() - .borders(Borders::ALL) - .title(app.i18n.get("process_details")), - ) - .style(Style::default().fg(Color::White)) - .wrap(Wrap { trim: true }); - - f.render_widget(details, chunks[0]); - - let connections: Vec<&Connection> = app - .connections - .iter() - .filter(|c| c.pid == Some(pid)) - .collect(); - - let connections_count = connections.len(); - - let mut items = Vec::new(); - for conn in &connections { - items.push(ListItem::new(Line::from(vec![ - Span::styled( - format!("{}: ", conn.protocol), - Style::default().fg(Color::Green), - ), - Span::raw(format!( - "{} -> {} ({})", - conn.local_addr, conn.remote_addr, conn.state - )), - ]))); - } - - let connections_list = List::new(items) - .block(Block::default().borders(Borders::ALL).title(format!( - "{} ({})", - app.i18n.get("process_connections"), - connections_count - ))) - .highlight_style(Style::default().add_modifier(Modifier::BOLD)) - .highlight_symbol("> "); - - f.render_widget(connections_list, chunks[1]); - } else { - let text = Paragraph::new(app.i18n.get("process_not_found")) - .block( - Block::default() - .borders(Borders::ALL) - .title(app.i18n.get("process_details")), - ) - .style(Style::default().fg(Color::Red)) - .alignment(ratatui::layout::Alignment::Center); - f.render_widget(text, area); } + + if let Some(ref user) = process.user { + details_text.push(Line::from(vec![ + Span::styled( + format!("{}: ", app.i18n.get("user")), + Style::default().fg(Color::Yellow), + ), + Span::raw(user), + ])); + } + + if let Some(cpu) = process.cpu_usage { + details_text.push(Line::from(vec![ + Span::styled( + format!("{}: ", app.i18n.get("cpu_usage")), + Style::default().fg(Color::Yellow), + ), + Span::raw(format!("{:.1}%", cpu)), + ])); + } + + if let Some(mem) = process.memory_usage { + details_text.push(Line::from(vec![ + Span::styled( + format!("{}: ", app.i18n.get("memory_usage")), + Style::default().fg(Color::Yellow), + ), + Span::raw(format_bytes(mem)), + ])); + } + + let details = Paragraph::new(details_text) + .block( + Block::default() + .borders(Borders::ALL) + .title(app.i18n.get("process_details")), + ) + .style(Style::default().fg(Color::White)) + .wrap(Wrap { trim: true }); + + f.render_widget(details, chunks[0]); + + // Find all connections for this process + let pid = process.pid; + let connections: Vec<&Connection> = app + .connections + .iter() + .filter(|c| c.pid == Some(pid)) + .collect(); + + let connections_count = connections.len(); + + let mut items = Vec::new(); + for conn in &connections { + items.push(ListItem::new(Line::from(vec![ + Span::styled( + format!("{}: ", conn.protocol), + Style::default().fg(Color::Green), + ), + Span::raw(format!( + "{} -> {} ({})", + conn.local_addr, conn.remote_addr, conn.state + )), + ]))); + } + + let connections_list = List::new(items) + .block(Block::default().borders(Borders::ALL).title(format!( + "{} ({})", + app.i18n.get("process_connections"), + connections_count + ))) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .highlight_symbol("> "); + + f.render_widget(connections_list, chunks[1]); } else { - let text = Paragraph::new(app.i18n.get("no_pid_for_connection")) + let text = Paragraph::new(app.i18n.get("process_not_found")) .block( Block::default() .borders(Borders::ALL)