use anyhow::{Result, anyhow};
use pcap::{Active, Capture, Device, Error as PcapError};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[cfg(target_os = "macos")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PktapUnavailable {
NoBpfDeviceAccess,
MissingRootPrivileges,
InterfaceSpecified,
BpfFilterIncompatible,
}
#[cfg(target_os = "macos")]
pub static PKTAP_DEGRADATION_REASON: std::sync::OnceLock<PktapUnavailable> =
std::sync::OnceLock::new();
#[derive(Debug, Clone)]
pub struct CaptureConfig {
pub interface: Option<String>,
pub snaplen: i32,
pub buffer_size: i32,
pub timeout_ms: i32,
pub filter: Option<String>,
}
impl Default for CaptureConfig {
fn default() -> Self {
Self {
interface: None,
snaplen: 1514, buffer_size: 20_000_000, timeout_ms: 150, filter: None, }
}
}
fn find_best_device() -> Result<Device> {
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...",
devices.len()
);
for d in &devices {
let has_valid_ip = d.addresses.iter().any(|addr| match &addr.addr {
std::net::IpAddr::V4(v4) => {
!v4.is_link_local() && !v4.is_loopback() && !v4.is_unspecified()
}
std::net::IpAddr::V6(v6) => {
!v6.is_loopback() && !v6.is_multicast() && !v6.is_unspecified()
}
});
log::debug!(
" Device: {} [up: {}, running: {}, has_ip: {}]",
d.name,
d.flags.is_up(),
d.flags.is_running(),
has_valid_ip
);
}
if devices.is_empty() {
return Err(anyhow!("No network devices found"));
}
let suitable_device = devices
.iter()
.find(|d| {
let desc_lower = d
.desc
.as_ref()
.map(|s| s.to_lowercase())
.unwrap_or_default();
let is_virtual = desc_lower.contains("hyper-v")
|| desc_lower.contains("vmware")
|| desc_lower.contains("virtualbox");
!d.name.starts_with("lo")
&& d.name != "any"
&& !is_virtual && d.flags.is_up()
&& d.flags.is_running()
&& d.addresses.iter().any(|addr| {
match &addr.addr {
std::net::IpAddr::V4(v4) => {
!v4.is_link_local() && !v4.is_loopback() && !v4.is_unspecified()
}
std::net::IpAddr::V6(_v6) => false, }
})
})
.or_else(|| {
devices.iter().find(|d| {
(d.name == "en0" || d.name == "en1" || d.name.starts_with("eth"))
&& d.flags.is_up()
&& d.addresses.iter().any(|addr| addr.addr.is_ipv4())
})
})
.or_else(|| {
devices.iter().find(|d| {
let desc_lower = d
.desc
.as_ref()
.map(|s| s.to_lowercase())
.unwrap_or_default();
let is_virtual = desc_lower.contains("hyper-v")
|| desc_lower.contains("virtual")
|| desc_lower.contains("vmware")
|| desc_lower.contains("virtualbox")
|| desc_lower.contains("loopback");
!d.name.starts_with("lo") &&
!d.name.starts_with("ap") && !d.name.starts_with("awdl") && !d.name.starts_with("llw") && !d.name.starts_with("bridge") && !d.name.starts_with("vmnet") && d.name != "any" &&
!is_virtual && d.flags.is_up() &&
!d.addresses.is_empty()
})
})
.cloned();
match suitable_device {
Some(device) => {
log::info!(
"Selected active device: {} ({} addresses)",
device.name,
device.addresses.len()
);
for addr in &device.addresses {
log::debug!(" Address: {}", addr.addr);
}
Ok(device)
}
None => {
log::error!("No suitable active network device found!");
log::error!("Try specifying an interface manually with -i flag");
Err(anyhow!(
"No active network interface found. Use -i to specify one manually."
))
}
}
}
pub fn setup_packet_capture(config: CaptureConfig) -> Result<(Capture<Active>, String, i32)> {
#[cfg(target_os = "macos")]
if config.interface.is_none() && config.filter.is_none() {
log::info!("Attempting to use PKTAP for process metadata on macOS");
match Capture::from_device("pktap") {
Ok(pktap_builder) => {
let pktap_cap = pktap_builder
.promisc(false) .snaplen(config.snaplen)
.buffer_size(config.buffer_size)
.timeout(config.timeout_ms)
.immediate_mode(true)
.want_pktap(true)
.open();
match pktap_cap {
Ok(mut cap) => {
if let Err(e) = cap.direction(pcap::Direction::InOut) {
log::debug!("Could not set PKTAP direction: {}", e);
}
let linktype = cap.get_datalink();
log::info!(
"✓ PKTAP enabled successfully, linktype: {} ({})",
linktype.0,
if linktype.0 == 149 {
"Apple PKTAP"
} else {
"Unknown"
}
);
if let Some(filter) = &config.filter {
log::info!("Applying BPF filter to PKTAP: {}", filter);
cap.filter(filter, true)?;
}
log::info!("PKTAP capture ready - process metadata will be available");
return Ok((cap, "pktap".to_string(), linktype.0));
}
Err(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)"
);
let _ = PKTAP_DEGRADATION_REASON.set(PktapUnavailable::NoBpfDeviceAccess);
}
}
}
Err(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)");
let _ = PKTAP_DEGRADATION_REASON.set(PktapUnavailable::MissingRootPrivileges);
}
}
}
#[cfg(target_os = "macos")]
{
if config.interface.is_some() {
let _ = PKTAP_DEGRADATION_REASON.set(PktapUnavailable::InterfaceSpecified);
}
if config.filter.is_some() {
log::warn!(
"BPF filter specified - using regular capture instead of PKTAP (BPF filters don't work with PKTAP)"
);
let _ = PKTAP_DEGRADATION_REASON.set(PktapUnavailable::BpfFilterIncompatible);
}
}
log::info!("Setting up regular packet capture");
let device = find_capture_device(&config.interface)?;
let is_tun = device.name.starts_with("tun") || device.name.starts_with("utun");
let is_tap = device.name.starts_with("tap");
let is_tunnel = is_tun || is_tap;
let tunnel_type = if is_tun {
"TUN (Layer 3)"
} else if is_tap {
"TAP (Layer 2)"
} else {
"N/A"
};
log::info!(
"Setting up capture on device: {} ({}){}",
device.name,
device.desc.as_deref().unwrap_or("no description"),
if is_tunnel {
format!(" [Tunnel: {}]", tunnel_type)
} else {
String::new()
}
);
let device_name = device.name.clone();
let cap = Capture::from_device(device)?
.promisc(false)
.snaplen(config.snaplen)
.buffer_size(config.buffer_size)
.timeout(config.timeout_ms)
.immediate_mode(true);
let mut cap = cap.open()?;
if let Some(filter) = &config.filter {
log::info!("Applying BPF filter: {}", filter);
cap.filter(filter, true)?;
}
let linktype = cap.get_datalink();
Ok((cap, device_name, linktype.0))
}
pub fn validate_interface(interface_name: &Option<String>) -> Result<()> {
if let Some(name) = interface_name {
find_capture_device(&Some(name.clone()))?;
}
Ok(())
}
fn find_capture_device(interface_name: &Option<String>) -> Result<Device> {
match interface_name {
Some(name) => {
log::info!("Looking for interface: {}", name);
if name == "any" {
#[cfg(not(target_os = "linux"))]
{
return Err(anyhow!(
"The 'any' interface is only supported on Linux.\n\
On your platform, please specify a specific interface with -i <interface>.\n\
Run without -i to auto-detect the default interface."
));
}
#[cfg(target_os = "linux")]
{
log::info!("Using 'any' pseudo-interface to capture on all interfaces");
}
}
let devices = Device::list()?;
if let Some(device) = devices.iter().find(|d| d.name == *name) {
return Ok(device.clone());
}
let name_lower = name.to_lowercase();
if let Some(device) = devices.iter().find(|d| d.name.to_lowercase() == name_lower) {
return Ok(device.clone());
}
let available: Vec<String> = devices.iter().map(|d| d.name.clone()).collect();
Err(anyhow!(
"Interface '{}' not found. Available interfaces: {}",
name,
available.join(", ")
))
}
None => {
log::info!("No interface specified, using default");
if let Some(active_ip) = std::net::UdpSocket::bind("0.0.0.0:0")
.and_then(|s| {
let _ = s.connect("8.8.8.8:53");
s.local_addr()
})
.ok()
.map(|addr| addr.ip())
{
log::info!("Found active routed IP: {}", active_ip);
if let Ok(devices) = Device::list()
&& let Some(device) = devices
.into_iter()
.find(|d| d.addresses.iter().any(|a| a.addr == active_ip))
{
log::info!("Selected interface {} based on active route", device.name);
return Ok(device);
}
}
log::info!("Fallback: using libpcap default device logic");
match Device::lookup() {
Ok(Some(device)) => {
log::info!(
"Found default device: {} ({})",
device.name,
device.desc.as_deref().unwrap_or("no description")
);
let has_valid_ip = device.addresses.iter().any(|addr| {
match &addr.addr {
std::net::IpAddr::V4(v4) => {
!v4.is_link_local() && !v4.is_loopback() && !v4.is_unspecified()
}
std::net::IpAddr::V6(_v6) => false, }
});
let is_problematic = device.name.starts_with("ap")
|| device.name.starts_with("awdl")
|| device.name.starts_with("llw")
|| device.name.starts_with("bridge")
|| device.name.starts_with("vmnet")
|| (device.name == "any" && !cfg!(target_os = "linux"))
|| device.flags.is_loopback();
if device.flags.is_up()
&& device.flags.is_running()
&& has_valid_ip
&& !is_problematic
{
log::info!("Default device appears active, using it");
Ok(device)
} else {
log::warn!(
"Default device '{}' is not suitable (up: {}, running: {}, has_ip: {}, problematic: {})",
device.name,
device.flags.is_up(),
device.flags.is_running(),
has_valid_ip,
is_problematic
);
log::info!("Looking for a better interface...");
find_best_device()
}
}
Ok(None) => {
log::info!("No default device found");
find_best_device()
}
Err(e) => Err(e.into()),
}
}
}
}
pub struct PacketReader {
capture: Capture<Active>,
}
#[derive(Debug, Clone)]
pub struct CapturedPacket {
pub data: Vec<u8>,
pub timestamp: SystemTime,
pub original_len: u32,
}
impl PacketReader {
pub fn new(capture: Capture<Active>) -> Self {
Self { capture }
}
pub fn next_packet(&mut self) -> Result<Option<CapturedPacket>> {
match self.capture.next_packet() {
Ok(packet) => {
let ts = packet.header.ts;
Ok(Some(CapturedPacket {
data: packet.data.to_vec(),
timestamp: timeval_to_system_time(ts.tv_sec, ts.tv_usec),
original_len: packet.header.len,
}))
}
Err(PcapError::TimeoutExpired) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn stats(&mut self) -> Result<CaptureStats> {
let stats = self.capture.stats()?;
let capture_stats = CaptureStats {
received: stats.received,
dropped: stats.dropped,
if_dropped: stats.if_dropped,
};
if capture_stats.total_dropped() > 0 {
log::debug!(
"Total {} packets dropped (kernel: {}, interface: {})",
capture_stats.total_dropped(),
capture_stats.dropped,
capture_stats.if_dropped
);
}
Ok(capture_stats)
}
}
fn timeval_to_system_time<S, U>(secs: S, usecs: U) -> SystemTime
where
S: Into<i64>,
U: Into<i64>,
{
let secs = secs.into();
let usecs = usecs.into().clamp(0, 999_999);
if secs < 0 {
UNIX_EPOCH
} else {
UNIX_EPOCH + Duration::from_secs(secs as u64) + Duration::from_micros(usecs as u64)
}
}
#[derive(Debug, Clone, Default)]
pub struct CaptureStats {
pub received: u32,
pub dropped: u32,
pub if_dropped: u32,
}
impl CaptureStats {
pub fn total_dropped(&self) -> u32 {
self.dropped.saturating_add(self.if_dropped)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = CaptureConfig::default();
assert_eq!(config.snaplen, 1514);
assert!(config.filter.is_none()); }
#[test]
fn test_udp_routing_resolution_can_execute() {
if let Ok(socket) = std::net::UdpSocket::bind("0.0.0.0:0")
&& socket.connect("8.8.8.8:53").is_ok()
&& let Ok(addr) = socket.local_addr()
{
assert!(
!addr.ip().is_loopback(),
"Active routed IP should not be loopback"
);
assert!(
!addr.ip().is_unspecified(),
"Active routed IP should not be unspecified"
);
}
}
}