diff --git a/USAGE.md b/USAGE.md index e4dcf43..7d415f8 100644 --- a/USAGE.md +++ b/USAGE.md @@ -787,3 +787,60 @@ When reporting issues: 5. Redact sensitive information before sharing For performance issues, trace-level logging provides the most detail but generates large log files quickly. + +### JSON Logging + +The `--json-log` option enables structured JSON logging of connection events to a file. Each line is a separate JSON object (JSONL format). + +```bash +# Enable JSON logging +sudo rustnet --json-log /tmp/connections.json + +# Combine with other options +sudo rustnet -i eth0 --json-log ~/network-events.json +``` + +**Event types:** +- `new_connection` - Logged when a new connection is first detected +- `connection_closed` - Logged when a connection is cleaned up after becoming inactive + +**JSON fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `timestamp` | string | RFC3339 UTC timestamp | +| `event` | string | Event type (`new_connection` or `connection_closed`) | +| `protocol` | string | Protocol (TCP, UDP, etc.) | +| `source_ip` | string | Local IP address | +| `source_port` | number | Local port number | +| `destination_ip` | string | Remote IP address | +| `destination_port` | number | Remote port number | +| `pid` | number | Process ID (if available) | +| `process_name` | string | Process name (if available) | +| `service_name` | string | Service name from port lookup (if available) | +| `direction` | string | Connection direction (`outgoing` or `incoming`), TCP only when handshake observed | +| `dpi_protocol` | string | Detected application protocol (if DPI enabled) | +| `dpi_domain` | string | Extracted domain/hostname (if available) | +| `bytes_sent` | number | Total bytes sent (connection_closed only) | +| `bytes_received` | number | Total bytes received (connection_closed only) | +| `duration_secs` | number | Connection duration in seconds (connection_closed only) | + +**Example output:** + +```json +{"timestamp":"2025-01-15T10:30:00Z","event":"new_connection","protocol":"TCP","source_ip":"192.168.1.100","source_port":54321,"destination_ip":"93.184.216.34","destination_port":443,"pid":1234,"process_name":"curl","service_name":"https","direction":"outgoing","dpi_protocol":"HTTPS","dpi_domain":"example.com"} +{"timestamp":"2025-01-15T10:30:05Z","event":"connection_closed","protocol":"TCP","source_ip":"192.168.1.100","source_port":54321,"destination_ip":"93.184.216.34","destination_port":443,"pid":1234,"process_name":"curl","service_name":"https","direction":"outgoing","bytes_sent":1024,"bytes_received":4096,"duration_secs":5} +``` + +**Processing JSON logs:** + +```bash +# Pretty-print latest events +tail -f /tmp/connections.json | jq . + +# Filter by process +cat /tmp/connections.json | jq 'select(.process_name == "firefox")' + +# Count connections by destination +cat /tmp/connections.json | jq -s 'group_by(.destination_ip) | map({ip: .[0].destination_ip, count: length})' +``` diff --git a/src/app.rs b/src/app.rs index 52d9e32..aecf5ce 100644 --- a/src/app.rs +++ b/src/app.rs @@ -75,6 +75,24 @@ fn log_connection_event( "destination_port": conn.remote_addr.port(), }); + // Add process information if available + if let Some(pid) = conn.pid { + event["pid"] = json!(pid); + } + if let Some(process_name) = &conn.process_name { + event["process_name"] = json!(process_name); + } + + // Add service name if available + if let Some(service_name) = &conn.service_name { + event["service_name"] = json!(service_name); + } + + // Add connection direction (only for TCP when we observed the handshake) + if let Some(is_outgoing) = conn.connection_direction { + event["direction"] = json!(if is_outgoing { "outgoing" } else { "incoming" }); + } + // Add DPI information if available if let Some(dpi) = &conn.dpi_info { event["dpi_protocol"] = json!(dpi.application.to_string()); @@ -503,7 +521,13 @@ impl App { let mut parsed_count = 0; for packet_data in &batch { if let Some(parsed) = parser.parse_packet(packet_data) { - update_connection(&connections, parsed, &stats, &json_log_path, &rtt_tracker); + update_connection( + &connections, + parsed, + &stats, + &json_log_path, + &rtt_tracker, + ); parsed_count += 1; } } @@ -898,12 +922,15 @@ impl App { } // Aggregate rates from all interfaces - let (total_rx, total_tx) = interface_rates.iter().fold((0u64, 0u64), |(rx, tx), entry| { - ( - rx + entry.value().rx_bytes_per_sec, - tx + entry.value().tx_bytes_per_sec, - ) - }); + let (total_rx, total_tx) = + interface_rates + .iter() + .fold((0u64, 0u64), |(rx, tx), entry| { + ( + rx + entry.value().rx_bytes_per_sec, + tx + entry.value().tx_bytes_per_sec, + ) + }); // Get connection count from snapshot let connection_count = connections_snapshot diff --git a/src/cli.rs b/src/cli.rs index 2a5fa7c..5d718bc 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,7 +10,8 @@ const INTERFACE_HELP: &str = "Network interface to monitor"; const BPF_HELP: &str = "BPF filter expression for packet capture (e.g., \"tcp port 443\"). Note: Using a BPF filter disables PKTAP (process info falls back to lsof)"; #[cfg(not(target_os = "macos"))] -const BPF_HELP: &str = "BPF filter expression for packet capture (e.g., \"tcp port 443\", \"dst port 80\")"; +const BPF_HELP: &str = + "BPF filter expression for packet capture (e.g., \"tcp port 443\", \"dst port 80\")"; pub fn build_cli() -> Command { let cmd = Command::new("rustnet") diff --git a/src/lib.rs b/src/lib.rs index 9bc2461..2cd1d06 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,9 @@ pub mod ui; #[cfg(target_os = "windows")] pub fn is_admin() -> bool { use windows::Win32::Foundation::HANDLE; - use windows::Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY}; + use windows::Win32::Security::{ + GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation, + }; use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken}; unsafe { diff --git a/src/main.rs b/src/main.rs index f73d22f..0e4105f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -92,7 +92,9 @@ fn main() -> Result<()> { // - Log files need to be created first #[cfg(all(target_os = "linux", feature = "landlock"))] { - use network::platform::sandbox::{apply_sandbox, SandboxConfig, SandboxMode, SandboxStatus}; + use network::platform::sandbox::{ + SandboxConfig, SandboxMode, SandboxStatus, apply_sandbox, + }; use std::path::PathBuf; let sandbox_mode = if matches.get_flag("no-sandbox") { @@ -594,8 +596,8 @@ fn run_ui_loop( // Try arboard first, fall back to wl-copy for Wayland (GNOME doesn't // support the wlr-data-control protocol that arboard relies on) - let result = Clipboard::new() - .and_then(|mut cb| cb.set_text(&remote_addr)); + let result = + Clipboard::new().and_then(|mut cb| cb.set_text(&remote_addr)); #[cfg(any(target_os = "linux", target_os = "freebsd"))] let result = result.or_else(|_| { @@ -636,7 +638,8 @@ fn run_ui_loop( let msg = format!("Clipboard error: {}", e); error!("{}", msg); - ui_state.clipboard_message = Some((msg, std::time::Instant::now())); + ui_state.clipboard_message = + Some((msg, std::time::Instant::now())); } } } @@ -779,7 +782,9 @@ fn check_dll_available(dll_name: &str) -> bool { #[cfg(target_os = "windows")] fn is_admin() -> bool { use windows::Win32::Foundation::HANDLE; - use windows::Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY}; + use windows::Win32::Security::{ + GetTokenInformation, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation, + }; use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken}; unsafe { diff --git a/src/network/capture.rs b/src/network/capture.rs index 8c17c45..14d9fe0 100644 --- a/src/network/capture.rs +++ b/src/network/capture.rs @@ -231,7 +231,9 @@ pub fn setup_packet_capture(config: CaptureConfig) -> Result<(Capture, S // Fallback to regular capture (original code) #[cfg(target_os = "macos")] if config.filter.is_some() { - log::warn!("BPF filter specified - using regular capture instead of PKTAP (BPF filters don't work with PKTAP)"); + log::warn!( + "BPF filter specified - using regular capture instead of PKTAP (BPF filters don't work with PKTAP)" + ); } log::info!("Setting up regular packet capture"); let device = find_capture_device(&config.interface)?; diff --git a/src/network/merge.rs b/src/network/merge.rs index f141137..82e0832 100644 --- a/src/network/merge.rs +++ b/src/network/merge.rs @@ -340,18 +340,38 @@ pub fn create_connection_from_packet(parsed: &ParsedPacket, now: SystemTime) -> // Set initial TCP state based on flags if TCP if let Some(tcp_header) = parsed.tcp_header { - conn.protocol_state = ProtocolState::Tcp(update_tcp_state( - TcpState::Unknown, - &tcp_header.flags, - parsed.is_outgoing, - )); + let tcp_state = update_tcp_state(TcpState::Unknown, &tcp_header.flags, parsed.is_outgoing); + conn.protocol_state = ProtocolState::Tcp(tcp_state); + + // Set connection direction only if we observed the TCP handshake + // SynSent = we initiated (outgoing), SynReceived = they initiated (incoming) + // Also detect from SYN+ACK: receiving SYN+ACK means we initiated (outgoing) + conn.connection_direction = match tcp_state { + TcpState::SynSent => Some(true), // outgoing - we sent SYN + TcpState::SynReceived => Some(false), // incoming - we received SYN + _ => { + // Check if first packet is SYN+ACK - can also determine direction + if tcp_header.flags.syn && tcp_header.flags.ack { + // SYN+ACK received = we initiated (outgoing) + // SYN+ACK sent = they initiated (incoming) + Some(!parsed.is_outgoing) + } else { + None // mid-stream capture, direction unknown + } + } + }; debug!( - "Created new {} connection: {:?} -> {:?}, state: {:?}", - parsed.protocol, parsed.local_addr, parsed.remote_addr, conn.protocol_state + "Created new {} connection: {:?} -> {:?}, state: {:?}, direction: {:?}", + parsed.protocol, + parsed.local_addr, + parsed.remote_addr, + conn.protocol_state, + conn.connection_direction ); } else { // For non-TCP protocols, use the provided state directly + // Connection direction is not determinable for stateless protocols conn.protocol_state = parsed.protocol_state; } diff --git a/src/network/platform/linux/process.rs b/src/network/platform/linux/process.rs index 7d4245c..8ec2b98 100644 --- a/src/network/platform/linux/process.rs +++ b/src/network/platform/linux/process.rs @@ -54,8 +54,7 @@ impl LinuxProcessLookup { } /// Build connection -> process mapping and PID -> name mapping - fn build_process_map() -> Result<(ConnectionProcessMap, PidNameMap)> - { + fn build_process_map() -> Result<(ConnectionProcessMap, PidNameMap)> { let mut process_map = HashMap::new(); // First, build inode -> process mapping and PID -> name mapping diff --git a/src/network/platform/linux/sandbox/capabilities.rs b/src/network/platform/linux/sandbox/capabilities.rs index 12de9a9..291c6ba 100644 --- a/src/network/platform/linux/sandbox/capabilities.rs +++ b/src/network/platform/linux/sandbox/capabilities.rs @@ -45,9 +45,7 @@ pub fn drop_cap_net_raw() -> Result { // Also drop from permitted set to prevent re-acquiring // This is optional but provides stronger security - if caps::has_cap(None, CapSet::Permitted, Capability::CAP_NET_RAW) - .unwrap_or(false) - { + if caps::has_cap(None, CapSet::Permitted, Capability::CAP_NET_RAW).unwrap_or(false) { if let Err(e) = caps::drop(None, CapSet::Permitted, Capability::CAP_NET_RAW) { // Not fatal - we already dropped from effective log::warn!("Could not drop CAP_NET_RAW from permitted set: {}", e); diff --git a/src/network/platform/linux/sandbox/landlock.rs b/src/network/platform/linux/sandbox/landlock.rs index 922ec99..88dfc6c 100644 --- a/src/network/platform/linux/sandbox/landlock.rs +++ b/src/network/platform/linux/sandbox/landlock.rs @@ -19,8 +19,8 @@ use anyhow::{Context, Result}; use landlock::{ - Access, AccessFs, AccessNet, BitFlags, LandlockStatus, PathBeneath, PathFd, Ruleset, - RulesetAttr, RulesetCreatedAttr, RulesetStatus, ABI, + ABI, Access, AccessFs, AccessNet, BitFlags, LandlockStatus, PathBeneath, PathFd, Ruleset, + RulesetAttr, RulesetCreatedAttr, RulesetStatus, }; use std::path::Path; @@ -95,7 +95,9 @@ pub fn apply_landlock(config: &SandboxConfig) -> Result { }; // Create the ruleset - let mut ruleset_created = ruleset.create().context("Failed to create Landlock ruleset")?; + let mut ruleset_created = ruleset + .create() + .context("Failed to create Landlock ruleset")?; // Add rule for /proc (read-only) // This is required for process identification via procfs diff --git a/src/network/platform/mod.rs b/src/network/platform/mod.rs index bc01fcb..ef912c0 100644 --- a/src/network/platform/mod.rs +++ b/src/network/platform/mod.rs @@ -22,10 +22,10 @@ mod windows; // Re-export factory functions and types from platform modules #[cfg(target_os = "freebsd")] pub use freebsd::{FreeBSDProcessLookup, FreeBSDStatsProvider, create_process_lookup}; -#[cfg(target_os = "linux")] -pub use linux::{LinuxStatsProvider, create_process_lookup}; #[cfg(all(target_os = "linux", feature = "landlock"))] pub use linux::sandbox; +#[cfg(target_os = "linux")] +pub use linux::{LinuxStatsProvider, create_process_lookup}; #[cfg(target_os = "macos")] pub use macos::{MacOSStatsProvider, create_process_lookup}; #[cfg(target_os = "windows")] diff --git a/src/network/types.rs b/src/network/types.rs index ed18296..9abaf00 100644 --- a/src/network/types.rs +++ b/src/network/types.rs @@ -953,12 +953,28 @@ impl AppProtocolDistribution { pub fn as_percentages(&self) -> Vec<(&'static str, usize, f64)> { let total = self.total().max(1) as f64; vec![ - ("HTTPS", self.https_count, self.https_count as f64 / total * 100.0), - ("QUIC", self.quic_count, self.quic_count as f64 / total * 100.0), - ("HTTP", self.http_count, self.http_count as f64 / total * 100.0), + ( + "HTTPS", + self.https_count, + self.https_count as f64 / total * 100.0, + ), + ( + "QUIC", + self.quic_count, + self.quic_count as f64 / total * 100.0, + ), + ( + "HTTP", + self.http_count, + self.http_count as f64 / total * 100.0, + ), ("DNS", self.dns_count, self.dns_count as f64 / total * 100.0), ("SSH", self.ssh_count, self.ssh_count as f64 / total * 100.0), - ("Other", self.other_count, self.other_count as f64 / total * 100.0), + ( + "Other", + self.other_count, + self.other_count as f64 / total * 100.0, + ), ] } } @@ -1200,6 +1216,10 @@ pub struct Connection { pub pid: Option, pub process_name: Option, + // Connection direction: true = outgoing (local initiated), false = incoming (remote initiated) + // Only set for TCP when we observe the handshake (SYN/SYN+ACK), None otherwise + pub connection_direction: Option, + // Traffic statistics pub bytes_sent: u64, pub bytes_received: u64, @@ -1253,6 +1273,7 @@ impl Connection { protocol_state: state, pid: None, process_name: None, + connection_direction: None, bytes_sent: 0, bytes_received: 0, packets_sent: 0, @@ -1786,16 +1807,24 @@ mod tests { // Simulate receiving packets - use internal rate_tracker directly for deterministic timing conn.bytes_sent = 1000; conn.bytes_received = 500; - conn.rate_tracker.update_at_time(start, conn.bytes_sent, conn.bytes_received); + conn.rate_tracker + .update_at_time(start, conn.bytes_sent, conn.bytes_received); conn.bytes_sent = 3000; conn.bytes_received = 1500; - conn.rate_tracker - .update_at_time(start + Duration::from_secs(1), conn.bytes_sent, conn.bytes_received); + conn.rate_tracker.update_at_time( + start + Duration::from_secs(1), + conn.bytes_sent, + conn.bytes_received, + ); // Update cached rate values - conn.current_outgoing_rate_bps = conn.rate_tracker.get_outgoing_rate_at(start + Duration::from_secs(1)); - conn.current_incoming_rate_bps = conn.rate_tracker.get_incoming_rate_at(start + Duration::from_secs(1)); + conn.current_outgoing_rate_bps = conn + .rate_tracker + .get_outgoing_rate_at(start + Duration::from_secs(1)); + conn.current_incoming_rate_bps = conn + .rate_tracker + .get_incoming_rate_at(start + Duration::from_secs(1)); // Verify backward compatibility fields are updated assert!(conn.current_outgoing_rate_bps >= 0.0); @@ -2007,13 +2036,17 @@ mod tests { // Simulate first packet conn.bytes_sent = 50_000; conn.bytes_received = 25_000; - conn.rate_tracker.update_at_time(start, conn.bytes_sent, conn.bytes_received); + conn.rate_tracker + .update_at_time(start, conn.bytes_sent, conn.bytes_received); // Simulate more traffic after 1 second conn.bytes_sent = 100_000; conn.bytes_received = 50_000; - conn.rate_tracker - .update_at_time(start + Duration::from_secs(1), conn.bytes_sent, conn.bytes_received); + conn.rate_tracker.update_at_time( + start + Duration::from_secs(1), + conn.bytes_sent, + conn.bytes_received, + ); // Update cached rates at the 1-second mark let check_time = start + Duration::from_secs(1); diff --git a/src/ui.rs b/src/ui.rs index 251e41a..db1a20f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -5,11 +5,16 @@ use ratatui::{ style::{Color, Modifier, Style}, symbols, text::{Line, Span}, - widgets::{Axis, Block, Borders, Cell, Chart, Dataset, GraphType, Paragraph, Row, Sparkline, Table, Tabs, Wrap}, + widgets::{ + Axis, Block, Borders, Cell, Chart, Dataset, GraphType, Paragraph, Row, Sparkline, Table, + Tabs, Wrap, + }, }; use crate::app::{App, AppStats}; -use crate::network::types::{AppProtocolDistribution, Connection, Protocol, ProtocolState, TcpState, TrafficHistory}; +use crate::network::types::{ + AppProtocolDistribution, Connection, Protocol, ProtocolState, TcpState, TrafficHistory, +}; pub type Terminal = RatatuiTerminal; @@ -848,7 +853,10 @@ fn draw_stats_panel( let available_indicator = if sandbox_info.landlock_available { Span::styled(" [kernel supported]", Style::default().fg(Color::DarkGray)) } else { - Span::styled(" [kernel unsupported]", Style::default().fg(Color::DarkGray)) + Span::styled( + " [kernel unsupported]", + Style::default().fg(Color::DarkGray), + ) }; vec![ @@ -1081,22 +1089,17 @@ fn draw_interface_stats_with_graph(f: &mut Frame, app: &App, area: Rect) -> Resu } /// Draw the Graph tab with traffic visualization -fn draw_graph_tab( - f: &mut Frame, - app: &App, - connections: &[Connection], - area: Rect, -) -> Result<()> { +fn draw_graph_tab(f: &mut Frame, app: &App, connections: &[Connection], area: Rect) -> Result<()> { let traffic_history = app.get_traffic_history(); // Main layout: traffic chart, health chart, legend, bottom row let main_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Percentage(35), // Traffic chart - Constraint::Percentage(20), // Network health + TCP states - Constraint::Length(1), // Legend row - Constraint::Min(0), // App distribution + top processes + Constraint::Percentage(35), // Traffic chart + Constraint::Percentage(20), // Network health + TCP states + Constraint::Length(1), // Legend row + Constraint::Min(0), // App distribution + top processes ]) .split(area); @@ -1204,16 +1207,14 @@ fn draw_traffic_chart(f: &mut Frame, history: &TrafficHistory, area: Rect) { /// Draw connections count sparkline fn draw_connections_sparkline(f: &mut Frame, history: &TrafficHistory, area: Rect) { - let block = Block::default() - .borders(Borders::ALL) - .title("Connections"); + let block = Block::default().borders(Borders::ALL).title("Connections"); let inner = block.inner(area); f.render_widget(block, area); if !history.has_enough_data() { - let placeholder = Paragraph::new("Collecting...") - .style(Style::default().fg(Color::DarkGray)); + let placeholder = + Paragraph::new("Collecting...").style(Style::default().fg(Color::DarkGray)); f.render_widget(placeholder, inner); return; } @@ -1339,8 +1340,8 @@ fn draw_top_processes(f: &mut Frame, connections: &[Connection], area: Rect) { .collect(); if rows.is_empty() { - let placeholder = Paragraph::new("No active processes") - .style(Style::default().fg(Color::DarkGray)); + let placeholder = + Paragraph::new("No active processes").style(Style::default().fg(Color::DarkGray)); f.render_widget(placeholder, inner); return; } @@ -1350,8 +1351,11 @@ fn draw_top_processes(f: &mut Frame, connections: &[Connection], area: Rect) { [Constraint::Percentage(60), Constraint::Percentage(40)], ) .header( - Row::new(vec!["Process", "Rate"]) - .style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), + Row::new(vec!["Process", "Rate"]).style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), ); f.render_widget(table, inner); @@ -1380,8 +1384,8 @@ fn draw_health_chart(f: &mut Frame, history: &TrafficHistory, area: Rect) { f.render_widget(block, area); if !history.has_enough_data() { - let placeholder = Paragraph::new("Collecting data...") - .style(Style::default().fg(Color::DarkGray)); + let placeholder = + Paragraph::new("Collecting data...").style(Style::default().fg(Color::DarkGray)); f.render_widget(placeholder, inner); return; } @@ -1406,8 +1410,8 @@ fn draw_health_chart(f: &mut Frame, history: &TrafficHistory, area: Rect) { }; // Thresholds for gauges - const RTT_MAX: f64 = 200.0; // 200ms max scale - const LOSS_MAX: f64 = 10.0; // 10% max scale + const RTT_MAX: f64 = 200.0; // 200ms max scale + const LOSS_MAX: f64 = 10.0; // 10% max scale let bar_width = inner.width.saturating_sub(18) as usize; // Leave room for label + value @@ -1454,20 +1458,34 @@ fn draw_health_chart(f: &mut Frame, history: &TrafficHistory, area: Rect) { let loss_line = Line::from(vec![ Span::styled(" Loss ", Style::default().fg(Color::White)), - Span::styled("█".repeat(filled.max(if current_loss > 0.0 { 1 } else { 0 })), Style::default().fg(loss_color)), - Span::styled("░".repeat(empty.min(bar_width)), Style::default().fg(Color::DarkGray)), - Span::styled(format!(" {:>6.2}%", current_loss), Style::default().fg(loss_color)), + Span::styled( + "█".repeat(filled.max(if current_loss > 0.0 { 1 } else { 0 })), + Style::default().fg(loss_color), + ), + Span::styled( + "░".repeat(empty.min(bar_width)), + Style::default().fg(Color::DarkGray), + ), + Span::styled( + format!(" {:>6.2}%", current_loss), + Style::default().fg(loss_color), + ), ]); // Build averages line let avg_line = Line::from(vec![ Span::styled(" avg: ", Style::default().fg(Color::DarkGray)), Span::styled( - avg_rtt.map(|r| format!("{:.0}ms", r)).unwrap_or_else(|| "--".to_string()), + avg_rtt + .map(|r| format!("{:.0}ms", r)) + .unwrap_or_else(|| "--".to_string()), Style::default().fg(Color::DarkGray), ), Span::styled(" / ", Style::default().fg(Color::DarkGray)), - Span::styled(format!("{:.2}%", avg_loss), Style::default().fg(Color::DarkGray)), + Span::styled( + format!("{:.2}%", avg_loss), + Style::default().fg(Color::DarkGray), + ), ]); let paragraph = Paragraph::new(vec![rtt_line, loss_line, avg_line]); @@ -1483,9 +1501,7 @@ fn draw_tcp_counters(f: &mut Frame, app: &App, area: Rect) { let out_of_order = stats.total_tcp_out_of_order.load(Ordering::Relaxed); let fast_retransmits = stats.total_tcp_fast_retransmits.load(Ordering::Relaxed); - let block = Block::default() - .borders(Borders::ALL) - .title("TCP Counters"); + let block = Block::default().borders(Borders::ALL).title("TCP Counters"); let inner = block.inner(area); f.render_widget(block, area); @@ -1518,15 +1534,24 @@ fn draw_tcp_counters(f: &mut Frame, app: &App, area: Rect) { let lines = vec![ Line::from(vec![ Span::styled(" Retransmits ", Style::default().fg(Color::White)), - Span::styled(format!("{:>8}", retransmits), Style::default().fg(retrans_color)), + Span::styled( + format!("{:>8}", retransmits), + Style::default().fg(retrans_color), + ), ]), Line::from(vec![ Span::styled(" Out of Order ", Style::default().fg(Color::White)), - Span::styled(format!("{:>8}", out_of_order), Style::default().fg(ooo_color)), + Span::styled( + format!("{:>8}", out_of_order), + Style::default().fg(ooo_color), + ), ]), Line::from(vec![ Span::styled(" Fast Retrans ", Style::default().fg(Color::White)), - Span::styled(format!("{:>8}", fast_retransmits), Style::default().fg(fast_color)), + Span::styled( + format!("{:>8}", fast_retransmits), + Style::default().fg(fast_color), + ), ]), ]; @@ -1564,9 +1589,18 @@ fn draw_tcp_states(f: &mut Frame, connections: &[Connection], area: Rect) { // Fixed order based on connection lifecycle (most important first) const STATE_ORDER: &[&str] = &[ - "ESTAB", "SYN_SENT", "SYN_RECV", "FIN_WAIT1", "FIN_WAIT2", - "TIME_WAIT", "CLOSE_WAIT", "LAST_ACK", "CLOSING", "CLOSED", - "LISTEN", "UNKNOWN", + "ESTAB", + "SYN_SENT", + "SYN_RECV", + "FIN_WAIT1", + "FIN_WAIT2", + "TIME_WAIT", + "CLOSE_WAIT", + "LAST_ACK", + "CLOSING", + "CLOSED", + "LISTEN", + "UNKNOWN", ]; // Build ordered list with only non-zero counts @@ -1580,8 +1614,7 @@ fn draw_tcp_states(f: &mut Frame, connections: &[Connection], area: Rect) { f.render_widget(block, area); if states.is_empty() { - let text = Paragraph::new("No TCP connections") - .style(Style::default().fg(Color::DarkGray)); + let text = Paragraph::new("No TCP connections").style(Style::default().fg(Color::DarkGray)); f.render_widget(text, inner); return; }