Skip to main content

netscli_core/ops/
scan.rs

1use std::net::IpAddr;
2use std::sync::Arc;
3
4use super::config::Ops;
5use super::validation::parse_limited_ipv4_subnet;
6use crate::error::Result;
7use crate::{
8    default_ipv4_subnet_string, default_ports, validate_ports, DiscoverEngine, Host, InspectEngine,
9    InspectResult, PortResult, PortScanner, SweepEngine, SweepEntry,
10};
11
12/// Resolve the caller's port list, validating it.
13///
14/// Every scanning entry point funnels through here so the limits hold no
15/// matter which surface the call arrives on. Previously the 4,096 cap and
16/// the port-0 rejection lived only in the *parsers* — `parse_ports_checked`
17/// for the CLI/GUI and the MCP server's own `normalize_ports` — so the
18/// answers diverged (`netscli scan -p 0` reported "Scanned 1 port" while
19/// the MCP tool rejected the same input), and a library consumer calling
20/// `Ops` with a hand-built `Vec<u16>` bypassed both.
21fn resolve_ports(ports: Option<Vec<u16>>) -> Result<Vec<u16>> {
22    match ports {
23        Some(ports) => {
24            validate_ports(&ports)?;
25            Ok(ports)
26        }
27        None => Ok(default_ports()),
28    }
29}
30
31impl Ops {
32    pub async fn discover_ipv4(
33        &self,
34        subnet: Option<String>,
35        resolve: bool,
36    ) -> Result<(String, Vec<Host>)> {
37        self.discover_ipv4_with_progress(subnet, resolve, None)
38            .await
39    }
40
41    pub async fn discover_ipv4_with_progress(
42        &self,
43        subnet: Option<String>,
44        resolve: bool,
45        progress: Option<Arc<dyn Fn(crate::discover::DiscoverProgress) + Send + Sync>>,
46    ) -> Result<(String, Vec<Host>)> {
47        let subnet_str = subnet.unwrap_or_else(default_ipv4_subnet_string);
48        let net = parse_limited_ipv4_subnet(&subnet_str)?;
49        let engine = DiscoverEngine::new_with_timeouts(
50            self.cfg.concurrency,
51            self.cfg.ping_timeout_ms,
52            self.cfg.dns_timeout_ms,
53        );
54        let hosts = engine
55            .scan_subnet_with_progress(net, resolve, progress)
56            .await?;
57        Ok((subnet_str, hosts))
58    }
59
60    pub async fn scan_ports(
61        &self,
62        host: &str,
63        ports: Option<Vec<u16>>,
64    ) -> Result<(IpAddr, Vec<PortResult>)> {
65        self.scan_ports_with_progress(host, ports, None).await
66    }
67
68    pub async fn scan_ports_with_progress(
69        &self,
70        host: &str,
71        ports: Option<Vec<u16>>,
72        progress: Option<Arc<dyn Fn(crate::scan::PortScanProgress) + Send + Sync>>,
73    ) -> Result<(IpAddr, Vec<PortResult>)> {
74        let ip = self.resolve_host_ip(host).await?;
75        let ports = resolve_ports(ports)?;
76        let scanner = PortScanner::new(self.cfg.concurrency);
77        let results = scanner
78            .scan_host_with_progress(ip, ports, self.cfg.scan_timeout_ms, progress)
79            .await?;
80        Ok((ip, results))
81    }
82
83    /// Probe `ports` on `host` over UDP, or [`crate::DEFAULT_UDP_PORTS`] when
84    /// none are given. See `scan/udp.rs` for what each status means.
85    pub async fn scan_udp_ports_with_progress(
86        &self,
87        host: &str,
88        ports: Option<Vec<u16>>,
89        progress: Option<Arc<dyn Fn(crate::scan::PortScanProgress) + Send + Sync>>,
90    ) -> Result<(IpAddr, Vec<PortResult>)> {
91        let ip = self.resolve_host_ip(host).await?;
92        let ports = match ports {
93            Some(ports) => {
94                validate_ports(&ports)?;
95                ports
96            }
97            None => crate::DEFAULT_UDP_PORTS.to_vec(),
98        };
99        let scanner = crate::UdpScanner::new(self.cfg.concurrency);
100        let results = scanner
101            .scan_host_with_progress(ip, ports, self.cfg.scan_timeout_ms, progress)
102            .await?;
103        Ok((ip, results))
104    }
105
106    pub async fn scan_udp_ports(
107        &self,
108        host: &str,
109        ports: Option<Vec<u16>>,
110    ) -> Result<(IpAddr, Vec<PortResult>)> {
111        self.scan_udp_ports_with_progress(host, ports, None).await
112    }
113
114    pub async fn inspect_host(
115        &self,
116        host: String,
117        ports: Option<Vec<u16>>,
118    ) -> Result<InspectResult> {
119        let ports = resolve_ports(ports)?;
120        let engine = InspectEngine::new_with_timeouts(
121            self.cfg.concurrency,
122            self.cfg.ping_timeout_ms,
123            self.cfg.scan_timeout_ms,
124            self.cfg.dns_timeout_ms,
125        );
126        engine.inspect(host, ports).await
127    }
128
129    pub async fn sweep_ipv4(
130        &self,
131        subnet: Option<String>,
132        ports: Option<Vec<u16>>,
133        resolve_hostnames: bool,
134    ) -> Result<(String, Vec<SweepEntry>)> {
135        self.sweep_ipv4_with_progress(subnet, ports, resolve_hostnames, None)
136            .await
137    }
138
139    pub async fn sweep_ipv4_with_progress(
140        &self,
141        subnet: Option<String>,
142        ports: Option<Vec<u16>>,
143        resolve_hostnames: bool,
144        progress: Option<Arc<dyn Fn(crate::sweep::SweepProgress) + Send + Sync>>,
145    ) -> Result<(String, Vec<SweepEntry>)> {
146        let subnet_str = subnet.unwrap_or_else(default_ipv4_subnet_string);
147        let net = parse_limited_ipv4_subnet(&subnet_str)?;
148        let ports = resolve_ports(ports)?;
149        let engine = SweepEngine::new_with_timeouts(
150            self.cfg.concurrency,
151            self.cfg.ping_timeout_ms,
152            self.cfg.scan_timeout_ms,
153            self.cfg.dns_timeout_ms,
154        );
155        let results = engine
156            .sweep_with_progress(net, ports, resolve_hostnames, progress)
157            .await?;
158        Ok((subnet_str, results))
159    }
160}