adding broken dns lookup

This commit is contained in:
Marco Cadetg
2025-04-29 20:01:35 +02:00
parent 0b126c2040
commit f0125737b4
7 changed files with 420 additions and 111 deletions
Generated
+33 -1
View File
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "adler2"
@@ -270,6 +270,18 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "dns-lookup"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5766087c2235fec47fafa4cfecc81e494ee679d0fd4a59887ea0919bfb0e4fc"
dependencies = [
"cfg-if",
"libc",
"socket2",
"windows-sys 0.48.0",
]
[[package]]
name = "either"
version = "1.15.0"
@@ -774,6 +786,7 @@ dependencies = [
"chrono",
"clap",
"crossterm",
"dns-lookup",
"log",
"maxminddb",
"pcap",
@@ -889,6 +902,16 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "socket2"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "stability"
version = "0.2.1"
@@ -1203,6 +1226,15 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
+1
View File
@@ -8,6 +8,7 @@ anyhow = "1.0"
chrono = "0.4"
clap = { version = "4.0", features = ["derive"] }
crossterm = "0.27"
dns-lookup = "2.0.4"
log = "0.4"
maxminddb = "0.24"
pcap = "2.0"
+51 -50
View File
@@ -1,62 +1,63 @@
# English translations for RustNet
# Basic UI elements
rustnet: "RustNet"
overview: "Overview"
connections: "Connections"
processes: "Processes"
help: "Help"
network: "Network"
statistics: "Statistics"
top_processes: "Top Processes"
connection_details: "Connection Details"
process_details: "Process Details"
traffic: "Traffic"
rustnet: 'RustNet'
overview: 'Overview'
connections: 'Connections'
processes: 'Processes'
help: 'Help'
network: 'Network'
statistics: 'Statistics'
top_processes: 'Top Processes'
connection_details: 'Connection Details'
process_details: 'Process Details'
traffic: 'Traffic'
# Properties
interface: "Interface"
protocol: "Protocol"
local_address: "Local Address"
remote_address: "Remote Address"
state: "State"
process: "Process"
pid: "PID"
age: "Age"
country: "Country"
city: "City"
bytes_sent: "Bytes Sent"
bytes_received: "Bytes Received"
packets_sent: "Packets Sent"
packets_received: "Packets Received"
last_activity: "Last Activity"
process_name: "Process Name"
command_line: "Command Line"
user: "User"
cpu_usage: "CPU Usage"
memory_usage: "Memory Usage"
process_connections: "Process Connections"
interface: 'Interface'
protocol: 'Protocol'
local_address: 'Local Address'
remote_address: 'Remote Address'
state: 'State'
process: 'Process'
pid: 'PID'
age: 'Age'
country: 'Country'
city: 'City'
bytes_sent: 'Bytes Sent'
bytes_received: 'Bytes Received'
packets_sent: 'Packets Sent'
packets_received: 'Packets Received'
last_activity: 'Last Activity'
process_name: 'Process Name'
command_line: 'Command Line'
user: 'User'
cpu_usage: 'CPU Usage'
memory_usage: 'Memory Usage'
process_connections: 'Process Connections'
# Statistics
tcp_connections: "TCP Connections"
udp_connections: "UDP Connections"
total_connections: "Total Connections"
tcp_connections: 'TCP Connections'
udp_connections: 'UDP Connections'
total_connections: 'Total Connections'
# Status messages
no_connections: "No connections found"
no_processes: "No processes found"
process_not_found: "Process not found"
no_pid_for_connection: "No process ID for this connection"
press_for_process_details: "Press for process details"
no_connections: 'No connections found'
no_processes: 'No processes found'
process_not_found: 'Process not found'
no_pid_for_connection: 'No process ID for this connection'
press_for_process_details: 'Press for process details'
press_h_for_help: "Press 'h' for help"
default: "default"
language: "Language"
default: 'default'
language: 'Language'
# Help screen
help_intro: "is a cross-platform network monitoring tool"
help_quit: "Quit the application"
help_refresh: "Refresh connections"
help_navigate: "Navigate up/down"
help_select: "Select connection/view details"
help_back: "Go back to previous view"
help_toggle_location: "Toggle IP location display"
help_toggle_help: "Toggle help screen"
help_intro: 'is a cross-platform network monitoring tool'
help_quit: 'Quit the application'
help_refresh: 'Refresh connections'
help_navigate: 'Navigate up/down'
help_select: 'Select connection/view details'
help_back: 'Go back to previous view'
help_toggle_location: 'Toggle IP location display'
help_toggle_help: 'Toggle help screen'
help_toggle_dns: 'Toggle DNS hostname display'
+46 -45
View File
@@ -1,62 +1,63 @@
# Traductions françaises pour RustNet
# Éléments de base de l'interface utilisateur
rustnet: "RustNet"
rustnet: 'RustNet'
overview: "Vue d'ensemble"
connections: "Connexions"
processes: "Processus"
help: "Aide"
network: "Réseau"
statistics: "Statistiques"
top_processes: "Processus principaux"
connection_details: "Détails de connexion"
process_details: "Détails du processus"
traffic: "Trafic"
connections: 'Connexions'
processes: 'Processus'
help: 'Aide'
network: 'Réseau'
statistics: 'Statistiques'
top_processes: 'Processus principaux'
connection_details: 'Détails de connexion'
process_details: 'Détails du processus'
traffic: 'Trafic'
# Propriétés
interface: "Interface"
protocol: "Protocole"
local_address: "Adresse locale"
remote_address: "Adresse distante"
state: "État"
process: "Processus"
pid: "PID"
age: "Âge"
country: "Pays"
city: "Ville"
bytes_sent: "Octets envoyés"
bytes_received: "Octets reçus"
packets_sent: "Paquets envoyés"
packets_received: "Paquets reçus"
last_activity: "Dernière activité"
process_name: "Nom du processus"
command_line: "Ligne de commande"
user: "Utilisateur"
cpu_usage: "Utilisation CPU"
memory_usage: "Utilisation mémoire"
process_connections: "Connexions du processus"
interface: 'Interface'
protocol: 'Protocole'
local_address: 'Adresse locale'
remote_address: 'Adresse distante'
state: 'État'
process: 'Processus'
pid: 'PID'
age: 'Âge'
country: 'Pays'
city: 'Ville'
bytes_sent: 'Octets envoyés'
bytes_received: 'Octets reçus'
packets_sent: 'Paquets envoyés'
packets_received: 'Paquets reçus'
last_activity: 'Dernière activité'
process_name: 'Nom du processus'
command_line: 'Ligne de commande'
user: 'Utilisateur'
cpu_usage: 'Utilisation CPU'
memory_usage: 'Utilisation mémoire'
process_connections: 'Connexions du processus'
# Statistiques
tcp_connections: "Connexions TCP"
udp_connections: "Connexions UDP"
total_connections: "Connexions totales"
tcp_connections: 'Connexions TCP'
udp_connections: 'Connexions UDP'
total_connections: 'Connexions totales'
# Messages d'état
no_connections: "Aucune connexion trouvée"
no_processes: "Aucun processus trouvé"
process_not_found: "Processus non trouvé"
no_connections: 'Aucune connexion trouvée'
no_processes: 'Aucun processus trouvé'
process_not_found: 'Processus non trouvé'
no_pid_for_connection: "Pas d'ID de processus pour cette connexion"
press_for_process_details: "Appuyez pour les détails du processus"
press_for_process_details: 'Appuyez pour les détails du processus'
press_h_for_help: "Appuyez sur 'h' pour l'aide"
default: "défaut"
language: "Langue"
default: 'défaut'
language: 'Langue'
# Écran d'aide
help_intro: "est un outil de surveillance réseau multiplateforme"
help_intro: 'est un outil de surveillance réseau multiplateforme'
help_quit: "Quitter l'application"
help_refresh: "Rafraîchir les connexions"
help_navigate: "Naviguer haut/bas"
help_select: "Sélectionner connexion/voir détails"
help_back: "Retourner à la vue précédente"
help_refresh: 'Rafraîchir les connexions'
help_navigate: 'Naviguer haut/bas'
help_select: 'Sélectionner connexion/voir détails'
help_back: 'Retourner à la vue précédente'
help_toggle_location: "Activer/désactiver l'affichage de localisation IP"
help_toggle_help: "Activer/désactiver l'écran d'aide"
help_toggle_dns: "Activer/désactiver l'affichage des noms d'hôte DNS"
+259 -4
View File
@@ -1,6 +1,8 @@
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use dns_lookup::lookup_addr;
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::{Arc, Mutex};
use std::thread;
@@ -39,12 +41,24 @@ pub struct App {
pub connections: Vec<Connection>,
/// Process map (pid to process)
pub processes: HashMap<u32, Process>,
/// Currently selected connection
pub selected_connection: Option<Connection>,
/// Currently selected connection index
pub selected_connection_idx: usize,
/// Currently selected process index
pub selected_process_idx: usize,
/// Show IP locations (requires MaxMind DB)
pub show_locations: bool,
/// Show DNS hostnames instead of IP addresses
pub show_hostnames: bool,
/// Last connection sort time
last_sort_time: std::time::Instant,
/// Connection order map (for stable ordering)
connection_order: HashMap<String, usize>,
/// Next order index for new connections
next_order_index: usize,
/// DNS cache to avoid repeated lookups
dns_cache: HashMap<IpAddr, String>,
}
impl App {
@@ -58,9 +72,15 @@ impl App {
network_monitor: None,
connections: Vec::new(),
processes: HashMap::new(),
selected_connection: None,
selected_connection_idx: 0,
selected_process_idx: 0,
show_locations: true,
show_hostnames: false,
last_sort_time: std::time::Instant::now(),
connection_order: HashMap::new(),
next_order_index: 0,
dns_cache: HashMap::new(),
})
}
@@ -121,15 +141,27 @@ impl App {
Some(Action::Quit)
}
KeyCode::Char('r') => Some(Action::Refresh),
KeyCode::Down | KeyCode::Char('j') => {
KeyCode::Down => {
if !self.connections.is_empty() {
self.selected_connection = Some(
self.connections
[(self.selected_connection_idx + 1) % self.connections.len()]
.clone(),
);
self.selected_connection_idx =
(self.selected_connection_idx + 1) % self.connections.len();
}
None
}
KeyCode::Up | KeyCode::Char('k') => {
KeyCode::Up => {
if !self.connections.is_empty() {
self.selected_connection = Some(
self.connections[self
.selected_connection_idx
.checked_sub(1)
.unwrap_or(self.connections.len() - 1)]
.clone(),
);
self.selected_connection_idx = self
.selected_connection_idx
.checked_sub(1)
@@ -151,6 +183,14 @@ impl App {
self.show_locations = !self.show_locations;
None
}
KeyCode::Char('d') => {
self.show_hostnames = !self.show_hostnames;
// Clear DNS cache when toggling off to ensure fresh lookups when toggled on again
if !self.show_hostnames {
self.dns_cache.clear();
}
None
}
_ => None,
}
}
@@ -194,10 +234,63 @@ impl App {
/// Update application state on tick
pub fn on_tick(&mut self) -> Result<()> {
// Store currently selected connection (if any)
let selected = self.selected_connection.clone();
// Update connections from network monitor if available
if let Some(monitor_arc) = &self.network_monitor {
let mut monitor = monitor_arc.lock().unwrap(); // Lock the mutex
self.connections = monitor.get_connections()?;
let mut new_connections = monitor.get_connections()?;
drop(monitor); // Release the mutex lock before self-mutation
// Extract keys for sorting
let mut keys_to_process = Vec::new();
for conn in &new_connections {
let key = self.get_connection_key(conn);
keys_to_process.push(key);
}
// Update connection order
for key in keys_to_process {
if !self.connection_order.contains_key(&key) {
self.connection_order.insert(key, self.next_order_index);
self.next_order_index += 1;
}
}
// Sort connections by their assigned order
new_connections.sort_by(|a, b| {
let key_a = self.get_connection_key(a);
let key_b = self.get_connection_key(b);
let order_a = self.connection_order.get(&key_a).unwrap_or(&usize::MAX);
let order_b = self.connection_order.get(&key_b).unwrap_or(&usize::MAX);
order_a.cmp(order_b)
});
// Update connections with the sorted list
self.connections = new_connections;
// Restore selected connection position if possible
if let Some(ref conn) = selected {
if let Some(idx) = self.find_connection_index(conn) {
self.selected_connection_idx = idx;
self.selected_connection = Some(self.connections[idx].clone());
} else if !self.connections.is_empty() {
// If previously selected connection is gone, select first one
self.selected_connection_idx = 0;
self.selected_connection = Some(self.connections[0].clone());
} else {
// If no connections left, clear selection
self.selected_connection_idx = 0;
self.selected_connection = None;
}
} else if !self.connections.is_empty() && self.selected_connection.is_none() {
// If no previous selection but we have connections, select the first one
self.selected_connection_idx = 0;
self.selected_connection = Some(self.connections[0].clone());
}
}
Ok(())
@@ -205,9 +298,58 @@ impl App {
/// Refresh application data
pub fn refresh(&mut self) -> Result<()> {
// Store currently selected connection (if any)
let selected = self.selected_connection.clone();
if let Some(monitor_arc) = &self.network_monitor {
let mut monitor = monitor_arc.lock().unwrap(); // Lock the mutex
self.connections = monitor.get_connections()?;
let mut new_connections = monitor.get_connections()?;
drop(monitor); // Release the mutex lock before self-mutation
// Extract keys for sorting
let mut keys_to_process = Vec::new();
for conn in &new_connections {
let key = self.get_connection_key(conn);
keys_to_process.push(key);
}
// Update connection order
for key in keys_to_process {
if !self.connection_order.contains_key(&key) {
self.connection_order.insert(key, self.next_order_index);
self.next_order_index += 1;
}
}
// Sort connections by their assigned order
new_connections.sort_by(|a, b| {
let key_a = self.get_connection_key(a);
let key_b = self.get_connection_key(b);
let order_a = self.connection_order.get(&key_a).unwrap_or(&usize::MAX);
let order_b = self.connection_order.get(&key_b).unwrap_or(&usize::MAX);
order_a.cmp(order_b)
});
// Update connections with the sorted list
self.connections = new_connections;
// Restore selected connection position if possible
if let Some(ref conn) = selected {
if let Some(idx) = self.find_connection_index(conn) {
self.selected_connection_idx = idx;
self.selected_connection = Some(self.connections[idx].clone());
} else if !self.connections.is_empty() {
// If previously selected connection is gone, select first one
self.selected_connection_idx = 0;
self.selected_connection = Some(self.connections[0].clone());
} else {
// If no connections left, clear selection
self.selected_connection_idx = 0;
self.selected_connection = None;
}
}
}
Ok(())
@@ -252,4 +394,117 @@ impl App {
None
}
/// Generate a unique key for a connection
fn get_connection_key(&self, conn: &Connection) -> String {
format!(
"{:?}-{}-{}-{:?}",
conn.protocol, conn.local_addr, conn.remote_addr, conn.state
)
}
/// Assign stable order indices to connections
fn update_connection_order(&mut self, connections: &mut Vec<Connection>) {
// This ensures that new connections get added at the end and existing connections maintain their order
for conn in connections.iter() {
let key = self.get_connection_key(conn);
if !self.connection_order.contains_key(&key) {
self.connection_order.insert(key, self.next_order_index);
self.next_order_index += 1;
}
}
}
/// Sort connections in a stable way
fn sort_connections_stable(&mut self, connections: &mut Vec<Connection>) {
// Update order indices for any new connections
self.update_connection_order(connections);
// Sort connections by their assigned order
connections.sort_by(|a, b| {
let key_a = self.get_connection_key(a);
let key_b = self.get_connection_key(b);
let order_a = self.connection_order.get(&key_a).unwrap_or(&usize::MAX);
let order_b = self.connection_order.get(&key_b).unwrap_or(&usize::MAX);
order_a.cmp(order_b)
});
}
/// Find the index of a connection that matches the selected connection
fn find_connection_index(&self, selected: &Connection) -> Option<usize> {
let selected_key = self.get_connection_key(selected);
for (i, conn) in self.connections.iter().enumerate() {
let key = self.get_connection_key(conn);
if key == selected_key {
return Some(i);
}
}
None
}
/// Resolve an IP address to a hostname with caching
pub fn resolve_hostname(&mut self, ip: IpAddr) -> String {
// Check if the IP is in the cache
if let Some(hostname) = self.dns_cache.get(&ip) {
return hostname.clone();
}
// Special handling for common IP addresses
if ip.is_loopback() {
let hostname = "localhost".to_string();
self.dns_cache.insert(ip, hostname.clone());
return hostname;
}
if ip.is_unspecified() {
let hostname = "*".to_string();
self.dns_cache.insert(ip, hostname.clone());
return hostname;
}
// Perform DNS resolution using the dns-lookup crate
match lookup_addr(&ip) {
Ok(hostname) => {
// Cache the result
let hostname = hostname.trim_end_matches('.').to_string();
self.dns_cache.insert(ip, hostname.clone());
hostname
}
Err(_) => {
// If resolution fails, return the IP as a string
let ip_str = ip.to_string();
self.dns_cache.insert(ip, ip_str.clone());
ip_str
}
}
}
/// Format a socket address with hostname if enabled (without mutating self)
pub fn format_socket_addr(&self, addr: std::net::SocketAddr) -> String {
if self.show_hostnames {
let ip = addr.ip();
// Check if it's in the cache
if let Some(hostname) = self.dns_cache.get(&ip) {
return format!("{}:{}", hostname, addr.port());
}
// Special handling without cache insertion
if ip.is_loopback() {
return format!("localhost:{}", addr.port());
}
if ip.is_unspecified() {
return format!("*:{}", addr.port());
}
// Just return the address as string if not in cache
addr.to_string()
} else {
addr.to_string()
}
}
}
+2 -4
View File
@@ -1,7 +1,7 @@
use anyhow::{anyhow, Result};
use log::{debug, error, info};
use maxminddb::geoip2;
use pcap::{Capture, Device, Packet};
use pcap::{Capture, Device};
use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
use std::time::{Duration, Instant, SystemTime};
@@ -16,8 +16,6 @@ use windows::*;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
use macos::get_interface_addresses;
/// Connection protocol
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -324,7 +322,7 @@ impl NetworkMonitor {
let process_single_packet =
|data: &[u8],
connections: &mut HashMap<String, Connection>,
interface: &Option<String>| {
_interface: &Option<String>| {
// Check if it's an ethernet frame
if data.len() < 14 {
return; // Too short for Ethernet
+28 -7
View File
@@ -144,14 +144,17 @@ fn draw_connections_list(f: &mut Frame, app: &App, area: Rect) {
.pid
.map(|p| p.to_string())
.unwrap_or_else(|| "-".to_string());
let process_str = conn.process_name.clone().unwrap_or_else(|| "-".to_string());
let process_display = format!("{} ({})", process_str, pid_str);
let remote_display = conn.remote_addr.to_string();
// Format addresses with hostnames if enabled - no mutable borrowing
let local_display = app.format_socket_addr(conn.local_addr);
let remote_display = app.format_socket_addr(conn.remote_addr);
let cells = [
Cell::from(conn.protocol.to_string()),
Cell::from(conn.local_addr.to_string()),
Cell::from(local_display),
Cell::from(remote_display),
Cell::from(conn.state.to_string()),
Cell::from(process_display),
@@ -159,6 +162,12 @@ fn draw_connections_list(f: &mut Frame, app: &App, area: Rect) {
rows.push(Row::new(cells));
}
// Create table state with current selection
let mut state = ratatui::widgets::TableState::default();
if !app.connections.is_empty() {
state.select(Some(app.selected_connection_idx));
}
let connections = Table::new(rows, &widths)
.header(header)
.block(
@@ -166,10 +175,10 @@ fn draw_connections_list(f: &mut Frame, app: &App, area: Rect) {
.borders(Borders::ALL)
.title(app.i18n.get("connections")),
)
.highlight_style(Style::default().add_modifier(Modifier::REVERSED)) // Changed from row_highlight_style to highlight_style
.highlight_style(Style::default().add_modifier(Modifier::REVERSED))
.highlight_symbol("> ");
f.render_widget(connections, area);
f.render_stateful_widget(connections, area, &mut state);
}
/// Draw side panel with stats
@@ -309,12 +318,16 @@ fn draw_connection_details(f: &mut Frame, app: &App, area: Rect) -> Result<()> {
Span::raw(conn.protocol.to_string()),
]));
// Format addresses with hostnames if enabled
let local_display = app.format_socket_addr(conn.local_addr);
let remote_display = app.format_socket_addr(conn.remote_addr);
details_text.push(Line::from(vec![
Span::styled(
format!("{}: ", app.i18n.get("local_address")),
Style::default().fg(Color::Yellow),
),
Span::raw(conn.local_addr.to_string()),
Span::raw(local_display),
]));
details_text.push(Line::from(vec![
@@ -322,7 +335,7 @@ fn draw_connection_details(f: &mut Frame, app: &App, area: Rect) -> Result<()> {
format!("{}: ", app.i18n.get("remote_address")),
Style::default().fg(Color::Yellow),
),
Span::raw(conn.remote_addr.to_string()),
Span::raw(remote_display),
]));
if app.show_locations && !conn.remote_addr.ip().is_unspecified() {
@@ -545,6 +558,10 @@ fn draw_process_details(f: &mut Frame, app: &mut App, area: Rect) -> Result<()>
let mut items = Vec::new();
for conn in &connections {
// Format addresses with hostnames if enabled
let local_display = app.format_socket_addr(conn.local_addr);
let remote_display = app.format_socket_addr(conn.remote_addr);
items.push(ListItem::new(Line::from(vec![
Span::styled(
format!("{}: ", conn.protocol),
@@ -552,7 +569,7 @@ fn draw_process_details(f: &mut Frame, app: &mut App, area: Rect) -> Result<()>
),
Span::raw(format!(
"{} -> {} ({})",
conn.local_addr, conn.remote_addr, conn.state
local_display, remote_display, conn.state
)),
])));
}
@@ -619,6 +636,10 @@ fn draw_help(f: &mut Frame, app: &App, area: Rect) -> Result<()> {
Span::styled("l ", Style::default().fg(Color::Yellow)),
Span::raw(app.i18n.get("help_toggle_location")),
]),
Line::from(vec![
Span::styled("d ", Style::default().fg(Color::Yellow)),
Span::raw(app.i18n.get("help_toggle_dns")),
]),
Line::from(vec![
Span::styled("h ", Style::default().fg(Color::Yellow)),
Span::raw(app.i18n.get("help_toggle_help")),