mirror of
https://github.com/domcyrus/rustnet.git
synced 2026-01-05 13:29:55 -06:00
feat(logging): add connection direction to JSON log for TCP
This commit is contained in:
5
USAGE.md
5
USAGE.md
@@ -818,6 +818,7 @@ sudo rustnet -i eth0 --json-log ~/network-events.json
|
||||
| `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) |
|
||||
@@ -827,8 +828,8 @@ sudo rustnet -i eth0 --json-log ~/network-events.json
|
||||
**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","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","bytes_sent":1024,"bytes_received":4096,"duration_secs":5}
|
||||
{"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:**
|
||||
|
||||
28
src/app.rs
28
src/app.rs
@@ -88,6 +88,11 @@ fn log_connection_event(
|
||||
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());
|
||||
@@ -516,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;
|
||||
}
|
||||
}
|
||||
@@ -911,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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
15
src/main.rs
15
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<B: ratatui::prelude::Backend>(
|
||||
|
||||
// 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<B: ratatui::prelude::Backend>(
|
||||
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 {
|
||||
|
||||
@@ -231,7 +231,9 @@ pub fn setup_packet_capture(config: CaptureConfig) -> Result<(Capture<Active>, 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)?;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -45,9 +45,7 @@ pub fn drop_cap_net_raw() -> Result<bool> {
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -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<LandlockResult> {
|
||||
};
|
||||
|
||||
// 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
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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<u32>,
|
||||
pub process_name: Option<String>,
|
||||
|
||||
// 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<bool>,
|
||||
|
||||
// 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);
|
||||
|
||||
117
src/ui.rs
117
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<B> = RatatuiTerminal<B>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user