Skip to main content

netscli_core/
pcap.rs

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/// Cooperative cancellation token for long-running PCAP capture loops.
31///
32/// This is intentionally very small and dependency-free so it can be used across
33/// CLI/TUI/MCP/Tauri surfaces without pulling in additional async utilities.
34#[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    /// Check whether libpcap/npf is available and list interfaces.
60    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            // Keep the read timeout reasonably small so duration/cancellation checks
85            // respond quickly even when there's no traffic.
86            .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            // Check duration limit
103            if let Some(duration) = config.duration {
104                if start.elapsed() >= duration {
105                    break;
106                }
107            }
108
109            // Check packet count limit
110            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                    // libpcap's dump writer doesn't surface write errors per-packet; flush
121                    // periodically so we can detect IO errors (disk full, permission, etc.).
122                    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    /// Check whether libpcap/npf is available and list interfaces.
148    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}