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
This commit is contained in:
Marco Cadetg
2025-10-04 15:33:42 +02:00
committed by GitHub
parent c832718833
commit 80b5b0c2c1
13 changed files with 548 additions and 62 deletions

View File

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

View File

@@ -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/<pid>/fd/` to find socket inodes
- **Linux**: Parses `/proc/net/tcp`, `/proc/net/udp`, and `/proc/<pid>/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)).

View File

@@ -102,6 +102,9 @@ pub struct App {
/// Whether PKTAP is active (macOS only) - used to disable process enrichment
pktap_active: Arc<AtomicBool>,
/// Current process detection method (e.g., "eBPF + procfs", "pktap", "lsof", "N/A")
process_detection_method: Arc<RwLock<String>>,
}
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<DashMap<String, Connection>>,
should_stop: Arc<AtomicBool>,
pktap_active: Arc<AtomicBool>,
process_detection_method: Arc<RwLock<String>>,
) -> 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");

View File

@@ -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::<String>("log-level") {
let log_level = log_level_str
@@ -528,6 +531,32 @@ fn run_ui_loop<B: ratatui::prelude::Backend>(
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;

View File

@@ -34,7 +34,8 @@ impl Default for CaptureConfig {
/// Find the best active network device
fn find_best_device() -> Result<Device> {
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<Active>, 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)");
}
}
}

View File

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

View File

@@ -224,4 +224,8 @@ impl ProcessLookup for LinuxProcessLookup {
Ok(())
}
fn get_detection_method(&self) -> &str {
"procfs"
}
}

View File

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

View File

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

View File

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

View File

@@ -56,4 +56,8 @@ impl ProcessLookup for WindowsProcessLookup {
*self.cache.write().unwrap() = new_cache;
Ok(())
}
fn get_detection_method(&self) -> &str {
"N/A"
}
}

369
src/network/privileges.rs Normal file
View File

@@ -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<String>,
/// Platform-specific instructions to gain privileges
pub instructions: Vec<String>,
}
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<String>, instructions: Vec<String>) -> 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<PrivilegeStatus> {
#[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<PrivilegeStatus> {
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<bool> {
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::<u32>().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<PrivilegeStatus> {
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::<Vec<_>>();
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<bool> {
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::<u32>()
.map_err(|e| anyhow!("Failed to parse UID: {}", e))?;
Ok(uid == 0)
}
#[cfg(target_os = "windows")]
fn check_windows_privileges() -> Result<PrivilegeStatus> {
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());
}
}

View File

@@ -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<Line> = 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)),