1use std::net::{IpAddr, Ipv4Addr, SocketAddr};
4use std::sync::Arc;
5use std::time::Duration;
6
7use hickory_resolver::config::{
8 ConnectionConfig, NameServerConfig, ProtocolConfig, ResolverConfig, ResolverOpts,
9};
10use hickory_resolver::net::runtime::TokioRuntimeProvider;
11use hickory_resolver::proto::rr::RecordType;
12use hickory_resolver::{Resolver, TokioResolver};
13
14use crate::error::Error;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum DnsVerdict {
18 InUse,
19 Absent,
21 NoAnswer,
22}
23
24#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
25pub struct DnsRecords {
26 pub by_type: Vec<(String, Vec<String>)>,
27}
28
29impl DnsRecords {
30 #[must_use]
31 pub fn is_empty(&self) -> bool {
32 self.by_type.is_empty()
33 }
34}
35
36const RECORD_TYPES: &[(RecordType, &str)] = &[
37 (RecordType::A, "a"),
38 (RecordType::AAAA, "aaaa"),
39 (RecordType::NS, "ns"),
40 (RecordType::MX, "mx"),
41 (RecordType::TXT, "txt"),
42 (RecordType::SOA, "soa"),
43];
44
45#[derive(Debug, Clone)]
47pub struct Resolvers {
48 strict: TokioResolver,
49 lenient: TokioResolver,
50}
51
52impl Resolvers {
53 pub(crate) async fn lookup(
54 &self,
55 name: &str,
56 record_type: RecordType,
57 ) -> Result<hickory_resolver::lookup::Lookup, hickory_resolver::net::NetError> {
58 match self.strict.lookup(name, record_type).await {
59 Ok(answer) => Ok(answer),
60 Err(error) if looks_absent(&error.to_string()) => Err(error),
62 Err(_) => self.lenient.lookup(name, record_type).await,
63 }
64 }
65
66 pub(crate) async fn lookup_ip(
67 &self,
68 host: &str,
69 ) -> Result<hickory_resolver::lookup_ip::LookupIp, hickory_resolver::net::NetError> {
70 match self.strict.lookup_ip(host).await {
71 Ok(answer) => Ok(answer),
72 Err(error) if looks_absent(&error.to_string()) => Err(error),
73 Err(_) => self.lenient.lookup_ip(host).await,
74 }
75 }
76}
77
78pub(crate) fn build(timeout: Duration) -> Result<Resolvers, Error> {
79 Ok(Resolvers {
80 strict: build_one(timeout, true)?,
81 lenient: build_one(timeout, false)?,
82 })
83}
84
85fn build_one(timeout: Duration, validate: bool) -> Result<TokioResolver, Error> {
86 let mut options = ResolverOpts::default();
87 options.timeout = (timeout / 4).max(Duration::from_secs(2));
89 options.attempts = 2;
90 options.try_tcp_on_error = true;
92 options.validate = validate;
93
94 if let Ok(builder) = Resolver::builder_tokio()
95 && let Ok(resolver) = builder.with_options(options.clone()).build()
96 {
97 return Ok(resolver);
98 }
99
100 let servers = [
102 IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)),
103 IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)),
104 ]
105 .into_iter()
106 .map(|ip| {
107 NameServerConfig::new(
108 ip,
109 true,
110 vec![
111 ConnectionConfig::new(ProtocolConfig::Udp),
112 ConnectionConfig::new(ProtocolConfig::Tcp),
113 ],
114 )
115 })
116 .collect();
117
118 let config = ResolverConfig::from_parts(None, Vec::new(), servers);
119 Resolver::builder_with_config(config, TokioRuntimeProvider::default())
120 .with_options(options)
121 .build()
122 .map_err(|source| Error::NetworkUnreachable {
123 source: Box::new(source),
124 })
125}
126
127pub(crate) async fn query(resolver: &Resolvers, domain: &str) -> DnsVerdict {
129 let mut saw_absence = false;
130
131 for record_type in [RecordType::NS, RecordType::SOA, RecordType::A] {
132 match resolver.lookup(domain, record_type).await {
133 Ok(answer) if !answer.answers().is_empty() => return DnsVerdict::InUse,
134 Ok(_) => saw_absence = true,
135 Err(error) => {
136 if looks_absent(&error.to_string()) {
137 saw_absence = true;
138 } else {
139 return DnsVerdict::NoAnswer;
140 }
141 }
142 }
143 }
144
145 if saw_absence {
146 DnsVerdict::Absent
147 } else {
148 DnsVerdict::NoAnswer
149 }
150}
151
152fn record_value(line: &str) -> String {
154 let value = line
155 .split_whitespace()
156 .skip(4)
157 .collect::<Vec<_>>()
158 .join(" ");
159 crate::lookup::scrub(&value)
160}
161
162pub async fn dns_records(resolver: &Resolvers, domain: &str) -> DnsRecords {
163 let mut by_type = Vec::new();
164
165 for (record_type, label) in RECORD_TYPES {
166 let Ok(answer) = resolver.lookup(domain, *record_type).await else {
167 continue;
168 };
169 let mut values: Vec<String> = answer
170 .answers()
171 .iter()
172 .filter(|record| record.record_type() == *record_type)
173 .map(|record| record_value(&record.to_string()))
174 .filter(|value| !value.is_empty())
175 .collect();
176 values.sort_unstable();
177 values.dedup();
178 if !values.is_empty() {
179 by_type.push(((*label).to_owned(), values));
180 }
181 }
182
183 DnsRecords { by_type }
184}
185
186fn looks_absent(message: &str) -> bool {
188 let message = message.to_lowercase();
189 let absent = message.contains("no records resolved")
190 || message.contains("nxdomain")
191 || message.contains("no record");
192 let unreachable = message.contains("timed out")
193 || message.contains("timeout")
194 || message.contains("connection")
195 || message.contains("no connections");
196 absent && !unreachable
197}
198
199#[derive(Debug)]
201pub(crate) struct HttpResolver(Resolvers);
202
203impl HttpResolver {
204 #[must_use]
205 pub(crate) fn new(resolver: &Resolvers) -> Arc<Self> {
206 Arc::new(Self(resolver.clone()))
207 }
208}
209
210impl reqwest::dns::Resolve for HttpResolver {
211 fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
212 let resolver = self.0.clone();
213 Box::pin(async move {
214 let resolved = resolver.lookup_ip(name.as_str()).await?;
215 let sockets: Vec<SocketAddr> =
217 resolved.iter().map(|ip| SocketAddr::new(ip, 0)).collect();
218 let addresses: reqwest::dns::Addrs = Box::new(sockets.into_iter());
219 Ok(addresses)
220 })
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn a_resolver_builds_without_panicking() {
230 assert!(build(Duration::from_secs(8)).is_ok());
231 }
232
233 #[test]
234 fn absence_is_told_apart_from_unreachability() {
235 assert!(looks_absent("no records resolved for Query"));
236 assert!(looks_absent("NXDOMAIN reported by server"));
237
238 assert!(!looks_absent("request timed out"));
239 assert!(!looks_absent("no connections available"));
240 assert!(!looks_absent("connection refused"));
241 assert!(!looks_absent("no records resolved: request timed out"));
242 }
243
244 #[test]
245 fn dns_has_no_way_to_say_available() {
246 for verdict in [DnsVerdict::InUse, DnsVerdict::Absent, DnsVerdict::NoAnswer] {
248 assert!(matches!(
249 verdict,
250 DnsVerdict::InUse | DnsVerdict::Absent | DnsVerdict::NoAnswer
251 ));
252 }
253 }
254
255 #[test]
256 fn empty_records_report_empty() {
257 assert!(DnsRecords::default().is_empty());
258 let filled = DnsRecords {
259 by_type: vec![("a".to_owned(), vec!["127.0.0.1".to_owned()])],
260 };
261 assert!(!filled.is_empty());
262 }
263
264 #[test]
265 fn record_data_carrying_an_escape_sequence_cannot_reach_the_terminal() {
266 let hostile = "example.test. 300 IN TXT \u{1b}[2K\u{1b}[Aforged";
267 let shaped = record_value(hostile);
268 assert!(
269 !shaped.contains('\u{1b}'),
270 "a zone owner must not be able to move the cursor"
271 );
272 assert!(shaped.contains("forged"), "the readable text still shows");
273 }
274}