Skip to main content

netscli_core/
pcap.rs

1#[cfg(feature = "pcap")]
2use pcap::{Capture, Device};
3use serde::Serialize;
4use std::path::PathBuf;
5
6use crate::error::{Error, Result};
7use std::sync::{
8    atomic::{AtomicBool, Ordering},
9    Arc,
10};
11use std::time::Duration;
12
13#[cfg(feature = "pcap")]
14use std::time::Instant;
15
16#[derive(Debug, Serialize)]
17pub struct PcapConfig {
18    pub interface: String,
19    pub filter: Option<String>,
20    pub output_file: PathBuf,
21    pub duration: Option<Duration>,
22    pub max_packets: Option<usize>,
23}
24
25#[derive(Debug, Serialize)]
26pub struct PcapResult {
27    pub packets_captured: usize,
28    pub duration: Duration,
29    pub file_path: PathBuf,
30}
31
32/// Cooperative cancellation token for long-running PCAP capture loops.
33///
34/// This is intentionally very small and dependency-free so it can be used across
35/// CLI/TUI/MCP/Tauri surfaces without pulling in additional async utilities.
36#[derive(Debug, Clone, Default)]
37pub struct PcapCancelToken {
38    cancelled: Arc<AtomicBool>,
39}
40
41impl PcapCancelToken {
42    pub fn new() -> Self {
43        Self {
44            cancelled: Arc::new(AtomicBool::new(false)),
45        }
46    }
47
48    pub fn cancel(&self) {
49        self.cancelled.store(true, Ordering::SeqCst);
50    }
51
52    pub fn is_cancelled(&self) -> bool {
53        self.cancelled.load(Ordering::SeqCst)
54    }
55}
56
57pub struct PcapEngine;
58
59#[cfg(feature = "pcap")]
60impl PcapEngine {
61    /// Check whether libpcap/npf is available and list interfaces.
62    pub fn check_support() -> Result<Vec<String>> {
63        let devices = Device::list()?;
64        if devices.is_empty() {
65            return Err(Error::unsupported(
66                "pcap available but no capture interfaces detected",
67            ));
68        }
69        Ok(devices.into_iter().map(|d| d.name).collect())
70    }
71
72    pub fn capture(config: PcapConfig) -> Result<PcapResult> {
73        Self::capture_with_cancel(config, None)
74    }
75
76    pub fn capture_with_cancel(
77        config: PcapConfig,
78        cancel: Option<PcapCancelToken>,
79    ) -> Result<PcapResult> {
80        let device = Device::list()?
81            .into_iter()
82            .find(|d| d.name == config.interface)
83            .ok_or_else(|| {
84                Error::invalid_input(format!("Interface not found: {}", config.interface))
85            })?;
86
87        let mut cap = Capture::from_device(device)?
88            .promisc(true)
89            .snaplen(65535)
90            // Keep the read timeout reasonably small so duration/cancellation checks
91            // respond quickly even when there's no traffic.
92            .timeout(250)
93            .open()?;
94
95        if let Some(filter) = &config.filter {
96            cap.filter(filter, true)?;
97        }
98
99        let mut savefile = cap.savefile(&config.output_file)?;
100        let start = Instant::now();
101        let mut packets_captured = 0;
102
103        loop {
104            if cancel.as_ref().is_some_and(|c| c.is_cancelled()) {
105                break;
106            }
107
108            // Check duration limit
109            if let Some(duration) = config.duration {
110                if start.elapsed() >= duration {
111                    break;
112                }
113            }
114
115            // Check packet count limit
116            if let Some(max) = config.max_packets {
117                if packets_captured >= max {
118                    break;
119                }
120            }
121
122            match cap.next_packet() {
123                Ok(packet) => {
124                    savefile.write(&packet);
125                    packets_captured += 1;
126                    // libpcap's dump writer doesn't surface write errors per-packet; flush
127                    // periodically so we can detect IO errors (disk full, permission, etc.).
128                    if packets_captured % 128 == 0 {
129                        savefile
130                            .flush()
131                            .map_err(|e| Error::Other(format!("failed to flush savefile: {e}")))?;
132                    }
133                }
134                Err(pcap::Error::TimeoutExpired) => continue,
135                Err(e) => return Err(e.into()),
136            }
137        }
138
139        savefile
140            .flush()
141            .map_err(|e| Error::Other(format!("failed to flush savefile: {e}")))?;
142
143        Ok(PcapResult {
144            packets_captured,
145            duration: start.elapsed(),
146            file_path: config.output_file,
147        })
148    }
149}
150
151#[cfg(not(feature = "pcap"))]
152impl PcapEngine {
153    /// Check whether libpcap/npf is available and list interfaces.
154    pub fn check_support() -> Result<Vec<String>> {
155        Err(Error::unsupported(
156            "pcap support disabled at compile time (built without feature 'pcap')",
157        ))
158    }
159
160    pub fn capture(_config: PcapConfig) -> Result<PcapResult> {
161        Err(Error::unsupported(
162            "pcap support disabled at compile time (built without feature 'pcap')",
163        ))
164    }
165
166    pub fn capture_with_cancel(
167        _config: PcapConfig,
168        _cancel: Option<PcapCancelToken>,
169    ) -> Result<PcapResult> {
170        Err(Error::unsupported(
171            "pcap support disabled at compile time (built without feature 'pcap')",
172        ))
173    }
174}