Skip to main content

netscli_core/dns/
lookup.rs

1use hickory_resolver::lookup::Lookup;
2use hickory_resolver::proto::rr::{RData, RecordType};
3use std::time::Duration;
4use tokio::time::timeout;
5
6use super::records::{normalize_value, ALL_RECORD_TYPES};
7use super::resolver::{fallback_resolver, shared_resolver};
8use super::types::DnsRecord;
9use crate::error::{Error, Result};
10
11pub async fn lookup_record_timeout(
12    host: &str,
13    record_type: RecordType,
14    timeout_ms: u64,
15) -> Result<Vec<DnsRecord>> {
16    let (response, resolver_source) = lookup_with_fallback(host, record_type, timeout_ms).await?;
17
18    // hickory 0.26 dropped `Lookup::record_iter()` in favor of explicit
19    // `.answers()` / `.authorities()` / `.additionals()` slice accessors.
20    // We only ever care about answer records.
21    // hickory 0.26 made `Record::data` a public field instead of a method,
22    // and dropped the previous Option wrapper around RData. Accessor is
23    // now plain `record.data`.
24    let mut records = Vec::new();
25    for record in response.answers() {
26        records.push(DnsRecord {
27            record_type: record_type.to_string(),
28            value: normalize_value(&record.data.to_string()),
29            name: Some(normalize_value(&record.name.to_string())),
30            ttl_seconds: Some(record.ttl),
31            resolver_source: Some(resolver_source.to_string()),
32        });
33    }
34    Ok(records)
35}
36
37/// Look up every supported record type, returning a merged list.
38///
39/// Queries run sequentially. An earlier attempt to parallelize via
40/// `join_all` produced surprising regressions on Windows — a single slow
41/// record type (commonly CAA for many domains) would cause the whole
42/// batch to surface its timeout error even when other types had already
43/// returned results. The cached `shared_resolver()` keeps each
44/// individual query fast by avoiding `resolv.conf`/registry reparsing,
45/// so sequential iteration is not noticeably slower in practice and
46/// yields reliable partial-result behavior.
47///
48/// Partial failures (some types timeout, others return results) yield
49/// `Ok(records)` — only a total failure with zero records surfaces the
50/// last error.
51pub async fn lookup_all_records_timeout(host: &str, timeout_ms: u64) -> Result<Vec<DnsRecord>> {
52    let mut records = Vec::new();
53    let mut last_err: Option<Error> = None;
54
55    for record_type in ALL_RECORD_TYPES {
56        match lookup_record_timeout(host, *record_type, timeout_ms).await {
57            Ok(mut found) => records.append(&mut found),
58            Err(e) => last_err = Some(e),
59        }
60    }
61
62    if records.is_empty() {
63        if let Some(err) = last_err {
64            return Err(err);
65        }
66        return Err(Error::dns(format!("no DNS records found for {host}")));
67    }
68
69    Ok(records)
70}
71
72pub async fn resolve_a(host: &str) -> Result<Vec<String>> {
73    resolve_a_timeout(host, crate::DEFAULT_DNS_TIMEOUT_MS).await
74}
75
76pub async fn resolve_a_timeout(host: &str, timeout_ms: u64) -> Result<Vec<String>> {
77    let (response, _) = lookup_with_fallback(host, RecordType::A, timeout_ms).await?;
78    // hickory 0.26 returns the flat `Lookup` here (it used to be a typed
79    // `Ipv4Lookup` wrapper that yielded `&Ipv4Addr` directly). We now
80    // extract the IPv4 from each answer's RData::A variant.
81    Ok(response
82        .answers()
83        .iter()
84        .filter_map(|r| match &r.data {
85            RData::A(a) => Some(a.to_string()),
86            _ => None,
87        })
88        .collect())
89}
90
91pub async fn resolve_aaaa(host: &str) -> Result<Vec<String>> {
92    resolve_aaaa_timeout(host, crate::DEFAULT_DNS_TIMEOUT_MS).await
93}
94
95pub async fn resolve_aaaa_timeout(host: &str, timeout_ms: u64) -> Result<Vec<String>> {
96    let (response, _) = lookup_with_fallback(host, RecordType::AAAA, timeout_ms).await?;
97    Ok(response
98        .answers()
99        .iter()
100        .filter_map(|r| match &r.data {
101            RData::AAAA(a) => Some(a.to_string()),
102            _ => None,
103        })
104        .collect())
105}
106
107async fn lookup_with_fallback(
108    host: &str,
109    record_type: RecordType,
110    timeout_ms: u64,
111) -> Result<(Lookup, &'static str)> {
112    let resolver = shared_resolver()?;
113    match timeout(
114        Duration::from_millis(timeout_ms),
115        resolver.lookup(host, record_type),
116    )
117    .await
118    {
119        Ok(Ok(resp)) => Ok((resp, "system")),
120        Ok(Err(system_err)) => {
121            if !super::resolver::should_use_public_fallback(host) {
122                return Err(Error::dns(format!(
123                    "{record_type} lookup failed: system resolver returned {system_err}; public fallback disabled"
124                )));
125            }
126            let fallback = fallback_resolver()?;
127            match timeout(
128                Duration::from_millis(timeout_ms),
129                fallback.lookup(host, record_type),
130            )
131            .await
132            {
133                Ok(Ok(resp)) => Ok((resp, "public_fallback")),
134                Ok(Err(fallback_err)) => Err(Error::dns(format!(
135                    "{record_type} lookup failed: system resolver returned {system_err}; public fallback returned {fallback_err}"
136                ))),
137                Err(_) => Err(Error::dns(format!(
138                    "{record_type} lookup failed: system resolver returned {system_err}; public fallback timed out after {timeout_ms}ms"
139                ))),
140            }
141        }
142        Err(_) => Err(Error::Timeout(timeout_ms)),
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::resolve_a;
149    use crate::dns::DnsRecord;
150
151    #[tokio::test]
152    async fn test_resolve_a_localhost() {
153        let result = resolve_a("localhost").await;
154        assert!(result.is_ok());
155        let ips = result.unwrap();
156        assert!(!ips.is_empty());
157        // localhost should resolve to 127.0.0.1
158        assert!(ips.contains(&"127.0.0.1".to_string()));
159    }
160
161    // NOTE: Unit tests avoid network-dependent DNS behavior.
162    // Network integration tests should be added separately (and marked as ignored).
163
164    #[test]
165    fn dns_record_serializes_additive_metadata() {
166        let record = DnsRecord {
167            record_type: "A".to_string(),
168            value: "127.0.0.1".to_string(),
169            name: Some("localhost".to_string()),
170            ttl_seconds: Some(60),
171            resolver_source: Some("system".to_string()),
172        };
173
174        let value = serde_json::to_value(record).expect("serialize DNS record");
175        assert_eq!(value["name"], "localhost");
176        assert_eq!(value["ttl_seconds"], 60);
177        assert_eq!(value["resolver_source"], "system");
178    }
179}