mirror of
https://github.com/domcyrus/rustnet.git
synced 2026-05-12 15:00:01 -05:00
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
+96
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
+7
-2
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+21
-8
@@ -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<Mutex<HashMap<String, String>>> =
|
||||
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<u64>) {
|
||||
fn log_connection_event(
|
||||
json_log_path: &str,
|
||||
event_type: &str,
|
||||
conn: &Connection,
|
||||
duration_secs: Option<u64>,
|
||||
) {
|
||||
// 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<RwLock<String>>,
|
||||
) -> 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())
|
||||
|
||||
@@ -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<ProcessCache>,
|
||||
}
|
||||
|
||||
struct ProcessCache {
|
||||
lookup: HashMap<ConnectionKey, (u32, String)>,
|
||||
last_refresh: Instant,
|
||||
}
|
||||
|
||||
impl FreeBSDProcessLookup {
|
||||
pub fn new() -> Result<Self> {
|
||||
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<HashMap<ConnectionKey, (u32, String)>> {
|
||||
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<HashMap<ConnectionKey, (u32, String)>> {
|
||||
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::<u32>() {
|
||||
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<SocketAddr> {
|
||||
// Handle wildcard addresses
|
||||
if addr_str.starts_with("*:") {
|
||||
let port = addr_str.strip_prefix("*:")?.parse::<u16>().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::<u16>().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::<u16>().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
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<Box<dyn ProcessLookup>> {
|
||||
pub fn create_process_lookup(_use_pktap: bool) -> Result<Box<dyn ProcessLookup>> {
|
||||
#[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"))
|
||||
}
|
||||
|
||||
+109
-4
@@ -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<String>, instructions: Vec<String>) -> Self {
|
||||
Self {
|
||||
has_privileges: false,
|
||||
@@ -88,7 +99,17 @@ pub fn check_packet_capture_privileges() -> Result<PrivilegeStatus> {
|
||||
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<PrivilegeStatus> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "freebsd")]
|
||||
fn check_freebsd_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 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::<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(),
|
||||
"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<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(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user