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