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}