1use ipnet::Ipv4Net;
2use serde::Serialize;
3use std::net::IpAddr;
4use std::str::FromStr;
5use std::sync::Arc;
6
7use crate::error::{Error, Result};
8use crate::{
9 default_ipv4_subnet_string, default_ports, ArpEntry, DiscoverEngine, Host, InspectEngine,
10 InspectResult, InterfaceInfo, NetworkManager, PcapCancelToken, PcapConfig, PcapEngine,
11 PcapResult, PingScanner, PortResult, PortScanner, SweepEngine, SweepEntry, DEFAULT_CONCURRENCY,
12 DEFAULT_DNS_TIMEOUT_MS, DEFAULT_PING_TIMEOUT_MS, DEFAULT_SCAN_TIMEOUT_MS,
13};
14
15const MAX_SUBNET_ADDRESSES: u64 = 1 << 16; fn ensure_subnet_limit(net: &Ipv4Net, subnet_str: &str) -> Result<()> {
18 let prefix = net.prefix_len() as u32;
19 let host_bits = 32u32.saturating_sub(prefix);
20 let total = 1u64.checked_shl(host_bits).unwrap_or(u64::MAX);
21 if total > MAX_SUBNET_ADDRESSES {
22 return Err(Error::invalid_input(format!(
23 "subnet too large: {subnet_str} (max /16)"
24 )));
25 }
26 Ok(())
27}
28
29#[derive(Debug, Clone)]
30pub struct OpsConfig {
31 pub concurrency: usize,
32 pub scan_timeout_ms: u64,
33 pub ping_timeout_ms: u64,
34 pub dns_timeout_ms: u64,
35}
36
37impl Default for OpsConfig {
38 fn default() -> Self {
39 Self {
40 concurrency: DEFAULT_CONCURRENCY,
41 scan_timeout_ms: DEFAULT_SCAN_TIMEOUT_MS,
42 ping_timeout_ms: DEFAULT_PING_TIMEOUT_MS,
43 dns_timeout_ms: DEFAULT_DNS_TIMEOUT_MS,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Default)]
50pub struct Ops {
51 cfg: OpsConfig,
52}
53
54impl Ops {
55 pub fn new(cfg: OpsConfig) -> Self {
56 let mut cfg = cfg;
57 cfg.concurrency = cfg.concurrency.clamp(1, 1024);
62 cfg.scan_timeout_ms = cfg.scan_timeout_ms.max(1);
63 cfg.ping_timeout_ms = cfg.ping_timeout_ms.max(1);
64 cfg.dns_timeout_ms = cfg.dns_timeout_ms.max(1);
65 Self { cfg }
66 }
67
68 pub fn config(&self) -> &OpsConfig {
69 &self.cfg
70 }
71
72 pub async fn resolve_host_ip(&self, host: &str) -> Result<IpAddr> {
73 resolve_host_ip_with_timeout(host, self.cfg.dns_timeout_ms).await
74 }
75
76 pub async fn discover_ipv4(
77 &self,
78 subnet: Option<String>,
79 resolve: bool,
80 ) -> Result<(String, Vec<Host>)> {
81 self.discover_ipv4_with_progress(subnet, resolve, None)
82 .await
83 }
84
85 pub async fn discover_ipv4_with_progress(
86 &self,
87 subnet: Option<String>,
88 resolve: bool,
89 progress: Option<Arc<dyn Fn(crate::discover::DiscoverProgress) + Send + Sync>>,
90 ) -> Result<(String, Vec<Host>)> {
91 let subnet_str = subnet.unwrap_or_else(default_ipv4_subnet_string);
92 let net: Ipv4Net = subnet_str.parse().map_err(|e| {
93 Error::invalid_input(format!("Invalid subnet format '{subnet_str}': {e}"))
94 })?;
95 ensure_subnet_limit(&net, &subnet_str)?;
96 let engine = DiscoverEngine::new_with_timeouts(
97 self.cfg.concurrency,
98 self.cfg.ping_timeout_ms,
99 self.cfg.dns_timeout_ms,
100 );
101 let hosts = engine
102 .scan_subnet_with_progress(net, resolve, progress)
103 .await;
104 Ok((subnet_str, hosts))
105 }
106
107 pub async fn scan_ports(
108 &self,
109 host: &str,
110 ports: Option<Vec<u16>>,
111 ) -> Result<(IpAddr, Vec<PortResult>)> {
112 self.scan_ports_with_progress(host, ports, None).await
113 }
114
115 pub async fn scan_ports_with_progress(
116 &self,
117 host: &str,
118 ports: Option<Vec<u16>>,
119 progress: Option<Arc<dyn Fn(crate::scan::PortScanProgress) + Send + Sync>>,
120 ) -> Result<(IpAddr, Vec<PortResult>)> {
121 let ip = self.resolve_host_ip(host).await?;
122 let ports = ports.unwrap_or_else(default_ports);
123 let scanner = PortScanner::new(self.cfg.concurrency);
124 let results = scanner
125 .scan_host_with_progress(ip, ports, self.cfg.scan_timeout_ms, progress)
126 .await;
127 Ok((ip, results))
128 }
129
130 pub async fn inspect_host(
131 &self,
132 host: String,
133 ports: Option<Vec<u16>>,
134 ) -> Result<InspectResult> {
135 let ports = ports.unwrap_or_else(default_ports);
136 let engine = InspectEngine::new_with_timeouts(
137 self.cfg.concurrency,
138 self.cfg.ping_timeout_ms,
139 self.cfg.scan_timeout_ms,
140 self.cfg.dns_timeout_ms,
141 );
142 engine.inspect(host, ports).await
143 }
144
145 pub async fn sweep_ipv4(
146 &self,
147 subnet: Option<String>,
148 ports: Option<Vec<u16>>,
149 resolve_hostnames: bool,
150 ) -> Result<(String, Vec<SweepEntry>)> {
151 self.sweep_ipv4_with_progress(subnet, ports, resolve_hostnames, None)
152 .await
153 }
154
155 pub async fn sweep_ipv4_with_progress(
156 &self,
157 subnet: Option<String>,
158 ports: Option<Vec<u16>>,
159 resolve_hostnames: bool,
160 progress: Option<Arc<dyn Fn(crate::sweep::SweepProgress) + Send + Sync>>,
161 ) -> Result<(String, Vec<SweepEntry>)> {
162 let subnet_str = subnet.unwrap_or_else(default_ipv4_subnet_string);
163 let net: Ipv4Net = subnet_str.parse().map_err(|e| {
164 Error::invalid_input(format!("Invalid subnet format '{subnet_str}': {e}"))
165 })?;
166 ensure_subnet_limit(&net, &subnet_str)?;
167 let ports = ports.unwrap_or_else(default_ports);
168 let engine = SweepEngine::new_with_timeouts(
169 self.cfg.concurrency,
170 self.cfg.ping_timeout_ms,
171 self.cfg.scan_timeout_ms,
172 self.cfg.dns_timeout_ms,
173 );
174 let results = engine
175 .sweep_with_progress(net, ports, resolve_hostnames, progress)
176 .await?;
177 Ok((subnet_str, results))
178 }
179
180 pub async fn ping_host_summary(&self, host: &str, count: u32) -> Result<PingSummary> {
181 let ip = self.resolve_host_ip(host).await?;
182 let scanner = PingScanner::new(1);
183 let mut sent: u32 = 0;
184 let mut received: u32 = 0;
185 let mut rtts: Vec<u64> = Vec::new();
186
187 for _ in 0..count {
188 sent += 1;
189 let res = scanner.ping(ip, self.cfg.ping_timeout_ms).await;
190 if res.alive {
191 received += 1;
192 if let Some(rtt) = res.rtt_ms {
193 rtts.push(rtt);
194 }
195 }
196 }
197
198 Ok(PingSummary::new(host.to_string(), ip, sent, received, rtts))
199 }
200
201 pub async fn dns_lookup(
202 &self,
203 host: &str,
204 record: Option<String>,
205 ) -> Result<Vec<crate::dns::DnsRecord>> {
206 let record = record.map(|r| r.trim().to_uppercase());
207 if record.as_deref().is_none() || matches!(record.as_deref(), Some("ALL" | "ANY")) {
208 return crate::dns::lookup_all_records_timeout(host, self.cfg.dns_timeout_ms).await;
209 }
210
211 let record = record.unwrap_or_else(|| "A".to_string());
212 let Some(parsed) = crate::dns::parse_record_type(&record) else {
213 return Err(Error::invalid_input(format!(
214 "unsupported DNS record type '{record}'"
215 )));
216 };
217
218 crate::dns::lookup_record_timeout(host, parsed, self.cfg.dns_timeout_ms).await
219 }
220
221 pub fn list_interfaces(&self) -> Vec<InterfaceInfo> {
222 NetworkManager::get_interfaces()
223 }
224
225 #[cfg(feature = "mdns")]
231 pub async fn discover_mdns(
232 &self,
233 service_types: &[String],
234 timeout: std::time::Duration,
235 ) -> Result<Vec<crate::mdns::MdnsService>> {
236 if service_types.is_empty() {
237 crate::mdns::MdnsEngine::discover_common(timeout).await
238 } else {
239 let refs: Vec<&str> = service_types.iter().map(String::as_str).collect();
240 crate::mdns::MdnsEngine::discover(&refs, timeout).await
241 }
242 }
243
244 pub fn get_arp_table(&self) -> Result<Vec<ArpEntry>> {
245 NetworkManager::get_arp_table()
246 }
247
248 pub fn pcap_check_support(&self) -> Result<Vec<String>> {
249 PcapEngine::check_support()
250 }
251
252 pub fn capture_pcap(
253 &self,
254 interface: String,
255 filter: Option<String>,
256 duration: Option<u64>,
257 output_file: Option<String>,
258 max_packets: Option<usize>,
259 ) -> Result<PcapResult> {
260 let cfg = PcapConfig {
261 interface,
262 filter,
263 output_file: output_file
264 .unwrap_or_else(|| "capture.pcap".to_string())
265 .into(),
266 duration: duration.map(std::time::Duration::from_secs),
267 max_packets,
268 };
269 PcapEngine::capture(cfg)
270 }
271
272 pub async fn capture_pcap_async(
278 &self,
279 interface: String,
280 filter: Option<String>,
281 duration: Option<u64>,
282 output_file: Option<String>,
283 max_packets: Option<usize>,
284 ) -> Result<PcapResult> {
285 self.capture_pcap_async_with_cancel(
286 interface,
287 filter,
288 duration,
289 output_file,
290 max_packets,
291 None,
292 )
293 .await
294 }
295
296 pub async fn capture_pcap_async_with_cancel(
297 &self,
298 interface: String,
299 filter: Option<String>,
300 duration: Option<u64>,
301 output_file: Option<String>,
302 max_packets: Option<usize>,
303 cancel: Option<PcapCancelToken>,
304 ) -> Result<PcapResult> {
305 let cfg = PcapConfig {
306 interface,
307 filter,
308 output_file: output_file
309 .unwrap_or_else(|| "capture.pcap".to_string())
310 .into(),
311 duration: duration.map(std::time::Duration::from_secs),
312 max_packets,
313 };
314
315 let task =
316 tokio::task::spawn_blocking(move || PcapEngine::capture_with_cancel(cfg, cancel));
317 match task.await {
318 Ok(res) => Ok(res?),
319 Err(e) => Err(Error::Other(format!("pcap capture task failed: {e}"))),
320 }
321 }
322}
323
324#[derive(Debug, Clone, Serialize)]
325pub struct PingSummary {
326 pub host: String,
327 pub ip: IpAddr,
328 pub sent: u32,
329 pub received: u32,
330 pub loss_pct: f64,
331 pub rtt_ms_min: Option<u64>,
332 pub rtt_ms_max: Option<u64>,
333 pub rtt_ms_avg: Option<f64>,
334}
335
336impl PingSummary {
337 fn new(host: String, ip: IpAddr, sent: u32, received: u32, rtts: Vec<u64>) -> Self {
338 let loss_pct = if sent == 0 {
339 0.0
340 } else {
341 100.0 * (sent - received) as f64 / sent as f64
342 };
343 let rtt_ms_min = rtts.iter().min().copied();
344 let rtt_ms_max = rtts.iter().max().copied();
345 let rtt_ms_avg = if rtts.is_empty() {
346 None
347 } else {
348 Some((rtts.iter().sum::<u64>() as f64) / (rtts.len() as f64))
349 };
350 Self {
351 host,
352 ip,
353 sent,
354 received,
355 loss_pct,
356 rtt_ms_min,
357 rtt_ms_max,
358 rtt_ms_avg,
359 }
360 }
361}
362
363pub async fn resolve_host_ip(host: &str) -> Result<IpAddr> {
368 resolve_host_ip_with_timeout(host, crate::DEFAULT_DNS_TIMEOUT_MS).await
369}
370
371pub async fn resolve_host_ip_with_timeout(host: &str, dns_timeout_ms: u64) -> Result<IpAddr> {
372 if let Ok(ip) = IpAddr::from_str(host) {
373 return Ok(ip);
374 }
375
376 if let Ok(v4s) = crate::dns::resolve_a_timeout(host, dns_timeout_ms).await {
377 if let Some(first) = v4s.first() {
378 return IpAddr::from_str(first).map_err(|e| {
379 Error::dns(format!("invalid IPv4 address '{first}' from resolver: {e}"))
380 });
381 }
382 }
383 if let Ok(v6s) = crate::dns::resolve_aaaa_timeout(host, dns_timeout_ms).await {
384 if let Some(first) = v6s.first() {
385 return IpAddr::from_str(first).map_err(|e| {
386 Error::dns(format!("invalid IPv6 address '{first}' from resolver: {e}"))
387 });
388 }
389 }
390
391 Err(Error::dns(format!("unable to resolve host '{host}'")))
392}