working code ;)

This commit is contained in:
Marco Cadetg
2025-06-30 14:15:40 +02:00
parent e2ee9fa9f1
commit 0eee869a2b
25 changed files with 5360 additions and 3495 deletions

97
Cargo.lock generated
View File

@@ -287,6 +287,62 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-epoch",
"crossbeam-queue",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crossterm"
version = "0.28.1"
@@ -365,6 +421,20 @@ dependencies = [
"syn",
]
[[package]]
name = "dashmap"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "deranged"
version = "0.4.0"
@@ -516,6 +586,12 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.4"
@@ -533,6 +609,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hex"
version = "0.4.3"
@@ -709,7 +791,7 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown",
"hashbrown 0.15.4",
]
[[package]]
@@ -761,6 +843,16 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "num_threads"
version = "0.1.7"
@@ -1100,9 +1192,12 @@ dependencies = [
"arboard",
"chrono",
"clap",
"crossbeam",
"crossterm 0.29.0",
"dashmap",
"dns-lookup",
"log",
"num_cpus",
"pcap",
"pnet_datalink",
"procfs",

View File

@@ -7,8 +7,11 @@ edition = "2024"
anyhow = "1.0"
arboard = "3.5"
crossterm = "0.29"
crossbeam = "0.8"
dashmap = "6.1"
dns-lookup = "2.0"
log = "0.4"
num_cpus = "1.17"
pcap = "2.2"
pnet_datalink = "0.35"
clap = { version = "4.5", features = ["derive"] }

View File

@@ -86,6 +86,31 @@ refresh_interval: 1000
show_locations: true
```
## Architecture
┌─────────────────┐
│ Packet Capture │ ──packets──> channel
└─────────────────┘ │
├──> ┌──────────────────┐
├──> │ Packet Processor │ ──> DashMap
├──> │ (Thread 0) │ │
└──> │ (Thread N) │ │
└──────────────────┘ │
┌─────────────────-┐ │
│Process Enrichment│ ──────────────────────────────────────────> DashMap
└─────────────────-┘ │
┌─────────────────┐ │
│Snapshot Provider│ <────────────────────────────────────────── DashMap
└─────────────────┘ │
│ │
└──> RwLock<Vec<Connection>> (for UI) │
┌─────────────────┐ │
│ Cleanup Thread │ <────────────────────────────────────────── DashMap
└─────────────────┘
## Internationalization
RustNet supports multiple languages. The application looks for language files in the following locations:
@@ -96,6 +121,7 @@ RustNet supports multiple languages. The application looks for language files in
4. `/usr/share/rustnet/i18n/[language].yml`
Currently supported languages:
- English (en)
- French (fr)
@@ -114,6 +140,7 @@ RustNet attempts to identify the process associated with each network connection
## TODOs
### GeoIP Lookup
For GeoIP lookup: MaxMind GeoLite2 City database (place `GeoLite2-City.mmdb` in the application directory)
When a MaxMind GeoLite2 City database is available, RustNet can display geographical information about remote IP addresses. To use this feature:

1072
src/app.rs

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,6 @@ use std::path::Path;
use std::time::Duration;
mod app;
mod config;
mod i18n;
mod network;
mod ui;
@@ -18,12 +16,12 @@ fn main() -> Result<()> {
// Set up logging
setup_logging()?;
info!("Starting RustNet");
info!("Starting RustNet Monitor");
// Parse command line arguments
let matches = Command::new("rustnet")
.version("0.1.0")
.author("Your Name")
.author("Network Monitor")
.about("Cross-platform network monitoring tool")
.arg(
Arg::new("interface")
@@ -34,74 +32,67 @@ fn main() -> Result<()> {
.required(false),
)
.arg(
Arg::new("config")
.short('c')
.long("config")
.value_name("FILE")
.help("Path to configuration file")
.required(false),
Arg::new("no-localhost")
.long("no-localhost")
.help("Filter out localhost connections")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("language")
.short('l')
.long("language")
.value_name("LANG")
.help("Interface language (en, fr, etc.)")
.required(false),
)
.arg(
Arg::new("packet_processing_interval")
.short('P')
.long("packet-processing-interval")
Arg::new("refresh-interval")
.short('r')
.long("refresh-interval")
.value_name("MILLISECONDS")
.help("Interval for packet processing loop sleep (ms). 0 for continuous.")
.help("UI refresh interval in milliseconds")
.value_parser(clap::value_parser!(u64))
.default_value("1000")
.required(false),
)
.arg(
Arg::new("no-dpi")
.long("no-dpi")
.help("Disable deep packet inspection")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
// Initialize configuration
let config_path = matches.get_one::<String>("config").map(String::as_str);
let mut config = config::Config::load(config_path)?;
// Build configuration from command line arguments
let mut config = app::Config::default();
info!("Configuration loaded");
// Override config with command line arguments if provided
if let Some(interface) = matches.get_one::<String>("interface") {
config.interface = Some(interface.to_string());
info!("Using interface: {}", interface);
}
if let Some(language) = matches.get_one::<String>("language") {
config.language = language.to_string();
info!("Using language: {}", language);
if matches.get_flag("no-localhost") {
config.filter_localhost = true;
info!("Filtering localhost connections");
}
if let Some(interval) = matches.get_one::<u64>("packet_processing_interval") {
config.packet_processing_interval_ms = *interval;
info!("Using packet processing interval: {}ms", interval);
if let Some(interval) = matches.get_one::<u64>("refresh-interval") {
config.refresh_interval = *interval;
info!("Using refresh interval: {}ms", interval);
}
// Initialize internationalization
let i18n = i18n::I18n::new(&config.language)?;
info!(
"Internationalization initialized for language: {}",
config.language
);
if matches.get_flag("no-dpi") {
config.enable_dpi = false;
info!("Deep packet inspection disabled");
}
// Set up terminal
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = ui::setup_terminal(backend)?;
info!("Terminal UI initialized");
// Create app state
let app = app::App::new(config, i18n)?;
info!("Application state initialized");
// Create and start the application
let mut app = app::App::new(config)?;
app.start()?;
info!("Application started");
// Run the application
let res = run_app(&mut terminal, app);
// Run the UI loop
let res = run_ui_loop(&mut terminal, &app);
// Restore terminal
// Cleanup
app.stop();
ui::restore_terminal(&mut terminal)?;
// Return any error that occurred
@@ -110,7 +101,7 @@ fn main() -> Result<()> {
println!("Error: {}", err);
}
info!("RustNet shutting down");
info!("RustNet Monitor shutting down");
Ok(())
}
@@ -135,66 +126,95 @@ fn setup_logging() -> Result<()> {
Ok(())
}
fn run_app<B: ratatui::prelude::Backend>(
fn run_ui_loop<B: ratatui::prelude::Backend>(
terminal: &mut ui::Terminal<B>,
mut app: app::App,
app: &app::App,
) -> Result<()> {
let tick_rate = Duration::from_millis(200); // Faster refresh for better loading animation
let tick_rate = Duration::from_millis(200);
let mut last_tick = std::time::Instant::now();
let mut capture_started = false;
let mut ui_state = ui::UIState::default();
loop {
// Draw the UI first to show loading screen immediately
// Get current connections and stats
let connections = app.get_connections();
let stats = app.get_stats();
// Draw the UI
terminal.draw(|f| {
if let Err(err) = ui::draw(f, &mut app) {
if let Err(err) = ui::draw(f, app, &ui_state, &connections, &stats) {
error!("UI draw error: {}", err);
}
})?;
// Start capture on first iteration (after first UI render)
if !capture_started {
if let Err(err) = app.start_capture() {
error!("Failed to start network capture: {}", err);
// Continue anyway, some features may still work
}
info!("Network capture started");
capture_started = true;
}
// Handle timeout (for periodic UI updates)
// Handle timeout for periodic updates
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or(Duration::from_secs(0));
// Update app state on tick (especially important during loading for spinner animation)
let should_tick = last_tick.elapsed() >= tick_rate;
if should_tick {
app.on_tick()?;
// Check if we should tick
if last_tick.elapsed() >= tick_rate {
last_tick = std::time::Instant::now();
}
// Handle input events (use shorter timeout during loading for responsive spinner)
let input_timeout = if app.is_loading {
Duration::from_millis(100)
} else {
timeout
};
if crossterm::event::poll(input_timeout)? {
// Handle input events
if crossterm::event::poll(timeout)? {
if let crossterm::event::Event::Key(key) = crossterm::event::read()? {
// Handle key event
if let Some(action) = app.handle_key(key) {
match action {
app::Action::Quit => {
info!("User requested application exit");
app.shutdown();
break;
}
app::Action::Refresh => {
info!("User requested refresh");
app.refresh()?;
} // Add more actions as needed
use crossterm::event::{KeyCode, KeyModifiers};
match (key.code, key.modifiers) {
// Quit
(KeyCode::Char('q'), _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
info!("User requested application exit");
break;
}
// Tab navigation
(KeyCode::Tab, _) => {
ui_state.selected_tab = (ui_state.selected_tab + 1) % 3;
}
// Help toggle
(KeyCode::Char('h'), _) => {
ui_state.show_help = !ui_state.show_help;
if ui_state.show_help {
ui_state.selected_tab = 2; // Switch to help tab
} else {
ui_state.selected_tab = 0; // Back to overview
}
}
// Navigation in connection list
(KeyCode::Up, _) | (KeyCode::Char('k'), _) => {
if !connections.is_empty() && ui_state.selected_connection > 0 {
ui_state.selected_connection -= 1;
}
}
(KeyCode::Down, _) | (KeyCode::Char('j'), _) => {
if !connections.is_empty()
&& ui_state.selected_connection < connections.len().saturating_sub(1)
{
ui_state.selected_connection += 1;
}
}
// Enter to view details
(KeyCode::Enter, _) => {
if ui_state.selected_tab == 0 && !connections.is_empty() {
ui_state.selected_tab = 1; // Switch to details view
}
}
// Escape to go back
(KeyCode::Esc, _) => {
if ui_state.selected_tab == 1 {
ui_state.selected_tab = 0; // Back to overview
} else if ui_state.selected_tab == 2 {
ui_state.selected_tab = 0; // Back to overview from help
}
}
_ => {}
}
}
}

380
src/network/capture.rs Normal file
View File

@@ -0,0 +1,380 @@
// network/capture.rs - Packet capture setup and utilities
use anyhow::{Result, anyhow};
use pcap::{Active, Capture, Device, Error as PcapError};
use std::time::Duration;
/// Packet capture configuration
#[derive(Debug, Clone)]
pub struct CaptureConfig {
/// Network interface name (None for default)
pub interface: Option<String>,
/// Promiscuous mode
pub promiscuous: bool,
/// Snapshot length (bytes to capture per packet)
pub snaplen: i32,
/// Buffer size for packet capture
pub buffer_size: i32,
/// Read timeout in milliseconds
pub timeout_ms: i32,
/// BPF filter string
pub filter: Option<String>,
}
impl Default for CaptureConfig {
fn default() -> Self {
Self {
interface: None,
promiscuous: true,
snaplen: 200, // Limit packet size to keep more in buffer (like Sniffnet)
buffer_size: 2_000_000, // 2MB buffer (same as Sniffnet)
timeout_ms: 150, // 150ms timeout for UI responsiveness (like Sniffnet)
filter: None, // Start without filter to ensure we see packets
}
}
}
/// Find the best active network device
fn find_best_device() -> Result<Device> {
let devices = Device::list()?;
log::info!(
"Scanning {} devices for best active interface...",
devices.len()
);
// Log all devices for debugging
for d in &devices {
let has_valid_ip = d.addresses.iter().any(|addr| match &addr.addr {
std::net::IpAddr::V4(v4) => {
!v4.is_link_local() && !v4.is_loopback() && !v4.is_unspecified()
}
std::net::IpAddr::V6(v6) => {
!v6.is_loopback() && !v6.is_multicast() && !v6.is_unspecified()
}
});
log::debug!(
" Device: {} [up: {}, running: {}, has_ip: {}]",
d.name,
d.flags.is_up(),
d.flags.is_running(),
has_valid_ip
);
}
if devices.is_empty() {
return Err(anyhow!("No network devices found"));
}
// Find the best active device
let suitable_device = devices
.iter()
// First priority: up, running, and has a valid IP address
.find(|d| {
!d.name.starts_with("lo")
&& d.name != "any"
&& d.flags.is_up()
&& d.flags.is_running()
&& d.addresses.iter().any(|addr| {
match &addr.addr {
std::net::IpAddr::V4(v4) => {
!v4.is_link_local() && !v4.is_loopback() && !v4.is_unspecified()
}
std::net::IpAddr::V6(v6) => false, // Skip IPv6 for now
}
})
})
// Second priority: common active interface names
.or_else(|| {
devices.iter().find(|d| {
(d.name == "en0" || d.name == "en1" || d.name.starts_with("eth"))
&& d.flags.is_up()
&& d.addresses.iter().any(|addr| addr.addr.is_ipv4())
})
})
// Third priority: any up interface with valid addresses (excluding problematic ones)
.or_else(|| {
devices.iter().find(|d| {
!d.name.starts_with("lo") &&
!d.name.starts_with("ap") && // Skip Apple's ap interfaces
!d.name.starts_with("awdl") && // Skip Apple Wireless Direct
!d.name.starts_with("llw") && // Skip Low latency WLAN
!d.name.starts_with("bridge") && // Skip bridges
!d.name.starts_with("utun") && // Skip tunnels
!d.name.starts_with("vmnet") && // Skip VM interfaces
d.name != "any" &&
d.flags.is_up() &&
!d.addresses.is_empty()
})
})
.cloned();
match suitable_device {
Some(device) => {
log::info!(
"Selected active device: {} ({} addresses)",
device.name,
device.addresses.len()
);
for addr in &device.addresses {
log::debug!(" Address: {}", addr.addr);
}
Ok(device)
}
None => {
log::error!("No suitable active network device found!");
log::error!("Try specifying an interface manually with -i flag");
Err(anyhow!(
"No active network interface found. Use -i to specify one manually."
))
}
}
}
/// Setup packet capture with the given configuration
pub fn setup_packet_capture(config: CaptureConfig) -> Result<Capture<Active>> {
// Find the capture device
let device = find_capture_device(&config.interface)?;
log::info!(
"Setting up capture on device: {} ({})",
device.name,
device.desc.as_deref().unwrap_or("no description")
);
// Create capture handle
let mut cap = Capture::from_device(device)?
.promisc(config.promiscuous)
.snaplen(config.snaplen)
.buffer_size(config.buffer_size)
.timeout(config.timeout_ms)
.immediate_mode(true); // Parse packets ASAP (like Sniffnet)
// Open the capture
let mut cap = cap.open()?;
// Apply BPF filter if specified
if let Some(filter) = &config.filter {
log::info!("Applying BPF filter: {}", filter);
cap.filter(filter, true)?;
}
// Note: We're not setting non-blocking mode as we're using timeout instead
Ok(cap)
}
/// Find a capture device by name or return the default
fn find_capture_device(interface_name: &Option<String>) -> Result<Device> {
match interface_name {
Some(name) => {
log::info!("Looking for interface: {}", name);
// List all devices
let devices = Device::list()?;
// Find exact match first
if let Some(device) = devices.iter().find(|d| d.name == *name) {
return Ok(device.clone());
}
// Try case-insensitive match
let name_lower = name.to_lowercase();
if let Some(device) = devices.iter().find(|d| d.name.to_lowercase() == name_lower) {
return Ok(device.clone());
}
// List available interfaces for error message
let available: Vec<String> = devices
.iter()
.map(|d| {
format!(
"{} ({})",
d.name,
d.desc.as_deref().unwrap_or("no description")
)
})
.collect();
Err(anyhow!(
"Interface '{}' not found. Available interfaces:\n{}",
name,
available.join("\n")
))
}
None => {
log::info!("No interface specified, using default");
// Try to get default device
match Device::lookup() {
Ok(Some(device)) => {
log::info!(
"Found default device: {} ({})",
device.name,
device.desc.as_deref().unwrap_or("no description")
);
// Check if the default device is actually active
let has_valid_ip = device.addresses.iter().any(|addr| {
match &addr.addr {
std::net::IpAddr::V4(v4) => {
!v4.is_link_local() && !v4.is_loopback() && !v4.is_unspecified()
}
std::net::IpAddr::V6(v6) => false, // Skip IPv6 for now
}
});
// Check if it's a problematic interface type
let is_problematic = device.name.starts_with("ap")
|| device.name.starts_with("awdl")
|| device.name.starts_with("llw")
|| device.name.starts_with("bridge")
|| device.name.starts_with("utun")
|| device.name.starts_with("vmnet")
|| device.name == "any"
|| device.flags.is_loopback();
if device.flags.is_up()
&& device.flags.is_running()
&& has_valid_ip
&& !is_problematic
{
log::info!("Default device appears active, using it");
Ok(device)
} else {
log::warn!(
"Default device '{}' is not suitable (up: {}, running: {}, has_ip: {}, problematic: {})",
device.name,
device.flags.is_up(),
device.flags.is_running(),
has_valid_ip,
is_problematic
);
log::info!("Looking for a better interface...");
// Fall through to the device selection logic below
find_best_device()
}
}
Ok(None) => {
log::info!("No default device found");
find_best_device()
}
Err(e) => Err(e.into()),
}
}
}
}
/// List available capture devices
pub fn list_devices() -> Result<Vec<DeviceInfo>> {
let devices = Device::list()?;
Ok(devices
.into_iter()
.map(|d| {
// Check if device is active by checking flags and addresses
let is_active = d.flags.is_up()
&& d.flags.is_running()
&& d.addresses.iter().any(|addr| {
// Has at least one non-link-local address
match &addr.addr {
std::net::IpAddr::V4(v4) => !v4.is_link_local() && !v4.is_loopback(),
std::net::IpAddr::V6(v6) => !v6.is_loopback() && !v6.is_multicast(),
}
});
DeviceInfo {
name: d.name,
description: d.desc,
addresses: d
.addresses
.into_iter()
.map(|addr| format!("{}", addr.addr))
.collect(),
is_loopback: d.flags.is_loopback(),
is_up: d.flags.is_up(),
is_running: d.flags.is_running(),
is_active,
}
})
.collect())
}
/// Information about a network device
#[derive(Debug, Clone)]
pub struct DeviceInfo {
pub name: String,
pub description: Option<String>,
pub addresses: Vec<String>,
pub is_loopback: bool,
pub is_up: bool,
pub is_running: bool,
pub is_active: bool,
}
/// Simple packet reader that handles timeouts gracefully
pub struct PacketReader {
capture: Capture<Active>,
}
impl PacketReader {
pub fn new(capture: Capture<Active>) -> Self {
Self { capture }
}
/// Read next packet, returning None on timeout
pub fn next_packet(&mut self) -> Result<Option<Vec<u8>>> {
match self.capture.next_packet() {
Ok(packet) => Ok(Some(packet.data.to_vec())),
Err(PcapError::TimeoutExpired) => Ok(None),
Err(e) => Err(e.into()),
}
}
/// Get capture statistics
pub fn stats(&mut self) -> Result<CaptureStats> {
let stats = self.capture.stats()?;
Ok(CaptureStats {
received: stats.received,
dropped: stats.dropped,
if_dropped: stats.if_dropped,
})
}
}
/// Packet capture statistics
#[derive(Debug, Clone, Default)]
pub struct CaptureStats {
pub received: u32,
pub dropped: u32,
pub if_dropped: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = CaptureConfig::default();
assert!(config.promiscuous);
assert_eq!(config.snaplen, 1024);
assert!(config.filter.is_some());
}
#[test]
fn test_list_devices() {
// This might fail in some test environments
if let Ok(devices) = list_devices() {
for device in devices {
println!("Device: {} - {:?}", device.name, device.description);
println!(" Addresses: {:?}", device.addresses);
println!(
" Loopback: {}, Up: {}, Running: {}",
device.is_loopback, device.is_up, device.is_running
);
}
}
}
}

75
src/network/dpi/dns.rs Normal file
View File

@@ -0,0 +1,75 @@
use crate::network::types::{DnsInfo, DnsQueryType};
pub fn analyze_dns(payload: &[u8]) -> Option<DnsInfo> {
if payload.len() < 12 {
return None;
}
let mut info = DnsInfo {
query_name: None,
query_type: None,
response_ips: Vec::new(),
is_response: false,
};
// DNS header flags
let flags = u16::from_be_bytes([payload[2], payload[3]]);
info.is_response = (flags & 0x8000) != 0; // QR bit
// Question count
let qdcount = u16::from_be_bytes([payload[4], payload[5]]);
if qdcount > 0 {
// Parse first question
let mut offset = 12;
let mut name = String::new();
// Parse domain name
while offset < payload.len() {
let label_len = payload[offset] as usize;
if label_len == 0 {
offset += 1;
break;
}
if label_len >= 0xC0 {
// Compressed name - skip for simplicity
offset += 2;
break;
}
if offset + 1 + label_len > payload.len() {
break;
}
if !name.is_empty() {
name.push('.');
}
if let Ok(label) = std::str::from_utf8(&payload[offset + 1..offset + 1 + label_len]) {
name.push_str(label);
}
offset += 1 + label_len;
}
if !name.is_empty() {
info.query_name = Some(name);
}
// Query type
if offset + 2 <= payload.len() {
let qtype = u16::from_be_bytes([payload[offset], payload[offset + 1]]);
info.query_type = Some(match qtype {
1 => DnsQueryType::A,
28 => DnsQueryType::AAAA,
5 => DnsQueryType::CNAME,
15 => DnsQueryType::MX,
16 => DnsQueryType::TXT,
other => DnsQueryType::Other(other),
});
}
}
Some(info)
}

130
src/network/dpi/http.rs Normal file
View File

@@ -0,0 +1,130 @@
use crate::network::types::{HttpInfo, HttpVersion};
/// Analyze payload for HTTP protocol
pub fn analyze_http(payload: &[u8]) -> Option<HttpInfo> {
if !is_likely_http(payload) {
return None;
}
let mut info = HttpInfo {
version: HttpVersion::Http11,
method: None,
host: None,
path: None,
status_code: None,
user_agent: None,
};
// Safe string conversion for HTTP parsing
let text = String::from_utf8_lossy(payload);
let lines: Vec<&str> = text.lines().collect();
if lines.is_empty() {
return None;
}
// Parse first line
let first_line = lines[0];
let parts: Vec<&str> = first_line.split_whitespace().collect();
if parts.len() >= 3 {
if first_line.starts_with("HTTP/") {
// Response line: HTTP/1.1 200 OK
info.version = parse_http_version(parts[0]);
info.status_code = parts[1].parse::<u16>().ok();
} else if is_http_method(parts[0]) {
// Request line: GET /path HTTP/1.1
info.method = Some(parts[0].to_string());
info.path = Some(parts[1].to_string());
if parts.len() >= 3 {
info.version = parse_http_version(parts[2]);
}
} else {
return None; // Not valid HTTP
}
} else {
return None;
}
// Parse headers
for line in lines.iter().skip(1) {
if line.is_empty() {
break; // End of headers
}
if let Some((key, value)) = line.split_once(':') {
let key = key.trim().to_lowercase();
let value = value.trim();
match key.as_str() {
"host" => info.host = Some(value.to_string()),
"user-agent" => info.user_agent = Some(value.to_string()),
_ => {}
}
}
}
Some(info)
}
/// Quick check if payload might be HTTP
fn is_likely_http(payload: &[u8]) -> bool {
if payload.len() < 4 {
return false;
}
// HTTP request methods
payload.starts_with(b"GET ") ||
payload.starts_with(b"POST ") ||
payload.starts_with(b"PUT ") ||
payload.starts_with(b"DELETE ") ||
payload.starts_with(b"HEAD ") ||
payload.starts_with(b"OPTIONS ") ||
payload.starts_with(b"CONNECT ") ||
payload.starts_with(b"TRACE ") ||
payload.starts_with(b"PATCH ") ||
// HTTP responses
payload.starts_with(b"HTTP/1.0 ") ||
payload.starts_with(b"HTTP/1.1 ") ||
payload.starts_with(b"HTTP/2 ")
}
fn is_http_method(s: &str) -> bool {
matches!(
s,
"GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "CONNECT" | "TRACE" | "PATCH"
)
}
fn parse_http_version(s: &str) -> HttpVersion {
match s {
"HTTP/1.0" => HttpVersion::Http10,
"HTTP/1.1" => HttpVersion::Http11,
"HTTP/2" => HttpVersion::Http2,
_ => HttpVersion::Http11,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_http_request() {
let payload = b"GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n";
let info = analyze_http(payload).unwrap();
assert_eq!(info.method.as_deref(), Some("GET"));
assert_eq!(info.path.as_deref(), Some("/index.html"));
assert_eq!(info.host.as_deref(), Some("example.com"));
}
#[test]
fn test_http_response() {
let payload = b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
let info = analyze_http(payload).unwrap();
assert_eq!(info.status_code, Some(200));
assert!(info.method.is_none());
}
}

100
src/network/dpi/mod.rs Normal file
View File

@@ -0,0 +1,100 @@
use crate::network::types::ApplicationProtocol;
mod dns;
mod http;
mod quic;
mod tls;
pub use dns::analyze_dns;
pub use http::analyze_http;
pub use quic::is_quic_packet;
pub use tls::analyze_tls;
/// Result of DPI analysis
#[derive(Debug, Clone)]
pub struct DpiResult {
pub application: ApplicationProtocol,
pub confidence: f32, // 0.0 to 1.0
pub needs_more_data: bool, // True if more packets would help
}
/// Analyze a TCP packet payload
pub fn analyze_tcp_packet(
payload: &[u8],
local_port: u16,
remote_port: u16,
is_outgoing: bool,
) -> Option<DpiResult> {
if payload.is_empty() {
return None;
}
// Try protocols in order of likelihood/speed
// 1. Check for HTTP (fast string matching)
if let Some(http_result) = http::analyze_http(payload) {
return Some(DpiResult {
application: ApplicationProtocol::Http(http_result),
confidence: 1.0,
needs_more_data: false,
});
}
// 2. Check for TLS/HTTPS (port 443 or TLS handshake)
if local_port == 443 || remote_port == 443 || tls::is_tls_handshake(payload) {
if let Some(tls_result) = tls::analyze_tls(payload) {
return Some(DpiResult {
application: ApplicationProtocol::Https(tls_result),
confidence: 1.0,
needs_more_data: false,
});
}
}
// 3. Check for SSH (port 22 or SSH banner)
if local_port == 22 || remote_port == 22 || payload.starts_with(b"SSH-") {
return Some(DpiResult {
application: ApplicationProtocol::Ssh,
confidence: 1.0,
needs_more_data: false,
});
}
// More protocols here...
None
}
/// Analyze a UDP packet payload
pub fn analyze_udp_packet(
payload: &[u8],
local_port: u16,
remote_port: u16,
is_outgoing: bool,
) -> Option<DpiResult> {
if payload.is_empty() {
return None;
}
// 1. DNS (port 53)
if local_port == 53 || remote_port == 53 {
if let Some(dns_result) = dns::analyze_dns(payload) {
return Some(DpiResult {
application: ApplicationProtocol::Dns(dns_result),
confidence: 1.0,
needs_more_data: false,
});
}
}
// 2. QUIC/HTTP3 (port 443)
if (local_port == 443 || remote_port == 443) && quic::is_quic_packet(payload) {
return Some(DpiResult {
application: ApplicationProtocol::Quic,
confidence: 0.9, // QUIC detection is less certain
needs_more_data: true,
});
}
None
}

22
src/network/dpi/quic.rs Normal file
View File

@@ -0,0 +1,22 @@
pub fn is_quic_packet(payload: &[u8]) -> bool {
if payload.len() < 5 {
return false;
}
// Check for QUIC long header (bit 7 set)
if (payload[0] & 0x80) != 0 {
// Check version
let version = u32::from_be_bytes([payload[1], payload[2], payload[3], payload[4]]);
// Known QUIC versions
return version == 0x00000001 || // QUIC v1
version == 0x6b3343cf || // QUIC v2
version == 0x51303530 || // Google QUIC
version == 0; // Version negotiation
}
// Could be short header QUIC packet
// These are harder to identify definitively, but if we see them on port 443 UDP,
// they're likely QUIC
true
}

201
src/network/dpi/tls.rs Normal file
View File

@@ -0,0 +1,201 @@
use crate::network::types::{TlsInfo, TlsVersion};
pub fn is_tls_handshake(payload: &[u8]) -> bool {
if payload.len() < 6 {
return false;
}
// TLS record header:
// - Content type (1 byte): 0x16 for handshake
// - Version (2 bytes): 0x0301-0x0304 for TLS 1.0-1.3
// - Length (2 bytes)
payload[0] == 0x16 && // Handshake content type
payload[1] == 0x03 && // Major version 3
(payload[2] >= 0x01 && payload[2] <= 0x04) // Minor version 1-4
}
pub fn analyze_tls(payload: &[u8]) -> Option<TlsInfo> {
if !is_tls_handshake(payload) || payload.len() < 9 {
return None;
}
let mut info = TlsInfo {
version: None,
sni: None,
alpn: Vec::new(),
cipher_suite: None,
};
// Record layer version
let record_version = match payload[2] {
0x01 => Some(TlsVersion::Tls10),
0x02 => Some(TlsVersion::Tls11),
0x03 => Some(TlsVersion::Tls12),
0x04 => Some(TlsVersion::Tls13),
_ => None,
};
// Skip TLS record header (5 bytes)
let handshake_data = &payload[5..];
if handshake_data.len() < 4 {
return Some(info);
}
let handshake_type = handshake_data[0];
match handshake_type {
0x01 => {
// Client Hello
info.version = record_version;
if let Some((sni, alpn)) = parse_client_hello_extensions(handshake_data) {
info.sni = sni;
info.alpn = alpn;
}
}
0x02 => {
// Server Hello
info.version = record_version;
// Could parse cipher suite here if needed
}
_ => {}
}
Some(info)
}
/// Parse Client Hello extensions for SNI and ALPN
fn parse_client_hello_extensions(handshake_data: &[u8]) -> Option<(Option<String>, Vec<String>)> {
if handshake_data.len() < 38 {
return None;
}
// Skip to extensions:
// - Handshake type (1) + Length (3) + Version (2) + Random (32) = 38
let mut offset = 38;
// Session ID
if offset >= handshake_data.len() {
return None;
}
let session_id_len = handshake_data[offset] as usize;
offset += 1 + session_id_len;
// Cipher suites
if offset + 2 > handshake_data.len() {
return None;
}
let cipher_suites_len =
u16::from_be_bytes([handshake_data[offset], handshake_data[offset + 1]]) as usize;
offset += 2 + cipher_suites_len;
// Compression methods
if offset >= handshake_data.len() {
return None;
}
let compression_len = handshake_data[offset] as usize;
offset += 1 + compression_len;
// Extensions length
if offset + 2 > handshake_data.len() {
return None;
}
let extensions_len =
u16::from_be_bytes([handshake_data[offset], handshake_data[offset + 1]]) as usize;
offset += 2;
if offset + extensions_len > handshake_data.len() {
return None;
}
// Parse extensions
let mut sni = None;
let mut alpn = Vec::new();
let extensions_data = &handshake_data[offset..offset + extensions_len];
let mut ext_offset = 0;
while ext_offset + 4 <= extensions_data.len() {
let ext_type =
u16::from_be_bytes([extensions_data[ext_offset], extensions_data[ext_offset + 1]]);
let ext_len = u16::from_be_bytes([
extensions_data[ext_offset + 2],
extensions_data[ext_offset + 3],
]) as usize;
if ext_offset + 4 + ext_len > extensions_data.len() {
break;
}
match ext_type {
0x0000 => {
// SNI
sni =
parse_sni_extension(&extensions_data[ext_offset + 4..ext_offset + 4 + ext_len]);
}
0x0010 => {
// ALPN
alpn = parse_alpn_extension(
&extensions_data[ext_offset + 4..ext_offset + 4 + ext_len],
);
}
_ => {}
}
ext_offset += 4 + ext_len;
}
Some((sni, alpn))
}
fn parse_sni_extension(data: &[u8]) -> Option<String> {
if data.len() < 5 {
return None;
}
// Skip server name list length (2 bytes)
let mut offset = 2;
while offset + 3 <= data.len() {
let name_type = data[offset];
let name_len = u16::from_be_bytes([data[offset + 1], data[offset + 2]]) as usize;
if name_type == 0x00 {
// host_name
if offset + 3 + name_len <= data.len() {
let hostname_bytes = &data[offset + 3..offset + 3 + name_len];
if let Ok(hostname) = std::str::from_utf8(hostname_bytes) {
return Some(hostname.to_string());
}
}
}
offset += 3 + name_len;
}
None
}
/// Parse ALPN extension
fn parse_alpn_extension(data: &[u8]) -> Vec<String> {
let mut protocols = Vec::new();
if data.len() < 2 {
return protocols;
}
// Skip ALPN extension length
let mut offset = 2;
while offset < data.len() {
let proto_len = data[offset] as usize;
if offset + 1 + proto_len <= data.len() {
if let Ok(proto) = std::str::from_utf8(&data[offset + 1..offset + 1 + proto_len]) {
protocols.push(proto.to_string());
}
}
offset += 1 + proto_len;
}
protocols
}

View File

@@ -1,183 +0,0 @@
// linux.rs
use anyhow::Result;
use std::collections::HashMap;
use std::fs;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use super::{Connection, Protocol, ProtocolState};
/// Get connections with process information from /proc
pub fn get_connections_with_process_info(connections: &mut Vec<Connection>) -> Result<()> {
// Parse TCP connections
parse_proc_net_file("/proc/net/tcp", Protocol::TCP, connections)?;
parse_proc_net_file("/proc/net/tcp6", Protocol::TCP, connections)?;
// Parse UDP connections
parse_proc_net_file("/proc/net/udp", Protocol::UDP, connections)?;
parse_proc_net_file("/proc/net/udp6", Protocol::UDP, connections)?;
// Build a map of inodes to process info
let inode_to_process = build_inode_to_process_map()?;
// Enrich connections with process info
for conn in connections.iter_mut() {
if let Some(inode) = get_socket_inode(conn) {
if let Some((pid, name)) = inode_to_process.get(&inode) {
conn.pid = Some(*pid);
conn.process_name = Some(name.clone());
}
}
}
Ok(())
}
/// Parse a /proc/net file and add connections
fn parse_proc_net_file(
path: &str,
protocol: Protocol,
connections: &mut Vec<Connection>,
) -> Result<()> {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return Ok(()), // File might not exist
};
for (i, line) in content.lines().enumerate() {
if i == 0 {
continue; // Skip header
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 10 {
continue;
}
// Parse local address
let local_addr = match parse_hex_address(parts[1]) {
Some(addr) => addr,
None => continue,
};
// Parse remote address
let remote_addr = match parse_hex_address(parts[2]) {
Some(addr) => addr,
None => continue,
};
// Create a basic connection with minimal state
let state = match protocol {
Protocol::TCP => ProtocolState::Tcp(super::TcpState::Established),
Protocol::UDP => ProtocolState::Udp,
_ => continue,
};
let mut conn = Connection::new(protocol, local_addr, remote_addr, state);
// Try to get inode from column 9 (0-indexed)
if parts.len() > 9 {
if let Ok(inode) = parts[9].parse::<u64>() {
// Store inode temporarily (we'll use a hack here - store in bytes_sent)
conn.bytes_sent = inode;
}
}
connections.push(conn);
}
Ok(())
}
/// Parse hex address from /proc/net format
fn parse_hex_address(hex_addr: &str) -> Option<SocketAddr> {
let parts: Vec<&str> = hex_addr.split(':').collect();
if parts.len() != 2 {
return None;
}
let ip_hex = parts[0];
let port = u16::from_str_radix(parts[1], 16).ok()?;
// Determine if IPv4 or IPv6 based on length
if ip_hex.len() == 8 {
// IPv4
let ip_bytes = u32::from_str_radix(ip_hex, 16).ok()?;
let ip = Ipv4Addr::from(ip_bytes.to_le_bytes());
Some(SocketAddr::new(IpAddr::V4(ip), port))
} else if ip_hex.len() == 32 {
// IPv6
let mut bytes = [0u8; 16];
for i in 0..4 {
let chunk = &ip_hex[i * 8..(i + 1) * 8];
let value = u32::from_str_radix(chunk, 16).ok()?;
bytes[i * 4..(i + 1) * 4].copy_from_slice(&value.to_le_bytes());
}
let ip = Ipv6Addr::from(bytes);
Some(SocketAddr::new(IpAddr::V6(ip), port))
} else {
None
}
}
/// Build a map of socket inodes to process information
fn build_inode_to_process_map() -> Result<HashMap<u64, (u32, String)>> {
let mut inode_map = HashMap::new();
// Iterate through /proc/[pid]/fd/
for entry in fs::read_dir("/proc")? {
let entry = entry?;
let path = entry.path();
// Check if it's a PID directory
if let Some(pid_str) = path.file_name().and_then(|s| s.to_str()) {
if let Ok(pid) = pid_str.parse::<u32>() {
// Get process name
let comm_path = path.join("comm");
let process_name = fs::read_to_string(&comm_path)
.unwrap_or_else(|_| "unknown".to_string())
.trim()
.to_string();
// Check all file descriptors
let fd_dir = path.join("fd");
if let Ok(fd_entries) = fs::read_dir(&fd_dir) {
for fd_entry in fd_entries {
if let Ok(fd_entry) = fd_entry {
if let Ok(link) = fs::read_link(fd_entry.path()) {
if let Some(link_str) = link.to_str() {
if link_str.starts_with("socket:[") {
if let Some(inode) = extract_socket_inode(link_str) {
inode_map.insert(inode, (pid, process_name.clone()));
}
}
}
}
}
}
}
}
}
}
Ok(inode_map)
}
/// Extract inode from socket link like "socket:[12345]"
fn extract_socket_inode(link: &str) -> Option<u64> {
if link.starts_with("socket:[") && link.ends_with(']') {
let inode_str = &link[8..link.len() - 1];
inode_str.parse().ok()
} else {
None
}
}
/// Get socket inode for a connection
fn get_socket_inode(conn: &Connection) -> Option<u64> {
// We stored the inode in bytes_sent temporarily
if conn.bytes_sent > 0 {
Some(conn.bytes_sent)
} else {
None
}
}

View File

@@ -1,391 +0,0 @@
use anyhow::Result;
use log::debug;
use std::collections::HashSet;
use std::net::SocketAddr;
use std::process::Command;
use super::{Connection, ConnectionState, NetworkMonitor, Process, Protocol};
/// Get platform-specific connections for macOS
pub fn get_platform_connections(
monitor: &NetworkMonitor,
connections: &mut Vec<Connection>,
) -> Result<()> {
// 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
);
Ok(())
}
impl NetworkMonitor {
/// Get connections from lsof command
pub(super) 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() {
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() < 8 {
continue;
}
// Get process name and PID
let process_name = fields[0].to_string();
let pid = fields[1].parse::<u32>().unwrap_or(0);
// Find the field with connection info - format usually has (LISTEN), (ESTABLISHED) etc.
let proto_addr_idx = 8;
if fields.len() <= proto_addr_idx {
continue;
}
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 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)) =
(super::parse_addr(parts[0]), super::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) = super::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()
};
// 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);
}
}
}
}
}
Ok(())
}
/// Get connections from netstat command
pub(super) fn get_connections_from_netstat(&self, connections: &mut Vec<Connection>) -> Result<()> {
// 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);
for line in text.lines().skip(2) {
// Skip headers
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 5 {
continue;
}
// Protocol is always TCP for this command
let protocol = Protocol::TCP;
// Parse state
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,
"CLOSE_WAIT" => ConnectionState::CloseWait,
"SYN_SENT" => ConnectionState::SynSent,
"SYN_RCVD" | "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 local and remote addresses
let local_idx = 3;
let remote_idx = 4;
if fields.len() <= local_idx || fields.len() <= remote_idx {
continue;
}
if let (Some(local), Some(remote)) = (
super::parse_addr(fields[local_idx]),
super::parse_addr(fields[remote_idx]),
) {
// 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);
}
}
}
}
// 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);
for line in text.lines().skip(2) {
// Skip headers
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 4 {
continue;
}
// Protocol is always UDP for this command
let protocol = Protocol::UDP;
// Parse local address
let local_idx = 3;
if fields.len() <= local_idx {
continue;
}
if let Some(local) = super::parse_addr(fields[local_idx]) {
// 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()
};
// 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(())
}
}
/// Parses the NAME field of lsof output to extract local and remote addresses.
pub(super) fn parse_lsof_addrs(addr_field: &str) -> Option<(SocketAddr, SocketAddr)> {
if let Some(arrow_idx) = addr_field.find("->") {
let local_str = &addr_field[..arrow_idx];
let remote_str = &addr_field[arrow_idx + 2..];
let local_addr = super::parse_addr(local_str)?;
let remote_addr = super::parse_addr(remote_str)?;
Some((local_addr, remote_addr))
} else {
let local_addr = super::parse_addr(addr_field)?;
let remote_addr = "0.0.0.0:0".parse().ok()?;
Some((local_addr, remote_addr))
}
}
/// Get process information using lsof command
pub(super) fn try_lsof_command(connection: &Connection) -> Option<Process> {
let proto_arg = match connection.protocol {
Protocol::TCP => "TCP",
Protocol::UDP => "UDP",
Protocol::ICMP => return None,
};
let output = Command::new("lsof")
.args(["-i", proto_arg, "-n", "-P"])
.output()
.ok()?;
if output.status.success() {
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines().skip(1) {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 9 {
continue;
}
if let Some((lsof_local, lsof_remote)) = parse_lsof_addrs(fields[8]) {
let c = connection;
let match1 = c.local_addr == lsof_local && c.remote_addr == lsof_remote;
let match2 = c.local_addr == lsof_remote && c.remote_addr == lsof_local;
if match1 || match2 {
if let Ok(pid) = fields[1].parse::<u32>() {
return Some(Process {
pid,
name: fields[0].to_string(),
});
}
}
}
}
}
None
}
/// Get process information using netstat command
pub(super) fn try_netstat_command(connection: &Connection) -> Option<Process> {
if let Some(process) = try_lsof_command(connection) {
return Some(process);
}
let output = Command::new("netstat")
.args(["-p", "tcp", "-v"])
.output()
.ok()?;
if output.status.success() {
let text = String::from_utf8_lossy(&output.stdout);
let local_port = connection.local_addr.port();
let remote_port = connection.remote_addr.port();
for line in text.lines().skip(2) {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 9 {
continue;
}
if let Some(local_addr_str) = fields.get(3) {
if let Some(remote_addr_str) = fields.get(4) {
if let (Some(local_addr), Some(remote_addr)) = (
super::parse_addr(local_addr_str),
super::parse_addr(remote_addr_str),
) {
if local_addr.port() == local_port && remote_addr.port() == remote_port {
if let Some(pid_str) = fields.get(8) {
if let Ok(pid) = pid_str.parse::<u32>() {
return get_process_name_by_pid(pid)
.map(|name| Process { pid, name });
}
}
}
}
}
}
}
}
None
}
/// Get process name by PID
#[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);
let name = text.trim();
if name.is_empty() {
None
} else {
Some(name.to_string())
}
}

303
src/network/merge.rs Normal file
View File

@@ -0,0 +1,303 @@
// network/merge.rs - Connection merging and update utilities
use crate::network::dpi::DpiResult;
use crate::network::parser::ParsedPacket;
use crate::network::types::{ApplicationProtocol, Connection, DpiInfo, RateInfo};
use std::time::{Instant, SystemTime};
/// Merge a parsed packet into an existing connection
pub fn merge_packet_into_connection(
mut conn: Connection,
parsed: &ParsedPacket,
now: SystemTime,
) -> Connection {
// Update timing
conn.last_activity = now;
// Update packet counts and bytes
if parsed.is_outgoing {
conn.packets_sent += 1;
conn.bytes_sent += parsed.packet_len as u64;
} else {
conn.packets_received += 1;
conn.bytes_received += parsed.packet_len as u64;
}
// Update protocol state (from packet flags/state)
conn.protocol_state = parsed.state;
// Update DPI info if available and better than what we have
if let Some(dpi_result) = &parsed.dpi_result {
merge_dpi_info(&mut conn, dpi_result);
}
conn
}
/// Create a new connection from a parsed packet
pub fn create_connection_from_packet(parsed: &ParsedPacket, now: SystemTime) -> Connection {
let mut conn = Connection::new(
parsed.protocol,
parsed.local_addr,
parsed.remote_addr,
parsed.state,
);
// Set initial stats based on packet direction
if parsed.is_outgoing {
conn.packets_sent = 1;
conn.bytes_sent = parsed.packet_len as u64;
} else {
conn.packets_received = 1;
conn.bytes_received = parsed.packet_len as u64;
}
// Apply DPI results if any
if let Some(dpi_result) = &parsed.dpi_result {
conn.dpi_info = Some(DpiInfo {
application: dpi_result.application.clone(),
first_packet_time: Instant::now(),
last_update_time: Instant::now(),
});
}
conn.created_at = now;
conn.last_activity = now;
conn
}
/// Merge DPI results into connection
fn merge_dpi_info(conn: &mut Connection, dpi_result: &DpiResult) {
match &conn.dpi_info {
None => {
// No existing DPI info, use the new one
conn.dpi_info = Some(DpiInfo {
application: dpi_result.application.clone(),
first_packet_time: Instant::now(),
last_update_time: Instant::now(),
});
}
Some(existing) => {
// Only update if new info has higher confidence or is more specific
if should_update_dpi(
&existing.application,
&dpi_result.application,
dpi_result.confidence,
) {
conn.dpi_info = Some(DpiInfo {
application: dpi_result.application.clone(),
first_packet_time: existing.first_packet_time,
last_update_time: Instant::now(),
});
}
}
}
}
/// Determine if we should update DPI info based on confidence and specificity
fn should_update_dpi(
existing: &ApplicationProtocol,
new: &ApplicationProtocol,
new_confidence: f32,
) -> bool {
// High confidence always wins
if new_confidence >= 0.95 {
return true;
}
// Specific protocols override Unknown
match (existing, new) {
(ApplicationProtocol::Unknown, _) => true,
(_, ApplicationProtocol::Unknown) => false,
// HTTPS is more specific than HTTP
(ApplicationProtocol::Http(_), ApplicationProtocol::Https(_)) => true,
(ApplicationProtocol::Https(_), ApplicationProtocol::Http(_)) => false,
// Otherwise, only update if confidence is good
_ => new_confidence >= 0.8,
}
}
/// Enrich connection with process information
pub fn enrich_with_process_info(
mut conn: Connection,
pid: u32,
process_name: String,
) -> Connection {
conn.pid = Some(pid);
conn.process_name = Some(process_name);
conn
}
/// Enrich connection with service name
pub fn enrich_with_service_name(mut conn: Connection, service_name: String) -> Connection {
conn.service_name = Some(service_name);
conn
}
/// Update connection rates based on current stats
pub fn update_connection_rates(mut conn: Connection, now: Instant) -> Connection {
let elapsed = now
.duration_since(conn.current_rate_bps.last_calculation)
.as_secs_f64();
if elapsed > 0.1 {
// Update at most every 100ms
conn.current_rate_bps = RateInfo {
outgoing_bps: (conn.bytes_sent as f64 * 8.0) / elapsed,
incoming_bps: (conn.bytes_received as f64 * 8.0) / elapsed,
last_calculation: now,
};
// Update backward compatibility fields
conn.current_incoming_rate_bps = conn.current_rate_bps.incoming_bps;
conn.current_outgoing_rate_bps = conn.current_rate_bps.outgoing_bps;
}
conn
}
/// Merge two connections (useful for combining data from different sources)
pub fn merge_connections(mut primary: Connection, secondary: &Connection) -> Connection {
// Use secondary's process info if primary doesn't have it
if primary.pid.is_none() && secondary.pid.is_some() {
primary.pid = secondary.pid;
primary.process_name = secondary.process_name.clone();
}
// Use secondary's service name if primary doesn't have it
if primary.service_name.is_none() && secondary.service_name.is_some() {
primary.service_name = secondary.service_name.clone();
}
// Merge traffic stats (take the maximum)
primary.bytes_sent = primary.bytes_sent.max(secondary.bytes_sent);
primary.bytes_received = primary.bytes_received.max(secondary.bytes_received);
primary.packets_sent = primary.packets_sent.max(secondary.packets_sent);
primary.packets_received = primary.packets_received.max(secondary.packets_received);
// Use the earlier creation time
if secondary.created_at < primary.created_at {
primary.created_at = secondary.created_at;
}
// Use the later last activity time
if secondary.last_activity > primary.last_activity {
primary.last_activity = secondary.last_activity;
}
// Merge DPI info (prefer more specific)
if let Some(secondary_dpi) = &secondary.dpi_info {
match &primary.dpi_info {
None => primary.dpi_info = Some(secondary_dpi.clone()),
Some(primary_dpi) => {
if should_update_dpi(&primary_dpi.application, &secondary_dpi.application, 0.9) {
primary.dpi_info = Some(secondary_dpi.clone());
}
}
}
}
primary
}
/// Check if two connections represent the same flow
pub fn connections_match(a: &Connection, b: &Connection) -> bool {
a.protocol == b.protocol && a.local_addr == b.local_addr && a.remote_addr == b.remote_addr
}
/// Check if a connection matches a parsed packet
pub fn connection_matches_packet(conn: &Connection, parsed: &ParsedPacket) -> bool {
conn.protocol == parsed.protocol
&& conn.local_addr == parsed.local_addr
&& conn.remote_addr == parsed.remote_addr
}
#[cfg(test)]
mod tests {
use super::*;
use crate::network::types::{Protocol, ProtocolState, TcpState};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
fn create_test_connection() -> Connection {
Connection::new(
Protocol::TCP,
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 12345),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 80),
ProtocolState::Tcp(TcpState::Established),
)
}
fn create_test_packet(is_outgoing: bool) -> ParsedPacket {
ParsedPacket {
connection_key: "test".to_string(),
protocol: Protocol::TCP,
local_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 12345),
remote_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 80),
state: ProtocolState::Tcp(TcpState::Established),
is_outgoing,
packet_len: 100,
dpi_result: None,
}
}
#[test]
fn test_merge_packet_into_connection() {
let mut conn = create_test_connection();
let packet = create_test_packet(true);
conn = merge_packet_into_connection(conn, &packet, SystemTime::now());
assert_eq!(conn.packets_sent, 1);
assert_eq!(conn.bytes_sent, 100);
assert_eq!(conn.packets_received, 0);
}
#[test]
fn test_create_connection_from_packet() {
let packet = create_test_packet(false);
let conn = create_connection_from_packet(&packet, SystemTime::now());
assert_eq!(conn.packets_received, 1);
assert_eq!(conn.bytes_received, 100);
assert_eq!(conn.packets_sent, 0);
}
#[test]
fn test_enrich_with_process_info() {
let conn = create_test_connection();
let enriched = enrich_with_process_info(conn, 1234, "firefox".to_string());
assert_eq!(enriched.pid, Some(1234));
assert_eq!(enriched.process_name, Some("firefox".to_string()));
}
#[test]
fn test_merge_connections() {
let mut primary = create_test_connection();
primary.bytes_sent = 1000;
let mut secondary = create_test_connection();
secondary.pid = Some(5678);
secondary.process_name = Some("chrome".to_string());
secondary.bytes_sent = 2000;
let merged = merge_connections(primary, &secondary);
assert_eq!(merged.pid, Some(5678));
assert_eq!(merged.process_name, Some("chrome".to_string()));
assert_eq!(merged.bytes_sent, 2000); // Takes the maximum
}
#[test]
fn test_connections_match() {
let conn1 = create_test_connection();
let conn2 = create_test_connection();
assert!(connections_match(&conn1, &conn2));
let mut conn3 = create_test_connection();
conn3.local_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 101)), 12345);
assert!(!connections_match(&conn1, &conn3));
}
}

File diff suppressed because it is too large Load Diff

1713
src/network/mod.rs.old Normal file

File diff suppressed because it is too large Load Diff

479
src/network/parser.rs Normal file
View File

@@ -0,0 +1,479 @@
// network/parser.rs - Updated with DPI integration
use crate::network::dpi::{self, DpiResult};
use crate::network::types::*;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
/// Result of parsing a packet
#[derive(Debug)]
pub struct ParsedPacket {
pub connection_key: String,
pub protocol: Protocol,
pub local_addr: SocketAddr,
pub remote_addr: SocketAddr,
pub state: ProtocolState,
pub is_outgoing: bool,
pub packet_len: usize,
pub dpi_result: Option<DpiResult>, // DPI results if available
}
pub struct ParserConfig {
pub enable_dpi: bool,
pub dpi_packet_limit: usize, // Only inspect first N packets per connection
}
impl Default for ParserConfig {
fn default() -> Self {
Self {
enable_dpi: true,
dpi_packet_limit: 10, // Only inspect first 10 packets
}
}
}
/// Packet parser - stateless, thread-safe
pub struct PacketParser {
local_ips: std::collections::HashSet<IpAddr>,
config: ParserConfig,
}
impl PacketParser {
pub fn new() -> Self {
let mut local_ips = std::collections::HashSet::new();
for iface in pnet_datalink::interfaces() {
for ip_network in iface.ips {
local_ips.insert(ip_network.ip());
}
}
Self {
local_ips,
config: ParserConfig::default(),
}
}
pub fn with_config(config: ParserConfig) -> Self {
let mut local_ips = std::collections::HashSet::new();
for iface in pnet_datalink::interfaces() {
for ip_network in iface.ips {
local_ips.insert(ip_network.ip());
}
}
Self { local_ips, config }
}
/// Parse a raw packet
pub fn parse_packet(&self, data: &[u8]) -> Option<ParsedPacket> {
if data.len() < 14 {
return None;
}
let ethertype = u16::from_be_bytes([data[12], data[13]]);
match ethertype {
0x0800 => self.parse_ipv4_packet(data),
0x86dd => self.parse_ipv6_packet(data),
0x0806 => self.parse_arp_packet(data),
_ => None,
}
}
fn parse_ipv4_packet(&self, data: &[u8]) -> Option<ParsedPacket> {
let ip_data = &data[14..];
if ip_data.len() < 20 {
return None;
}
let version = ip_data[0] >> 4;
if version != 4 {
return None;
}
let protocol_num = ip_data[9];
let src_ip = IpAddr::V4(Ipv4Addr::new(
ip_data[12],
ip_data[13],
ip_data[14],
ip_data[15],
));
let dst_ip = IpAddr::V4(Ipv4Addr::new(
ip_data[16],
ip_data[17],
ip_data[18],
ip_data[19],
));
let ihl = ip_data[0] & 0x0F;
let ip_header_len = (ihl as usize) * 4;
if ip_data.len() < ip_header_len {
return None;
}
let transport_data = &ip_data[ip_header_len..];
let is_outgoing = self.local_ips.contains(&src_ip);
match protocol_num {
1 => self.parse_icmp(transport_data, src_ip, dst_ip, is_outgoing, data.len()),
6 => self.parse_tcp(transport_data, src_ip, dst_ip, is_outgoing, data.len()),
17 => self.parse_udp(transport_data, src_ip, dst_ip, is_outgoing, data.len()),
_ => None,
}
}
fn parse_ipv6_packet(&self, data: &[u8]) -> Option<ParsedPacket> {
let ip_data = &data[14..];
if ip_data.len() < 40 {
return None;
}
let version = ip_data[0] >> 4;
if version != 6 {
return None;
}
let next_header = ip_data[6];
// Extract IPv6 addresses
let src_ip = IpAddr::V6(Ipv6Addr::new(
u16::from_be_bytes([ip_data[8], ip_data[9]]),
u16::from_be_bytes([ip_data[10], ip_data[11]]),
u16::from_be_bytes([ip_data[12], ip_data[13]]),
u16::from_be_bytes([ip_data[14], ip_data[15]]),
u16::from_be_bytes([ip_data[16], ip_data[17]]),
u16::from_be_bytes([ip_data[18], ip_data[19]]),
u16::from_be_bytes([ip_data[20], ip_data[21]]),
u16::from_be_bytes([ip_data[22], ip_data[23]]),
));
let dst_ip = IpAddr::V6(Ipv6Addr::new(
u16::from_be_bytes([ip_data[24], ip_data[25]]),
u16::from_be_bytes([ip_data[26], ip_data[27]]),
u16::from_be_bytes([ip_data[28], ip_data[29]]),
u16::from_be_bytes([ip_data[30], ip_data[31]]),
u16::from_be_bytes([ip_data[32], ip_data[33]]),
u16::from_be_bytes([ip_data[34], ip_data[35]]),
u16::from_be_bytes([ip_data[36], ip_data[37]]),
u16::from_be_bytes([ip_data[38], ip_data[39]]),
));
let transport_data = &ip_data[40..];
let is_outgoing = self.local_ips.contains(&src_ip);
// Handle extension headers if needed
let (final_next_header, transport_offset) =
self.parse_ipv6_extension_headers(next_header, transport_data);
let final_transport_data = &transport_data[transport_offset..];
match final_next_header {
58 => self.parse_icmpv6(
final_transport_data,
src_ip,
dst_ip,
is_outgoing,
data.len(),
),
6 => self.parse_tcp(
final_transport_data,
src_ip,
dst_ip,
is_outgoing,
data.len(),
),
17 => self.parse_udp(
final_transport_data,
src_ip,
dst_ip,
is_outgoing,
data.len(),
),
_ => None,
}
}
fn parse_tcp(
&self,
transport_data: &[u8],
src_ip: IpAddr,
dst_ip: IpAddr,
is_outgoing: bool,
packet_len: usize,
) -> Option<ParsedPacket> {
if transport_data.len() < 20 {
return None;
}
let src_port = u16::from_be_bytes([transport_data[0], transport_data[1]]);
let dst_port = u16::from_be_bytes([transport_data[2], transport_data[3]]);
let flags = transport_data[13];
let tcp_state = parse_tcp_flags(flags);
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),
)
};
// Perform DPI if enabled and there's payload
let dpi_result = if self.config.enable_dpi {
let tcp_header_len = ((transport_data[12] >> 4) as usize) * 4;
if transport_data.len() > tcp_header_len {
let payload = &transport_data[tcp_header_len..];
dpi::analyze_tcp_packet(payload, local_addr.port(), remote_addr.port(), is_outgoing)
} else {
None
}
} else {
None
};
Some(ParsedPacket {
connection_key: format!("TCP:{}-TCP:{}", local_addr, remote_addr),
protocol: Protocol::TCP,
local_addr,
remote_addr,
state: ProtocolState::Tcp(tcp_state),
is_outgoing,
packet_len,
dpi_result,
})
}
fn parse_udp(
&self,
transport_data: &[u8],
src_ip: IpAddr,
dst_ip: IpAddr,
is_outgoing: bool,
packet_len: usize,
) -> Option<ParsedPacket> {
if transport_data.len() < 8 {
return None;
}
let src_port = u16::from_be_bytes([transport_data[0], transport_data[1]]);
let dst_port = u16::from_be_bytes([transport_data[2], transport_data[3]]);
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),
)
};
// Perform DPI if enabled and there's payload
let dpi_result = if self.config.enable_dpi && transport_data.len() > 8 {
let payload = &transport_data[8..];
dpi::analyze_udp_packet(payload, local_addr.port(), remote_addr.port(), is_outgoing)
} else {
None
};
Some(ParsedPacket {
connection_key: format!("UDP:{}-UDP:{}", local_addr, remote_addr),
protocol: Protocol::UDP,
local_addr,
remote_addr,
state: ProtocolState::Udp,
is_outgoing,
packet_len,
dpi_result,
})
}
fn parse_icmp(
&self,
transport_data: &[u8],
src_ip: IpAddr,
dst_ip: IpAddr,
is_outgoing: bool,
packet_len: usize,
) -> Option<ParsedPacket> {
if transport_data.is_empty() {
return None;
}
let icmp_type = transport_data[0];
let icmp_code = if transport_data.len() > 1 {
transport_data[1]
} else {
0
};
let (local_addr, remote_addr) = if is_outgoing {
(SocketAddr::new(src_ip, 0), SocketAddr::new(dst_ip, 0))
} else {
(SocketAddr::new(dst_ip, 0), SocketAddr::new(src_ip, 0))
};
Some(ParsedPacket {
connection_key: format!("ICMP:{}-ICMP:{}", local_addr, remote_addr),
protocol: Protocol::ICMP,
local_addr,
remote_addr,
state: ProtocolState::Icmp {
icmp_type,
icmp_code,
},
is_outgoing,
packet_len,
dpi_result: None,
})
}
fn parse_icmpv6(
&self,
transport_data: &[u8],
src_ip: IpAddr,
dst_ip: IpAddr,
is_outgoing: bool,
packet_len: usize,
) -> Option<ParsedPacket> {
if transport_data.is_empty() {
return None;
}
let icmp_type = transport_data[0];
let icmp_code = if transport_data.len() > 1 {
transport_data[1]
} else {
0
};
let (local_addr, remote_addr) = if is_outgoing {
(SocketAddr::new(src_ip, 0), SocketAddr::new(dst_ip, 0))
} else {
(SocketAddr::new(dst_ip, 0), SocketAddr::new(src_ip, 0))
};
Some(ParsedPacket {
connection_key: format!("ICMP:{}-ICMP:{}", local_addr, remote_addr),
protocol: Protocol::ICMP,
local_addr,
remote_addr,
state: ProtocolState::Icmp {
icmp_type,
icmp_code,
},
is_outgoing,
packet_len,
dpi_result: None, // No DPI for ICMPv6
})
}
fn parse_arp_packet(&self, data: &[u8]) -> Option<ParsedPacket> {
let arp_data = &data[14..];
if arp_data.len() < 28 {
return None;
}
let hardware_type = u16::from_be_bytes([arp_data[0], arp_data[1]]);
let protocol_type = u16::from_be_bytes([arp_data[2], arp_data[3]]);
let opcode = u16::from_be_bytes([arp_data[6], arp_data[7]]);
if hardware_type != 1 || protocol_type != 0x0800 {
return None;
}
let sender_ip = IpAddr::from([arp_data[14], arp_data[15], arp_data[16], arp_data[17]]);
let target_ip = IpAddr::from([arp_data[24], arp_data[25], arp_data[26], arp_data[27]]);
let operation = match opcode {
1 => ArpOperation::Request,
2 => ArpOperation::Reply,
_ => return None,
};
let is_outgoing = self.local_ips.contains(&sender_ip);
let (local_addr, remote_addr) = if is_outgoing {
(SocketAddr::new(sender_ip, 0), SocketAddr::new(target_ip, 0))
} else {
(SocketAddr::new(target_ip, 0), SocketAddr::new(sender_ip, 0))
};
Some(ParsedPacket {
connection_key: format!("ARP:{}-ARP:{}", local_addr, remote_addr),
protocol: Protocol::ARP,
local_addr,
remote_addr,
state: ProtocolState::Arp { operation },
is_outgoing,
packet_len: data.len(),
dpi_result: None,
})
}
fn parse_ipv6_extension_headers(&self, mut next_header: u8, data: &[u8]) -> (u8, usize) {
let mut offset = 0;
const HOP_BY_HOP: u8 = 0;
const ROUTING: u8 = 43;
const FRAGMENT: u8 = 44;
const ENCAPSULATING_SECURITY: u8 = 50;
const AUTHENTICATION: u8 = 51;
const DESTINATION_OPTIONS: u8 = 60;
loop {
match next_header {
HOP_BY_HOP | ROUTING | DESTINATION_OPTIONS => {
if data.len() < offset + 2 {
return (next_header, offset);
}
next_header = data[offset];
let header_len = ((data[offset + 1] as usize) + 1) * 8;
offset += header_len;
}
FRAGMENT => {
if data.len() < offset + 8 {
return (next_header, offset);
}
next_header = data[offset];
offset += 8;
}
AUTHENTICATION => {
if data.len() < offset + 2 {
return (next_header, offset);
}
next_header = data[offset];
let header_len = ((data[offset + 1] as usize) + 2) * 4;
offset += header_len;
}
ENCAPSULATING_SECURITY => {
return (next_header, offset);
}
_ => {
return (next_header, offset);
}
}
if offset >= data.len() {
return (next_header, offset);
}
}
}
}
// ... rest of parsing methods
fn parse_tcp_flags(flags: u8) -> TcpState {
match flags {
0x02 => TcpState::SynSent,
0x12 => TcpState::SynReceived,
0x10 => TcpState::Established,
0x01 => TcpState::FinWait1,
0x11 => TcpState::FinWait2,
0x04 => TcpState::Closed,
0x14 => TcpState::Closing,
_ => TcpState::Established,
}
}

View File

@@ -0,0 +1,228 @@
// network/platform/linux.rs - Linux process lookup
use super::{ConnectionKey, ProcessLookup};
use crate::types::{Connection, Protocol};
use anyhow::Result;
use std::collections::HashMap;
use std::fs;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::RwLock;
use std::time::{Duration, Instant};
pub struct LinuxProcessLookup {
// Cache: ConnectionKey -> (pid, process_name)
cache: RwLock<ProcessCache>,
}
struct ProcessCache {
lookup: HashMap<ConnectionKey, (u32, String)>,
last_refresh: Instant,
}
impl LinuxProcessLookup {
pub fn new() -> Result<Self> {
Ok(Self {
cache: RwLock::new(ProcessCache {
lookup: HashMap::new(),
last_refresh: Instant::now() - Duration::from_secs(3600),
}),
})
}
/// Build connection -> process mapping
fn build_process_map() -> Result<HashMap<ConnectionKey, (u32, String)>> {
let mut process_map = HashMap::new();
// First, build inode -> process mapping
let inode_to_process = Self::build_inode_map()?;
// Then, parse network files to map connections -> inodes -> processes
Self::parse_and_map(
"/proc/net/tcp",
Protocol::TCP,
&inode_to_process,
&mut process_map,
)?;
Self::parse_and_map(
"/proc/net/tcp6",
Protocol::TCP,
&inode_to_process,
&mut process_map,
)?;
Self::parse_and_map(
"/proc/net/udp",
Protocol::UDP,
&inode_to_process,
&mut process_map,
)?;
Self::parse_and_map(
"/proc/net/udp6",
Protocol::UDP,
&inode_to_process,
&mut process_map,
)?;
Ok(process_map)
}
/// Build inode -> (pid, process_name) mapping
fn build_inode_map() -> Result<HashMap<u64, (u32, String)>> {
let mut inode_map = HashMap::new();
for entry in fs::read_dir("/proc")? {
let entry = entry?;
let path = entry.path();
if let Some(pid_str) = path.file_name().and_then(|s| s.to_str()) {
if let Ok(pid) = pid_str.parse::<u32>() {
if pid == 0 {
continue;
}
// Get process name
let comm_path = path.join("comm");
let process_name = fs::read_to_string(&comm_path)
.unwrap_or_else(|_| "unknown".to_string())
.trim()
.to_string();
// Check file descriptors
let fd_dir = path.join("fd");
if let Ok(fd_entries) = fs::read_dir(&fd_dir) {
for fd_entry in fd_entries.flatten() {
if let Ok(link) = fs::read_link(fd_entry.path()) {
if let Some(link_str) = link.to_str() {
if let Some(inode) = Self::extract_socket_inode(link_str) {
inode_map.insert(inode, (pid, process_name.clone()));
}
}
}
}
}
}
}
}
Ok(inode_map)
}
/// Parse /proc/net file and map connections to processes
fn parse_and_map(
path: &str,
protocol: Protocol,
inode_map: &HashMap<u64, (u32, String)>,
result: &mut HashMap<ConnectionKey, (u32, String)>,
) -> Result<()> {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return Ok(()), // File might not exist
};
for (i, line) in content.lines().enumerate() {
if i == 0 {
continue; // Skip header
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 10 {
continue;
}
// Parse addresses
let local_addr = match Self::parse_hex_address(parts[1]) {
Some(addr) => addr,
None => continue,
};
let remote_addr = match Self::parse_hex_address(parts[2]) {
Some(addr) => addr,
None => continue,
};
// Get inode
if let Ok(inode) = parts[9].parse::<u64>() {
if let Some((pid, name)) = inode_map.get(&inode) {
let key = ConnectionKey {
protocol,
local_addr,
remote_addr,
};
result.insert(key, (*pid, name.clone()));
}
}
}
Ok(())
}
fn parse_hex_address(hex_addr: &str) -> Option<SocketAddr> {
let parts: Vec<&str> = hex_addr.split(':').collect();
if parts.len() != 2 {
return None;
}
let ip_hex = parts[0];
let port = u16::from_str_radix(parts[1], 16).ok()?;
if ip_hex.len() == 8 {
// IPv4
let ip_bytes = u32::from_str_radix(ip_hex, 16).ok()?;
let ip = Ipv4Addr::from(ip_bytes.to_le_bytes());
Some(SocketAddr::new(IpAddr::V4(ip), port))
} else if ip_hex.len() == 32 {
// IPv6
let mut bytes = [0u8; 16];
for i in 0..4 {
let chunk = &ip_hex[i * 8..(i + 1) * 8];
let value = u32::from_str_radix(chunk, 16).ok()?;
bytes[i * 4..(i + 1) * 4].copy_from_slice(&value.to_le_bytes());
}
let ip = Ipv6Addr::from(bytes);
Some(SocketAddr::new(IpAddr::V6(ip), port))
} else {
None
}
}
fn extract_socket_inode(link: &str) -> Option<u64> {
if link.starts_with("socket:[") && link.ends_with(']') {
let inode_str = &link[8..link.len() - 1];
inode_str.parse().ok()
} else {
None
}
}
}
impl ProcessLookup for LinuxProcessLookup {
fn get_process_for_connection(&self, conn: &Connection) -> Option<(u32, String)> {
let key = ConnectionKey::from_connection(conn);
// Try cache first
{
let cache = self.cache.read().unwrap();
if cache.last_refresh.elapsed() < Duration::from_secs(2) {
if let Some(process_info) = cache.lookup.get(&key) {
return Some(process_info.clone());
}
}
}
// Cache is stale or miss, refresh
if self.refresh().is_ok() {
let cache = self.cache.read().unwrap();
cache.lookup.get(&key).cloned()
} else {
None
}
}
fn refresh(&self) -> Result<()> {
let process_map = Self::build_process_map()?;
let mut cache = self.cache.write().unwrap();
cache.lookup = process_map;
cache.last_refresh = Instant::now();
Ok(())
}
}

View File

@@ -0,0 +1,81 @@
use super::{ConnectionKey, ProcessLookup};
use crate::network::types::{Connection, Protocol};
use anyhow::Result;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::process::Command;
use std::sync::RwLock;
pub struct MacOSProcessLookup {
cache: RwLock<HashMap<ConnectionKey, (u32, String)>>,
}
impl MacOSProcessLookup {
pub fn new() -> Result<Self> {
Ok(Self {
cache: RwLock::new(HashMap::new()),
})
}
fn parse_lsof() -> Result<HashMap<ConnectionKey, (u32, String)>> {
let mut lookup = HashMap::new();
// Run lsof to get network connections
let output = Command::new("lsof")
.args(&["-i", "-n", "-P", "+c", "0"])
.output()?;
if !output.status.success() {
return Ok(lookup);
}
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines().skip(1) {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 10 {
continue;
}
let process_name = parts[0].to_string();
let pid = match parts[1].parse::<u32>() {
Ok(p) => p,
Err(_) => continue,
};
// Parse connection from NAME field
if let Some((protocol, local, remote)) = parse_lsof_connection(parts[8]) {
let key = ConnectionKey {
protocol,
local_addr: local,
remote_addr: remote,
};
lookup.insert(key, (pid, process_name));
}
}
Ok(lookup)
}
}
impl ProcessLookup for MacOSProcessLookup {
fn get_process_for_connection(&self, conn: &Connection) -> Option<(u32, String)> {
let key = ConnectionKey::from_connection(conn);
self.cache.read().unwrap().get(&key).cloned()
}
fn refresh(&self) -> Result<()> {
let new_cache = Self::parse_lsof()?;
*self.cache.write().unwrap() = new_cache;
Ok(())
}
}
fn parse_lsof_connection(name: &str) -> Option<(Protocol, SocketAddr, SocketAddr)> {
// Parse lsof NAME field format:
// "192.168.1.1:443->10.0.0.1:12345"
// Determine protocol and parse addresses
// Implementation would parse the connection string
None // Placeholder
}

View File

@@ -0,0 +1,73 @@
// network/platform/mod.rs - Platform process lookup
use crate::network::types::{Connection, Protocol};
use anyhow::Result;
use std::net::SocketAddr;
// Platform-specific modules
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "windows")]
mod windows;
// Re-export the appropriate implementation
#[cfg(target_os = "linux")]
pub use linux::LinuxProcessLookup;
#[cfg(target_os = "macos")]
pub use macos::MacOSProcessLookup;
#[cfg(target_os = "windows")]
pub use windows::WindowsProcessLookup;
/// Trait for platform-specific process lookup
pub trait ProcessLookup: Send + Sync {
/// Look up process information for a connection
/// Returns (pid, process_name) if found
fn get_process_for_connection(&self, conn: &Connection) -> Option<(u32, String)>;
/// Refresh internal caches if any (best-effort)
fn refresh(&self) -> Result<()> {
Ok(()) // Default no-op
}
}
/// Create a platform-specific process lookup
pub fn create_process_lookup() -> Result<Box<dyn ProcessLookup>> {
#[cfg(target_os = "linux")]
{
Ok(Box::new(LinuxProcessLookup::new()?))
}
#[cfg(target_os = "windows")]
{
Ok(Box::new(WindowsProcessLookup::new()?))
}
#[cfg(target_os = "macos")]
{
Ok(Box::new(MacOSProcessLookup::new()?))
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
Err(anyhow::anyhow!("Unsupported platform"))
}
}
/// Connection identifier for lookups
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct ConnectionKey {
pub protocol: Protocol,
pub local_addr: SocketAddr,
pub remote_addr: SocketAddr,
}
impl ConnectionKey {
pub fn from_connection(conn: &Connection) -> Self {
Self {
protocol: conn.protocol,
local_addr: conn.local_addr,
remote_addr: conn.remote_addr,
}
}
}

View File

@@ -0,0 +1,59 @@
use super::{ConnectionKey, ProcessLookup};
use crate::network::types::{Connection, Protocol};
use anyhow::Result;
use std::collections::HashMap;
use std::sync::RwLock;
pub struct WindowsProcessLookup {
// Windows can get process info directly from connection tables
cache: RwLock<HashMap<ConnectionKey, (u32, String)>>,
}
impl WindowsProcessLookup {
pub fn new() -> Result<Self> {
Ok(Self {
cache: RwLock::new(HashMap::new()),
})
}
fn refresh_tcp_processes(
&self,
cache: &mut HashMap<ConnectionKey, (u32, String)>,
) -> Result<()> {
// Use GetExtendedTcpTable to get TCP connections with PIDs
// This is pseudo-code - actual implementation would use winapi
// For each connection in the table:
// - Extract local/remote addresses
// - Get PID from dwOwningPid
// - Look up process name from PID
// - Insert into cache
Ok(())
}
fn refresh_udp_processes(
&self,
cache: &mut HashMap<ConnectionKey, (u32, String)>,
) -> Result<()> {
// Similar to TCP using GetExtendedUdpTable
Ok(())
}
}
impl ProcessLookup for WindowsProcessLookup {
fn get_process_for_connection(&self, conn: &Connection) -> Option<(u32, String)> {
let key = ConnectionKey::from_connection(conn);
self.cache.read().unwrap().get(&key).cloned()
}
fn refresh(&self) -> Result<()> {
let mut new_cache = HashMap::new();
self.refresh_tcp_processes(&mut new_cache)?;
self.refresh_udp_processes(&mut new_cache)?;
*self.cache.write().unwrap() = new_cache;
Ok(())
}
}

272
src/network/services.rs Normal file
View File

@@ -0,0 +1,272 @@
use crate::network::types::Protocol;
use anyhow::Result;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
/// Service name lookup table
#[derive(Debug, Clone)]
pub struct ServiceLookup {
/// Map of (port, protocol) -> service name
services: HashMap<(u16, Protocol), String>,
/// Common alternative names for services
aliases: HashMap<String, String>,
}
impl ServiceLookup {
/// Create an empty service lookup
pub fn new() -> Self {
Self {
services: HashMap::new(),
aliases: HashMap::new(),
}
}
/// Load services from a file (typically /etc/services format)
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut services = HashMap::new();
let mut aliases = HashMap::new();
let file = File::open(path)?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
let line = line.trim();
// Skip comments and empty lines
if line.is_empty() || line.starts_with('#') {
continue;
}
// Parse line format: service-name port/protocol [aliases...]
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 {
continue;
}
let service_name = parts[0];
let port_protocol = parts[1];
// Parse port/protocol
let port_parts: Vec<&str> = port_protocol.split('/').collect();
if port_parts.len() != 2 {
continue;
}
let port = match port_parts[0].parse::<u16>() {
Ok(p) => p,
Err(_) => continue,
};
let protocol = match port_parts[1].to_lowercase().as_str() {
"tcp" => Protocol::TCP,
"udp" => Protocol::UDP,
_ => continue,
};
// Store the service
services
.entry((port, protocol))
.or_insert_with(|| service_name.to_string());
// Store aliases if any
for &alias in &parts[2..] {
if !alias.starts_with('#') {
aliases.insert(alias.to_string(), service_name.to_string());
} else {
break; // Rest is comment
}
}
}
Ok(Self { services, aliases })
}
/// Create with common well-known services
pub fn with_defaults() -> Self {
let mut lookup = Self::new();
// Common TCP services
lookup.add_service(20, Protocol::TCP, "ftp-data");
lookup.add_service(21, Protocol::TCP, "ftp");
lookup.add_service(22, Protocol::TCP, "ssh");
lookup.add_service(23, Protocol::TCP, "telnet");
lookup.add_service(25, Protocol::TCP, "smtp");
lookup.add_service(53, Protocol::TCP, "dns");
lookup.add_service(80, Protocol::TCP, "http");
lookup.add_service(110, Protocol::TCP, "pop3");
lookup.add_service(143, Protocol::TCP, "imap");
lookup.add_service(443, Protocol::TCP, "https");
lookup.add_service(445, Protocol::TCP, "microsoft-ds");
lookup.add_service(587, Protocol::TCP, "submission");
lookup.add_service(993, Protocol::TCP, "imaps");
lookup.add_service(995, Protocol::TCP, "pop3s");
lookup.add_service(1433, Protocol::TCP, "mssql");
lookup.add_service(3306, Protocol::TCP, "mysql");
lookup.add_service(3389, Protocol::TCP, "rdp");
lookup.add_service(5432, Protocol::TCP, "postgresql");
lookup.add_service(5900, Protocol::TCP, "vnc");
lookup.add_service(6379, Protocol::TCP, "redis");
lookup.add_service(8080, Protocol::TCP, "http-alt");
lookup.add_service(8443, Protocol::TCP, "https-alt");
lookup.add_service(27017, Protocol::TCP, "mongodb");
// Common UDP services
lookup.add_service(53, Protocol::UDP, "dns");
lookup.add_service(67, Protocol::UDP, "dhcp-server");
lookup.add_service(68, Protocol::UDP, "dhcp-client");
lookup.add_service(123, Protocol::UDP, "ntp");
lookup.add_service(161, Protocol::UDP, "snmp");
lookup.add_service(443, Protocol::UDP, "https"); // QUIC
lookup.add_service(500, Protocol::UDP, "isakmp");
lookup.add_service(1194, Protocol::UDP, "openvpn");
lookup.add_service(4500, Protocol::UDP, "ipsec-nat");
lookup.add_service(5060, Protocol::UDP, "sip");
lookup
}
/// Add a service mapping
pub fn add_service(&mut self, port: u16, protocol: Protocol, name: &str) {
self.services.insert((port, protocol), name.to_string());
}
/// Look up a service name by port and protocol
pub fn lookup(&self, port: u16, protocol: Protocol) -> Option<&str> {
self.services.get(&(port, protocol)).map(|s| s.as_str())
}
/// Look up service name with fallback to common names
pub fn lookup_with_fallback(&self, port: u16, protocol: Protocol) -> Option<String> {
if let Some(name) = self.lookup(port, protocol) {
return Some(name.to_string());
}
// Common dynamic port ranges with generic names
match port {
1024..=5000 => Some("user-port".to_string()),
5001..=32767 => Some("dynamic".to_string()),
32768..=60999 => Some("private".to_string()),
61000..=65535 => Some("ephemeral".to_string()),
_ => None,
}
}
/// Get a display name for a service (formats well-known services better)
pub fn display_name(&self, port: u16, protocol: Protocol) -> String {
match self.lookup(port, protocol) {
Some("http") => "HTTP".to_string(),
Some("https") => "HTTPS".to_string(),
Some("ssh") => "SSH".to_string(),
Some("ftp") => "FTP".to_string(),
Some("smtp") => "SMTP".to_string(),
Some("imap") => "IMAP".to_string(),
Some("pop3") => "POP3".to_string(),
Some("dns") => "DNS".to_string(),
Some("dhcp-server") => "DHCP Server".to_string(),
Some("dhcp-client") => "DHCP Client".to_string(),
Some("ntp") => "NTP".to_string(),
Some("rdp") => "RDP".to_string(),
Some("vnc") => "VNC".to_string(),
Some(name) => {
// Capitalize first letter
let mut chars = name.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
None => format!("{}/{}", port, protocol),
}
}
/// Get all services for a specific protocol
pub fn services_by_protocol(&self, protocol: Protocol) -> Vec<(u16, &str)> {
let mut services: Vec<(u16, &str)> = self
.services
.iter()
.filter_map(|((port, proto), name)| {
if *proto == protocol {
Some((*port, name.as_str()))
} else {
None
}
})
.collect();
services.sort_by_key(|(port, _)| *port);
services
}
/// Get the number of services loaded
pub fn len(&self) -> usize {
self.services.len()
}
/// Check if the lookup table is empty
pub fn is_empty(&self) -> bool {
self.services.is_empty()
}
}
impl Default for ServiceLookup {
fn default() -> Self {
Self::with_defaults()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_services() {
let lookup = ServiceLookup::with_defaults();
assert_eq!(lookup.lookup(80, Protocol::TCP), Some("http"));
assert_eq!(lookup.lookup(443, Protocol::TCP), Some("https"));
assert_eq!(lookup.lookup(22, Protocol::TCP), Some("ssh"));
assert_eq!(lookup.lookup(53, Protocol::UDP), Some("dns"));
}
#[test]
fn test_display_names() {
let lookup = ServiceLookup::with_defaults();
assert_eq!(lookup.display_name(80, Protocol::TCP), "HTTP");
assert_eq!(lookup.display_name(443, Protocol::TCP), "HTTPS");
assert_eq!(lookup.display_name(12345, Protocol::TCP), "12345/TCP");
}
#[test]
fn test_lookup_with_fallback() {
let lookup = ServiceLookup::with_defaults();
assert_eq!(
lookup.lookup_with_fallback(80, Protocol::TCP),
Some("http".to_string())
);
assert_eq!(
lookup.lookup_with_fallback(50000, Protocol::TCP),
Some("private".to_string())
);
assert_eq!(
lookup.lookup_with_fallback(65000, Protocol::TCP),
Some("ephemeral".to_string())
);
}
#[test]
fn test_services_by_protocol() {
let lookup = ServiceLookup::with_defaults();
let tcp_services = lookup.services_by_protocol(Protocol::TCP);
assert!(tcp_services.iter().any(|(port, _)| *port == 80));
assert!(tcp_services.iter().any(|(port, _)| *port == 443));
let udp_services = lookup.services_by_protocol(Protocol::UDP);
assert!(udp_services.iter().any(|(port, _)| *port == 53));
}
}

273
src/network/types.rs Normal file
View File

@@ -0,0 +1,273 @@
use std::net::SocketAddr;
use std::time::{Duration, Instant, SystemTime};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Protocol {
TCP,
UDP,
ICMP,
ARP,
}
impl std::fmt::Display for Protocol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Protocol::TCP => write!(f, "TCP"),
Protocol::UDP => write!(f, "UDP"),
Protocol::ICMP => write!(f, "ICMP"),
Protocol::ARP => write!(f, "ARP"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TcpState {
Listen,
SynSent,
SynReceived,
Established,
FinWait1,
FinWait2,
CloseWait,
LastAck,
TimeWait,
Closing,
Closed,
}
#[derive(Debug, Clone, Copy)]
pub enum ProtocolState {
Tcp(TcpState),
Udp,
Icmp { icmp_type: u8, icmp_code: u8 },
Arp { operation: ArpOperation },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArpOperation {
Request,
Reply,
}
#[derive(Debug, Clone)]
pub enum ApplicationProtocol {
Http(HttpInfo),
Https(TlsInfo),
Dns(DnsInfo),
Ssh,
Quic,
Unknown,
}
#[derive(Debug, Clone)]
pub struct HttpInfo {
pub version: HttpVersion,
pub method: Option<String>,
pub host: Option<String>,
pub path: Option<String>,
pub status_code: Option<u16>,
pub user_agent: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpVersion {
Http10,
Http11,
Http2,
Http3,
}
#[derive(Debug, Clone)]
pub struct TlsInfo {
pub version: Option<TlsVersion>,
pub sni: Option<String>,
pub alpn: Vec<String>,
pub cipher_suite: Option<u16>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TlsVersion {
Ssl3,
Tls10,
Tls11,
Tls12,
Tls13,
}
#[derive(Debug, Clone)]
pub struct DnsInfo {
pub query_name: Option<String>,
pub query_type: Option<DnsQueryType>,
pub response_ips: Vec<std::net::IpAddr>,
pub is_response: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DnsQueryType {
A,
AAAA,
CNAME,
MX,
TXT,
Other(u16),
}
#[derive(Debug, Clone)]
pub struct DpiInfo {
pub application: ApplicationProtocol,
pub first_packet_time: Instant,
pub last_update_time: Instant,
}
#[derive(Debug, Clone)]
pub struct RateInfo {
pub incoming_bps: f64,
pub outgoing_bps: f64,
pub last_calculation: Instant,
}
impl Default for RateInfo {
fn default() -> Self {
Self {
incoming_bps: 0.0,
outgoing_bps: 0.0,
last_calculation: Instant::now(),
}
}
}
#[derive(Debug, Clone)]
pub struct Connection {
// Core identification
pub protocol: Protocol,
pub local_addr: SocketAddr,
pub remote_addr: SocketAddr,
// Protocol state
pub protocol_state: ProtocolState,
// Process information
pub pid: Option<u32>,
pub process_name: Option<String>,
// Traffic statistics
pub bytes_sent: u64,
pub bytes_received: u64,
pub packets_sent: u64,
pub packets_received: u64,
// Timing
pub created_at: SystemTime,
pub last_activity: SystemTime,
// Service identification
pub service_name: Option<String>,
// Deep packet inspection
pub dpi_info: Option<DpiInfo>,
// Performance metrics
pub current_rate_bps: RateInfo,
pub rtt_estimate: Option<Duration>,
// Backward compatibility fields
pub current_incoming_rate_bps: f64,
pub current_outgoing_rate_bps: f64,
}
impl Connection {
/// Create a new connection
pub fn new(
protocol: Protocol,
local_addr: SocketAddr,
remote_addr: SocketAddr,
state: ProtocolState,
) -> Self {
let now = SystemTime::now();
Self {
protocol,
local_addr,
remote_addr,
protocol_state: state,
pid: None,
process_name: None,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
created_at: now,
last_activity: now,
service_name: None,
dpi_info: None,
current_rate_bps: RateInfo::default(),
rtt_estimate: None,
current_incoming_rate_bps: 0.0,
current_outgoing_rate_bps: 0.0,
}
}
/// Generate a unique key for this connection
pub fn key(&self) -> String {
format!(
"{:?}:{}-{:?}:{}",
self.protocol, self.local_addr, self.protocol, self.remote_addr
)
}
/// Check if connection is active (had activity in the last minute)
pub fn is_active(&self) -> bool {
self.last_activity.elapsed().unwrap_or_default() < Duration::from_secs(60)
}
/// Get the age of the connection
pub fn age(&self) -> Duration {
self.created_at.elapsed().unwrap_or_default()
}
/// Get time since last activity
pub fn idle_time(&self) -> Duration {
self.last_activity.elapsed().unwrap_or_default()
}
/// Get display state
pub fn state(&self) -> String {
match &self.protocol_state {
ProtocolState::Tcp(tcp_state) => format!("{:?}", tcp_state),
ProtocolState::Udp => "ACTIVE".to_string(),
ProtocolState::Icmp { icmp_type, .. } => match icmp_type {
8 => "ECHO_REQUEST".to_string(),
0 => "ECHO_REPLY".to_string(),
3 => "DEST_UNREACH".to_string(),
11 => "TIME_EXCEEDED".to_string(),
_ => "UNKNOWN".to_string(),
},
ProtocolState::Arp { operation } => match operation {
ArpOperation::Request => "ARP_REQUEST".to_string(),
ArpOperation::Reply => "ARP_REPLY".to_string(),
},
}
}
/// Update transfer rates
pub fn update_rates(&mut self, new_sent: u64, new_received: u64) {
let now = Instant::now();
let elapsed = now
.duration_since(self.current_rate_bps.last_calculation)
.as_secs_f64();
if elapsed > 0.1 {
let sent_diff = new_sent.saturating_sub(self.bytes_sent) as f64;
let recv_diff = new_received.saturating_sub(self.bytes_received) as f64;
self.current_rate_bps = RateInfo {
outgoing_bps: (sent_diff * 8.0) / elapsed,
incoming_bps: (recv_diff * 8.0) / elapsed,
last_calculation: now,
};
// Update backward compatibility fields
self.current_incoming_rate_bps = self.current_rate_bps.incoming_bps;
self.current_outgoing_rate_bps = self.current_rate_bps.outgoing_bps;
}
}
}

View File

@@ -1,212 +0,0 @@
use anyhow::Result;
use std::process::Command;
use super::{Connection, ConnectionState, NetworkMonitor, Process, Protocol};
/// Get platform-specific connections for Windows
pub fn get_platform_connections(
monitor: &NetworkMonitor,
connections: &mut Vec<Connection>,
) -> Result<()> {
// Use netstat on Windows for both TCP and UDP
monitor.get_connections_from_netstat(connections)?;
Ok(())
}
// Methods below remain part of NetworkMonitor impl
impl NetworkMonitor {
/// Get platform-specific process for a connection
pub(super) fn get_platform_process_for_connection(
&self,
connection: &Connection,
) -> Option<Process> {
// Try netstat
if let Some(process) = try_netstat_command(connection) {
return Some(process);
}
// Fall back to API calls if we implement them
try_windows_api(connection)
}
/// Get process information by PID
pub(super) fn get_process_by_pid(&self, pid: u32) -> Option<Process> {
// Use tasklist to get process info
if let Ok(output) = Command::new("tasklist")
.args(["/FI", &format!("PID eq {}", pid), "/FO", "CSV", "/NH"])
.output()
{
let text = String::from_utf8_lossy(&output.stdout);
let line = text.lines().next().unwrap_or("");
// Parse CSV format
let parts: Vec<&str> = line.split(',').collect();
if parts.len() >= 2 {
// Remove quotes
let name = parts[0].trim_matches('"').to_string();
return Some(Process {
pid,
name,
});
}
}
None
}
/// Get connections from netstat command
pub(super) fn get_connections_from_netstat(&self, connections: &mut Vec<Connection>) -> Result<()> {
let output = Command::new("netstat").args(["-ano"]).output()?;
if output.status.success() {
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines().skip(4) {
// Skip headers
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 5 {
continue;
}
// Parse protocol
let protocol = match fields[0].to_lowercase().as_str() {
"tcp" | "tcp6" => Protocol::TCP,
"udp" | "udp6" => Protocol::UDP,
_ => continue,
};
// Parse state
let state_pos = 3;
let state = if fields.len() > state_pos {
match fields[state_pos] {
"ESTABLISHED" => ConnectionState::Established,
"LISTENING" | "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 local and remote addresses
let local_idx = 1;
let remote_idx = 2;
if let (Some(local), Some(remote)) = (
super::parse_addr(fields[local_idx]),
super::parse_addr(fields[remote_idx]),
) {
let mut conn = Connection::new(protocol, local, remote, state);
// Parse PID
let pid_pos = 4;
if fields.len() > pid_pos && fields[pid_pos] != "-" {
if let Ok(pid) = fields[pid_pos].parse::<u32>() {
conn.pid = Some(pid);
}
}
connections.push(conn);
}
}
}
Ok(())
}
}
/// Get process information using netstat command
pub(super) fn try_netstat_command(connection: &Connection) -> Option<Process> {
let output = Command::new("netstat").args(["-ano"]).output().ok()?;
if output.status.success() {
let text = String::from_utf8_lossy(&output.stdout);
let local_addr = format!("{}", connection.local_addr);
let remote_addr = format!("{}", connection.remote_addr);
for line in text.lines().skip(2) {
// Skip headers
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 5 {
continue;
}
// Check if this line matches our connection
let local_idx = 1;
let remote_idx = 2;
let proto_idx = 0;
let matches_protocol = match connection.protocol {
Protocol::TCP => {
fields[proto_idx].eq_ignore_ascii_case("tcp")
|| fields[proto_idx].eq_ignore_ascii_case("tcp6")
}
Protocol::UDP => {
fields[proto_idx].eq_ignore_ascii_case("udp")
|| fields[proto_idx].eq_ignore_ascii_case("udp6")
}
_ => false,
};
if matches_protocol
&& (fields[local_idx].contains(&local_addr)
|| fields[local_idx].contains(&format!(":{}", connection.local_addr.port())))
&& (fields[remote_idx].contains(&remote_addr)
|| fields[remote_idx].contains(&format!(":{}", connection.remote_addr.port())))
{
// Found matching connection, get PID
let pid_pos = 4;
if fields.len() > pid_pos && fields[pid_pos] != "-" {
if let Ok(pid) = fields[pid_pos].parse::<u32>() {
// Get process name
let name = get_process_name_by_pid(pid)
.unwrap_or_else(|| format!("process-{}", pid));
return Some(Process {
pid,
name,
});
}
}
break;
}
}
}
None
}
/// Try Windows API to get process information
pub(super) fn try_windows_api(_connection: &Connection) -> Option<Process> {
// This would require using the Windows API (like GetExtendedTcpTable)
// For simplicity, we'll just return None as a placeholder
// In a real implementation, you'd use the windows crate to make API calls
None
}
/// Get process name by PID
fn get_process_name_by_pid(pid: u32) -> Option<String> {
let output = Command::new("tasklist")
.args(["/FI", &format!("PID eq {}", pid), "/FO", "CSV", "/NH"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout);
let line = text.lines().next()?;
// Parse CSV format (remove quotes)
let name_end = line.find(',')? - 1;
let name = line[1..name_end].to_string();
Some(name)
}

555
src/ui.rs
View File

@@ -6,11 +6,10 @@ use ratatui::{
text::{Line, Span},
widgets::{Block, Borders, Cell, Paragraph, Row, Table, Tabs, Wrap},
};
// Removed unused import: use std::collections::HashMap;
use std::net::SocketAddr; // Import SocketAddr
use std::time::Instant;
use crate::app::{App, DetailFocusField, ViewMode}; // Added DetailFocusField
use crate::network::Protocol;
use crate::app::{App, AppStats};
use crate::network::types::{Connection, Protocol};
pub type Terminal<B> = RatatuiTerminal<B>;
@@ -40,11 +39,34 @@ pub fn restore_terminal<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>
Ok(())
}
/// UI state for managing the interface
pub struct UIState {
pub selected_tab: usize,
pub selected_connection: usize,
pub show_help: bool,
}
impl Default for UIState {
fn default() -> Self {
Self {
selected_tab: 0,
selected_connection: 0,
show_help: false,
}
}
}
/// Draw the UI
pub fn draw(f: &mut Frame, app: &mut App) -> Result<()> {
// If still loading, show loading screen instead of normal UI
if app.is_loading {
draw_loading_screen(f, app);
pub fn draw(
f: &mut Frame,
app: &App,
ui_state: &UIState,
connections: &[Connection],
stats: &AppStats,
) -> Result<()> {
// If still loading, show loading screen
if app.is_loading() {
draw_loading_screen(f);
return Ok(());
}
@@ -57,41 +79,35 @@ pub fn draw(f: &mut Frame, app: &mut App) -> Result<()> {
])
.split(f.area());
draw_tabs(f, app, chunks[0]);
draw_tabs(f, ui_state, chunks[0]);
match app.mode {
ViewMode::Overview => draw_overview(f, app, chunks[1])?,
ViewMode::ConnectionDetails => draw_connection_details(f, app, chunks[1])?,
ViewMode::Help => draw_help(f, app, chunks[1])?,
match ui_state.selected_tab {
0 => draw_overview(f, ui_state, connections, stats, chunks[1])?,
1 => draw_connection_details(f, ui_state, connections, chunks[1])?,
2 => draw_help(f, chunks[1])?,
_ => {}
}
draw_status_bar(f, app, chunks[2]);
draw_status_bar(f, connections.len(), chunks[2]);
Ok(())
}
/// Draw mode tabs
fn draw_tabs(f: &mut Frame, app: &App, area: Rect) {
fn draw_tabs(f: &mut Frame, ui_state: &UIState, area: Rect) {
let titles = vec![
Span::styled(app.i18n.get("overview"), Style::default().fg(Color::Green)),
Span::styled(
app.i18n.get("connections"),
Style::default().fg(Color::Green),
),
Span::styled(app.i18n.get("help"), Style::default().fg(Color::Green)),
Span::styled("Overview", Style::default().fg(Color::Green)),
Span::styled("Details", Style::default().fg(Color::Green)),
Span::styled("Help", Style::default().fg(Color::Green)),
];
let tabs = Tabs::new(titles.into_iter().map(Line::from).collect::<Vec<_>>())
.block(
Block::default()
.borders(Borders::ALL)
.title(app.i18n.get("rustnet")),
.title("RustNet Monitor"),
)
.select(match app.mode {
ViewMode::Overview => 0,
ViewMode::ConnectionDetails => 1,
ViewMode::Help => 2,
})
.select(ui_state.selected_tab)
.style(Style::default().fg(Color::White))
.highlight_style(
Style::default()
@@ -103,27 +119,38 @@ fn draw_tabs(f: &mut Frame, app: &App, area: Rect) {
}
/// Draw the overview mode
fn draw_overview(f: &mut Frame, app: &mut App, area: Rect) -> Result<()> {
fn draw_overview(
f: &mut Frame,
ui_state: &UIState,
connections: &[Connection],
stats: &AppStats,
area: Rect,
) -> Result<()> {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
.split(area);
draw_connections_list(f, app, chunks[0]);
draw_side_panel(f, app, chunks[1])?;
draw_connections_list(f, ui_state, connections, chunks[0]);
draw_stats_panel(f, connections, stats, chunks[1])?;
Ok(())
}
/// Draw connections list
fn draw_connections_list(f: &mut Frame, app: &mut App, area: Rect) {
fn draw_connections_list(
f: &mut Frame,
ui_state: &UIState,
connections: &[Connection],
area: Rect,
) {
let widths = [
Constraint::Length(6), // Protocol
Constraint::Length(28), // Local Address
Constraint::Length(38), // Remote Address - Increased Width
Constraint::Length(38), // Remote Address
Constraint::Length(12), // State
Constraint::Length(10), // Service
Constraint::Length(22), // Bandwidth (Down/Up)
Constraint::Length(22), // Bandwidth
Constraint::Min(10), // Process
];
@@ -133,7 +160,7 @@ fn draw_connections_list(f: &mut Frame, app: &mut App, area: Rect) {
"Remote Address",
"State",
"Service",
"Down / Up", // Updated Header
"Down / Up",
"Process",
]
.iter()
@@ -146,167 +173,153 @@ fn draw_connections_list(f: &mut Frame, app: &mut App, area: Rect) {
});
let header = Row::new(header_cells).height(1).bottom_margin(1);
let mut rows = Vec::new();
// Collect addresses to format to avoid borrowing issues with app.format_socket_addr
let addresses_to_format: Vec<(SocketAddr, SocketAddr)> = app
.connections
let rows: Vec<Row> = connections
.iter()
.map(|conn| (conn.local_addr, conn.remote_addr))
.map(|conn| {
let pid_str = conn
.pid
.map(|p| p.to_string())
.unwrap_or_else(|| "-".to_string());
let process_str = conn.process_name.clone().unwrap_or_else(|| "-".to_string());
let process_display = if conn.pid.is_some() {
format!("{} ({})", process_str, pid_str)
} else {
process_str
};
let service_display = conn.service_name.clone().unwrap_or_else(|| "-".to_string());
let incoming_rate = format_rate(conn.current_incoming_rate_bps);
let outgoing_rate = format_rate(conn.current_outgoing_rate_bps);
let bandwidth_display = format!("{} / {}", incoming_rate, outgoing_rate);
let cells = [
Cell::from(conn.protocol.to_string()),
Cell::from(conn.local_addr.to_string()),
Cell::from(conn.remote_addr.to_string()),
Cell::from(conn.state()),
Cell::from(service_display),
Cell::from(bandwidth_display),
Cell::from(process_display),
];
Row::new(cells)
})
.collect();
let mut formatted_addresses = Vec::new();
for (local_addr, remote_addr) in addresses_to_format {
let local_display = app.format_socket_addr(local_addr);
let remote_display = app.format_socket_addr(remote_addr);
formatted_addresses.push((local_display, remote_display));
}
for (idx, conn) in app.connections.iter().enumerate() {
let pid_str = conn
.pid
.map(|p| p.to_string())
.unwrap_or_else(|| "-".to_string());
let process_str = conn.process_name.clone().unwrap_or_else(|| "-".to_string());
let process_display = format!("{} ({})", process_str, pid_str);
let (local_display, remote_display) = formatted_addresses[idx].clone();
let service_display = conn.service_name.clone().unwrap_or_else(|| "-".to_string());
let incoming_rate_str = format_rate_from_bytes_per_second(conn.current_incoming_rate_bps);
let outgoing_rate_str = format_rate_from_bytes_per_second(conn.current_outgoing_rate_bps);
let bandwidth_display = format!("{} / {}", incoming_rate_str, outgoing_rate_str);
let cells = [
Cell::from(conn.protocol.to_string()),
Cell::from(local_display),
Cell::from(remote_display),
Cell::from(conn.state()),
Cell::from(service_display),
Cell::from(bandwidth_display), // Updated Cell
Cell::from(process_display),
];
rows.push(Row::new(cells));
}
// Create table state with current selection
let mut state = ratatui::widgets::TableState::default();
if !app.connections.is_empty() {
state.select(Some(app.selected_connection_idx));
if !connections.is_empty() {
state.select(Some(
ui_state
.selected_connection
.min(connections.len().saturating_sub(1)),
));
}
let connections = Table::new(rows, &widths)
let connections_table = Table::new(rows, &widths)
.header(header)
.block(
Block::default()
.borders(Borders::ALL)
.title(app.i18n.get("connections")),
.title("Active Connections"),
)
.row_highlight_style(Style::default().add_modifier(Modifier::REVERSED))
.highlight_symbol("> ");
f.render_stateful_widget(connections, area, &mut state);
f.render_stateful_widget(connections_table, area, &mut state);
}
/// Draw side panel with stats
fn draw_side_panel(f: &mut Frame, app: &App, area: Rect) -> Result<()> {
/// Draw stats panel
fn draw_stats_panel(
f: &mut Frame,
connections: &[Connection],
stats: &AppStats,
area: Rect,
) -> Result<()> {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Interface
Constraint::Min(0), // Summary stats (takes remaining space)
Constraint::Length(8), // Connection stats
Constraint::Min(0), // Traffic stats
])
.split(area);
let interface_text = format!(
"{}: {}",
app.i18n.get("interface"),
app.config
.interface
.clone()
.unwrap_or_else(|| app.i18n.get("default").to_string())
);
let interface_para = Paragraph::new(interface_text)
.block(
Block::default()
.borders(Borders::ALL)
.title(app.i18n.get("network")),
)
.style(Style::default().fg(Color::White));
f.render_widget(interface_para, chunks[0]);
let tcp_count = app
.connections
// Connection statistics
let tcp_count = connections
.iter()
.filter(|c| c.protocol == Protocol::TCP)
.count();
let udp_count = app
.connections
let udp_count = connections
.iter()
.filter(|c| c.protocol == Protocol::UDP)
.count();
let process_count = app.processes.len();
let stats_text: Vec<Line> = vec![
let conn_stats_text: Vec<Line> = vec![
Line::from(format!("TCP Connections: {}", tcp_count)),
Line::from(format!("UDP Connections: {}", udp_count)),
Line::from(format!("Total Connections: {}", connections.len())),
Line::from(""),
Line::from(format!(
"{}: {}",
app.i18n.get("tcp_connections"),
tcp_count
"Packets Processed: {}",
stats
.packets_processed
.load(std::sync::atomic::Ordering::Relaxed)
)),
Line::from(format!(
"{}: {}",
app.i18n.get("udp_connections"),
udp_count
)),
Line::from(format!(
"{}: {}",
app.i18n.get("total_connections"),
app.connections.len()
)),
Line::from(format!("{}: {}", app.i18n.get("processes"), process_count)),
Line::from(""), // Spacer
Line::from(format!(
"{}: {}",
app.i18n.get("total_incoming"),
format_rate_from_bytes_per_second(
app.connections
.iter()
.map(|c| c.current_incoming_rate_bps)
.sum()
)
)),
Line::from(format!(
"{}: {}",
app.i18n.get("total_outgoing"),
format_rate_from_bytes_per_second(
app.connections
.iter()
.map(|c| c.current_outgoing_rate_bps)
.sum()
)
"Packets Dropped: {}",
stats
.packets_dropped
.load(std::sync::atomic::Ordering::Relaxed)
)),
];
let stats_para = Paragraph::new(stats_text)
.block(
Block::default()
.borders(Borders::ALL)
.title(app.i18n.get("statistics")),
)
let conn_stats = Paragraph::new(conn_stats_text)
.block(Block::default().borders(Borders::ALL).title("Statistics"))
.style(Style::default().fg(Color::White));
f.render_widget(stats_para, chunks[1]); // Render stats into the second chunk which now takes remaining space
f.render_widget(conn_stats, chunks[0]);
// Traffic statistics
let total_incoming: f64 = connections
.iter()
.map(|c| c.current_incoming_rate_bps)
.sum();
let total_outgoing: f64 = connections
.iter()
.map(|c| c.current_outgoing_rate_bps)
.sum();
let traffic_stats_text: Vec<Line> = vec![
Line::from(format!("Total Incoming: {}", format_rate(total_incoming))),
Line::from(format!("Total Outgoing: {}", format_rate(total_outgoing))),
Line::from(""),
Line::from(format!(
"Last Update: {:?} ago",
stats.last_update.read().unwrap().elapsed()
)),
];
let traffic_stats = Paragraph::new(traffic_stats_text)
.block(Block::default().borders(Borders::ALL).title("Traffic"))
.style(Style::default().fg(Color::White));
f.render_widget(traffic_stats, chunks[1]);
Ok(())
}
/// Draw connection details view
fn draw_connection_details(f: &mut Frame, app: &mut App, area: Rect) -> Result<()> {
if app.connections.is_empty() {
let text = Paragraph::new(app.i18n.get("no_connections"))
fn draw_connection_details(
f: &mut Frame,
ui_state: &UIState,
connections: &[Connection],
area: Rect,
) -> Result<()> {
if connections.is_empty() {
let text = Paragraph::new("No connections available")
.block(
Block::default()
.borders(Borders::ALL)
.title(app.i18n.get("connection_details")),
.title("Connection Details"),
)
.style(Style::default().fg(Color::Red))
.alignment(ratatui::layout::Alignment::Center);
@@ -314,89 +327,46 @@ fn draw_connection_details(f: &mut Frame, app: &mut App, area: Rect) -> Result<(
return Ok(());
}
let conn_idx = app.selected_connection_idx;
let local_addr_to_format = app.connections[conn_idx].local_addr;
let remote_addr_to_format = app.connections[conn_idx].remote_addr;
// Format addresses before further immutable borrows of app.connections
let local_display = app.format_socket_addr(local_addr_to_format);
let remote_display = app.format_socket_addr(remote_addr_to_format);
let conn = &app.connections[conn_idx]; // Now we can immutably borrow again
let conn_idx = ui_state
.selected_connection
.min(connections.len().saturating_sub(1));
let conn = &connections[conn_idx];
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area);
// Connection details
let mut details_text: Vec<Line> = Vec::new();
// Styles for focused IP
let local_ip_style = if app.detail_focus == DetailFocusField::LocalIp {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
let remote_ip_style = if app.detail_focus == DetailFocusField::RemoteIp {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
details_text.push(Line::from(vec![
Span::styled(
format!("{}: ", app.i18n.get("protocol")),
Style::default().fg(Color::Yellow),
),
Span::styled("Protocol: ", Style::default().fg(Color::Yellow)),
Span::raw(conn.protocol.to_string()),
]));
// Use pre-formatted addresses
details_text.push(Line::from(vec![
Span::styled(
format!("{}: ", app.i18n.get("local_address")),
Style::default().fg(Color::Yellow),
),
Span::styled(local_display, local_ip_style), // Apply style
Span::styled("Local Address: ", Style::default().fg(Color::Yellow)),
Span::raw(conn.local_addr.to_string()),
]));
details_text.push(Line::from(vec![
Span::styled(
format!("{}: ", app.i18n.get("remote_address")),
Style::default().fg(Color::Yellow),
),
Span::styled(remote_display, remote_ip_style), // Apply style
Span::styled("Remote Address: ", Style::default().fg(Color::Yellow)),
Span::raw(conn.remote_addr.to_string()),
]));
if app.show_locations && !conn.remote_addr.ip().is_unspecified() {
// Commented out private field access
}
details_text.push(Line::from(vec![
Span::styled(
format!("{}: ", app.i18n.get("state")),
Style::default().fg(Color::Yellow),
),
Span::styled("State: ", Style::default().fg(Color::Yellow)),
Span::raw(conn.state()),
]));
details_text.push(Line::from(vec![
Span::styled(
format!("{}: ", app.i18n.get("process")),
Style::default().fg(Color::Yellow),
),
Span::styled("Process: ", Style::default().fg(Color::Yellow)),
Span::raw(conn.process_name.clone().unwrap_or_else(|| "-".to_string())),
]));
details_text.push(Line::from(vec![
Span::styled(
format!("{}: ", app.i18n.get("pid")),
Style::default().fg(Color::Yellow),
),
Span::styled("PID: ", Style::default().fg(Color::Yellow)),
Span::raw(
conn.pid
.map(|p| p.to_string())
@@ -405,76 +375,59 @@ fn draw_connection_details(f: &mut Frame, app: &mut App, area: Rect) -> Result<(
]));
details_text.push(Line::from(vec![
Span::styled(
format!("{}: ", app.i18n.get("age")),
Style::default().fg(Color::Yellow),
),
Span::raw(format!("{:?}", conn.age())),
Span::styled("Service: ", Style::default().fg(Color::Yellow)),
Span::raw(conn.service_name.clone().unwrap_or_else(|| "-".to_string())),
]));
details_text.push(Line::from("")); // Spacer
details_text.push(Line::from(Span::styled(
"Use Up/Down to select IP, 'c' to copy.", // Hint text
Style::default().fg(Color::DarkGray),
)));
let details = Paragraph::new(details_text)
.block(
Block::default()
.borders(Borders::ALL)
.title(app.i18n.get("connection_details")),
.title("Connection Information"),
)
.style(Style::default().fg(Color::White))
.wrap(Wrap { trim: true });
f.render_widget(details, chunks[0]);
// Traffic details
let mut traffic_text: Vec<Line> = Vec::new();
traffic_text.push(Line::from(vec![
Span::styled(
format!("{}: ", app.i18n.get("bytes_sent")),
Style::default().fg(Color::Yellow),
),
Span::styled("Bytes Sent: ", Style::default().fg(Color::Yellow)),
Span::raw(format_bytes(conn.bytes_sent)),
]));
traffic_text.push(Line::from(vec![
Span::styled(
format!("{}: ", app.i18n.get("bytes_received")),
Style::default().fg(Color::Yellow),
),
Span::styled("Bytes Received: ", Style::default().fg(Color::Yellow)),
Span::raw(format_bytes(conn.bytes_received)),
]));
traffic_text.push(Line::from(vec![
Span::styled(
format!("{}: ", app.i18n.get("packets_sent")),
Style::default().fg(Color::Yellow),
),
Span::styled("Packets Sent: ", Style::default().fg(Color::Yellow)),
Span::raw(conn.packets_sent.to_string()),
]));
traffic_text.push(Line::from(vec![
Span::styled(
format!("{}: ", app.i18n.get("packets_received")),
Style::default().fg(Color::Yellow),
),
Span::styled("Packets Received: ", Style::default().fg(Color::Yellow)),
Span::raw(conn.packets_received.to_string()),
]));
traffic_text.push(Line::from(vec![
Span::styled(
format!("{}: ", app.i18n.get("last_activity")),
Style::default().fg(Color::Yellow),
),
Span::raw(format!("{:?}", conn.idle_time())),
Span::styled("Current Rate (In): ", Style::default().fg(Color::Yellow)),
Span::raw(format_rate(conn.current_incoming_rate_bps)),
]));
traffic_text.push(Line::from(vec![
Span::styled("Current Rate (Out): ", Style::default().fg(Color::Yellow)),
Span::raw(format_rate(conn.current_outgoing_rate_bps)),
]));
let traffic = Paragraph::new(traffic_text)
.block(
Block::default()
.borders(Borders::ALL)
.title(app.i18n.get("traffic")),
.title("Traffic Statistics"),
)
.style(Style::default().fg(Color::White))
.wrap(Wrap { trim: true });
@@ -485,62 +438,48 @@ fn draw_connection_details(f: &mut Frame, app: &mut App, area: Rect) -> Result<(
}
/// Draw help screen
fn draw_help(f: &mut Frame, app: &App, area: Rect) -> Result<()> {
fn draw_help(f: &mut Frame, area: Rect) -> Result<()> {
let help_text: Vec<Line> = vec![
Line::from(vec![
Span::styled(
"RustNet ",
"RustNet Monitor ",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::raw(app.i18n.get("help_intro")),
Span::raw("- Network Connection Monitor"),
]),
Line::from(""),
Line::from(vec![
Span::styled("q, Ctrl+C ", Style::default().fg(Color::Yellow)),
Span::raw(app.i18n.get("help_quit")),
Span::raw("Quit application"),
]),
Line::from(vec![
Span::styled("r ", Style::default().fg(Color::Yellow)),
Span::raw(app.i18n.get("help_refresh")),
Span::styled("Tab ", Style::default().fg(Color::Yellow)),
Span::raw("Switch between tabs"),
]),
Line::from(vec![
Span::styled("↑/k, ↓/j ", Style::default().fg(Color::Yellow)),
Span::raw(app.i18n.get("help_navigate")),
Span::raw("Navigate connections"),
]),
Line::from(vec![
Span::styled("Enter ", Style::default().fg(Color::Yellow)),
Span::raw(app.i18n.get("help_select")),
Span::raw("View connection details"),
]),
Line::from(vec![
Span::styled("Esc ", Style::default().fg(Color::Yellow)),
Span::raw(app.i18n.get("help_back")),
]),
Line::from(vec![
Span::styled("l ", Style::default().fg(Color::Yellow)),
Span::raw(app.i18n.get("help_toggle_location")),
]),
Line::from(vec![
Span::styled("d ", Style::default().fg(Color::Yellow)),
Span::raw(app.i18n.get("help_toggle_dns")),
Span::raw("Return to overview"),
]),
Line::from(vec![
Span::styled("h ", Style::default().fg(Color::Yellow)),
Span::raw(app.i18n.get("help_toggle_help")),
]),
Line::from(vec![
Span::styled("Ctrl+D ", Style::default().fg(Color::Yellow)),
Span::raw(app.i18n.get("help_dump_connections")),
Span::raw("Toggle this help screen"),
]),
Line::from(""),
Line::from("Press any key to continue..."),
];
let help = Paragraph::new(help_text)
.block(
Block::default()
.borders(Borders::ALL)
.title(app.i18n.get("help")),
)
.block(Block::default().borders(Borders::ALL).title("Help"))
.style(Style::default().fg(Color::White))
.wrap(Wrap { trim: true })
.alignment(ratatui::layout::Alignment::Left);
@@ -551,12 +490,10 @@ fn draw_help(f: &mut Frame, app: &App, area: Rect) -> Result<()> {
}
/// Draw status bar
fn draw_status_bar(f: &mut Frame, app: &App, area: Rect) {
fn draw_status_bar(f: &mut Frame, connection_count: usize, area: Rect) {
let status = format!(
"{} | {} | {}",
app.i18n.get("press_h_for_help"),
format!("{}: {}", app.i18n.get("language"), app.config.language),
format!("{}: {}", app.i18n.get("connections"), app.connections.len())
" Press 'h' for help | Connections: {} | Tab to switch views ",
connection_count
);
let status_bar = Paragraph::new(status)
@@ -566,98 +503,64 @@ fn draw_status_bar(f: &mut Frame, app: &App, area: Rect) {
f.render_widget(status_bar, area);
}
/// Draw loading screen with progress message
fn draw_loading_screen(f: &mut Frame, app: &App) {
/// Draw loading screen
fn draw_loading_screen(f: &mut Frame) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Header
Constraint::Min(0), // Content
Constraint::Length(1), // Status
])
.split(f.area());
// Draw header
let header = Paragraph::new("RustNet - Network Monitor")
.style(
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
)
.alignment(ratatui::layout::Alignment::Center)
.block(Block::default().borders(Borders::ALL));
f.render_widget(header, chunks[0]);
// Draw loading content
let loading_content = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(40),
Constraint::Length(5),
Constraint::Percentage(40),
])
.split(chunks[1]);
.split(f.area());
let loading_text = vec![
Line::from(""),
Line::from(vec![
Span::styled(app.get_spinner_char(), Style::default().fg(Color::Yellow)),
Span::styled(" ", Style::default()),
Span::styled(&app.loading_message, Style::default().fg(Color::White)),
Span::styled("", Style::default().fg(Color::Yellow)),
Span::styled(
"Loading network connections...",
Style::default().fg(Color::White),
),
]),
Line::from(""),
Line::from(vec![Span::styled(
"Please wait while we discover network connections",
Style::default().fg(Color::Cyan),
)]),
Line::from(""),
Line::from(vec![Span::styled(
"This may take 10-30 seconds depending on your system",
"This may take a few seconds",
Style::default().fg(Color::DarkGray),
)]),
];
let loading_paragraph = Paragraph::new(loading_text)
.alignment(ratatui::layout::Alignment::Center)
.block(Block::default().borders(Borders::ALL).title("Loading"));
f.render_widget(loading_paragraph, loading_content[1]);
.block(
Block::default()
.borders(Borders::ALL)
.title("RustNet Monitor"),
);
// Draw status
let status = "Press Ctrl+C to cancel";
let status_bar = Paragraph::new(status)
.style(Style::default().fg(Color::White).bg(Color::Blue))
.alignment(ratatui::layout::Alignment::Center);
f.render_widget(status_bar, chunks[2]);
f.render_widget(loading_paragraph, chunks[1]);
}
// format_rate function removed as it's no longer used.
/// Format rate (given as f64 bytes_per_second) to human readable form (KB/s, MB/s, etc.)
fn format_rate_from_bytes_per_second(bytes_per_second: f64) -> String {
/// Format rate to human readable form
fn format_rate(bytes_per_second: f64) -> String {
const KB_PER_SEC: f64 = 1024.0;
const MB_PER_SEC: f64 = KB_PER_SEC * 1024.0;
const GB_PER_SEC: f64 = MB_PER_SEC * 1024.0;
if bytes_per_second.is_nan() || bytes_per_second.is_infinite() {
return "-".to_string();
}
if bytes_per_second >= GB_PER_SEC {
format!("{:.2} GB/s", bytes_per_second / GB_PER_SEC)
} else if bytes_per_second >= MB_PER_SEC {
format!("{:.2} MB/s", bytes_per_second / MB_PER_SEC)
} else if bytes_per_second >= KB_PER_SEC {
format!("{:.2} KB/s", bytes_per_second / KB_PER_SEC)
} else if bytes_per_second > 0.1 || bytes_per_second == 0.0 {
// Show B/s for very small rates or zero
} else if bytes_per_second > 0.0 {
format!("{:.0} B/s", bytes_per_second)
} else {
// For very small, non-zero rates, indicate less than 1 B/s
"<1 B/s".to_string()
"-".to_string()
}
}
/// Format bytes to human readable form (KB, MB, etc.)
/// Format bytes to human readable form
fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;