netscli_core/dns/
lookup.rs1use 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 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
37pub 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 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 assert!(ips.contains(&"127.0.0.1".to_string()));
159 }
160
161 #[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}