cargo fmt

This commit is contained in:
Marco Cadetg
2025-09-09 15:45:14 +02:00
parent 88837b1887
commit 3329eed6c5
12 changed files with 560 additions and 426 deletions

View File

@@ -607,7 +607,10 @@ impl App {
}
/// Start rate refresh thread to update rates for idle connections
fn start_rate_refresh_thread(&self, connections: Arc<DashMap<String, Connection>>) -> Result<()> {
fn start_rate_refresh_thread(
&self,
connections: Arc<DashMap<String, Connection>>,
) -> 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<Connection> {
let connections = self.connections_snapshot.read().unwrap().clone();
if filter_query.trim().is_empty() {
return connections;
}

View File

@@ -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"),
}
}
}
}

View File

@@ -173,256 +173,283 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
// 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;
}
}
}
}
}

View File

@@ -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<Vec<String>> {
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 {

View File

@@ -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),
});

View File

@@ -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<Vec<String>> {
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<TlsInfo> {
&& 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);

View File

@@ -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

View File

@@ -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<ParsedPacket> {
fn parse_tcp(&self, transport_data: &[u8], params: TransportParams) -> Option<ParsedPacket> {
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<ParsedPacket> {
fn parse_udp(&self, transport_data: &[u8], params: TransportParams) -> Option<ParsedPacket> {
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<ParsedPacket> {
fn parse_icmp(&self, transport_data: &[u8], params: TransportParams) -> Option<ParsedPacket> {
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<ParsedPacket> {
fn parse_icmpv6(&self, transport_data: &[u8], params: TransportParams) -> Option<ParsedPacket> {
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 {

View File

@@ -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::<u32>() {
if pid == 0 {
continue;
}
&& let Ok(pid) = pid_str.parse::<u32>()
{
if pid == 0 {
continue;
}
// Get process name
let comm_path = path.join("comm");
let process_name = fs::read_to_string(&comm_path)
.unwrap_or_else(|_| "unknown".to_string())
.trim()
.to_string();
// 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::<u64>()
&& 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());
}
}

View File

@@ -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;
}

View File

@@ -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]

View File

@@ -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 });