From 80b5b0c2c10412940fec1969b3444fd8b794dfba Mon Sep 17 00:00:00 2001 From: Marco Cadetg Date: Sat, 4 Oct 2025 15:33:42 +0200 Subject: [PATCH] feat: privilege detection (#31) * feat: detect insufficient privileges before network interface access - Add privilege detection module for Linux, macOS, and Windows - Check privileges before TUI initialization for visible errors - Provide platform-specific instructions (sudo, setcap, Docker flags) - Detect container environments and provide Docker-specific guidance --- EBPF_BUILD.md | 11 +- README.md | 86 +++--- src/app.rs | 59 +++- src/main.rs | 29 ++ src/network/capture.rs | 17 +- src/network/mod.rs | 1 + src/network/platform/linux.rs | 4 + src/network/platform/linux_enhanced.rs | 16 ++ src/network/platform/macos.rs | 4 + src/network/platform/mod.rs | 7 + src/network/platform/windows.rs | 4 + src/network/privileges.rs | 369 +++++++++++++++++++++++++ src/ui.rs | 3 + 13 files changed, 548 insertions(+), 62 deletions(-) create mode 100644 src/network/privileges.rs diff --git a/EBPF_BUILD.md b/EBPF_BUILD.md index 8c926ac..b28f70f 100644 --- a/EBPF_BUILD.md +++ b/EBPF_BUILD.md @@ -189,10 +189,19 @@ cargo build --features ebpf # If compilation fails, check for missing definitions # and add them to your minimal header -# Verify eBPF program loads (requires root) +# Verify eBPF program loads +# Option 1: Run with sudo (always works) sudo cargo run --features ebpf + +# Option 2: Set capabilities (Linux only, see README.md Permissions section) +sudo setcap 'cap_net_raw,cap_net_admin,cap_sys_admin,cap_bpf,cap_perfmon+eip' ./target/debug/rustnet +cargo run --features ebpf + +# Check the TUI Statistics panel to verify it shows "Process Detection: eBPF + procfs" ``` +**Note**: eBPF kprobe programs require specific Linux capabilities. See the main [README.md Permissions section](README.md#permissions) for detailed capability requirements. The required capabilities may vary by kernel version. + ## Best Practices 1. **Start minimal**: Only include structures and fields you actually access diff --git a/README.md b/README.md index 749bb70..e6f1134 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ sudo apt-get install -f # Run with sudo sudo rustnet -# Optional: Grant capabilities to run without sudo +# Optional: Grant capabilities to run without sudo (see Permissions section) sudo setcap cap_net_raw,cap_net_admin=eip /usr/bin/rustnet rustnet ``` @@ -163,7 +163,7 @@ sudo dnf install Rustnet_LinuxRPM_x86_64.rpm # Run with sudo sudo rustnet -# Optional: Grant capabilities to run without sudo +# Optional: Grant capabilities to run without sudo (see Permissions section) sudo setcap cap_net_raw,cap_net_admin=eip /usr/bin/rustnet rustnet ``` @@ -298,36 +298,18 @@ docker run --rm ghcr.io/domcyrus/rustnet:0.7.0 --help ### Running RustNet -On Unix-like systems (Linux/macOS), packet capture typically requires elevated privileges: +Packet capture requires elevated privileges on most systems. See the [Permissions](#permissions) section below for detailed setup instructions for each platform. -#### When built from source: +**Quick start:** ```bash -# Run with sudo -sudo ./target/release/rustnet +# Run with sudo (works on all platforms) +sudo rustnet -# Or set capabilities on Linux (to avoid needing sudo) -sudo setcap cap_net_raw,cap_net_admin=eip ./target/release/rustnet -./target/release/rustnet -``` - -#### When installed via cargo: - -```bash -# Option 1: Use full path with sudo -sudo $(which rustnet) - -# Option 2: Set capabilities on the cargo-installed binary (Linux only) -sudo setcap cap_net_raw,cap_net_admin=eip ~/.cargo/bin/rustnet -rustnet # Can now run without sudo - -# Option 3: Create system-wide symlink -sudo ln -s ~/.cargo/bin/rustnet /usr/local/bin/rustnet -sudo rustnet # Works from anywhere - -# Option 4: Install globally with cargo (requires sudo) -sudo cargo install --root /usr/local rustnet-monitor -sudo rustnet # Binary installed to /usr/local/bin/rustnet +# Or grant capabilities to run without sudo (see Permissions section for details) +# Linux example: +sudo setcap cap_net_raw,cap_net_admin=eip /path/to/rustnet +rustnet ``` ## Usage @@ -752,8 +734,11 @@ RustNet is built with the following key dependencies: RustNet uses platform-specific APIs to associate network connections with processes: -- **Linux**: Parses `/proc/net/tcp`, `/proc/net/udp`, and `/proc//fd/` to find socket inodes +- **Linux**: Parses `/proc/net/tcp`, `/proc/net/udp`, and `/proc//fd/` to find socket inodes. With eBPF enabled, uses kernel probes for enhanced performance. - **macOS**: Uses PKTAP (Packet Tap) headers when available for process identification from packet metadata, with fallback to `lsof` system commands for process-socket associations. PKTAP extracts process information directly from kernel packet headers when supported. + - **Important**: PKTAP requires `sudo` even with `wireshark-chmodbpf` installed, as it accesses a privileged kernel interface + - Without `sudo`: Falls back to `lsof` for process detection (slower but functional) + - The TUI displays which detection method is in use in the Statistics panel - **Windows**: Uses nothing so far :) ### Network Interfaces @@ -836,7 +821,7 @@ sudo ./target/release/rustnet Add your user to the `access_bpf` group for passwordless packet capture: -**Using Wireshark's ChmodBPF (Easiest):** +**Using Wireshark's ChmodBPF (For basic packet capture):** ```bash # Install Wireshark's BPF permission helper @@ -844,9 +829,14 @@ brew install --cask wireshark-chmodbpf # Log out and back in for group changes to take effect # Then run rustnet without sudo: -rustnet +rustnet # Uses lsof for process detection (slower) + +# For PKTAP support with process metadata from packet headers, use sudo: +sudo rustnet # Uses PKTAP for faster process detection ``` +**Note**: `wireshark-chmodbpf` grants access to `/dev/bpf*` for packet capture, but **PKTAP** is a separate privileged kernel interface that requires root privileges regardless of BPF permissions. The TUI will display which detection method is active ("pktap" with sudo, or "lsof" without). + **Manual BPF Group Setup:** ```bash @@ -914,30 +904,38 @@ rustnet **For experimental eBPF-enabled builds (enhanced Linux performance):** -eBPF is an experimental feature that requires additional capabilities for kernel program loading and performance monitoring: +eBPF is an experimental feature that provides lower-overhead process identification using kernel probes: ```bash # Build with eBPF support cargo build --release --features ebpf -# Grant full capability set for eBPF (modern kernels with CAP_BPF support) +# Try modern capabilities first (Linux 5.8+) sudo setcap 'cap_net_raw,cap_net_admin,cap_bpf,cap_perfmon+eip' ./target/release/rustnet - -# OR for older kernels (fallback to CAP_SYS_ADMIN) -sudo setcap 'cap_net_raw,cap_net_admin,cap_sys_admin+eip' ./target/release/rustnet - -# Run without sudo - eBPF programs will load automatically if capabilities are sufficient ./target/release/rustnet + +# If eBPF fails to load, add CAP_SYS_ADMIN (may be required depending on kernel version) +sudo setcap 'cap_net_raw,cap_net_admin,cap_sys_admin,cap_bpf,cap_perfmon+eip' ./target/release/rustnet +./target/release/rustnet +# Check TUI Statistics panel - should show "Process Detection: eBPF + procfs" ``` **Capability requirements for eBPF:** -- `CAP_NET_RAW` - Raw socket access for packet capture -- `CAP_NET_ADMIN` - Network administration -- `CAP_BPF` - BPF program loading (Linux 5.8+, preferred) -- `CAP_PERFMON` - Performance monitoring (Linux 5.8+, preferred) -- `CAP_SYS_ADMIN` - System administration (fallback for older kernels) -The application will automatically detect available capabilities and fall back to procfs-only mode if eBPF cannot be loaded. +Base capabilities (always required): +- `CAP_NET_RAW` - Raw socket access for packet capture +- `CAP_NET_ADMIN` - Network administration + +eBPF-specific capabilities (Linux 5.8+): +- `CAP_BPF` - BPF program loading and map operations +- `CAP_PERFMON` - Performance monitoring and tracing operations + +Additional capability (may be required): +- `CAP_SYS_ADMIN` - Some kernel versions or configurations may still require this for kprobe attachment, even with CAP_BPF and CAP_PERFMON available. Requirements vary by kernel version and configuration. + +**Fallback behavior**: If eBPF cannot load (e.g., insufficient capabilities, incompatible kernel), the application automatically uses procfs-only mode. The TUI Statistics panel displays which detection method is active: +- `Process Detection: eBPF + procfs` - eBPF successfully loaded +- `Process Detection: procfs` - Using procfs fallback **Note:** eBPF support is experimental and may have limitations with process name display (see [eBPF Enhanced Process Identification](#ebpf-enhanced-process-identification-experimental)). diff --git a/src/app.rs b/src/app.rs index c4b1c8a..0af08db 100644 --- a/src/app.rs +++ b/src/app.rs @@ -102,6 +102,9 @@ pub struct App { /// Whether PKTAP is active (macOS only) - used to disable process enrichment pktap_active: Arc, + + /// Current process detection method (e.g., "eBPF + procfs", "pktap", "lsof", "N/A") + process_detection_method: Arc>, } impl App { @@ -123,6 +126,7 @@ impl App { current_interface: Arc::new(RwLock::new(None)), linktype: Arc::new(RwLock::new(None)), pktap_active: Arc::new(AtomicBool::new(false)), + process_detection_method: Arc::new(RwLock::new(String::from("initializing..."))), }) } @@ -280,10 +284,22 @@ impl App { ); } Err(e) => { - error!("Failed to start packet capture: {}", e); - error!( - "Make sure you have permission to capture packets (try running with sudo)" - ); + let error_msg = format!("{}", e); + + // Check if this is a privilege error + if error_msg.contains("Insufficient privileges") { + error!("Failed to start packet capture due to insufficient privileges:"); + // The error message already contains detailed instructions + for line in error_msg.lines() { + error!("{}", line); + } + } else { + error!("Failed to start packet capture: {}", e); + error!( + "Make sure you have permission to capture packets (try running with sudo)" + ); + } + warn!("Application will run in process-only mode"); } } @@ -380,6 +396,7 @@ impl App { ) -> Result<()> { let pktap_active = Arc::clone(&self.pktap_active); let should_stop = Arc::clone(&self.should_stop); + let process_detection_method = Arc::clone(&self.process_detection_method); thread::spawn(move || { // On macOS, wait for PKTAP detection to avoid unnecessary lsof calls @@ -394,6 +411,9 @@ impl App { info!( "🚫 Skipping process enrichment thread - PKTAP is active and provides process metadata" ); + if let Ok(mut method) = process_detection_method.write() { + *method = String::from("pktap"); + } return; } // Check more frequently for faster detection @@ -405,6 +425,9 @@ impl App { info!( "🚫 Skipping process enrichment thread - PKTAP became active during startup" ); + if let Ok(mut method) = process_detection_method.write() { + *method = String::from("pktap"); + } return; } else { info!( @@ -417,7 +440,7 @@ impl App { } // Start the actual process enrichment - if let Err(e) = Self::run_process_enrichment(connections, should_stop, pktap_active) { + if let Err(e) = Self::run_process_enrichment(connections, should_stop, pktap_active, process_detection_method) { error!("Process enrichment thread failed: {}", e); } }); @@ -430,12 +453,24 @@ impl App { connections: Arc>, should_stop: Arc, pktap_active: Arc, + process_detection_method: Arc>, ) -> Result<()> { - let process_lookup = - create_process_lookup_with_pktap_status(pktap_active.load(Ordering::Relaxed))?; + // Check PKTAP status before creating process lookup + let is_pktap = pktap_active.load(Ordering::Relaxed); + + let process_lookup = create_process_lookup_with_pktap_status(is_pktap)?; let interval = Duration::from_secs(2); // Use default interval - info!("Process enrichment thread started"); + // Get and set the detection method from the process lookup implementation + // Only set if not already detected as pktap (to handle race conditions) + if let Ok(mut method) = process_detection_method.write() + && method.as_str() != "pktap" + { + *method = process_lookup.get_detection_method().to_string(); + } + + info!("Process enrichment thread started with detection method: {}", + process_lookup.get_detection_method()); let mut last_refresh = Instant::now(); loop { @@ -748,6 +783,14 @@ impl App { self.current_interface.read().unwrap().clone() } + /// Get the current process detection method + pub fn get_process_detection_method(&self) -> String { + self.process_detection_method + .read() + .map(|s| s.clone()) + .unwrap_or_else(|_| String::from("unknown")) + } + /// Stop all threads gracefully pub fn stop(&self) { info!("Stopping application"); diff --git a/src/main.rs b/src/main.rs index bc36f10..ab84b97 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,9 @@ fn main() -> Result<()> { // Parse command line arguments let matches = cli::build_cli().get_matches(); + + // Check privileges BEFORE initializing TUI (so error messages are visible) + check_privileges_early()?; // Set up logging only if log-level was provided if let Some(log_level_str) = matches.get_one::("log-level") { let log_level = log_level_str @@ -528,6 +531,32 @@ fn run_ui_loop( Ok(()) } +/// Check if we have privileges for packet capture before starting the TUI +fn check_privileges_early() -> Result<()> { + match network::privileges::check_packet_capture_privileges() { + Ok(status) if !status.has_privileges => { + // Print error to stderr before TUI starts + eprintln!("\n╔═══════════════════════════════════════════════════════════════════════════╗"); + eprintln!("ā•‘ INSUFFICIENT PRIVILEGES ā•‘"); + eprintln!("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•"); + eprintln!(); + eprintln!("{}", status.error_message()); + + return Err(anyhow::anyhow!("Insufficient privileges for packet capture")); + } + Err(e) => { + // Privilege check failed - warn but continue + eprintln!("Warning: Failed to check privileges: {}", e); + eprintln!("Continuing anyway, but packet capture may fail...\n"); + } + _ => { + // Privileges OK + } + } + + Ok(()) +} + #[cfg(target_os = "windows")] fn check_windows_dependencies() -> Result<()> { use anyhow::anyhow; diff --git a/src/network/capture.rs b/src/network/capture.rs index ef465d6..2eb9831 100644 --- a/src/network/capture.rs +++ b/src/network/capture.rs @@ -34,7 +34,8 @@ impl Default for CaptureConfig { /// Find the best active network device fn find_best_device() -> Result { - let devices = Device::list()?; + let devices = Device::list() + .map_err(|e| anyhow!("Failed to list network devices: {}. This may indicate insufficient privileges.", e))?; log::info!( "Scanning {} devices for best active interface...", @@ -180,18 +181,16 @@ pub fn setup_packet_capture(config: CaptureConfig) -> Result<(Capture, S return Ok((cap, "pktap".to_string(), linktype.0)); } Err(e) => { - log::warn!( - "Failed to open PKTAP capture: {}, falling back to regular capture", - e - ); + log::warn!("Failed to open PKTAP capture: {}", e); + log::info!("PKTAP requires root privileges - run with 'sudo' for process metadata support"); + log::info!("Falling back to regular capture (process detection will use lsof)"); } } } Err(e) => { - log::warn!( - "Failed to create PKTAP device: {}, falling back to regular capture", - e - ); + log::warn!("Failed to create PKTAP device: {}", e); + log::info!("PKTAP requires root privileges - run with 'sudo' for process metadata support"); + log::info!("Falling back to regular capture (process detection will use lsof)"); } } } diff --git a/src/network/mod.rs b/src/network/mod.rs index 9f4a125..0984b25 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -5,5 +5,6 @@ pub mod parser; #[cfg(target_os = "macos")] pub mod pktap; pub mod platform; +pub mod privileges; pub mod services; pub mod types; diff --git a/src/network/platform/linux.rs b/src/network/platform/linux.rs index 12f0f55..2b1aac5 100644 --- a/src/network/platform/linux.rs +++ b/src/network/platform/linux.rs @@ -224,4 +224,8 @@ impl ProcessLookup for LinuxProcessLookup { Ok(()) } + + fn get_detection_method(&self) -> &str { + "procfs" + } } diff --git a/src/network/platform/linux_enhanced.rs b/src/network/platform/linux_enhanced.rs index 813c8b0..efe0fd5 100644 --- a/src/network/platform/linux_enhanced.rs +++ b/src/network/platform/linux_enhanced.rs @@ -306,6 +306,14 @@ mod ebpf_enhanced { debug!("Enhanced process lookup refreshed"); Ok(()) } + + fn get_detection_method(&self) -> &str { + if self.is_ebpf_available() { + "eBPF + procfs" + } else { + "procfs" + } + } } impl Clone for LookupStats { @@ -520,6 +528,14 @@ mod procfs_only { debug!("Enhanced process lookup refreshed"); Ok(()) } + + fn get_detection_method(&self) -> &str { + if self.is_ebpf_available() { + "eBPF + procfs" + } else { + "procfs" + } + } } impl Clone for LookupStats { diff --git a/src/network/platform/macos.rs b/src/network/platform/macos.rs index fecda10..5ef3cb5 100644 --- a/src/network/platform/macos.rs +++ b/src/network/platform/macos.rs @@ -184,6 +184,10 @@ impl ProcessLookup for MacOSProcessLookup { info!("Process lookup cache refreshed with {} entries", cache_size); Ok(()) } + + fn get_detection_method(&self) -> &str { + "lsof" + } } fn parse_lsof_connection_with_hint( diff --git a/src/network/platform/mod.rs b/src/network/platform/mod.rs index 5981bdc..37dfa62 100644 --- a/src/network/platform/mod.rs +++ b/src/network/platform/mod.rs @@ -35,6 +35,9 @@ pub trait ProcessLookup: Send + Sync { fn refresh(&self) -> Result<()> { Ok(()) // Default no-op } + + /// Get the detection method name for display purposes + fn get_detection_method(&self) -> &str; } /// No-op process lookup for when PKTAP is providing process metadata @@ -50,6 +53,10 @@ impl ProcessLookup for NoOpProcessLookup { fn refresh(&self) -> Result<()> { Ok(()) // Nothing to refresh } + + fn get_detection_method(&self) -> &str { + "pktap" + } } /// Create a platform-specific process lookup with PKTAP status awareness diff --git a/src/network/platform/windows.rs b/src/network/platform/windows.rs index a0d1b23..e3734d4 100644 --- a/src/network/platform/windows.rs +++ b/src/network/platform/windows.rs @@ -56,4 +56,8 @@ impl ProcessLookup for WindowsProcessLookup { *self.cache.write().unwrap() = new_cache; Ok(()) } + + fn get_detection_method(&self) -> &str { + "N/A" + } } diff --git a/src/network/privileges.rs b/src/network/privileges.rs new file mode 100644 index 0000000..d9582a6 --- /dev/null +++ b/src/network/privileges.rs @@ -0,0 +1,369 @@ +//! Network privilege detection for packet capture +//! +//! This module checks if the application has sufficient privileges to capture +//! network packets on different platforms (Linux, macOS, Windows). + +use anyhow::Result; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use anyhow::anyhow; +use log::{debug, info}; +#[cfg(any( + not(any(target_os = "linux", target_os = "macos", target_os = "windows")), + target_os = "linux", + target_os = "windows" +))] +use log::warn; + +/// Privilege check result with detailed information +#[derive(Debug, Clone)] +pub struct PrivilegeStatus { + /// Whether sufficient privileges are available + pub has_privileges: bool, + /// Missing capabilities or permissions + pub missing: Vec, + /// Platform-specific instructions to gain privileges + pub instructions: Vec, +} + +impl PrivilegeStatus { + /// Create a status indicating sufficient privileges + pub fn sufficient() -> Self { + Self { + has_privileges: true, + missing: Vec::new(), + instructions: Vec::new(), + } + } + + /// Create a status indicating insufficient privileges + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", test))] + pub fn insufficient(missing: Vec, instructions: Vec) -> Self { + Self { + has_privileges: false, + missing, + instructions, + } + } + + /// Get a human-readable error message + pub fn error_message(&self) -> String { + if self.has_privileges { + return String::new(); + } + + let mut msg = String::from("Insufficient privileges for network packet capture.\n\n"); + + if !self.missing.is_empty() { + msg.push_str("Missing:\n"); + for item in &self.missing { + msg.push_str(&format!(" • {}\n", item)); + } + msg.push('\n'); + } + + if !self.instructions.is_empty() { + msg.push_str("How to fix:\n"); + for (i, instruction) in self.instructions.iter().enumerate() { + msg.push_str(&format!(" {}. {}\n", i + 1, instruction)); + } + } + + msg + } +} + +/// Check if the current process has sufficient privileges for packet capture +pub fn check_packet_capture_privileges() -> Result { + #[cfg(target_os = "linux")] + { + check_linux_privileges() + } + + #[cfg(target_os = "macos")] + { + check_macos_privileges() + } + + #[cfg(target_os = "windows")] + { + check_windows_privileges() + } + + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + { + // Unknown platform - return optimistic result + warn!("Privilege check not implemented for this platform"); + Ok(PrivilegeStatus::sufficient()) + } +} + +#[cfg(target_os = "linux")] +fn check_linux_privileges() -> Result { + use std::fs; + + // Check if running as root by reading /proc/self/status + let is_root = is_root_user()?; + + if is_root { + info!("Running as root - all privileges available"); + return Ok(PrivilegeStatus::sufficient()); + } + + debug!("Not running as root, checking capabilities"); + + // Check for required capabilities via /proc/self/status + let status = fs::read_to_string("/proc/self/status") + .map_err(|e| anyhow!("Failed to read /proc/self/status: {}", e))?; + + // Parse CapEff (effective capabilities) line + let cap_value = status + .lines() + .find(|line| line.starts_with("CapEff:")) + .and_then(|line| line.split_whitespace().nth(1)) + .and_then(|cap_hex| u64::from_str_radix(cap_hex, 16).ok()) + .ok_or_else(|| anyhow!("Failed to parse effective capabilities"))?; + + debug!("Current effective capabilities: 0x{:x}", cap_value); + + // Required capabilities for packet capture + const CAP_NET_RAW: u64 = 13; // For packet capture + const CAP_NET_ADMIN: u64 = 12; // For network administration + + let mut missing = Vec::new(); + let mut has_net_raw = false; + let mut has_net_admin = false; + + // Check CAP_NET_RAW + if (cap_value & (1u64 << CAP_NET_RAW)) != 0 { + debug!("CAP_NET_RAW: present"); + has_net_raw = true; + } else { + debug!("CAP_NET_RAW: missing"); + missing.push("CAP_NET_RAW capability".to_string()); + } + + // Check CAP_NET_ADMIN + if (cap_value & (1u64 << CAP_NET_ADMIN)) != 0 { + debug!("CAP_NET_ADMIN: present"); + has_net_admin = true; + } else { + debug!("CAP_NET_ADMIN: missing"); + missing.push("CAP_NET_ADMIN capability".to_string()); + } + + // Need at least CAP_NET_RAW for basic packet capture + if has_net_raw { + if !has_net_admin { + warn!("CAP_NET_ADMIN missing - some features may not work"); + } + return Ok(PrivilegeStatus::sufficient()); + } + + // Build instructions for gaining privileges + let mut instructions = vec![ + "Run with sudo: sudo rustnet".to_string(), + "Set capabilities: sudo setcap cap_net_raw,cap_net_admin=eip $(which rustnet)".to_string(), + ]; + + // Add Docker-specific instructions if it looks like we're in a container + if is_running_in_container() { + instructions.push( + "If running in Docker, add these flags:\n \ + --cap-add=NET_RAW --cap-add=NET_ADMIN --cap-add=BPF --cap-add=PERFMON --cap-add=SYS_PTRACE \ + --net=host --pid=host" + .to_string(), + ); + } + + Ok(PrivilegeStatus::insufficient(missing, instructions)) +} + +/// Check if running as root user (UID 0) by reading /proc/self/status +#[cfg(target_os = "linux")] +fn is_root_user() -> Result { + use std::fs; + + let status = fs::read_to_string("/proc/self/status") + .map_err(|e| anyhow!("Failed to read /proc/self/status: {}", e))?; + + // Look for "Uid:" line which contains Real, Effective, Saved Set, and Filesystem UIDs + // Format: "Uid: 1000 1000 1000 1000" + let is_root = status + .lines() + .find(|line| line.starts_with("Uid:")) + .and_then(|line| { + // Get the effective UID (second field) + line.split_whitespace().nth(2) + }) + .and_then(|uid| uid.parse::().ok()) + .map(|uid| uid == 0) + .unwrap_or(false); + + Ok(is_root) +} + +/// Detect if running inside a container +#[cfg(target_os = "linux")] +fn is_running_in_container() -> bool { + use std::fs; + + // Check for .dockerenv file + if fs::metadata("/.dockerenv").is_ok() { + return true; + } + + // Check cgroup + if let Ok(cgroup) = fs::read_to_string("/proc/self/cgroup") + && (cgroup.contains("docker") || cgroup.contains("kubepods") || cgroup.contains("lxc")) + { + return true; + } + + false +} + +#[cfg(target_os = "macos")] +fn check_macos_privileges() -> Result { + use std::fs; + + // Check if running as root by reading effective UID from process + let is_root = is_root_user()?; + + if is_root { + info!("Running as root - all privileges available"); + return Ok(PrivilegeStatus::sufficient()); + } + + debug!("Not running as root, checking BPF device permissions"); + + // On macOS, packet capture requires access to BPF devices + // Try to open a BPF device to check permissions + let bpf_devices = (0..10) + .map(|i| format!("/dev/bpf{}", i)) + .collect::>(); + + let mut can_access_bpf = false; + for bpf_device in &bpf_devices { + if fs::metadata(bpf_device).is_ok() { + debug!("Checking BPF device: {}", bpf_device); + + // Try to actually open it (this is the real test) + if std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(bpf_device) + .is_ok() + { + can_access_bpf = true; + debug!("Successfully opened BPF device: {}", bpf_device); + break; + } + } + } + + if can_access_bpf { + return Ok(PrivilegeStatus::sufficient()); + } + + // No BPF access - build error message + let missing = vec!["Access to BPF devices (/dev/bpf*)".to_string()]; + + let instructions = vec![ + "Run with sudo: sudo rustnet".to_string(), + "Change BPF device permissions (temporary):\n \ + sudo chmod o+rw /dev/bpf*" + .to_string(), + "Install BPF permission helper (persistent):\n \ + brew install wireshark && sudo /usr/local/bin/install-bpf" + .to_string(), + ]; + + Ok(PrivilegeStatus::insufficient(missing, instructions)) +} + +/// Check if running as root user on macOS +#[cfg(target_os = "macos")] +fn is_root_user() -> Result { + use std::process::Command; + + // Use `id -u` command to get effective UID safely + let output = Command::new("id") + .arg("-u") + .output() + .map_err(|e| anyhow!("Failed to run 'id -u': {}", e))?; + + if !output.status.success() { + return Err(anyhow!("'id -u' command failed")); + } + + let uid_str = String::from_utf8_lossy(&output.stdout); + let uid = uid_str + .trim() + .parse::() + .map_err(|e| anyhow!("Failed to parse UID: {}", e))?; + + Ok(uid == 0) +} + +#[cfg(target_os = "windows")] +fn check_windows_privileges() -> Result { + use pcap::Device; + + debug!("Checking Windows privileges by attempting to list network interfaces"); + + // Try to list network devices - this will fail if we don't have sufficient privileges + match Device::list() { + Ok(devices) => { + info!( + "Successfully listed {} network devices - privileges sufficient", + devices.len() + ); + Ok(PrivilegeStatus::sufficient()) + } + Err(e) => { + debug!("Failed to list network devices: {}", e); + + // Check if the error indicates a permissions issue + let error_str = e.to_string().to_lowercase(); + if error_str.contains("access") || error_str.contains("denied") || error_str.contains("permission") { + let missing = vec!["Administrator privileges".to_string()]; + + let instructions = vec![ + "Run as Administrator: Right-click the terminal and select 'Run as Administrator'".to_string(), + "If using Npcap: Ensure it was installed with 'WinPcap API-compatible Mode' enabled".to_string(), + ]; + + Ok(PrivilegeStatus::insufficient(missing, instructions)) + } else { + // Some other error - assume it's not a privilege issue + warn!("Network device enumeration failed but error doesn't indicate privilege issue: {}", e); + Ok(PrivilegeStatus::sufficient()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_privilege_status_error_message() { + let status = PrivilegeStatus::insufficient( + vec!["CAP_NET_RAW".to_string()], + vec!["Run with sudo".to_string()], + ); + + let msg = status.error_message(); + assert!(msg.contains("Insufficient privileges")); + assert!(msg.contains("CAP_NET_RAW")); + assert!(msg.contains("Run with sudo")); + } + + #[test] + fn test_sufficient_privileges() { + let status = PrivilegeStatus::sufficient(); + assert!(status.has_privileges); + assert!(status.error_message().is_empty()); + } +} diff --git a/src/ui.rs b/src/ui.rs index 1083d56..7899d43 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -734,8 +734,11 @@ fn draw_stats_panel( .get_current_interface() .unwrap_or_else(|| "Unknown".to_string()); + let process_detection_method = app.get_process_detection_method(); + let conn_stats_text: Vec = vec![ Line::from(format!("Interface: {}", interface_name)), + Line::from(format!("Process Detection: {}", process_detection_method)), Line::from(""), Line::from(format!("TCP Connections: {}", tcp_count)), Line::from(format!("UDP Connections: {}", udp_count)),