mirror of
https://github.com/domcyrus/rustnet.git
synced 2026-01-05 21:40:01 -06:00
cargo fmt
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
199
src/filter.rs
199
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
433
src/main.rs
433
src/main.rs
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
53
src/ui.rs
53
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 });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user