Skip to main content

netscli_core/ops/
host.rs

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        // Clamped, not rejected: asking for more pings than the cap is a
16        // request for "a lot", and the loop below is sequential, so the count
17        // multiplies directly into how long this call blocks.
18        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
124/// Resolve a host string to an IP address.
125///
126/// - Accepts literal IPv4/IPv6 strings.
127/// - Otherwise resolves A first, then AAAA.
128pub 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}