1#[cfg(feature = "pcap")]
2use pcap::{Capture, Device};
3use serde::Serialize;
4use std::path::PathBuf;
5use std::sync::{
6 atomic::{AtomicBool, Ordering},
7 Arc,
8};
9use std::time::Duration;
10
11#[cfg(feature = "pcap")]
12use std::time::Instant;
13
14#[derive(Debug, Serialize)]
15pub struct PcapConfig {
16 pub interface: String,
17 pub filter: Option<String>,
18 pub output_file: PathBuf,
19 pub duration: Option<Duration>,
20 pub max_packets: Option<usize>,
21}
22
23#[derive(Debug, Serialize)]
24pub struct PcapResult {
25 pub packets_captured: usize,
26 pub duration: Duration,
27 pub file_path: PathBuf,
28}
29
30#[derive(Debug, Clone, Default)]
35pub struct PcapCancelToken {
36 cancelled: Arc<AtomicBool>,
37}
38
39impl PcapCancelToken {
40 pub fn new() -> Self {
41 Self {
42 cancelled: Arc::new(AtomicBool::new(false)),
43 }
44 }
45
46 pub fn cancel(&self) {
47 self.cancelled.store(true, Ordering::SeqCst);
48 }
49
50 pub fn is_cancelled(&self) -> bool {
51 self.cancelled.load(Ordering::SeqCst)
52 }
53}
54
55pub struct PcapEngine;
56
57#[cfg(feature = "pcap")]
58impl PcapEngine {
59 pub fn check_support() -> anyhow::Result<Vec<String>> {
61 let devices = Device::list().map_err(|e| anyhow::anyhow!("pcap unavailable: {e}"))?;
62 if devices.is_empty() {
63 anyhow::bail!("pcap available but no capture interfaces detected");
64 }
65 Ok(devices.into_iter().map(|d| d.name).collect())
66 }
67
68 pub fn capture(config: PcapConfig) -> anyhow::Result<PcapResult> {
69 Self::capture_with_cancel(config, None)
70 }
71
72 pub fn capture_with_cancel(
73 config: PcapConfig,
74 cancel: Option<PcapCancelToken>,
75 ) -> anyhow::Result<PcapResult> {
76 let device = Device::list()?
77 .into_iter()
78 .find(|d| d.name == config.interface)
79 .ok_or_else(|| anyhow::anyhow!("Interface not found"))?;
80
81 let mut cap = Capture::from_device(device)?
82 .promisc(true)
83 .snaplen(65535)
84 .timeout(250)
87 .open()?;
88
89 if let Some(filter) = &config.filter {
90 cap.filter(filter, true)?;
91 }
92
93 let mut savefile = cap.savefile(&config.output_file)?;
94 let start = Instant::now();
95 let mut packets_captured = 0;
96
97 loop {
98 if cancel.as_ref().is_some_and(|c| c.is_cancelled()) {
99 break;
100 }
101
102 if let Some(duration) = config.duration {
104 if start.elapsed() >= duration {
105 break;
106 }
107 }
108
109 if let Some(max) = config.max_packets {
111 if packets_captured >= max {
112 break;
113 }
114 }
115
116 match cap.next_packet() {
117 Ok(packet) => {
118 savefile.write(&packet);
119 packets_captured += 1;
120 if packets_captured % 128 == 0 {
123 savefile
124 .flush()
125 .map_err(|e| anyhow::anyhow!("failed to flush savefile: {e}"))?;
126 }
127 }
128 Err(pcap::Error::TimeoutExpired) => continue,
129 Err(e) => return Err(e.into()),
130 }
131 }
132
133 savefile
134 .flush()
135 .map_err(|e| anyhow::anyhow!("failed to flush savefile: {e}"))?;
136
137 Ok(PcapResult {
138 packets_captured,
139 duration: start.elapsed(),
140 file_path: config.output_file,
141 })
142 }
143}
144
145#[cfg(not(feature = "pcap"))]
146impl PcapEngine {
147 pub fn check_support() -> anyhow::Result<Vec<String>> {
149 anyhow::bail!("pcap support disabled at compile time (built without feature 'pcap')")
150 }
151
152 pub fn capture(_config: PcapConfig) -> anyhow::Result<PcapResult> {
153 anyhow::bail!("pcap support disabled at compile time (built without feature 'pcap')")
154 }
155
156 pub fn capture_with_cancel(
157 _config: PcapConfig,
158 _cancel: Option<PcapCancelToken>,
159 ) -> anyhow::Result<PcapResult> {
160 anyhow::bail!("pcap support disabled at compile time (built without feature 'pcap')")
161 }
162}