From 3329eed6c591659ab3ae644ee183c54e2f778787 Mon Sep 17 00:00:00 2001 From: Marco Cadetg Date: Tue, 9 Sep 2025 15:45:14 +0200 Subject: [PATCH] cargo fmt --- src/app.rs | 7 +- src/filter.rs | 199 +++++++++------- src/main.rs | 433 ++++++++++++++++++---------------- src/network/dpi/https.rs | 10 +- src/network/dpi/mod.rs | 6 +- src/network/dpi/quic.rs | 24 +- src/network/merge.rs | 13 +- src/network/parser.rs | 61 ++--- src/network/platform/linux.rs | 60 ++--- src/network/platform/macos.rs | 3 +- src/network/types.rs | 117 +++++---- src/ui.rs | 53 +++-- 12 files changed, 560 insertions(+), 426 deletions(-) diff --git a/src/app.rs b/src/app.rs index 95c49fd..e855880 100644 --- a/src/app.rs +++ b/src/app.rs @@ -607,7 +607,10 @@ impl App { } /// Start rate refresh thread to update rates for idle connections - fn start_rate_refresh_thread(&self, connections: Arc>) -> Result<()> { + fn start_rate_refresh_thread( + &self, + connections: Arc>, + ) -> Result<()> { let should_stop = Arc::clone(&self.should_stop); thread::spawn(move || { @@ -709,7 +712,7 @@ impl App { /// Get filtered connections for UI display pub fn get_filtered_connections(&self, filter_query: &str) -> Vec { let connections = self.connections_snapshot.read().unwrap().clone(); - + if filter_query.trim().is_empty() { return connections; } diff --git a/src/filter.rs b/src/filter.rs index f28ede9..6b17dd8 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -34,14 +34,14 @@ impl ConnectionFilter { /// Parse filter query string into filter criteria pub fn parse(query: &str) -> Self { let mut criteria = Vec::new(); - + if query.trim().is_empty() { return Self { criteria }; } // Split by whitespace and process each part let parts: Vec<&str> = query.split_whitespace().collect(); - + for part in parts { if let Some((keyword, value)) = part.split_once(':') { // Handle keyword-based filters @@ -100,75 +100,102 @@ impl ConnectionFilter { } // All criteria must match (AND operation) - self.criteria.iter().all(|criterion| { - match criterion { - FilterCriteria::General(text) => self.matches_general(connection, text), - FilterCriteria::Port(port_text) => { - connection.local_addr.port().to_string().contains(port_text) - || connection.remote_addr.port().to_string().contains(port_text) - } - FilterCriteria::SourcePort(port_text) => { - connection.local_addr.port().to_string().contains(port_text) - } - FilterCriteria::DestinationPort(port_text) => { - connection.remote_addr.port().to_string().contains(port_text) - } - FilterCriteria::SourceIp(ip_text) => { - connection.local_addr.ip().to_string().to_lowercase().contains(ip_text) - } - FilterCriteria::DestinationIp(ip_text) => { - connection.remote_addr.ip().to_string().to_lowercase().contains(ip_text) - } - FilterCriteria::Protocol(proto_text) => { - connection.protocol.to_string().to_lowercase().contains(proto_text) - } - FilterCriteria::Process(process_text) => { - if let Some(ref process_name) = connection.process_name { - process_name.to_lowercase().contains(process_text) - } else { - false - } - } - FilterCriteria::Service(service_text) => { - if let Some(ref service_name) = connection.service_name { - service_name.to_lowercase().contains(service_text) - } else { - false - } - } - FilterCriteria::Sni(sni_text) => self.matches_sni(connection, sni_text), - FilterCriteria::Application(app_text) => self.matches_application(connection, app_text), + self.criteria.iter().all(|criterion| match criterion { + FilterCriteria::General(text) => self.matches_general(connection, text), + FilterCriteria::Port(port_text) => { + connection.local_addr.port().to_string().contains(port_text) + || connection + .remote_addr + .port() + .to_string() + .contains(port_text) } + FilterCriteria::SourcePort(port_text) => { + connection.local_addr.port().to_string().contains(port_text) + } + FilterCriteria::DestinationPort(port_text) => connection + .remote_addr + .port() + .to_string() + .contains(port_text), + FilterCriteria::SourceIp(ip_text) => connection + .local_addr + .ip() + .to_string() + .to_lowercase() + .contains(ip_text), + FilterCriteria::DestinationIp(ip_text) => connection + .remote_addr + .ip() + .to_string() + .to_lowercase() + .contains(ip_text), + FilterCriteria::Protocol(proto_text) => connection + .protocol + .to_string() + .to_lowercase() + .contains(proto_text), + FilterCriteria::Process(process_text) => { + if let Some(ref process_name) = connection.process_name { + process_name.to_lowercase().contains(process_text) + } else { + false + } + } + FilterCriteria::Service(service_text) => { + if let Some(ref service_name) = connection.service_name { + service_name.to_lowercase().contains(service_text) + } else { + false + } + } + FilterCriteria::Sni(sni_text) => self.matches_sni(connection, sni_text), + FilterCriteria::Application(app_text) => self.matches_application(connection, app_text), }) } /// Check if connection matches general text search across all fields fn matches_general(&self, connection: &Connection, text: &str) -> bool { // Check basic connection info - if connection.protocol.to_string().to_lowercase().contains(text) - || connection.local_addr.to_string().to_lowercase().contains(text) - || connection.remote_addr.to_string().to_lowercase().contains(text) + if connection + .protocol + .to_string() + .to_lowercase() + .contains(text) + || connection + .local_addr + .to_string() + .to_lowercase() + .contains(text) + || connection + .remote_addr + .to_string() + .to_lowercase() + .contains(text) { return true; } // Check process info if let Some(ref process_name) = connection.process_name - && process_name.to_lowercase().contains(text) { - return true; - } + && process_name.to_lowercase().contains(text) + { + return true; + } // Check service info if let Some(ref service_name) = connection.service_name - && service_name.to_lowercase().contains(text) { - return true; - } + && service_name.to_lowercase().contains(text) + { + return true; + } // Check DPI info if let Some(ref dpi_info) = connection.dpi_info - && self.matches_dpi_general(&dpi_info.application, text) { - return true; - } + && self.matches_dpi_general(&dpi_info.application, text) + { + return true; + } false } @@ -179,15 +206,17 @@ impl ConnectionFilter { match &dpi_info.application { ApplicationProtocol::Https(info) => { if let Some(ref tls_info) = info.tls_info - && let Some(ref sni) = tls_info.sni { - return sni.to_lowercase().contains(sni_text); - } + && let Some(ref sni) = tls_info.sni + { + return sni.to_lowercase().contains(sni_text); + } } ApplicationProtocol::Quic(info) => { if let Some(ref tls_info) = info.tls_info - && let Some(ref sni) = tls_info.sni { - return sni.to_lowercase().contains(sni_text); - } + && let Some(ref sni) = tls_info.sni + { + return sni.to_lowercase().contains(sni_text); + } } ApplicationProtocol::Http(info) => { if let Some(ref host) = info.host { @@ -203,7 +232,11 @@ impl ConnectionFilter { /// Check if application protocol matches the search text fn matches_application(&self, connection: &Connection, app_text: &str) -> bool { if let Some(ref dpi_info) = connection.dpi_info { - dpi_info.application.to_string().to_lowercase().contains(app_text) + dpi_info + .application + .to_string() + .to_lowercase() + .contains(app_text) } else { false } @@ -220,24 +253,28 @@ impl ConnectionFilter { match application { ApplicationProtocol::Http(info) => { if let Some(ref host) = info.host - && host.to_lowercase().contains(text) { - return true; - } + && host.to_lowercase().contains(text) + { + return true; + } if let Some(ref path) = info.path - && path.to_lowercase().contains(text) { - return true; - } + && path.to_lowercase().contains(text) + { + return true; + } if let Some(ref method) = info.method - && method.to_lowercase().contains(text) { - return true; - } + && method.to_lowercase().contains(text) + { + return true; + } } ApplicationProtocol::Https(info) => { if let Some(ref tls_info) = info.tls_info { if let Some(ref sni) = tls_info.sni - && sni.to_lowercase().contains(text) { - return true; - } + && sni.to_lowercase().contains(text) + { + return true; + } // Check ALPN protocols for alpn in &tls_info.alpn { if alpn.to_lowercase().contains(text) { @@ -248,16 +285,18 @@ impl ConnectionFilter { } ApplicationProtocol::Dns(info) => { if let Some(ref query_name) = info.query_name - && query_name.to_lowercase().contains(text) { - return true; - } + && query_name.to_lowercase().contains(text) + { + return true; + } } ApplicationProtocol::Quic(info) => { if let Some(ref tls_info) = info.tls_info { if let Some(ref sni) = tls_info.sni - && sni.to_lowercase().contains(text) { - return true; - } + && sni.to_lowercase().contains(text) + { + return true; + } // Check ALPN protocols for alpn in &tls_info.alpn { if alpn.to_lowercase().contains(text) { @@ -320,17 +359,17 @@ mod tests { fn test_parse_sport_dport_filters() { let filter = ConnectionFilter::parse("sport:80 dport:443"); assert_eq!(filter.criteria.len(), 2); - + // Check source port filter match &filter.criteria[0] { FilterCriteria::SourcePort(text) => assert_eq!(text, "80"), _ => panic!("Expected SourcePort filter"), } - + // Check destination port filter match &filter.criteria[1] { FilterCriteria::DestinationPort(text) => assert_eq!(text, "443"), _ => panic!("Expected DestinationPort filter"), } } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 2bb935c..0566967 100644 --- a/src/main.rs +++ b/src/main.rs @@ -173,256 +173,283 @@ fn run_ui_loop( // Clear clipboard message after timeout if let Some((_, time)) = &ui_state.clipboard_message - && time.elapsed().as_secs() >= 3 { + && time.elapsed().as_secs() >= 3 + { ui_state.clipboard_message = None; } // Handle input events if crossterm::event::poll(timeout)? - && let crossterm::event::Event::Key(key) = crossterm::event::read()? { - use crossterm::event::{KeyCode, KeyModifiers}; + && let crossterm::event::Event::Key(key) = crossterm::event::read()? + { + use crossterm::event::{KeyCode, KeyModifiers}; - if ui_state.filter_mode { - // Handle input in filter mode - match key.code { - KeyCode::Enter => { - // Apply filter and exit input mode (now optional) - debug!("Exiting filter mode. Filter: '{}'", ui_state.filter_query); - ui_state.exit_filter_mode(); - debug!("Filter mode now: {}", ui_state.filter_mode); - } - KeyCode::Esc => { - // Clear filter and exit filter mode - ui_state.clear_filter(); - } - KeyCode::Backspace => { - ui_state.filter_backspace(); - } - KeyCode::Delete => { - // Handle delete key (remove character after cursor) - if ui_state.filter_cursor_position < ui_state.filter_query.len() { - ui_state.filter_query.remove(ui_state.filter_cursor_position); - } - } - KeyCode::Left => { - ui_state.filter_cursor_left(); - } - KeyCode::Right => { - ui_state.filter_cursor_right(); - } - KeyCode::Home => { - ui_state.filter_cursor_position = 0; - } - KeyCode::End => { - ui_state.filter_cursor_position = ui_state.filter_query.len(); - } - // Allow navigation while in filter mode! - KeyCode::Up => { - // Navigate filtered connections while typing - let nav_connections = if ui_state.filter_query.is_empty() { - app.get_connections() - } else { - app.get_filtered_connections(&ui_state.filter_query) - }; - debug!("Filter mode navigation UP: {} connections available", nav_connections.len()); - ui_state.move_selection_up(&nav_connections); - } - KeyCode::Down => { - // Navigate filtered connections while typing - let nav_connections = if ui_state.filter_query.is_empty() { - app.get_connections() - } else { - app.get_filtered_connections(&ui_state.filter_query) - }; - debug!("Filter mode navigation DOWN: {} connections available", nav_connections.len()); - ui_state.move_selection_down(&nav_connections); - } - KeyCode::Char(c) => { - // Handle navigation keys (j/k) and text input - match c { - 'k' => { - // Vim-style up navigation while filtering - let nav_connections = if ui_state.filter_query.is_empty() { - app.get_connections() - } else { - app.get_filtered_connections(&ui_state.filter_query) - }; - debug!("Filter mode navigation UP (k): {} connections available", nav_connections.len()); - ui_state.move_selection_up(&nav_connections); - } - 'j' => { - // Vim-style down navigation while filtering - let nav_connections = if ui_state.filter_query.is_empty() { - app.get_connections() - } else { - app.get_filtered_connections(&ui_state.filter_query) - }; - debug!("Filter mode navigation DOWN (j): {} connections available", nav_connections.len()); - ui_state.move_selection_down(&nav_connections); - } - _ => { - // Regular character input for filter - ui_state.filter_add_char(c); - } - } - } - _ => {} + if ui_state.filter_mode { + // Handle input in filter mode + match key.code { + KeyCode::Enter => { + // Apply filter and exit input mode (now optional) + debug!("Exiting filter mode. Filter: '{}'", ui_state.filter_query); + ui_state.exit_filter_mode(); + debug!("Filter mode now: {}", ui_state.filter_mode); } - } else { - // Handle input in normal mode - match (key.code, key.modifiers) { - // Enter filter mode with '/' - (KeyCode::Char('/'), _) => { - ui_state.quit_confirmation = false; - debug!("Entering filter mode"); - ui_state.enter_filter_mode(); - debug!("Filter mode now: {}", ui_state.filter_mode); + KeyCode::Esc => { + // Clear filter and exit filter mode + ui_state.clear_filter(); + } + KeyCode::Backspace => { + ui_state.filter_backspace(); + } + KeyCode::Delete => { + // Handle delete key (remove character after cursor) + if ui_state.filter_cursor_position < ui_state.filter_query.len() { + ui_state + .filter_query + .remove(ui_state.filter_cursor_position); } - - // Quit with confirmation - (KeyCode::Char('q'), _) => { - if ui_state.quit_confirmation { - info!("User confirmed application exit"); - break; - } else { - info!("User requested quit - showing confirmation"); - ui_state.quit_confirmation = true; + } + KeyCode::Left => { + ui_state.filter_cursor_left(); + } + KeyCode::Right => { + ui_state.filter_cursor_right(); + } + KeyCode::Home => { + ui_state.filter_cursor_position = 0; + } + KeyCode::End => { + ui_state.filter_cursor_position = ui_state.filter_query.len(); + } + // Allow navigation while in filter mode! + KeyCode::Up => { + // Navigate filtered connections while typing + let nav_connections = if ui_state.filter_query.is_empty() { + app.get_connections() + } else { + app.get_filtered_connections(&ui_state.filter_query) + }; + debug!( + "Filter mode navigation UP: {} connections available", + nav_connections.len() + ); + ui_state.move_selection_up(&nav_connections); + } + KeyCode::Down => { + // Navigate filtered connections while typing + let nav_connections = if ui_state.filter_query.is_empty() { + app.get_connections() + } else { + app.get_filtered_connections(&ui_state.filter_query) + }; + debug!( + "Filter mode navigation DOWN: {} connections available", + nav_connections.len() + ); + ui_state.move_selection_down(&nav_connections); + } + KeyCode::Char(c) => { + // Handle navigation keys (j/k) and text input + match c { + 'k' => { + // Vim-style up navigation while filtering + let nav_connections = if ui_state.filter_query.is_empty() { + app.get_connections() + } else { + app.get_filtered_connections(&ui_state.filter_query) + }; + debug!( + "Filter mode navigation UP (k): {} connections available", + nav_connections.len() + ); + ui_state.move_selection_up(&nav_connections); + } + 'j' => { + // Vim-style down navigation while filtering + let nav_connections = if ui_state.filter_query.is_empty() { + app.get_connections() + } else { + app.get_filtered_connections(&ui_state.filter_query) + }; + debug!( + "Filter mode navigation DOWN (j): {} connections available", + nav_connections.len() + ); + ui_state.move_selection_down(&nav_connections); + } + _ => { + // Regular character input for filter + ui_state.filter_add_char(c); } } + } + _ => {} + } + } else { + // Handle input in normal mode + match (key.code, key.modifiers) { + // Enter filter mode with '/' + (KeyCode::Char('/'), _) => { + ui_state.quit_confirmation = false; + debug!("Entering filter mode"); + ui_state.enter_filter_mode(); + debug!("Filter mode now: {}", ui_state.filter_mode); + } - // Ctrl+C always quits immediately - (KeyCode::Char('c'), KeyModifiers::CONTROL) => { - info!("User requested immediate exit with Ctrl+C"); + // Quit with confirmation + (KeyCode::Char('q'), _) => { + if ui_state.quit_confirmation { + info!("User confirmed application exit"); break; + } else { + info!("User requested quit - showing confirmation"); + ui_state.quit_confirmation = true; } + } - // Tab navigation - (KeyCode::Tab, _) => { - ui_state.quit_confirmation = false; - ui_state.selected_tab = (ui_state.selected_tab + 1) % 3; + // Ctrl+C always quits immediately + (KeyCode::Char('c'), KeyModifiers::CONTROL) => { + info!("User requested immediate exit with Ctrl+C"); + break; + } + + // Tab navigation + (KeyCode::Tab, _) => { + ui_state.quit_confirmation = false; + ui_state.selected_tab = (ui_state.selected_tab + 1) % 3; + } + + // Help toggle + (KeyCode::Char('h'), _) => { + ui_state.quit_confirmation = false; + 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 } + } - // Help toggle - (KeyCode::Char('h'), _) => { - ui_state.quit_confirmation = false; - 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'), _) => { - ui_state.quit_confirmation = false; - // Refresh connections for navigation to ensure we have current filtered list - let nav_connections = if ui_state.filter_query.is_empty() && !ui_state.filter_mode { + // Navigation in connection list + (KeyCode::Up, _) | (KeyCode::Char('k'), _) => { + ui_state.quit_confirmation = false; + // Refresh connections for navigation to ensure we have current filtered list + let nav_connections = + if ui_state.filter_query.is_empty() && !ui_state.filter_mode { app.get_connections() } else { app.get_filtered_connections(&ui_state.filter_query) }; - debug!("Navigation UP: {} connections available", nav_connections.len()); - ui_state.move_selection_up(&nav_connections); - } + debug!( + "Navigation UP: {} connections available", + nav_connections.len() + ); + ui_state.move_selection_up(&nav_connections); + } - (KeyCode::Down, _) | (KeyCode::Char('j'), _) => { - ui_state.quit_confirmation = false; - // Refresh connections for navigation to ensure we have current filtered list - let nav_connections = if ui_state.filter_query.is_empty() && !ui_state.filter_mode { + (KeyCode::Down, _) | (KeyCode::Char('j'), _) => { + ui_state.quit_confirmation = false; + // Refresh connections for navigation to ensure we have current filtered list + let nav_connections = + if ui_state.filter_query.is_empty() && !ui_state.filter_mode { app.get_connections() } else { app.get_filtered_connections(&ui_state.filter_query) }; - debug!("Navigation DOWN: {} connections available", nav_connections.len()); - ui_state.move_selection_down(&nav_connections); - } + debug!( + "Navigation DOWN: {} connections available", + nav_connections.len() + ); + ui_state.move_selection_down(&nav_connections); + } - // Page Up/Down navigation - (KeyCode::PageUp, _) => { - ui_state.quit_confirmation = false; - // Refresh connections for navigation - let nav_connections = if ui_state.filter_query.is_empty() && !ui_state.filter_mode { + // Page Up/Down navigation + (KeyCode::PageUp, _) => { + ui_state.quit_confirmation = false; + // Refresh connections for navigation + let nav_connections = + if ui_state.filter_query.is_empty() && !ui_state.filter_mode { app.get_connections() } else { app.get_filtered_connections(&ui_state.filter_query) }; - // Move up by roughly 10 items (or adjust based on terminal height) - ui_state.move_selection_page_up(&nav_connections, 10); - } + // Move up by roughly 10 items (or adjust based on terminal height) + ui_state.move_selection_page_up(&nav_connections, 10); + } - (KeyCode::PageDown, _) => { - ui_state.quit_confirmation = false; - // Refresh connections for navigation - let nav_connections = if ui_state.filter_query.is_empty() && !ui_state.filter_mode { + (KeyCode::PageDown, _) => { + ui_state.quit_confirmation = false; + // Refresh connections for navigation + let nav_connections = + if ui_state.filter_query.is_empty() && !ui_state.filter_mode { app.get_connections() } else { app.get_filtered_connections(&ui_state.filter_query) }; - // Move down by roughly 10 items (or adjust based on terminal height) - ui_state.move_selection_page_down(&nav_connections, 10); - } + // Move down by roughly 10 items (or adjust based on terminal height) + ui_state.move_selection_page_down(&nav_connections, 10); + } - // Enter to view details - (KeyCode::Enter, _) => { - ui_state.quit_confirmation = false; - if ui_state.selected_tab == 0 && !connections.is_empty() { - ui_state.selected_tab = 1; // Switch to details view - } + // Enter to view details + (KeyCode::Enter, _) => { + ui_state.quit_confirmation = false; + if ui_state.selected_tab == 0 && !connections.is_empty() { + ui_state.selected_tab = 1; // Switch to details view } + } - // Copy remote address to clipboard - (KeyCode::Char('c'), _) => { - ui_state.quit_confirmation = false; - if let Some(selected_idx) = ui_state.get_selected_index(&connections) - && let Some(conn) = connections.get(selected_idx) { - let remote_addr = conn.remote_addr.to_string(); - match Clipboard::new() { - Ok(mut clipboard) => { - if let Err(e) = clipboard.set_text(&remote_addr) { - error!("Failed to copy to clipboard: {}", e); - ui_state.clipboard_message = Some(( - format!("Failed to copy: {}", e), - std::time::Instant::now(), - )); - } else { - info!("Copied {} to clipboard", remote_addr); - ui_state.clipboard_message = Some(( - format!("Copied {} to clipboard", remote_addr), - std::time::Instant::now(), - )); - } - } - Err(e) => { - error!("Failed to access clipboard: {}", e); + // Copy remote address to clipboard + (KeyCode::Char('c'), _) => { + ui_state.quit_confirmation = false; + if let Some(selected_idx) = ui_state.get_selected_index(&connections) + && let Some(conn) = connections.get(selected_idx) + { + let remote_addr = conn.remote_addr.to_string(); + match Clipboard::new() { + Ok(mut clipboard) => { + if let Err(e) = clipboard.set_text(&remote_addr) { + error!("Failed to copy to clipboard: {}", e); ui_state.clipboard_message = Some(( - format!("Clipboard error: {}", e), + format!("Failed to copy: {}", e), + std::time::Instant::now(), + )); + } else { + info!("Copied {} to clipboard", remote_addr); + ui_state.clipboard_message = Some(( + format!("Copied {} to clipboard", remote_addr), std::time::Instant::now(), )); } } + Err(e) => { + error!("Failed to access clipboard: {}", e); + ui_state.clipboard_message = Some(( + format!("Clipboard error: {}", e), + std::time::Instant::now(), + )); + } } } - - // Escape to go back or clear filter - (KeyCode::Esc, _) => { - ui_state.quit_confirmation = false; - if !ui_state.filter_query.is_empty() { - // Clear filter if one is active - ui_state.clear_filter(); - } else 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 - } - } - - // Any other key resets quit confirmation - _ => { - ui_state.quit_confirmation = false; - } + } + + // Escape to go back or clear filter + (KeyCode::Esc, _) => { + ui_state.quit_confirmation = false; + if !ui_state.filter_query.is_empty() { + // Clear filter if one is active + ui_state.clear_filter(); + } else 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 + } + } + + // Any other key resets quit confirmation + _ => { + ui_state.quit_confirmation = false; } } + } } } diff --git a/src/network/dpi/https.rs b/src/network/dpi/https.rs index f032af6..55eff27 100644 --- a/src/network/dpi/https.rs +++ b/src/network/dpi/https.rs @@ -274,9 +274,10 @@ fn parse_extensions(data: &[u8], info: &mut TlsInfo, is_client_hello: bool) { 0x0010 => { // ALPN (Application-Layer Protocol Negotiation) if let Some(alpn) = parse_alpn_extension_resilient(ext_data) - && !alpn.is_empty() { - info.alpn = alpn; - } + && !alpn.is_empty() + { + info.alpn = alpn; + } } 0x002b => { // Supported Versions @@ -365,7 +366,8 @@ fn parse_alpn_extension_resilient(data: &[u8]) -> Option> { let actual_len = proto_len.min(available_len); if actual_len > 0 - && let Ok(proto) = std::str::from_utf8(&data[offset..offset + actual_len]) { + && let Ok(proto) = std::str::from_utf8(&data[offset..offset + actual_len]) + { if actual_len < proto_len { protocols.push(format!("{}[PARTIAL]", proto)); } else { diff --git a/src/network/dpi/mod.rs b/src/network/dpi/mod.rs index 9a47a03..0f561a4 100644 --- a/src/network/dpi/mod.rs +++ b/src/network/dpi/mod.rs @@ -37,7 +37,8 @@ pub fn analyze_tcp_packet( // 2. Check for TLS/HTTPS (port 443 or TLS handshake) if (local_port == 443 || remote_port == 443 || https::is_tls_handshake(payload)) - && let Some(tls_result) = https::analyze_https(payload) { + && let Some(tls_result) = https::analyze_https(payload) + { return Some(DpiResult { application: ApplicationProtocol::Https(tls_result), }); @@ -68,7 +69,8 @@ pub fn analyze_udp_packet( // 1. DNS (port 53) if (local_port == 53 || remote_port == 53) - && let Some(dns_result) = dns::analyze_dns(payload) { + && let Some(dns_result) = dns::analyze_dns(payload) + { return Some(DpiResult { application: ApplicationProtocol::Dns(dns_result), }); diff --git a/src/network/dpi/quic.rs b/src/network/dpi/quic.rs index 7de5ca4..c4446fb 100644 --- a/src/network/dpi/quic.rs +++ b/src/network/dpi/quic.rs @@ -553,7 +553,8 @@ pub fn process_crypto_frames_in_packet( let crypto_data = payload[offset..offset + available].to_vec(); if let Some(reassembler) = &mut quic_info.crypto_reassembler - && let Err(e) = reassembler.add_fragment(crypto_offset, crypto_data) { + && let Err(e) = reassembler.add_fragment(crypto_offset, crypto_data) + { warn!("QUIC: Failed to add CRYPTO fragment: {}", e); } } @@ -700,7 +701,8 @@ pub fn process_crypto_frames_in_packet( if found_crypto_frames && let Some(reassembler) = &mut quic_info.crypto_reassembler - && let Some(tls_info) = try_extract_tls_from_reassembler(reassembler) { + && let Some(tls_info) = try_extract_tls_from_reassembler(reassembler) + { debug!( "QUIC: Successfully extracted TLS info: SNI={:?}", tls_info.sni @@ -761,9 +763,11 @@ pub fn try_extract_tls_from_reassembler( // Only try to parse fragments that look like they contain complete TLS structures // Check if fragment starts with TLS handshake header (0x01 for ClientHello) - if fragment_data.len() >= 4 && fragment_data[0] == 0x01 + if fragment_data.len() >= 4 + && fragment_data[0] == 0x01 && let Some(tls_info) = parse_partial_tls_handshake(fragment_data) - && (tls_info.sni.is_some() || !tls_info.alpn.is_empty()) { + && (tls_info.sni.is_some() || !tls_info.alpn.is_empty()) + { debug!( "QUIC: Found TLS info from individual fragment at offset {}", offset @@ -773,9 +777,11 @@ pub fn try_extract_tls_from_reassembler( } // Also try direct TLS pattern matching, but only for fragments that look like TLS records - if fragment_data.len() >= 6 && fragment_data[0] == 0x16 + if fragment_data.len() >= 6 + && fragment_data[0] == 0x16 && let Some(tls_info) = try_parse_unencrypted_crypto_frames(fragment_data) - && (tls_info.sni.is_some() || !tls_info.alpn.is_empty()) { + && (tls_info.sni.is_some() || !tls_info.alpn.is_empty()) + { debug!( "QUIC: Found TLS info from pattern matching in fragment at offset {}", offset @@ -1208,7 +1214,8 @@ fn parse_alpn_extension(data: &[u8]) -> Option> { offset += 1; if offset + proto_len <= data.len() - && let Ok(proto) = std::str::from_utf8(&data[offset..offset + proto_len]) { + && let Ok(proto) = std::str::from_utf8(&data[offset..offset + proto_len]) + { protocols.push(proto.to_string()); } @@ -1558,7 +1565,8 @@ fn try_parse_unencrypted_crypto_frames(payload: &[u8]) -> Option { && name_len <= 253 && (3..=256).contains(&list_len) && list_len == name_len + 3 - && let Some(sni) = parse_sni_extension(ext_data) { + && let Some(sni) = parse_sni_extension(ext_data) + { debug!("QUIC: Found SNI directly in packet: {}", sni); let mut tls_info = TlsInfo::new(); tls_info.sni = Some(sni); diff --git a/src/network/merge.rs b/src/network/merge.rs index 2c4c952..4eb45dc 100644 --- a/src/network/merge.rs +++ b/src/network/merge.rs @@ -272,7 +272,8 @@ pub fn create_connection_from_packet(parsed: &ParsedPacket, now: SystemTime) -> // Initialize the rate tracker with the initial byte counts // This prevents incorrect delta calculation on the first update - conn.rate_tracker.initialize_with_counts(conn.bytes_sent, conn.bytes_received); + conn.rate_tracker + .initialize_with_counts(conn.bytes_sent, conn.bytes_received); conn } @@ -640,22 +641,22 @@ mod tests { // Test that the rate tracker is properly initialized for new connections let packet = create_test_packet(true, false); let conn = create_connection_from_packet(&packet, SystemTime::now()); - + // The connection should have initial bytes assert_eq!(conn.bytes_sent, 100); assert_eq!(conn.bytes_received, 0); - + // Now simulate merging another packet let packet2 = create_test_packet(true, false); let mut updated_conn = merge_packet_into_connection(conn, &packet2, SystemTime::now()); - + // Bytes should have increased assert_eq!(updated_conn.bytes_sent, 200); assert_eq!(updated_conn.bytes_received, 0); - + // Update rates - this should not cause a huge spike updated_conn.update_rates(); - + // The rate should be reasonable (not include the initial 100 bytes as a spike) // Since we just added 100 bytes, the rate should be based on that delta // not on the full 200 bytes diff --git a/src/network/parser.rs b/src/network/parser.rs index 618978e..4dee03b 100644 --- a/src/network/parser.rs +++ b/src/network/parser.rs @@ -125,7 +125,8 @@ impl PacketParser { // Check if this is PKTAP data #[cfg(target_os = "macos")] if let Some(linktype) = self.linktype - && pktap::is_pktap_linktype(linktype) { + && pktap::is_pktap_linktype(linktype) + { return self.parse_pktap_packet(data); } @@ -356,11 +357,7 @@ impl PacketParser { } } - fn parse_tcp( - &self, - transport_data: &[u8], - params: TransportParams, - ) -> Option { + fn parse_tcp(&self, transport_data: &[u8], params: TransportParams) -> Option { if transport_data.len() < 20 { return None; } @@ -388,7 +385,12 @@ impl PacketParser { 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(), params.is_outgoing) + dpi::analyze_tcp_packet( + payload, + local_addr.port(), + remote_addr.port(), + params.is_outgoing, + ) } else { None } @@ -411,11 +413,7 @@ impl PacketParser { }) } - fn parse_udp( - &self, - transport_data: &[u8], - params: TransportParams, - ) -> Option { + fn parse_udp(&self, transport_data: &[u8], params: TransportParams) -> Option { if transport_data.len() < 8 { return None; } @@ -438,7 +436,12 @@ impl PacketParser { // 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(), params.is_outgoing) + dpi::analyze_udp_packet( + payload, + local_addr.port(), + remote_addr.port(), + params.is_outgoing, + ) } else { None }; @@ -458,11 +461,7 @@ impl PacketParser { }) } - fn parse_icmp( - &self, - transport_data: &[u8], - params: TransportParams, - ) -> Option { + fn parse_icmp(&self, transport_data: &[u8], params: TransportParams) -> Option { if transport_data.is_empty() { return None; } @@ -475,9 +474,15 @@ impl PacketParser { }; let (local_addr, remote_addr) = if params.is_outgoing { - (SocketAddr::new(params.src_ip, 0), SocketAddr::new(params.dst_ip, 0)) + ( + SocketAddr::new(params.src_ip, 0), + SocketAddr::new(params.dst_ip, 0), + ) } else { - (SocketAddr::new(params.dst_ip, 0), SocketAddr::new(params.src_ip, 0)) + ( + SocketAddr::new(params.dst_ip, 0), + SocketAddr::new(params.src_ip, 0), + ) }; Some(ParsedPacket { @@ -498,11 +503,7 @@ impl PacketParser { }) } - fn parse_icmpv6( - &self, - transport_data: &[u8], - params: TransportParams, - ) -> Option { + fn parse_icmpv6(&self, transport_data: &[u8], params: TransportParams) -> Option { if transport_data.is_empty() { return None; } @@ -515,9 +516,15 @@ impl PacketParser { }; let (local_addr, remote_addr) = if params.is_outgoing { - (SocketAddr::new(params.src_ip, 0), SocketAddr::new(params.dst_ip, 0)) + ( + SocketAddr::new(params.src_ip, 0), + SocketAddr::new(params.dst_ip, 0), + ) } else { - (SocketAddr::new(params.dst_ip, 0), SocketAddr::new(params.src_ip, 0)) + ( + SocketAddr::new(params.dst_ip, 0), + SocketAddr::new(params.src_ip, 0), + ) }; Some(ParsedPacket { diff --git a/src/network/platform/linux.rs b/src/network/platform/linux.rs index 8b6e096..12f0f55 100644 --- a/src/network/platform/linux.rs +++ b/src/network/platform/linux.rs @@ -73,29 +73,31 @@ impl LinuxProcessLookup { let path = entry.path(); if let Some(pid_str) = path.file_name().and_then(|s| s.to_str()) - && let Ok(pid) = pid_str.parse::() { - if pid == 0 { - continue; - } + && let Ok(pid) = pid_str.parse::() + { + 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(); + // 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()) - && let Some(link_str) = link.to_str() - && let Some(inode) = Self::extract_socket_inode(link_str) { - inode_map.insert(inode, (pid, process_name.clone())); - } + // 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()) + && let Some(link_str) = link.to_str() + && let Some(inode) = Self::extract_socket_inode(link_str) + { + inode_map.insert(inode, (pid, process_name.clone())); } } + } } } @@ -137,13 +139,14 @@ impl LinuxProcessLookup { // Get inode if let Ok(inode) = parts[9].parse::() - && let Some((pid, name)) = inode_map.get(&inode) { - let key = ConnectionKey { - protocol, - local_addr, - remote_addr, - }; - result.insert(key, (*pid, name.clone())); + && let Some((pid, name)) = inode_map.get(&inode) + { + let key = ConnectionKey { + protocol, + local_addr, + remote_addr, + }; + result.insert(key, (*pid, name.clone())); } } @@ -197,8 +200,9 @@ impl ProcessLookup for LinuxProcessLookup { { let cache = self.cache.read().unwrap(); if cache.last_refresh.elapsed() < Duration::from_secs(2) - && let Some(process_info) = cache.lookup.get(&key) { - return Some(process_info.clone()); + && let Some(process_info) = cache.lookup.get(&key) + { + return Some(process_info.clone()); } } diff --git a/src/network/platform/macos.rs b/src/network/platform/macos.rs index 1eb209c..fecda10 100644 --- a/src/network/platform/macos.rs +++ b/src/network/platform/macos.rs @@ -312,7 +312,8 @@ fn decode_lsof_string(input: &str) -> String { let hex_digits: String = chars.by_ref().take(2).collect(); if hex_digits.len() == 2 && let Ok(byte_val) = u8::from_str_radix(&hex_digits, 16) - && let Some(decoded_char) = std::char::from_u32(byte_val as u32) { + && let Some(decoded_char) = std::char::from_u32(byte_val as u32) + { result.push(decoded_char); continue; } diff --git a/src/network/types.rs b/src/network/types.rs index 9de2bf2..d33dbb5 100644 --- a/src/network/types.rs +++ b/src/network/types.rs @@ -656,7 +656,7 @@ impl RateTracker { let oldest = self.samples.front().unwrap(); let newest = self.samples.back().unwrap(); - + // Calculate the time span of our samples let time_span = newest .timestamp @@ -669,19 +669,20 @@ impl RateTracker { } // Sum all deltas in the window (skip the first sample as it might have incomplete delta) - let total_bytes: u64 = self.samples + let total_bytes: u64 = self + .samples .iter() - .skip(1) // Skip first sample which might have delta from before window + .skip(1) // Skip first sample which might have delta from before window .map(delta_getter) .sum(); // Calculate base rate let base_rate = total_bytes as f64 / time_span; - + // Apply time-based decay more gently, similar to iftop's approach let now = Instant::now(); let time_since_last_sample = now.duration_since(newest.timestamp).as_secs_f64(); - + // More gentle decay - start decay after 3 seconds, fully decay by 10 seconds if time_since_last_sample > 10.0 { // After 10 seconds of no traffic, rate should be very close to zero @@ -1237,24 +1238,24 @@ mod tests { // with cumulative byte counts tracker.update(1_000_000, 500_000); // 1MB sent, 500KB received total thread::sleep(Duration::from_millis(100)); - + tracker.update(1_100_000, 550_000); // 100KB more sent, 50KB more received thread::sleep(Duration::from_millis(100)); - + tracker.update(1_200_000, 600_000); // 100KB more sent, 50KB more received - + // The rate should be based on the deltas, not the cumulative values // We sent 200KB in ~200ms = ~1MB/s, received 100KB in ~200ms = ~500KB/s let outgoing_rate = tracker.get_outgoing_rate_bps(); let incoming_rate = tracker.get_incoming_rate_bps(); - + // Should be approximately 1MB/s outgoing (1_000_000 bytes/sec) assert!( outgoing_rate > 800_000.0 && outgoing_rate < 1_200_000.0, "Outgoing rate should be ~1MB/s, got: {}", outgoing_rate ); - + // Should be approximately 500KB/s incoming (500_000 bytes/sec) assert!( incoming_rate > 400_000.0 && incoming_rate < 600_000.0, @@ -1273,22 +1274,22 @@ mod tests { tracker.update(0, 0); thread::sleep(Duration::from_millis(100)); tracker.update(100_000, 50_000); // 100KB sent, 50KB received - + thread::sleep(Duration::from_millis(100)); tracker.update(200_000, 100_000); // Another 100KB sent, 50KB received - + // Wait for window to slide past first samples thread::sleep(Duration::from_millis(600)); - + // Add new samples with same rate tracker.update(300_000, 150_000); // Another 100KB sent, 50KB received thread::sleep(Duration::from_millis(100)); tracker.update(400_000, 200_000); // Another 100KB sent, 50KB received - + // Rate should still be consistent despite window sliding let outgoing_rate = tracker.get_outgoing_rate_bps(); let incoming_rate = tracker.get_incoming_rate_bps(); - + // We're sending at ~1MB/s and receiving at ~500KB/s consistently assert!( outgoing_rate > 800_000.0 && outgoing_rate < 1_200_000.0, @@ -1306,81 +1307,111 @@ mod tests { fn test_rate_decay_for_idle_connections() { // Test that rates decay to zero when connections become idle let mut tracker = RateTracker::new(); - + // Simulate active traffic tracker.update(0, 0); thread::sleep(Duration::from_millis(100)); tracker.update(100_000, 50_000); // 100KB sent, 50KB received - + // Should have non-zero rate immediately after traffic let initial_out = tracker.get_outgoing_rate_bps(); let initial_in = tracker.get_incoming_rate_bps(); assert!(initial_out > 0.0, "Should have outgoing traffic"); assert!(initial_in > 0.0, "Should have incoming traffic"); - + // Wait 2 seconds (should still show full rate - no decay yet) thread::sleep(Duration::from_millis(2000)); - + let still_active_out = tracker.get_outgoing_rate_bps(); let still_active_in = tracker.get_incoming_rate_bps(); - + // Rates should still be the same (no decay for first 3 seconds) - assert_eq!(still_active_out, initial_out, "Should not decay within 3 seconds"); - assert_eq!(still_active_in, initial_in, "Should not decay within 3 seconds"); - + assert_eq!( + still_active_out, initial_out, + "Should not decay within 3 seconds" + ); + assert_eq!( + still_active_in, initial_in, + "Should not decay within 3 seconds" + ); + // Wait until decay starts (total 4 seconds - should start decay) thread::sleep(Duration::from_millis(2000)); - + let decayed_out = tracker.get_outgoing_rate_bps(); let decayed_in = tracker.get_incoming_rate_bps(); - + // Rates should be lower due to decay - assert!(decayed_out < initial_out, "Outgoing rate should start decaying after 3s"); - assert!(decayed_in < initial_in, "Incoming rate should start decaying after 3s"); + assert!( + decayed_out < initial_out, + "Outgoing rate should start decaying after 3s" + ); + assert!( + decayed_in < initial_in, + "Incoming rate should start decaying after 3s" + ); assert!(decayed_out > 0.0, "Should still have some rate at 4s"); - + // Wait for full decay (total 11 seconds - should be zero) thread::sleep(Duration::from_millis(7000)); - + let final_out = tracker.get_outgoing_rate_bps(); let final_in = tracker.get_incoming_rate_bps(); - + // After 10+ seconds of idle, rates should be zero - assert_eq!(final_out, 0.0, "Outgoing rate should be zero after 10+ seconds idle"); - assert_eq!(final_in, 0.0, "Incoming rate should be zero after 10+ seconds idle"); + assert_eq!( + final_out, 0.0, + "Outgoing rate should be zero after 10+ seconds idle" + ); + assert_eq!( + final_in, 0.0, + "Incoming rate should be zero after 10+ seconds idle" + ); } #[test] fn test_connection_refresh_rates() { // Test that refresh_rates() properly updates cached rate values let mut conn = create_test_connection(); - + // Initialize the rate tracker properly conn.rate_tracker.initialize_with_counts(0, 0); - + // Simulate first packet conn.bytes_sent = 50_000; conn.bytes_received = 25_000; conn.update_rates(); - + thread::sleep(Duration::from_millis(100)); - + // Simulate more traffic conn.bytes_sent = 100_000; conn.bytes_received = 50_000; conn.update_rates(); - + // Should have non-zero rates after recent traffic - assert!(conn.current_outgoing_rate_bps > 0.0, "Should have outgoing rate"); - assert!(conn.current_incoming_rate_bps > 0.0, "Should have incoming rate"); - + assert!( + conn.current_outgoing_rate_bps > 0.0, + "Should have outgoing rate" + ); + assert!( + conn.current_incoming_rate_bps > 0.0, + "Should have incoming rate" + ); + // Now simulate longer idle time and refresh (need >10s for zero) thread::sleep(Duration::from_millis(11000)); conn.refresh_rates(); - + // Rates should be zero after refresh with long idle connection - assert_eq!(conn.current_outgoing_rate_bps, 0.0, "Should be zero after 10+ seconds idle refresh"); - assert_eq!(conn.current_incoming_rate_bps, 0.0, "Should be zero after 10+ seconds idle refresh"); + assert_eq!( + conn.current_outgoing_rate_bps, 0.0, + "Should be zero after 10+ seconds idle refresh" + ); + assert_eq!( + conn.current_incoming_rate_bps, 0.0, + "Should be zero after 10+ seconds idle refresh" + ); } #[test] diff --git a/src/ui.rs b/src/ui.rs index 07a7ae1..1b72c18 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -80,15 +80,22 @@ impl UIState { } let current_index = self.get_selected_index(connections).unwrap_or(0); - log::debug!("move_selection_up: current_index={}, total_connections={}", current_index, connections.len()); - + log::debug!( + "move_selection_up: current_index={}, total_connections={}", + current_index, + connections.len() + ); + if current_index > 0 { self.set_selected_by_index(connections, current_index - 1); log::debug!("move_selection_up: moved to index {}", current_index - 1); } else { // Wrap around to the bottom self.set_selected_by_index(connections, connections.len() - 1); - log::debug!("move_selection_up: wrapped to bottom index {}", connections.len() - 1); + log::debug!( + "move_selection_up: wrapped to bottom index {}", + connections.len() - 1 + ); } } @@ -100,8 +107,12 @@ impl UIState { } let current_index = self.get_selected_index(connections).unwrap_or(0); - log::debug!("move_selection_down: current_index={}, total_connections={}", current_index, connections.len()); - + log::debug!( + "move_selection_down: current_index={}, total_connections={}", + current_index, + connections.len() + ); + if current_index < connections.len().saturating_sub(1) { self.set_selected_by_index(connections, current_index + 1); log::debug!("move_selection_down: moved to index {}", current_index + 1); @@ -150,8 +161,12 @@ impl UIState { } let current_index = self.get_selected_index(connections); - log::debug!("ensure_valid_selection: current_index={:?}, total_connections={}", current_index, connections.len()); - + log::debug!( + "ensure_valid_selection: current_index={:?}, total_connections={}", + current_index, + connections.len() + ); + // If no selection or selection is no longer valid, select first connection if self.selected_connection_key.is_none() || current_index.is_none() { log::debug!("ensure_valid_selection: selecting first connection (index 0)"); @@ -206,7 +221,6 @@ impl UIState { } } - /// Draw the UI pub fn draw( f: &mut Frame, @@ -360,7 +374,8 @@ fn draw_connections_list( // Debug: Log the raw process data to understand what's changing if let Some(ref raw_process_name) = conn.process_name - && raw_process_name.contains("firefox") { + && raw_process_name.contains("firefox") + { log::debug!( "🔍 Raw process name for {}: '{:?}' (len:{}, bytes: {:?})", conn.key(), @@ -841,14 +856,12 @@ fn draw_help(f: &mut Frame, area: Rect) -> Result<()> { Span::raw("Enter filter mode (navigate while typing!)"), ]), Line::from(""), - Line::from(vec![ - Span::styled( - "Filter Examples:", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - ]), + Line::from(vec![Span::styled( + "Filter Examples:", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )]), Line::from(vec![ Span::styled(" /google ", Style::default().fg(Color::Green)), Span::raw("Search for 'google' in all fields"), @@ -913,11 +926,7 @@ fn draw_filter_input(f: &mut Frame, ui_state: &UIState, area: Rect) { }; let filter_input = Paragraph::new(input_text) - .block( - Block::default() - .borders(Borders::ALL) - .title(title) - ) + .block(Block::default().borders(Borders::ALL).title(title)) .style(style) .wrap(Wrap { trim: false });