Skip to main content

netscli_core/ops/
pcap.rs

1use super::config::Ops;
2use crate::error::{Error, Result};
3use crate::{
4    PcapCancelToken, PcapConfig, PcapEngine, PcapParseResult, PcapResult,
5    DEFAULT_PCAP_CAPTURE_SECONDS, MAX_PCAP_CAPTURE_PACKETS, MAX_PCAP_CAPTURE_SECONDS,
6};
7use std::path::PathBuf;
8
9impl Ops {
10    pub fn pcap_check_support(&self) -> Result<Vec<String>> {
11        PcapEngine::check_support()
12    }
13
14    pub fn capture_pcap(
15        &self,
16        interface: String,
17        filter: Option<String>,
18        duration: Option<u64>,
19        output_file: Option<String>,
20        max_packets: Option<usize>,
21    ) -> Result<PcapResult> {
22        let (duration, max_packets) = normalize_capture_limits(duration, max_packets)?;
23        let cfg = PcapConfig {
24            interface,
25            filter,
26            output_file: normalize_capture_output(output_file)?,
27            duration,
28            max_packets,
29        };
30        PcapEngine::capture(cfg)
31    }
32
33    pub fn parse_pcap_file(
34        &self,
35        input_file: String,
36        max_packets: Option<usize>,
37    ) -> Result<PcapParseResult> {
38        PcapEngine::parse_file(input_file.into(), max_packets)
39    }
40
41    /// Async-friendly PCAP capture wrapper.
42    ///
43    /// PCAP capture is inherently blocking (libpcap read loop + file I/O). This
44    /// runs it in a dedicated blocking thread so async runtimes (CLI/Tauri/MCP)
45    /// remain responsive.
46    pub async fn capture_pcap_async(
47        &self,
48        interface: String,
49        filter: Option<String>,
50        duration: Option<u64>,
51        output_file: Option<String>,
52        max_packets: Option<usize>,
53    ) -> Result<PcapResult> {
54        self.capture_pcap_async_with_cancel(
55            interface,
56            filter,
57            duration,
58            output_file,
59            max_packets,
60            None,
61        )
62        .await
63    }
64
65    pub async fn capture_pcap_async_with_cancel(
66        &self,
67        interface: String,
68        filter: Option<String>,
69        duration: Option<u64>,
70        output_file: Option<String>,
71        max_packets: Option<usize>,
72        cancel: Option<PcapCancelToken>,
73    ) -> Result<PcapResult> {
74        let (duration, max_packets) = normalize_capture_limits(duration, max_packets)?;
75        let cfg = PcapConfig {
76            interface,
77            filter,
78            output_file: normalize_capture_output(output_file)?,
79            duration,
80            max_packets,
81        };
82
83        let task =
84            tokio::task::spawn_blocking(move || PcapEngine::capture_with_cancel(cfg, cancel));
85        match task.await {
86            Ok(res) => Ok(res?),
87            Err(e) => Err(Error::Other(format!("pcap capture task failed: {e}"))),
88        }
89    }
90}
91
92fn normalize_capture_limits(
93    duration: Option<u64>,
94    max_packets: Option<usize>,
95) -> Result<(Option<std::time::Duration>, Option<usize>)> {
96    let max_packets = match max_packets {
97        Some(0) => return Err(Error::invalid_input("max packets must be greater than 0")),
98        Some(n) if n > MAX_PCAP_CAPTURE_PACKETS => {
99            return Err(Error::invalid_input(format!(
100                "max packets too large: {n} (max {MAX_PCAP_CAPTURE_PACKETS})"
101            )));
102        }
103        Some(n) => Some(n),
104        None => None,
105    };
106
107    let duration = match duration {
108        Some(0) => return Err(Error::invalid_input("duration must be greater than 0")),
109        Some(seconds) if seconds > MAX_PCAP_CAPTURE_SECONDS => {
110            return Err(Error::invalid_input(format!(
111                "duration too long: {seconds}s (max {MAX_PCAP_CAPTURE_SECONDS}s)"
112            )));
113        }
114        Some(seconds) => Some(std::time::Duration::from_secs(seconds)),
115        None if max_packets.is_none() => {
116            Some(std::time::Duration::from_secs(DEFAULT_PCAP_CAPTURE_SECONDS))
117        }
118        // A packet count with no duration used to mean "no time limit at
119        // all", so a capture waiting for packets that never arrive on a quiet
120        // interface ran until something else stopped it -- past the very
121        // ceiling this function enforces when a duration *is* given. The cap
122        // is the cap either way.
123        None => Some(std::time::Duration::from_secs(MAX_PCAP_CAPTURE_SECONDS)),
124    };
125
126    Ok((duration, max_packets))
127}
128
129fn normalize_capture_output(output_file: Option<String>) -> Result<PathBuf> {
130    let path = PathBuf::from(output_file.unwrap_or_else(|| "capture.pcap".to_string()));
131    let is_pcap = path
132        .extension()
133        .and_then(|ext| ext.to_str())
134        .is_some_and(|ext| ext.eq_ignore_ascii_case("pcap"));
135    if !is_pcap {
136        return Err(Error::invalid_input("pcap output file must end in .pcap"));
137    }
138    Ok(path)
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn capture_limits_default_to_bounded_duration() {
147        let (duration, max_packets) = normalize_capture_limits(None, None).unwrap();
148        assert_eq!(
149            duration,
150            Some(std::time::Duration::from_secs(DEFAULT_PCAP_CAPTURE_SECONDS))
151        );
152        assert_eq!(max_packets, None);
153    }
154
155    #[test]
156    fn capture_limits_reject_zero_and_too_large_values() {
157        assert!(normalize_capture_limits(Some(0), None).is_err());
158        assert!(normalize_capture_limits(None, Some(0)).is_err());
159        assert!(normalize_capture_limits(Some(MAX_PCAP_CAPTURE_SECONDS + 1), None).is_err());
160        assert!(normalize_capture_limits(None, Some(MAX_PCAP_CAPTURE_PACKETS + 1)).is_err());
161    }
162
163    #[test]
164    fn capture_output_requires_pcap_extension() {
165        assert!(normalize_capture_output(Some("capture.pcap".to_string())).is_ok());
166        assert!(normalize_capture_output(Some("capture.txt".to_string())).is_err());
167    }
168}