mirror of
https://github.com/domcyrus/rustnet.git
synced 2026-01-05 13:29:55 -06:00
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:
@@ -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
|
||||
|
||||
86
README.md
86
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/<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)).
|
||||
|
||||
|
||||
59
src/app.rs
59
src/app.rs
@@ -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");
|
||||
|
||||
29
src/main.rs
29
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::<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;
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -224,4 +224,8 @@ impl ProcessLookup for LinuxProcessLookup {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_detection_method(&self) -> &str {
|
||||
"procfs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
369
src/network/privileges.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user