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