1use anyhow::{Result, anyhow};
18use pcap::{Active, Capture, Device, Error as PcapError};
19use std::time::{Duration, SystemTime, UNIX_EPOCH};
20
21#[cfg(target_os = "macos")]
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum PktapUnavailable {
32 NoBpfDeviceAccess,
34 MissingRootPrivileges,
36 InterfaceSpecified,
38 BpfFilterIncompatible,
40}
41
42#[cfg(target_os = "macos")]
44pub static PKTAP_DEGRADATION_REASON: std::sync::OnceLock<PktapUnavailable> =
45 std::sync::OnceLock::new();
46
47#[derive(Debug, Clone)]
49pub struct CaptureConfig {
50 pub interface: Option<String>,
52 pub snaplen: i32,
54 pub buffer_size: i32,
56 pub timeout_ms: i32,
58 pub filter: Option<String>,
60}
61
62impl Default for CaptureConfig {
63 fn default() -> Self {
64 Self {
65 interface: None,
66 snaplen: 1514, buffer_size: 20_000_000, timeout_ms: 150, filter: None, }
71 }
72}
73
74fn find_best_device() -> Result<Device> {
76 let devices = Device::list().map_err(|e| {
77 anyhow!(
78 "Failed to list network devices: {}. This may indicate insufficient privileges.",
79 e
80 )
81 })?;
82
83 log::info!(
84 "Scanning {} devices for best active interface...",
85 devices.len()
86 );
87
88 for d in &devices {
90 let has_valid_ip = d.addresses.iter().any(|addr| match &addr.addr {
91 std::net::IpAddr::V4(v4) => {
92 !v4.is_link_local() && !v4.is_loopback() && !v4.is_unspecified()
93 }
94 std::net::IpAddr::V6(v6) => {
95 !v6.is_loopback() && !v6.is_multicast() && !v6.is_unspecified()
96 }
97 });
98
99 log::debug!(
100 " Device: {} [up: {}, running: {}, has_ip: {}]",
101 d.name,
102 d.flags.is_up(),
103 d.flags.is_running(),
104 has_valid_ip
105 );
106 }
107
108 if devices.is_empty() {
109 return Err(anyhow!("No network devices found"));
110 }
111
112 let suitable_device = devices
114 .iter()
115 .find(|d| {
117 let desc_lower = d
119 .desc
120 .as_ref()
121 .map(|s| s.to_lowercase())
122 .unwrap_or_default();
123 let is_virtual = desc_lower.contains("hyper-v")
124 || desc_lower.contains("vmware")
125 || desc_lower.contains("virtualbox");
126
127 !d.name.starts_with("lo")
128 && d.name != "any"
131 && !is_virtual && d.flags.is_up()
133 && d.flags.is_running()
134 && d.addresses.iter().any(|addr| {
135 match &addr.addr {
136 std::net::IpAddr::V4(v4) => {
137 !v4.is_link_local() && !v4.is_loopback() && !v4.is_unspecified()
138 }
139 std::net::IpAddr::V6(_v6) => false, }
141 })
142 })
143 .or_else(|| {
145 devices.iter().find(|d| {
146 (d.name == "en0" || d.name == "en1" || d.name.starts_with("eth"))
147 && d.flags.is_up()
148 && d.addresses.iter().any(|addr| addr.addr.is_ipv4())
149 })
150 })
151 .or_else(|| {
153 devices.iter().find(|d| {
154 let desc_lower = d
156 .desc
157 .as_ref()
158 .map(|s| s.to_lowercase())
159 .unwrap_or_default();
160 let is_virtual = desc_lower.contains("hyper-v")
161 || desc_lower.contains("virtual")
162 || desc_lower.contains("vmware")
163 || desc_lower.contains("virtualbox")
164 || desc_lower.contains("loopback");
165
166 !d.name.starts_with("lo") &&
167 !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" &&
176 !is_virtual && d.flags.is_up() &&
178 !d.addresses.is_empty()
179 })
180 })
181 .cloned();
182
183 match suitable_device {
184 Some(device) => {
185 log::info!(
186 "Selected active device: {} ({} addresses)",
187 device.name,
188 device.addresses.len()
189 );
190 for addr in &device.addresses {
191 log::debug!(" Address: {}", addr.addr);
192 }
193 Ok(device)
194 }
195 None => {
196 log::error!("No suitable active network device found!");
197 log::error!("Try specifying an interface manually with -i flag");
198 Err(anyhow!(
199 "No active network interface found. Use -i to specify one manually."
200 ))
201 }
202 }
203}
204
205pub fn setup_packet_capture(config: CaptureConfig) -> Result<(Capture<Active>, String, i32)> {
207 #[cfg(target_os = "macos")]
211 if config.interface.is_none() && config.filter.is_none() {
212 log::info!("Attempting to use PKTAP for process metadata on macOS");
213
214 match Capture::from_device("pktap") {
215 Ok(pktap_builder) => {
216 let pktap_cap = pktap_builder
217 .promisc(false) .snaplen(config.snaplen)
219 .buffer_size(config.buffer_size)
220 .timeout(config.timeout_ms)
221 .immediate_mode(true)
222 .want_pktap(true)
223 .open();
224
225 match pktap_cap {
226 Ok(mut cap) => {
227 if let Err(e) = cap.direction(pcap::Direction::InOut) {
229 log::debug!("Could not set PKTAP direction: {}", e);
230 }
231
232 let linktype = cap.get_datalink();
233 log::info!(
234 "✓ PKTAP enabled successfully, linktype: {} ({})",
235 linktype.0,
236 if linktype.0 == 149 {
237 "Apple PKTAP"
238 } else {
239 "Unknown"
240 }
241 );
242
243 if let Some(filter) = &config.filter {
245 log::info!("Applying BPF filter to PKTAP: {}", filter);
246 cap.filter(filter, true)?;
247 }
248
249 log::info!("PKTAP capture ready - process metadata will be available");
250 return Ok((cap, "pktap".to_string(), linktype.0));
251 }
252 Err(e) => {
253 log::warn!("Failed to open PKTAP capture: {}", e);
254 log::info!(
255 "PKTAP requires root privileges - run with 'sudo' for process metadata support"
256 );
257 log::info!(
258 "Falling back to regular capture (process detection will use lsof)"
259 );
260 let _ = PKTAP_DEGRADATION_REASON.set(PktapUnavailable::NoBpfDeviceAccess);
262 }
263 }
264 }
265 Err(e) => {
266 log::warn!("Failed to create PKTAP device: {}", e);
267 log::info!(
268 "PKTAP requires root privileges - run with 'sudo' for process metadata support"
269 );
270 log::info!("Falling back to regular capture (process detection will use lsof)");
271 let _ = PKTAP_DEGRADATION_REASON.set(PktapUnavailable::MissingRootPrivileges);
273 }
274 }
275 }
276
277 #[cfg(target_os = "macos")]
279 {
280 if config.interface.is_some() {
281 let _ = PKTAP_DEGRADATION_REASON.set(PktapUnavailable::InterfaceSpecified);
283 }
284 if config.filter.is_some() {
285 log::warn!(
286 "BPF filter specified - using regular capture instead of PKTAP (BPF filters don't work with PKTAP)"
287 );
288 let _ = PKTAP_DEGRADATION_REASON.set(PktapUnavailable::BpfFilterIncompatible);
290 }
291 }
292
293 log::info!("Setting up regular packet capture");
295 let device = find_capture_device(&config.interface)?;
296
297 let is_tun = device.name.starts_with("tun") || device.name.starts_with("utun");
302 let is_tap = device.name.starts_with("tap");
303 let is_tunnel = is_tun || is_tap;
304 let tunnel_type = if is_tun {
305 "TUN (Layer 3)"
306 } else if is_tap {
307 "TAP (Layer 2)"
308 } else {
309 "N/A"
310 };
311
312 log::info!(
313 "Setting up capture on device: {} ({}){}",
314 device.name,
315 device.desc.as_deref().unwrap_or("no description"),
316 if is_tunnel {
317 format!(" [Tunnel: {}]", tunnel_type)
318 } else {
319 String::new()
320 }
321 );
322
323 let device_name = device.name.clone();
324
325 let cap = Capture::from_device(device)?
328 .promisc(false)
329 .snaplen(config.snaplen)
330 .buffer_size(config.buffer_size)
331 .timeout(config.timeout_ms)
332 .immediate_mode(true); let mut cap = cap.open()?;
336
337 if let Some(filter) = &config.filter {
339 log::info!("Applying BPF filter: {}", filter);
340 cap.filter(filter, true)?;
341 }
342
343 let linktype = cap.get_datalink();
345
346 Ok((cap, device_name, linktype.0))
347}
348
349pub fn validate_interface(interface_name: &Option<String>) -> Result<()> {
352 if let Some(name) = interface_name {
353 find_capture_device(&Some(name.clone()))?;
355 }
356 Ok(())
357}
358
359fn find_capture_device(interface_name: &Option<String>) -> Result<Device> {
361 match interface_name {
362 Some(name) => {
363 log::info!("Looking for interface: {}", name);
364
365 if name == "any" {
367 #[cfg(not(target_os = "linux"))]
368 {
369 return Err(anyhow!(
370 "The 'any' interface is only supported on Linux.\n\
371 On your platform, please specify a specific interface with -i <interface>.\n\
372 Run without -i to auto-detect the default interface."
373 ));
374 }
375
376 #[cfg(target_os = "linux")]
377 {
378 log::info!("Using 'any' pseudo-interface to capture on all interfaces");
379 }
380 }
381
382 let devices = Device::list()?;
384
385 if let Some(device) = devices.iter().find(|d| d.name == *name) {
387 return Ok(device.clone());
388 }
389
390 let name_lower = name.to_lowercase();
392 if let Some(device) = devices.iter().find(|d| d.name.to_lowercase() == name_lower) {
393 return Ok(device.clone());
394 }
395
396 let available: Vec<String> = devices.iter().map(|d| d.name.clone()).collect();
398
399 Err(anyhow!(
400 "Interface '{}' not found. Available interfaces: {}",
401 name,
402 available.join(", ")
403 ))
404 }
405 None => {
406 log::info!("No interface specified, using default");
407
408 if let Some(active_ip) = std::net::UdpSocket::bind("0.0.0.0:0")
410 .and_then(|s| {
411 let _ = s.connect("8.8.8.8:53");
412 s.local_addr()
413 })
414 .ok()
415 .map(|addr| addr.ip())
416 {
417 log::info!("Found active routed IP: {}", active_ip);
418 if let Ok(devices) = Device::list()
419 && let Some(device) = devices
420 .into_iter()
421 .find(|d| d.addresses.iter().any(|a| a.addr == active_ip))
422 {
423 log::info!("Selected interface {} based on active route", device.name);
424 return Ok(device);
425 }
426 }
427 log::info!("Fallback: using libpcap default device logic");
428
429 match Device::lookup() {
431 Ok(Some(device)) => {
432 log::info!(
433 "Found default device: {} ({})",
434 device.name,
435 device.desc.as_deref().unwrap_or("no description")
436 );
437
438 let has_valid_ip = device.addresses.iter().any(|addr| {
440 match &addr.addr {
441 std::net::IpAddr::V4(v4) => {
442 !v4.is_link_local() && !v4.is_loopback() && !v4.is_unspecified()
443 }
444 std::net::IpAddr::V6(_v6) => false, }
446 });
447
448 let is_problematic = device.name.starts_with("ap")
451 || device.name.starts_with("awdl")
452 || device.name.starts_with("llw")
453 || device.name.starts_with("bridge")
454 || device.name.starts_with("vmnet")
456 || (device.name == "any" && !cfg!(target_os = "linux"))
457 || device.flags.is_loopback();
458
459 if device.flags.is_up()
460 && device.flags.is_running()
461 && has_valid_ip
462 && !is_problematic
463 {
464 log::info!("Default device appears active, using it");
465 Ok(device)
466 } else {
467 log::warn!(
468 "Default device '{}' is not suitable (up: {}, running: {}, has_ip: {}, problematic: {})",
469 device.name,
470 device.flags.is_up(),
471 device.flags.is_running(),
472 has_valid_ip,
473 is_problematic
474 );
475 log::info!("Looking for a better interface...");
476
477 find_best_device()
479 }
480 }
481 Ok(None) => {
482 log::info!("No default device found");
483 find_best_device()
484 }
485 Err(e) => Err(e.into()),
486 }
487 }
488 }
489}
490
491pub struct PacketReader {
493 capture: Capture<Active>,
494}
495
496#[derive(Debug, Clone)]
498pub struct CapturedPacket {
499 pub data: Vec<u8>,
500 pub timestamp: SystemTime,
501 pub original_len: u32,
502}
503
504impl PacketReader {
505 pub fn new(capture: Capture<Active>) -> Self {
506 Self { capture }
507 }
508
509 pub fn next_packet(&mut self) -> Result<Option<CapturedPacket>> {
511 match self.capture.next_packet() {
512 Ok(packet) => {
513 let ts = packet.header.ts;
514 Ok(Some(CapturedPacket {
515 data: packet.data.to_vec(),
516 timestamp: timeval_to_system_time(ts.tv_sec, ts.tv_usec),
517 original_len: packet.header.len,
518 }))
519 }
520 Err(PcapError::TimeoutExpired) => Ok(None),
521 Err(e) => Err(e.into()),
522 }
523 }
524
525 pub fn stats(&mut self) -> Result<CaptureStats> {
527 let stats = self.capture.stats()?;
528 let capture_stats = CaptureStats {
529 received: stats.received,
530 dropped: stats.dropped,
531 if_dropped: stats.if_dropped,
532 };
533
534 if capture_stats.total_dropped() > 0 {
536 log::debug!(
537 "Total {} packets dropped (kernel: {}, interface: {})",
538 capture_stats.total_dropped(),
539 capture_stats.dropped,
540 capture_stats.if_dropped
541 );
542 }
543
544 Ok(capture_stats)
545 }
546}
547
548fn timeval_to_system_time<S, U>(secs: S, usecs: U) -> SystemTime
549where
550 S: Into<i64>,
551 U: Into<i64>,
552{
553 let secs = secs.into();
554 let usecs = usecs.into().clamp(0, 999_999);
555 if secs < 0 {
556 UNIX_EPOCH
557 } else {
558 UNIX_EPOCH + Duration::from_secs(secs as u64) + Duration::from_micros(usecs as u64)
559 }
560}
561
562#[derive(Debug, Clone, Default)]
564pub struct CaptureStats {
565 pub received: u32,
566 pub dropped: u32,
567 pub if_dropped: u32,
569}
570
571impl CaptureStats {
572 pub fn total_dropped(&self) -> u32 {
574 self.dropped.saturating_add(self.if_dropped)
575 }
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581
582 #[test]
583 fn test_default_config() {
584 let config = CaptureConfig::default();
585 assert_eq!(config.snaplen, 1514);
586 assert!(config.filter.is_none()); }
588
589 #[test]
590 fn test_udp_routing_resolution_can_execute() {
591 if let Ok(socket) = std::net::UdpSocket::bind("0.0.0.0:0")
594 && socket.connect("8.8.8.8:53").is_ok()
595 && let Ok(addr) = socket.local_addr()
596 {
597 assert!(
598 !addr.ip().is_loopback(),
599 "Active routed IP should not be loopback"
600 );
601 assert!(
602 !addr.ip().is_unspecified(),
603 "Active routed IP should not be unspecified"
604 );
605 }
606 }
607}