fix(linux): sync eBPF loading before sandbox drops capabilities

Race condition: sandbox dropped CAP_BPF/CAP_PERFMON before the
background thread finished loading eBPF, causing silent fallback
to procfs-only when running with setcap. Also harmonize setcap
syntax to +eip across docs and error messages.
This commit is contained in:
Marco Cadetg
2026-03-29 18:24:28 +02:00
parent fc30c17852
commit dafb888f52
7 changed files with 56 additions and 26 deletions
+12 -12
View File
@@ -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)
+1 -1
View File
@@ -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
```
+3 -3
View File
@@ -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.
+1 -1
View File
@@ -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
```
+19 -3
View File
@@ -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<std::sync::mpsc::Receiver<()>> {
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<DashMap<String, Connection>>,
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<AtomicBool>,
pktap_active: Arc<AtomicBool>,
process_detection_status: Arc<RwLock<ProcessDetectionStatus>>,
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
+17 -3
View File
@@ -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"))]
+3 -3
View File
@@ -162,9 +162,9 @@ fn check_linux_privileges() -> Result<PrivilegeStatus> {
// 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