make it work on mac

This commit is contained in:
Marco Cadetg
2025-04-28 10:28:09 +02:00
parent f6a32d65cf
commit 0b126c2040
4 changed files with 1020 additions and 362 deletions

View File

@@ -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<Process> {
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
}
}

View File

@@ -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<Connection>) -> 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<Vec<IpAddr>> {
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<Process> {
// 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<Process> {
// 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<Connection>,
interface: &Option<String>,
) -> 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<Connection>) -> 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::<u32>().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<Connection>) -> 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<Connection>) -> 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<Process> {
let output = Command::new("lsof")
.args(["-i", "-n", "-P"])
pub(super) fn try_lsof_command(connection: &Connection) -> Option<Process> {
// 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::<u32>() {
// 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<Process> {
// 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<Process> {
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::<u32>() {
// 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::<u32>() {
// 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<Process> {
None
}
/// Get process information using netstat command
fn try_netstat_command(connection: &Connection) -> Option<Process> {
// 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<String> {
#[allow(dead_code)]
pub(super) fn get_process_name_by_pid(pid: u32) -> Option<String> {
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())
}
}

View File

@@ -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<Capture<pcap::Active>>,
connections: HashMap<String, Connection>,
geo_db: Option<maxminddb::Reader<Vec<u8>>>,
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<Vec<String>> {
let devices = Device::list()?;
@@ -237,54 +269,266 @@ impl NetworkMonitor {
/// Get active connections
pub fn get_connections(&mut self) -> Result<Vec<Connection>> {
// 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<SocketAddr> {
// 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::<SocketAddr>() {
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<String, Connection>,
interface: &Option<String>| {
// 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::<IpAddr>(), port_part.parse::<u16>()) {
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<Process> {
#[cfg(target_os = "linux")]
@@ -315,23 +559,67 @@ impl NetworkMonitor {
}
}
/// Get platform-specific connections
fn get_platform_connections(&mut self, connections: &mut Vec<Connection>) -> 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<Vec<IpAddr>> {
#[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<IpLocation> {
if let Some(ref reader) = self.geo_db {
// Access fields directly on the lookup result (geoip2::City)
if let Ok(lookup_result) = reader.lookup::<geoip2::City>(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<std::net::SocketAddr> {
// 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::<u16>() {
return Some(std::net::SocketAddr::new(
std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)),
port,
));
}
}
None
}
}

224
src/ui.rs
View File

@@ -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<Line> = 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<Line> = 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)