iroh_net/dns.rs
1//! This module exports a DNS resolver, which is also the default resolver used in the
2//! [`crate::Endpoint`] if no custom resolver is configured.
3//!
4//! It also exports [`ResolverExt`]: A extension trait over [`DnsResolver`] to perform DNS queries
5//! by ipv4, ipv6, name and node_id. See the [`node_info`] module documentation for details on how
6//! iroh node records are structured.
7
8use std::{
9 fmt::Write,
10 net::{IpAddr, Ipv6Addr},
11 time::Duration,
12};
13
14use anyhow::Result;
15use futures_lite::{Future, StreamExt};
16use hickory_resolver::{AsyncResolver, IntoName, TokioAsyncResolver};
17use iroh_base::{key::NodeId, node_addr::NodeAddr};
18use once_cell::sync::Lazy;
19
20pub mod node_info;
21
22/// The DNS resolver type used throughout `iroh-net`.
23pub type DnsResolver = TokioAsyncResolver;
24
25static DNS_RESOLVER: Lazy<TokioAsyncResolver> =
26 Lazy::new(|| create_default_resolver().expect("unable to create DNS resolver"));
27
28/// Get a reference to the default DNS resolver.
29///
30/// The default resolver can be cheaply cloned and is shared throughout the running process.
31/// It is configured to use the system's DNS configuration.
32pub fn default_resolver() -> &'static DnsResolver {
33 &DNS_RESOLVER
34}
35
36/// Get the DNS resolver used within iroh-net.
37pub fn resolver() -> &'static TokioAsyncResolver {
38 Lazy::force(&DNS_RESOLVER)
39}
40
41/// Deprecated IPv6 site-local anycast addresses still configured by windows.
42///
43/// Windows still configures these site-local addresses as soon even as an IPv6 loopback
44/// interface is configured. We do not want to use these DNS servers, the chances of them
45/// being usable are almost always close to zero, while the chance of DNS configuration
46/// **only** relying on these servers and not also being configured normally are also almost
47/// zero. The chance of the DNS resolver accidentally trying one of these and taking a
48/// bunch of timeouts to figure out they're no good are on the other hand very high.
49const WINDOWS_BAD_SITE_LOCAL_DNS_SERVERS: [IpAddr; 3] = [
50 IpAddr::V6(Ipv6Addr::new(0xfec0, 0, 0, 0xffff, 0, 0, 0, 1)),
51 IpAddr::V6(Ipv6Addr::new(0xfec0, 0, 0, 0xffff, 0, 0, 0, 2)),
52 IpAddr::V6(Ipv6Addr::new(0xfec0, 0, 0, 0xffff, 0, 0, 0, 3)),
53];
54
55/// Get resolver to query MX records.
56///
57/// We first try to read the system's resolver from `/etc/resolv.conf`.
58/// This does not work at least on some Androids, therefore we fallback
59/// to the default `ResolverConfig` which uses eg. to google's `8.8.8.8` or `8.8.4.4`.
60fn create_default_resolver() -> Result<TokioAsyncResolver> {
61 let (system_config, mut options) =
62 hickory_resolver::system_conf::read_system_conf().unwrap_or_default();
63
64 // Copy all of the system config, but strip the bad windows nameservers. Unfortunately
65 // there is no easy way to do this.
66 let mut config = hickory_resolver::config::ResolverConfig::new();
67 if let Some(name) = system_config.domain() {
68 config.set_domain(name.clone());
69 }
70 for name in system_config.search() {
71 config.add_search(name.clone());
72 }
73 for nameserver_cfg in system_config.name_servers() {
74 if !WINDOWS_BAD_SITE_LOCAL_DNS_SERVERS.contains(&nameserver_cfg.socket_addr.ip()) {
75 config.add_name_server(nameserver_cfg.clone());
76 }
77 }
78
79 // see [`ResolverExt::lookup_ipv4_ipv6`] for info on why we avoid `LookupIpStrategy::Ipv4AndIpv6`
80 options.ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv4thenIpv6;
81
82 let resolver = AsyncResolver::tokio(config, options);
83 Ok(resolver)
84}
85
86/// Extension trait to [`DnsResolver`].
87pub trait ResolverExt {
88 /// Perform an ipv4 lookup with a timeout.
89 fn lookup_ipv4<N: IntoName>(
90 &self,
91 host: N,
92 timeout: Duration,
93 ) -> impl Future<Output = Result<impl Iterator<Item = IpAddr>>>;
94
95 /// Perform an ipv6 lookup with a timeout.
96 fn lookup_ipv6<N: IntoName>(
97 &self,
98 host: N,
99 timeout: Duration,
100 ) -> impl Future<Output = Result<impl Iterator<Item = IpAddr>>>;
101
102 /// Race an ipv4 and ipv6 lookup with a timeout.
103 fn lookup_ipv4_ipv6<N: IntoName + Clone>(
104 &self,
105 host: N,
106 timeout: Duration,
107 ) -> impl Future<Output = Result<impl Iterator<Item = IpAddr>>>;
108
109 /// Looks up node info by DNS name.
110 fn lookup_by_name(&self, name: &str) -> impl Future<Output = Result<NodeAddr>>;
111
112 /// Looks up node info by [`NodeId`] and origin domain name.
113 fn lookup_by_id(
114 &self,
115 node_id: &NodeId,
116 origin: &str,
117 ) -> impl Future<Output = Result<NodeAddr>>;
118
119 /// Perform an ipv4 lookup with a timeout in a staggered fashion.
120 ///
121 /// From the moment this function is called, each lookup is scheduled after the delays in
122 /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls
123 /// at T+0ms, T+200ms and T+300ms. The `timeout` is applied to each call individually. The
124 /// result of the first successful call is returned, or a summary of all errors otherwise.
125 fn lookup_ipv4_staggered<N: IntoName + Clone>(
126 &self,
127 host: N,
128 timeout: Duration,
129 delays_ms: &[u64],
130 ) -> impl Future<Output = Result<impl Iterator<Item = IpAddr>>>;
131
132 /// Perform an ipv6 lookup with a timeout in a staggered fashion.
133 ///
134 /// From the moment this function is called, each lookup is scheduled after the delays in
135 /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls
136 /// at T+0ms, T+200ms and T+300ms. The `timeout` is applied to each call individually. The
137 /// result of the first successful call is returned, or a summary of all errors otherwise.
138 fn lookup_ipv6_staggered<N: IntoName + Clone>(
139 &self,
140 host: N,
141 timeout: Duration,
142 delays_ms: &[u64],
143 ) -> impl Future<Output = Result<impl Iterator<Item = IpAddr>>>;
144
145 /// Race an ipv4 and ipv6 lookup with a timeout in a staggered fashion.
146 ///
147 /// From the moment this function is called, each lookup is scheduled after the delays in
148 /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls
149 /// at T+0ms, T+200ms and T+300ms. The `timeout` is applied as stated in
150 /// [`Self::lookup_ipv4_ipv6`]. The result of the first successful call is returned, or a
151 /// summary of all errors otherwise.
152 fn lookup_ipv4_ipv6_staggered<N: IntoName + Clone>(
153 &self,
154 host: N,
155 timeout: Duration,
156 delays_ms: &[u64],
157 ) -> impl Future<Output = Result<impl Iterator<Item = IpAddr>>>;
158
159 /// Looks up node info by DNS name in a staggered fashion.
160 ///
161 /// From the moment this function is called, each lookup is scheduled after the delays in
162 /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls
163 /// at T+0ms, T+200ms and T+300ms. The result of the first successful call is returned, or a
164 /// summary of all errors otherwise.
165 fn lookup_by_name_staggered(
166 &self,
167 name: &str,
168 delays_ms: &[u64],
169 ) -> impl Future<Output = Result<NodeAddr>>;
170
171 /// Looks up node info by [`NodeId`] and origin domain name.
172 ///
173 /// From the moment this function is called, each lookup is scheduled after the delays in
174 /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls
175 /// at T+0ms, T+200ms and T+300ms. The result of the first successful call is returned, or a
176 /// summary of all errors otherwise.
177 fn lookup_by_id_staggered(
178 &self,
179 node_id: &NodeId,
180 origin: &str,
181 delays_ms: &[u64],
182 ) -> impl Future<Output = Result<NodeAddr>>;
183}
184
185impl ResolverExt for DnsResolver {
186 async fn lookup_ipv4<N: IntoName>(
187 &self,
188 host: N,
189 timeout: Duration,
190 ) -> Result<impl Iterator<Item = IpAddr>> {
191 let addrs = tokio::time::timeout(timeout, self.ipv4_lookup(host)).await??;
192 Ok(addrs.into_iter().map(|ip| IpAddr::V4(ip.0)))
193 }
194
195 async fn lookup_ipv6<N: IntoName>(
196 &self,
197 host: N,
198 timeout: Duration,
199 ) -> Result<impl Iterator<Item = IpAddr>> {
200 let addrs = tokio::time::timeout(timeout, self.ipv6_lookup(host)).await??;
201 Ok(addrs.into_iter().map(|ip| IpAddr::V6(ip.0)))
202 }
203
204 /// Resolve IPv4 and IPv6 in parallel.
205 ///
206 /// `LookupIpStrategy::Ipv4AndIpv6` will wait for ipv6 resolution timeout, even if it is
207 /// not usable on the stack, so we manually query both lookups concurrently and time them out
208 /// individually.
209 async fn lookup_ipv4_ipv6<N: IntoName + Clone>(
210 &self,
211 host: N,
212 timeout: Duration,
213 ) -> Result<impl Iterator<Item = IpAddr>> {
214 let res = tokio::join!(
215 self.lookup_ipv4(host.clone(), timeout),
216 self.lookup_ipv6(host, timeout)
217 );
218
219 match res {
220 (Ok(ipv4), Ok(ipv6)) => Ok(LookupIter::Both(ipv4.chain(ipv6))),
221 (Ok(ipv4), Err(_)) => Ok(LookupIter::Ipv4(ipv4)),
222 (Err(_), Ok(ipv6)) => Ok(LookupIter::Ipv6(ipv6)),
223 (Err(ipv4_err), Err(ipv6_err)) => {
224 anyhow::bail!("Ipv4: {:?}, Ipv6: {:?}", ipv4_err, ipv6_err)
225 }
226 }
227 }
228
229 /// Looks up node info by DNS name.
230 ///
231 /// The resource records returned for `name` must either contain an [`node_info::IROH_TXT_NAME`] TXT
232 /// record or be a CNAME record that leads to an [`node_info::IROH_TXT_NAME`] TXT record.
233 async fn lookup_by_name(&self, name: &str) -> Result<NodeAddr> {
234 let attrs = node_info::TxtAttrs::<node_info::IrohAttr>::lookup_by_name(self, name).await?;
235 let info: node_info::NodeInfo = attrs.into();
236 Ok(info.into())
237 }
238
239 /// Looks up node info by [`NodeId`] and origin domain name.
240 async fn lookup_by_id(&self, node_id: &NodeId, origin: &str) -> Result<NodeAddr> {
241 let attrs =
242 node_info::TxtAttrs::<node_info::IrohAttr>::lookup_by_id(self, node_id, origin).await?;
243 let info: node_info::NodeInfo = attrs.into();
244 Ok(info.into())
245 }
246
247 /// Perform an ipv4 lookup with a timeout in a staggered fashion.
248 ///
249 /// From the moment this function is called, each lookup is scheduled after the delays in
250 /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls
251 /// at T+0ms, T+200ms and T+300ms. The `timeout` is applied to each call individually. The
252 /// result of the first successful call is returned, or a summary of all errors otherwise.
253 async fn lookup_ipv4_staggered<N: IntoName + Clone>(
254 &self,
255 host: N,
256 timeout: Duration,
257 delays_ms: &[u64],
258 ) -> Result<impl Iterator<Item = IpAddr>> {
259 let f = || self.lookup_ipv4(host.clone(), timeout);
260 stagger_call(f, delays_ms).await
261 }
262
263 /// Perform an ipv6 lookup with a timeout in a staggered fashion.
264 ///
265 /// From the moment this function is called, each lookup is scheduled after the delays in
266 /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls
267 /// at T+0ms, T+200ms and T+300ms. The `timeout` is applied to each call individually. The
268 /// result of the first successful call is returned, or a summary of all errors otherwise.
269 async fn lookup_ipv6_staggered<N: IntoName + Clone>(
270 &self,
271 host: N,
272 timeout: Duration,
273 delays_ms: &[u64],
274 ) -> Result<impl Iterator<Item = IpAddr>> {
275 let f = || self.lookup_ipv6(host.clone(), timeout);
276 stagger_call(f, delays_ms).await
277 }
278
279 /// Race an ipv4 and ipv6 lookup with a timeout in a staggered fashion.
280 ///
281 /// From the moment this function is called, each lookup is scheduled after the delays in
282 /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls
283 /// at T+0ms, T+200ms and T+300ms. The `timeout` is applied as stated in
284 /// [`Self::lookup_ipv4_ipv6`]. The result of the first successful call is returned, or a
285 /// summary of all errors otherwise.
286 async fn lookup_ipv4_ipv6_staggered<N: IntoName + Clone>(
287 &self,
288 host: N,
289 timeout: Duration,
290 delays_ms: &[u64],
291 ) -> Result<impl Iterator<Item = IpAddr>> {
292 let f = || self.lookup_ipv4_ipv6(host.clone(), timeout);
293 stagger_call(f, delays_ms).await
294 }
295
296 /// Looks up node info by DNS name in a staggered fashion.
297 ///
298 /// From the moment this function is called, each lookup is scheduled after the delays in
299 /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls
300 /// at T+0ms, T+200ms and T+300ms. The result of the first successful call is returned, or a
301 /// summary of all errors otherwise.
302 async fn lookup_by_name_staggered(&self, name: &str, delays_ms: &[u64]) -> Result<NodeAddr> {
303 let f = || self.lookup_by_name(name);
304 stagger_call(f, delays_ms).await
305 }
306
307 /// Looks up node info by [`NodeId`] and origin domain name.
308 ///
309 /// From the moment this function is called, each lookup is scheduled after the delays in
310 /// `delays_ms` with the first call being done immediately. `[200ms, 300ms]` results in calls
311 /// at T+0ms, T+200ms and T+300ms. The result of the first successful call is returned, or a
312 /// summary of all errors otherwise.
313 async fn lookup_by_id_staggered(
314 &self,
315 node_id: &NodeId,
316 origin: &str,
317 delays_ms: &[u64],
318 ) -> Result<NodeAddr> {
319 let f = || self.lookup_by_id(node_id, origin);
320 stagger_call(f, delays_ms).await
321 }
322}
323
324/// Helper enum to give a unified type to the iterators of [`ResolverExt::lookup_ipv4_ipv6`].
325enum LookupIter<A, B> {
326 Ipv4(A),
327 Ipv6(B),
328 Both(std::iter::Chain<A, B>),
329}
330
331impl<A: Iterator<Item = IpAddr>, B: Iterator<Item = IpAddr>> Iterator for LookupIter<A, B> {
332 type Item = IpAddr;
333
334 fn next(&mut self) -> Option<Self::Item> {
335 match self {
336 LookupIter::Ipv4(iter) => iter.next(),
337 LookupIter::Ipv6(iter) => iter.next(),
338 LookupIter::Both(iter) => iter.next(),
339 }
340 }
341}
342
343/// Staggers calls to the future F with the given delays.
344///
345/// The first call is performed immediately. The first call to succeed generates an Ok result
346/// ignoring any previous error. If all calls fail, an error summarizing all errors is returned.
347async fn stagger_call<T, F: Fn() -> Fut, Fut: Future<Output = Result<T>>>(
348 f: F,
349 delays_ms: &[u64],
350) -> Result<T> {
351 let mut calls = futures_buffered::FuturesUnorderedBounded::new(delays_ms.len() + 1);
352 // NOTE: we add the 0 delay here to have a uniform set of futures. This is more performant than
353 // using alternatives that allow futures of different types.
354 for delay in std::iter::once(&0u64).chain(delays_ms) {
355 let delay = std::time::Duration::from_millis(*delay);
356 let fut = f();
357 let staggered_fut = async move {
358 tokio::time::sleep(delay).await;
359 fut.await
360 };
361 calls.push(staggered_fut)
362 }
363
364 let mut errors = vec![];
365 while let Some(call_result) = calls.next().await {
366 match call_result {
367 Ok(t) => return Ok(t),
368 Err(e) => errors.push(e),
369 }
370 }
371
372 anyhow::bail!(
373 "no calls succeed: [ {}]",
374 errors.into_iter().fold(String::new(), |mut summary, e| {
375 write!(summary, "{e} ").expect("infallible");
376 summary
377 })
378 )
379}
380
381#[cfg(test)]
382pub(crate) mod tests {
383 use std::sync::atomic::AtomicUsize;
384
385 use super::*;
386 use crate::defaults::staging::NA_RELAY_HOSTNAME;
387 const TIMEOUT: Duration = Duration::from_secs(5);
388 const STAGGERING_DELAYS: &[u64] = &[200, 300];
389
390 #[tokio::test]
391 async fn test_dns_lookup_ipv4_ipv6() {
392 let _logging = iroh_test::logging::setup();
393 let resolver = default_resolver();
394 let res: Vec<_> = resolver
395 .lookup_ipv4_ipv6_staggered(NA_RELAY_HOSTNAME, TIMEOUT, STAGGERING_DELAYS)
396 .await
397 .unwrap()
398 .collect();
399 assert!(!res.is_empty());
400 dbg!(res);
401 }
402
403 #[tokio::test]
404 async fn stagger_basic() {
405 let _logging = iroh_test::logging::setup();
406 const CALL_RESULTS: &[Result<u8, u8>] = &[Err(2), Ok(3), Ok(5), Ok(7)];
407 static DONE_CALL: AtomicUsize = AtomicUsize::new(0);
408 let f = || {
409 let r_pos = DONE_CALL.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
410 async move {
411 tracing::info!(r_pos, "call");
412 CALL_RESULTS[r_pos].map_err(|e| anyhow::anyhow!("{e}"))
413 }
414 };
415
416 let delays = [1000, 15];
417 let result = stagger_call(f, &delays).await.unwrap();
418 assert_eq!(result, 5)
419 }
420}