mirror of
https://github.com/domcyrus/rustnet.git
synced 2026-01-13 18:00:22 -06:00
make it work on mac
This commit is contained in:
70
src/app.rs
70
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<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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
224
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<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)
|
||||
|
||||
Reference in New Issue
Block a user