1use serde::Serialize;
2use std::net::IpAddr;
3use std::str::FromStr;
4
5use super::config::Ops;
6use crate::error::{Error, Result};
7use crate::PingScanner;
8
9impl Ops {
10 pub async fn resolve_host_ip(&self, host: &str) -> Result<IpAddr> {
11 resolve_host_ip_with_timeout(host, self.cfg.dns_timeout_ms).await
12 }
13
14 pub async fn ping_host_summary(&self, host: &str, count: u32) -> Result<PingSummary> {
15 let count = count.min(crate::MAX_PING_COUNT);
19 let ip = self.resolve_host_ip(host).await?;
20 let scanner = PingScanner::new(1);
21 let mut sent: u32 = 0;
22 let mut received: u32 = 0;
23 let mut rtts: Vec<u64> = Vec::new();
24
25 for _ in 0..count {
26 sent += 1;
27 let res = scanner.ping(ip, self.cfg.ping_timeout_ms).await;
28 if res.alive {
29 received += 1;
30 if let Some(rtt) = res.rtt_ms {
31 rtts.push(rtt);
32 }
33 }
34 }
35
36 Ok(PingSummary::new(host.to_string(), ip, sent, received, rtts))
37 }
38
39 pub async fn trace_route(
40 &self,
41 host: &str,
42 max_hops: u32,
43 resolve: bool,
44 ) -> Result<crate::TraceResult> {
45 crate::trace_route(host, max_hops, resolve, None).await
46 }
47
48 pub async fn trace_route_with_progress(
49 &self,
50 host: &str,
51 max_hops: u32,
52 resolve: bool,
53 progress: Option<tokio::sync::watch::Sender<String>>,
54 ) -> Result<crate::TraceResult> {
55 crate::trace_route(host, max_hops, resolve, progress).await
56 }
57
58 pub async fn reverse_lookup(&self, ip: &str) -> Result<Option<String>> {
59 let ip = IpAddr::from_str(ip)
60 .map_err(|e| Error::invalid_input(format!("invalid IP address '{ip}': {e}")))?;
61 Ok(crate::dns::reverse_lookup_best_effort_timeout(ip, self.cfg.dns_timeout_ms).await)
62 }
63
64 pub async fn dns_lookup(
65 &self,
66 host: &str,
67 record: Option<String>,
68 ) -> Result<Vec<crate::dns::DnsRecord>> {
69 let record = record.map(|r| r.trim().to_uppercase());
70 if record.as_deref().is_none() || matches!(record.as_deref(), Some("ALL" | "ANY")) {
71 return crate::dns::lookup_all_records_timeout(host, self.cfg.dns_timeout_ms).await;
72 }
73
74 let record = record.unwrap_or_else(|| "A".to_string());
75 let Some(parsed) = crate::dns::parse_record_type(&record) else {
76 return Err(Error::invalid_input(format!(
77 "unsupported DNS record type '{record}'"
78 )));
79 };
80
81 crate::dns::lookup_record_timeout(host, parsed, self.cfg.dns_timeout_ms).await
82 }
83}
84
85#[derive(Debug, Clone, Serialize)]
86pub struct PingSummary {
87 pub host: String,
88 pub ip: IpAddr,
89 pub sent: u32,
90 pub received: u32,
91 pub loss_pct: f64,
92 pub rtt_ms_min: Option<u64>,
93 pub rtt_ms_max: Option<u64>,
94 pub rtt_ms_avg: Option<f64>,
95}
96
97impl PingSummary {
98 fn new(host: String, ip: IpAddr, sent: u32, received: u32, rtts: Vec<u64>) -> Self {
99 let loss_pct = if sent == 0 {
100 0.0
101 } else {
102 100.0 * (sent - received) as f64 / sent as f64
103 };
104 let rtt_ms_min = rtts.iter().min().copied();
105 let rtt_ms_max = rtts.iter().max().copied();
106 let rtt_ms_avg = if rtts.is_empty() {
107 None
108 } else {
109 Some((rtts.iter().sum::<u64>() as f64) / (rtts.len() as f64))
110 };
111 Self {
112 host,
113 ip,
114 sent,
115 received,
116 loss_pct,
117 rtt_ms_min,
118 rtt_ms_max,
119 rtt_ms_avg,
120 }
121 }
122}
123
124pub async fn resolve_host_ip(host: &str) -> Result<IpAddr> {
129 resolve_host_ip_with_timeout(host, crate::DEFAULT_DNS_TIMEOUT_MS).await
130}
131
132pub async fn resolve_host_ip_with_timeout(host: &str, dns_timeout_ms: u64) -> Result<IpAddr> {
133 if let Ok(ip) = IpAddr::from_str(host) {
134 return Ok(ip);
135 }
136
137 if let Ok(v4s) = crate::dns::resolve_a_timeout(host, dns_timeout_ms).await {
138 if let Some(first) = v4s.first() {
139 return IpAddr::from_str(first).map_err(|e| {
140 Error::dns(format!("invalid IPv4 address '{first}' from resolver: {e}"))
141 });
142 }
143 }
144 if let Ok(v6s) = crate::dns::resolve_aaaa_timeout(host, dns_timeout_ms).await {
145 if let Some(first) = v6s.first() {
146 return IpAddr::from_str(first).map_err(|e| {
147 Error::dns(format!("invalid IPv6 address '{first}' from resolver: {e}"))
148 });
149 }
150 }
151
152 Err(Error::dns(format!("unable to resolve host '{host}'")))
153}