diff --git a/INSTALL.md b/INSTALL.md index e7953da..9c0838b 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -107,7 +107,7 @@ sudo apt install rustnet sudo rustnet # Optional: Grant capabilities to run without sudo (modern kernel 5.8+) -sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' /usr/bin/rustnet +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/bin/rustnet rustnet ``` @@ -196,7 +196,7 @@ sudo dnf install rustnet sudo rustnet # Optional: Grant capabilities to run without sudo (modern kernel 5.8+) -sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' /usr/bin/rustnet +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/bin/rustnet rustnet ``` @@ -217,7 +217,7 @@ brew install rustnet brew install domcyrus/rustnet/rustnet # Grant capabilities to the Homebrew-installed binary (modern kernel 5.8+) -sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' $(brew --prefix)/bin/rustnet +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' $(brew --prefix)/bin/rustnet # Run without sudo rustnet @@ -239,7 +239,7 @@ tar xzf rustnet-vX.Y.Z-x86_64-unknown-linux-musl.tar.gz sudo mv rustnet-vX.Y.Z-x86_64-unknown-linux-musl/rustnet /usr/local/bin/ # Grant capabilities (modern kernel 5.8+) -sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' /usr/local/bin/rustnet +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/local/bin/rustnet # Run without sudo rustnet @@ -618,7 +618,7 @@ Grant specific network capabilities to the binary without full root privileges: cargo build --release # Grant capabilities to the binary (modern kernel 5.8+, with eBPF support) -sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' ./target/release/rustnet +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' ./target/release/rustnet # Now run without sudo ./target/release/rustnet @@ -628,7 +628,7 @@ sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' ./target/release/rustnet ```bash # If installed via cargo install rustnet-monitor (modern kernel 5.8+) -sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' ~/.cargo/bin/rustnet +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' ~/.cargo/bin/rustnet # Now run without sudo rustnet @@ -643,11 +643,11 @@ eBPF is enabled by default on Linux and provides lower-overhead process identifi cargo build --release # Modern Linux (5.8+) - works with just these three capabilities: -sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' ./target/release/rustnet +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' ./target/release/rustnet ./target/release/rustnet # Legacy Linux (older kernels without CAP_BPF) - use CAP_SYS_ADMIN as fallback: -sudo setcap 'cap_net_raw,cap_sys_admin=eip' ./target/release/rustnet +sudo setcap 'cap_net_raw,cap_sys_admin+eip' ./target/release/rustnet ./target/release/rustnet # Check TUI Statistics panel - should show "Process Detection: eBPF + procfs" @@ -679,7 +679,7 @@ sudo setcap 'cap_net_raw,cap_sys_admin=eip' ./target/release/rustnet ```bash # If installed via package manager or copied to /usr/local/bin (modern kernel 5.8+) -sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' /usr/local/bin/rustnet +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/local/bin/rustnet rustnet ``` @@ -721,8 +721,8 @@ getcap ~/.cargo/bin/rustnet # For system-wide installations: getcap $(which rustnet) -# Modern (5.8+): Should show cap_net_raw,cap_bpf,cap_perfmon=eip -# Legacy: Should show cap_net_raw,cap_sys_admin=eip +# Modern (5.8+): Should show cap_net_raw,cap_bpf,cap_perfmon+eip +# Legacy: Should show cap_net_raw,cap_sys_admin+eip # Test without sudo rustnet --help @@ -852,7 +852,7 @@ RustNet auto-discovers databases from standard locations. Run `rustnet --help` t #### Operation Not Permitted (with capabilities set) - Capabilities may have been removed by system updates -- Re-apply capabilities (modern): `sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' $(which rustnet)` +- Re-apply capabilities (modern): `sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' $(which rustnet)` - Some filesystems don't support extended attributes (capabilities) - Try copying the binary to a different filesystem (e.g., from NFS to local disk) diff --git a/README.md b/README.md index e004551..8080ee9 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ Packet capture requires elevated privileges: sudo rustnet # Linux: Grant capabilities to run without sudo (recommended) -sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' $(which rustnet) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' $(which rustnet) rustnet ``` diff --git a/SECURITY.md b/SECURITY.md index f482281..f29847e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -134,13 +134,13 @@ Instead of running as root, grant only the required capabilities: ```bash # Modern Linux (5.8+): packet capture + eBPF -sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' $(which rustnet) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' $(which rustnet) # Legacy Linux (pre-5.8): packet capture + eBPF -sudo setcap 'cap_net_raw,cap_sys_admin=eip' $(which rustnet) +sudo setcap 'cap_net_raw,cap_sys_admin+eip' $(which rustnet) # Packet capture only (no eBPF process detection) -sudo setcap cap_net_raw=eip $(which rustnet) +sudo setcap cap_net_raw+eip $(which rustnet) ``` After sandbox application, `CAP_NET_RAW` is dropped - the process retains only the minimum privileges needed. diff --git a/USAGE.md b/USAGE.md index dbb72b4..0e62236 100644 --- a/USAGE.md +++ b/USAGE.md @@ -28,7 +28,7 @@ sudo rustnet # Or grant capabilities to run without sudo (see INSTALL.md for details) # Linux example (modern kernel 5.8+): -sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' /path/to/rustnet +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /path/to/rustnet rustnet ``` diff --git a/src/app.rs b/src/app.rs index 9f8b8c2..9487ed5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -570,7 +570,11 @@ impl App { } /// Start all background threads - pub fn start(&mut self) -> Result<()> { + /// + /// Returns a receiver that signals when process detection initialization is complete + /// (including eBPF loading). The caller should wait on this before dropping + /// eBPF capabilities to avoid a race condition. + pub fn start(&mut self) -> Result> { info!("Starting network monitor application"); // Use stored connection map @@ -579,8 +583,11 @@ impl App { // Start packet capture pipeline self.start_packet_capture_pipeline(connections.clone())?; + // Create channel to signal when process detection (incl. eBPF) is ready + let (process_ready_tx, process_ready_rx) = std::sync::mpsc::sync_channel(1); + // Start process enrichment thread (but delay for PKTAP detection on macOS) - self.start_process_enrichment_conditional(connections.clone())?; + self.start_process_enrichment_conditional(connections.clone(), process_ready_tx)?; // Start GeoIP enrichment thread self.start_geoip_enrichment_thread(connections.clone())?; @@ -607,7 +614,7 @@ impl App { is_loading.store(false, Ordering::Relaxed); }); - Ok(()) + Ok(process_ready_rx) } /// Start packet capture and processing pipeline @@ -946,6 +953,7 @@ impl App { fn start_process_enrichment_conditional( &self, connections: Arc>, + process_ready_tx: std::sync::mpsc::SyncSender<()>, ) -> Result<()> { let pktap_active = Arc::clone(&self.pktap_active); let should_stop = Arc::clone(&self.should_stop); @@ -967,6 +975,7 @@ impl App { if let Ok(mut status) = process_detection_status.write() { *status = ProcessDetectionStatus::with_method("pktap"); } + let _ = process_ready_tx.send(()); return; } // Check more frequently for faster detection @@ -981,6 +990,7 @@ impl App { if let Ok(mut status) = process_detection_status.write() { *status = ProcessDetectionStatus::with_method("pktap"); } + let _ = process_ready_tx.send(()); return; } else { info!( @@ -998,6 +1008,7 @@ impl App { should_stop, pktap_active, process_detection_status, + process_ready_tx, ) { error!("Process enrichment thread failed: {}", e); } @@ -1012,6 +1023,7 @@ impl App { should_stop: Arc, pktap_active: Arc, process_detection_status: Arc>, + process_ready_tx: std::sync::mpsc::SyncSender<()>, ) -> Result<()> { use crate::network::platform::DegradationReason; @@ -1019,6 +1031,10 @@ impl App { let use_pktap = pktap_active.load(Ordering::Relaxed); let process_lookup = create_process_lookup(use_pktap)?; + + // Signal that process detection (including eBPF loading) is complete. + // The main thread waits for this before dropping eBPF capabilities. + let _ = process_ready_tx.send(()); let interval = Duration::from_secs(2); // Use default interval // Build and set the detection status from the process lookup implementation diff --git a/src/main.rs b/src/main.rs index e705de6..9dd90ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -127,7 +127,7 @@ fn main() -> Result<()> { // Create and start the application let mut app = app::App::new(config.clone())?; - app.start()?; + let process_ready_rx = app.start()?; info!("Application started"); // Pre-create sidecar JSONL file for PCAP export (needed for Landlock permissions) @@ -146,9 +146,23 @@ fn main() -> Result<()> { } } + // Wait for process detection (including eBPF loading) to complete before + // applying the sandbox, which drops CAP_BPF and CAP_PERFMON. + // Without this synchronization, the sandbox could drop these capabilities + // before the background thread has finished loading eBPF programs. + match process_ready_rx.recv_timeout(std::time::Duration::from_secs(10)) { + Ok(()) => info!("Process detection initialized, safe to apply sandbox"), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + warn!("Timed out waiting for process detection init, applying sandbox anyway"); + } + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => { + warn!("Process detection thread exited early, applying sandbox anyway"); + } + } + // Apply Landlock sandbox (Linux only) - // This must be done AFTER app.start() because: - // - eBPF programs need to be loaded first (access to /sys/kernel/btf) + // This must be done AFTER process detection is initialized because: + // - eBPF programs need to be loaded first (requires CAP_BPF + CAP_PERFMON) // - Packet capture handles need to be opened first (access to /dev) // - Log files need to be created first #[cfg(all(target_os = "linux", feature = "landlock"))] diff --git a/src/network/privileges.rs b/src/network/privileges.rs index 28d7117..9f94579 100644 --- a/src/network/privileges.rs +++ b/src/network/privileges.rs @@ -162,9 +162,9 @@ fn check_linux_privileges() -> Result { // Build instructions for gaining privileges let mut instructions = vec![ "Run with sudo: sudo rustnet".to_string(), - "Set capabilities (modern Linux 5.8+, with eBPF): sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' $(which rustnet)".to_string(), - "Set capabilities (legacy/older kernels, with eBPF): sudo setcap 'cap_net_raw,cap_sys_admin=eip' $(which rustnet)".to_string(), - "Set capabilities (packet capture only, no eBPF): sudo setcap 'cap_net_raw=eip' $(which rustnet)".to_string(), + "Set capabilities (modern Linux 5.8+, with eBPF): sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' $(which rustnet)".to_string(), + "Set capabilities (legacy/older kernels, with eBPF): sudo setcap 'cap_net_raw,cap_sys_admin+eip' $(which rustnet)".to_string(), + "Set capabilities (packet capture only, no eBPF): sudo setcap 'cap_net_raw+eip' $(which rustnet)".to_string(), ]; // Add Docker-specific instructions if it looks like we're in a container