From 85b2662c85cd4ed0ee359b5a76382c9652abcc18 Mon Sep 17 00:00:00 2001 From: Marco Cadetg Date: Sun, 2 Nov 2025 19:47:26 +0100 Subject: [PATCH] feat: add freebsd (#71) * feat: add freebsd --- .github/workflows/release.yml | 4 + Cargo.toml | 4 + INSTALL.md | 96 ++++++++++ README.md | 2 +- ROADMAP.md | 9 +- build.rs | 5 + src/app.rs | 29 ++- src/network/platform/freebsd.rs | 329 ++++++++++++++++++++++++++++++++ src/network/platform/mod.rs | 23 ++- src/network/privileges.rs | 113 ++++++++++- tests/integration_tests.rs | 6 +- 11 files changed, 597 insertions(+), 23 deletions(-) create mode 100644 src/network/platform/freebsd.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8031e40..68c3095 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,7 @@ jobs: - linux-aarch64-gnu - linux-armv7-gnueabihf - linux-x64-gnu + - freebsd-x64 - macos-aarch64 - macos-x64 - windows-x64-msvc @@ -54,6 +55,9 @@ jobs: cargo: cross - build: linux-x64-gnu target: x86_64-unknown-linux-gnu + - build: freebsd-x64 + target: x86_64-unknown-freebsd + cargo: cross - build: macos-aarch64 os: macos-14 target: aarch64-apple-darwin diff --git a/Cargo.toml b/Cargo.toml index e4fb2fd..badd19e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,10 @@ windows = { version = "0.62", features = [ "Win32_System_Threading", ] } +# FreeBSD support uses the system's sockstat command for process lookup +# and native libpcap (via pcap crate) for packet capture. +# No additional FreeBSD-specific dependencies required at this time. + [build-dependencies] anyhow = "1.0" clap = { version = "4.5", features = ["derive"] } diff --git a/INSTALL.md b/INSTALL.md index c41dae3..578868d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -8,6 +8,7 @@ This guide covers all installation methods for RustNet across different platform - [macOS DMG Installation](#macos-dmg-installation) - [Windows MSI Installation](#windows-msi-installation) - [Linux Package Installation](#linux-package-installation) + - [FreeBSD Installation](#freebsd-installation) - [Install via Cargo](#install-via-cargo) - [Building from Source](#building-from-source) - [Using Docker](#using-docker) @@ -203,6 +204,100 @@ sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon=eip' $(brew --prefix)/bin/rustnet rustnet ``` +### FreeBSD Installation + +FreeBSD support is available starting from version 0.15.0. + +#### From Ports or Packages (Future) + +Once available in FreeBSD ports: +```bash +# Using pkg (binary packages) +pkg install rustnet + +# Or build from ports +cd /usr/ports/net/rustnet && make install clean +``` + +#### From GitHub Releases + +Download the FreeBSD binary from [GitHub Releases](https://github.com/domcyrus/rustnet/releases): + +```bash +# Download the appropriate package +fetch https://github.com/domcyrus/rustnet/releases/download/vX.Y.Z/rustnet-vX.Y.Z-x86_64-unknown-freebsd.tar.gz + +# Extract the archive +tar xzf rustnet-vX.Y.Z-x86_64-unknown-freebsd.tar.gz + +# Move binary to PATH +sudo mv rustnet-vX.Y.Z-x86_64-unknown-freebsd/rustnet /usr/local/bin/ + +# Make it executable +sudo chmod +x /usr/local/bin/rustnet + +# Run with sudo +sudo rustnet +``` + +#### Building from Source on FreeBSD + +```bash +# Install dependencies +pkg install rust libpcap + +# Clone the repository +git clone https://github.com/domcyrus/rustnet.git +cd rustnet + +# Build in release mode +cargo build --release + +# The executable will be in target/release/rustnet +sudo ./target/release/rustnet +``` + +#### Permission Setup for FreeBSD + +FreeBSD requires access to BPF (Berkeley Packet Filter) devices for packet capture. + +**Option 1: Run with sudo (Simplest)** +```bash +sudo rustnet +``` + +**Option 2: Add user to the bpf group (Recommended)** +```bash +# Add your user to the bpf group +sudo pw groupmod bpf -m $(whoami) + +# Log out and back in for group changes to take effect + +# Now run without sudo +rustnet +``` + +**Option 3: Change BPF device permissions (Temporary)** +```bash +# This will reset on reboot +sudo chmod o+rw /dev/bpf* + +# Now run without sudo +rustnet +``` + +**Verifying FreeBSD Permissions:** +```bash +# Check if you're in the bpf group +groups | grep bpf + +# Check BPF device permissions +ls -la /dev/bpf* + +# Test without sudo +rustnet --help +``` + ## Install via Cargo ```bash @@ -223,6 +318,7 @@ After installation, see the [Permissions Setup](#permissions-setup) section to c - libpcap or similar packet capture library: - **Linux**: `sudo apt-get install libpcap-dev` (Debian/Ubuntu) or `sudo yum install libpcap-devel` (RedHat/CentOS) - **macOS**: Included by default + - **FreeBSD**: `pkg install libpcap` (included in base system, but headers needed for building) - **Windows**: Install Npcap and Npcap SDK (see [Windows Build Setup](#windows-build-setup) below) - **For eBPF support (enabled by default on Linux)**: - `sudo apt-get install libelf-dev clang llvm` (Debian/Ubuntu) diff --git a/README.md b/README.md index 35acf2e..c927289 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ A cross-platform network monitoring tool built with Rust. RustNet provides real- - **Smart Connection Lifecycle**: Protocol-aware timeouts with visual staleness indicators (white → yellow → red) before cleanup - **Process Identification**: Associate network connections with running processes - **Service Name Resolution**: Identify well-known services using port numbers -- **Cross-platform Support**: Works on Linux, macOS, Windows and potentially BSD systems +- **Cross-platform Support**: Works on Linux, macOS, Windows, and FreeBSD - **Advanced Filtering**: Real-time vim/fzf-style filtering with keyword support (`port:`, `src:`, `dst:`, `sni:`, `process:`, `state:`) - **Terminal User Interface**: Beautiful TUI built with ratatui with adjustable column widths - **Multi-threaded Processing**: Concurrent packet processing for high performance diff --git a/ROADMAP.md b/ROADMAP.md index 88989e5..0a85a69 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -14,7 +14,12 @@ This document outlines the planned features and improvements for RustNet. - Npcap SDK and runtime integration - MSI installation packages for 64-bit and 32-bit - Process identification via Windows IP Helper API (GetExtendedTcpTable/GetExtendedUdpTable) -- [ ] **BSD Support**: Add support for FreeBSD, OpenBSD, and NetBSD +- [x] **FreeBSD Support**: Full support including: + - Process identification via `sockstat` command parsing + - BPF device access and permissions setup + - Native libpcap packet capture + - Cross-compilation support from Linux +- [ ] **OpenBSD and NetBSD Support**: Future platforms to support - [x] **Linux Process Identification**: **Experimental eBPF Support Implemented** - Basic eBPF-based process identification now available with `--features ebpf`. Provides efficient kernel-level process-to-connection mapping with lower overhead than procfs. Currently has limitations (see eBPF Improvements section below). ## eBPF Improvements (Linux) @@ -60,7 +65,7 @@ The experimental eBPF support provides efficient process identification but has - [x] **Connection Lifecycle Management**: Smart protocol-aware timeouts with visual staleness indicators (yellow at 75%, red at 90%) - [x] **Process Identification**: Associate network connections with running processes (with experimental eBPF support on Linux) - [x] **Service Name Resolution**: Identify well-known services using port numbers -- [x] **Cross-platform Support**: Works on Linux, macOS, Windows +- [x] **Cross-platform Support**: Works on Linux, macOS, Windows, and FreeBSD - [ ] **DNS Reverse Lookup**: Add optional hostname resolution (toggle between IP and hostname display) - [ ] **IPv6 Support**: Full IPv6 connection tracking and display, including DNS resolution (needs testing) diff --git a/build.rs b/build.rs index 540e219..3a4b169 100644 --- a/build.rs +++ b/build.rs @@ -39,6 +39,11 @@ fn setup_cross_compilation_libs() { println!("cargo:rustc-link-lib=elf"); println!("cargo:rustc-link-lib=z"); } + "x86_64-unknown-freebsd" => { + // FreeBSD uses libpcap from base system (in /usr/lib) + // When cross-compiling, the sysroot should provide these + println!("cargo:rustc-link-lib=pcap"); + } _ => { // For other targets, including native builds, let pkg-config handle it } diff --git a/src/app.rs b/src/app.rs index 71ccd5a..3ef539c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -17,7 +17,7 @@ use crate::network::{ capture::{CaptureConfig, PacketReader, setup_packet_capture}, merge::{create_connection_from_packet, merge_packet_into_connection}, parser::{PacketParser, ParsedPacket, ParserConfig}, - platform::create_process_lookup_with_pktap_status, + platform::create_process_lookup, services::ServiceLookup, types::{ApplicationProtocol, Connection, Protocol}, }; @@ -31,7 +31,12 @@ static QUIC_CONNECTION_MAPPING: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); /// Helper function to log connection events as JSON -fn log_connection_event(json_log_path: &str, event_type: &str, conn: &Connection, duration_secs: Option) { +fn log_connection_event( + json_log_path: &str, + event_type: &str, + conn: &Connection, + duration_secs: Option, +) { // Build JSON object based on event type let mut event = json!({ "timestamp": chrono::Utc::now().to_rfc3339(), @@ -517,7 +522,12 @@ impl App { } // Start the actual process enrichment - if let Err(e) = Self::run_process_enrichment(connections, should_stop, pktap_active, process_detection_method) { + if let Err(e) = Self::run_process_enrichment( + connections, + should_stop, + pktap_active, + process_detection_method, + ) { error!("Process enrichment thread failed: {}", e); } }); @@ -533,9 +543,9 @@ impl App { process_detection_method: Arc>, ) -> Result<()> { // Check PKTAP status before creating process lookup - let is_pktap = pktap_active.load(Ordering::Relaxed); + let use_pktap = pktap_active.load(Ordering::Relaxed); - let process_lookup = create_process_lookup_with_pktap_status(is_pktap)?; + let process_lookup = create_process_lookup(use_pktap)?; let interval = Duration::from_secs(2); // Use default interval // Get and set the detection method from the process lookup implementation @@ -546,8 +556,10 @@ impl App { *method = process_lookup.get_detection_method().to_string(); } - info!("Process enrichment thread started with detection method: {}", - process_lookup.get_detection_method()); + info!( + "Process enrichment thread started with detection method: {}", + process_lookup.get_detection_method() + ); let mut last_refresh = Instant::now(); loop { @@ -890,7 +902,8 @@ impl App { && let Some(dlt) = *linktype_opt { // Get interface name to detect TUN/TAP more accurately - let interface_name = self.current_interface + let interface_name = self + .current_interface .read() .ok() .and_then(|opt| opt.clone()) diff --git a/src/network/platform/freebsd.rs b/src/network/platform/freebsd.rs new file mode 100644 index 0000000..444e453 --- /dev/null +++ b/src/network/platform/freebsd.rs @@ -0,0 +1,329 @@ +// network/platform/freebsd.rs - FreeBSD process lookup +use super::{ConnectionKey, ProcessLookup}; +use crate::network::types::{Connection, Protocol}; +use anyhow::{Context, Result}; +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::sync::RwLock; +use std::time::{Duration, Instant}; + +pub struct FreeBSDProcessLookup { + // Cache: ConnectionKey -> (pid, process_name) + cache: RwLock, +} + +struct ProcessCache { + lookup: HashMap, + last_refresh: Instant, +} + +impl FreeBSDProcessLookup { + pub fn new() -> Result { + Ok(Self { + cache: RwLock::new(ProcessCache { + lookup: HashMap::new(), + last_refresh: Instant::now() - Duration::from_secs(3600), + }), + }) + } + + /// Build connection -> process mapping using sysctl + fn build_process_map() -> Result> { + let mut process_map = HashMap::new(); + + // Parse TCP connections + if let Ok(tcp_connections) = Self::parse_sockstat_output("tcp") { + process_map.extend(tcp_connections); + } + + // Parse TCP6 connections + if let Ok(tcp6_connections) = Self::parse_sockstat_output("tcp6") { + process_map.extend(tcp6_connections); + } + + // Parse UDP connections + if let Ok(udp_connections) = Self::parse_sockstat_output("udp") { + process_map.extend(udp_connections); + } + + // Parse UDP6 connections + if let Ok(udp6_connections) = Self::parse_sockstat_output("udp6") { + process_map.extend(udp6_connections); + } + + Ok(process_map) + } + + /// Parse sockstat output for a given protocol + /// Format: user command pid fd proto local_addr foreign_addr + fn parse_sockstat_output(proto: &str) -> Result> { + use std::process::Command; + + let mut result = HashMap::new(); + + // Determine protocol type + let protocol = if proto.starts_with("tcp") { + Protocol::TCP + } else { + Protocol::UDP + }; + + // Run sockstat command + // -4: IPv4, -6: IPv6, -c: connected sockets, -l: listening sockets, -n: numeric + let ipv6_flag = proto.ends_with('6'); + + let output = Command::new("sockstat") + .arg(if ipv6_flag { "-6" } else { "-4" }) + .arg("-n") // numeric output + .arg("-P") + .arg(if proto.starts_with("tcp") { "tcp" } else { "udp" }) + .output() + .context("Failed to execute sockstat")?; + + if !output.status.success() { + return Ok(result); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + + for line in stdout.lines().skip(1) { + // Skip header + let parts: Vec<&str> = line.split_whitespace().collect(); + + // Expected format: + // USER COMMAND PID FD PROTO LOCAL ADDRESS FOREIGN ADDRESS + // root sshd 1234 3 tcp4 192.168.1.1:22 192.168.1.2:54321 + + if parts.len() < 7 { + continue; + } + + // Extract fields + let process_name = parts[1].to_string(); + let pid = match parts[2].parse::() { + Ok(p) => p, + Err(_) => continue, + }; + + // Parse local address (index 5) + let local_addr = match Self::parse_address(parts[5]) { + Some(addr) => addr, + None => continue, + }; + + // Parse foreign address (index 6) + let foreign_addr = match Self::parse_address(parts[6]) { + Some(addr) => addr, + None => continue, + }; + + let key = ConnectionKey { + protocol, + local_addr, + remote_addr: foreign_addr, + }; + + result.insert(key, (pid, process_name)); + } + + Ok(result) + } + + /// Parse address in format "ip:port", "*:port", or "[ipv6]:port" + fn parse_address(addr_str: &str) -> Option { + // Handle wildcard addresses + if addr_str.starts_with("*:") { + let port = addr_str.strip_prefix("*:")?.parse::().ok()?; + // Use unspecified address for wildcards + return Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port)); + } + + // Handle IPv6 with brackets: [::1]:8080 + if addr_str.starts_with('[') { + let closing_bracket = addr_str.find(']')?; + let ip_str = &addr_str[1..closing_bracket]; + let port_str = addr_str.get(closing_bracket + 2..)?; // Skip "]:" + let port = port_str.parse::().ok()?; + let ip = IpAddr::V6(ip_str.parse().ok()?); + return Some(SocketAddr::new(ip, port)); + } + + // Split by last colon to handle addresses + let last_colon = addr_str.rfind(':')?; + let (ip_str, port_str) = addr_str.split_at(last_colon); + let port_str = &port_str[1..]; // Remove the colon + + let port = port_str.parse::().ok()?; + + // Detect IPv6 (contains colons) vs IPv4 + let ip = if ip_str.contains(':') { + // IPv6 address without brackets (e.g., "::1" or "fe80::1") + IpAddr::V6(ip_str.parse().ok()?) + } else { + // IPv4 address + IpAddr::V4(ip_str.parse().ok()?) + }; + + Some(SocketAddr::new(ip, port)) + } +} + +impl ProcessLookup for FreeBSDProcessLookup { + fn get_process_for_connection(&self, conn: &Connection) -> Option<(u32, String)> { + let key = ConnectionKey::from_connection(conn); + + // Simple cache lookup with no refresh on cache miss. + // The enrichment thread handles periodic refresh. + let cache = self.cache.read().unwrap(); + cache.lookup.get(&key).cloned() + } + + fn refresh(&self) -> Result<()> { + let process_map = Self::build_process_map()?; + + let mut cache = self.cache.write().unwrap(); + cache.lookup = process_map; + cache.last_refresh = Instant::now(); + + Ok(()) + } + + fn get_detection_method(&self) -> &str { + "sockstat" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + + #[test] + fn test_parse_ipv4_address() { + let addr = FreeBSDProcessLookup::parse_address("192.168.1.1:8080"); + assert_eq!( + addr, + Some(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), + 8080 + )) + ); + } + + #[test] + fn test_parse_ipv4_loopback() { + let addr = FreeBSDProcessLookup::parse_address("127.0.0.1:80"); + assert_eq!( + addr, + Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 80)) + ); + } + + #[test] + fn test_parse_ipv6_with_brackets() { + let addr = FreeBSDProcessLookup::parse_address("[::1]:8080"); + assert_eq!( + addr, + Some(SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8080)) + ); + } + + #[test] + fn test_parse_ipv6_full_address_with_brackets() { + let addr = FreeBSDProcessLookup::parse_address("[2001:db8::1]:443"); + assert_eq!( + addr, + Some(SocketAddr::new( + IpAddr::V6("2001:db8::1".parse().unwrap()), + 443 + )) + ); + } + + #[test] + fn test_parse_ipv6_link_local_with_brackets() { + let addr = FreeBSDProcessLookup::parse_address("[fe80::1]:22"); + assert_eq!( + addr, + Some(SocketAddr::new( + IpAddr::V6("fe80::1".parse().unwrap()), + 22 + )) + ); + } + + #[test] + fn test_parse_ipv6_without_brackets() { + // This may occur in some sockstat outputs + let addr = FreeBSDProcessLookup::parse_address("::1:8080"); + // This should parse as IPv6 ::1 with port 8080 + // Note: This is ambiguous, but our logic treats multiple colons as IPv6 + assert!(addr.is_some()); + if let Some(socket_addr) = addr { + assert_eq!(socket_addr.port(), 8080); + } + } + + #[test] + fn test_parse_wildcard_address() { + let addr = FreeBSDProcessLookup::parse_address("*:80"); + assert_eq!( + addr, + Some(SocketAddr::new( + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + 80 + )) + ); + } + + #[test] + fn test_parse_wildcard_high_port() { + let addr = FreeBSDProcessLookup::parse_address("*:65535"); + assert_eq!( + addr, + Some(SocketAddr::new( + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + 65535 + )) + ); + } + + #[test] + fn test_parse_invalid_address() { + // Missing port + assert_eq!(FreeBSDProcessLookup::parse_address("192.168.1.1"), None); + } + + #[test] + fn test_parse_invalid_ipv6_brackets() { + // Missing closing bracket + assert_eq!(FreeBSDProcessLookup::parse_address("[::1:8080"), None); + } + + #[test] + fn test_parse_invalid_port() { + // Port out of range + assert_eq!( + FreeBSDProcessLookup::parse_address("192.168.1.1:99999"), + None + ); + } + + #[test] + fn test_parse_empty_string() { + assert_eq!(FreeBSDProcessLookup::parse_address(""), None); + } + + #[test] + fn test_parse_ipv4_mapped_ipv6() { + // IPv4-mapped IPv6 address + let addr = FreeBSDProcessLookup::parse_address("[::ffff:192.168.1.1]:80"); + assert_eq!( + addr, + Some(SocketAddr::new( + IpAddr::V6("::ffff:192.168.1.1".parse().unwrap()), + 80 + )) + ); + } +} diff --git a/src/network/platform/mod.rs b/src/network/platform/mod.rs index f91f0a5..c42170d 100644 --- a/src/network/platform/mod.rs +++ b/src/network/platform/mod.rs @@ -4,6 +4,8 @@ use anyhow::Result; use std::net::SocketAddr; // Platform-specific modules +#[cfg(target_os = "freebsd")] +mod freebsd; #[cfg(target_os = "linux")] mod linux; #[cfg(all(target_os = "linux", feature = "ebpf"))] @@ -16,6 +18,8 @@ mod macos; mod windows; // Re-export the appropriate implementation +#[cfg(target_os = "freebsd")] +pub use freebsd::FreeBSDProcessLookup; #[cfg(target_os = "linux")] pub use linux::LinuxProcessLookup; #[cfg(target_os = "linux")] @@ -60,14 +64,12 @@ impl ProcessLookup for NoOpProcessLookup { } /// Create a platform-specific process lookup with PKTAP status awareness -pub fn create_process_lookup_with_pktap_status( - _pktap_active: bool, -) -> Result> { +pub fn create_process_lookup(_use_pktap: bool) -> Result> { #[cfg(target_os = "macos")] { use crate::network::platform::macos::MacOSProcessLookup; - if _pktap_active { + if _use_pktap { log::info!("Using no-op process lookup - PKTAP provides process metadata"); Ok(Box::new(NoOpProcessLookup)) } else { @@ -104,7 +106,18 @@ pub fn create_process_lookup_with_pktap_status( Ok(Box::new(WindowsProcessLookup::new()?)) } - #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))] + #[cfg(target_os = "freebsd")] + { + log::info!("Using FreeBSD process lookup (sockstat)"); + Ok(Box::new(FreeBSDProcessLookup::new()?)) + } + + #[cfg(not(any( + target_os = "linux", + target_os = "windows", + target_os = "macos", + target_os = "freebsd" + )))] { Err(anyhow::anyhow!("Unsupported platform")) } diff --git a/src/network/privileges.rs b/src/network/privileges.rs index b6f1f5b..1927642 100644 --- a/src/network/privileges.rs +++ b/src/network/privileges.rs @@ -4,11 +4,16 @@ //! network packets on different platforms (Linux, macOS, Windows). use anyhow::Result; -#[cfg(any(target_os = "linux", target_os = "macos"))] +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] use anyhow::anyhow; use log::{debug, info}; #[cfg(any( - not(any(target_os = "linux", target_os = "macos", target_os = "windows")), + not(any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "freebsd" + )), target_os = "windows" ))] use log::warn; @@ -35,7 +40,13 @@ impl PrivilegeStatus { } /// Create a status indicating insufficient privileges - #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", test))] + #[cfg(any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "freebsd", + test + ))] pub fn insufficient(missing: Vec, instructions: Vec) -> Self { Self { has_privileges: false, @@ -88,7 +99,17 @@ pub fn check_packet_capture_privileges() -> Result { check_windows_privileges() } - #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + #[cfg(target_os = "freebsd")] + { + check_freebsd_privileges() + } + + #[cfg(not(any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "freebsd" + )))] { // Unknown platform - return optimistic result warn!("Privilege check not implemented for this platform"); @@ -330,6 +351,90 @@ fn check_windows_privileges() -> Result { } } +#[cfg(target_os = "freebsd")] +fn check_freebsd_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 FreeBSD, 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(), + "Add your user to the bpf group:\n \ + sudo pw groupmod bpf -m $(whoami)\n \ + Then logout and login again" + .to_string(), + "Change BPF device permissions (temporary):\n \ + sudo chmod o+rw /dev/bpf*" + .to_string(), + ]; + + Ok(PrivilegeStatus::insufficient(missing, instructions)) +} + +/// Check if running as root user on FreeBSD +#[cfg(target_os = "freebsd")] +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(test)] mod tests { use super::*; diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index e3de2df..d44fc78 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2,7 +2,7 @@ #[cfg(target_os = "linux")] mod linux_tests { - use rustnet_monitor::network::platform::create_process_lookup_with_pktap_status; + use rustnet_monitor::network::platform::create_process_lookup; #[test] fn test_process_lookup_creation() { @@ -31,12 +31,12 @@ mod linux_tests { #[cfg(target_os = "macos")] mod other_platforms { - use rustnet_monitor::network::platform::create_process_lookup_with_pktap_status; + use rustnet_monitor::network::platform::create_process_lookup; #[test] fn test_other_platform_lookup() { // Test that other platforms can create process lookups - let result = create_process_lookup_with_pktap_status(false); + let result = create_process_lookup(false); assert!(result.is_ok(), "Should work on other platforms too"); } }