Skip to main content

nym_http_api_client/dns/
mod.rs

1// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4//! DNS resolver configuration for internal lookups.
5//!
6//! The resolver itself is the combination of the Cloudflare and Quad9 endpoints supporting DoH
7//! and DoT.
8//!
9//! ```rust
10//! use nym_http_api_client::HickoryDnsResolver;
11//! # use nym_http_api_client::ResolveError;
12//! # type Err = ResolveError;
13//! # async fn run() -> Result<(), Err> {
14//! let resolver = HickoryDnsResolver::new();
15//! resolver.resolve_str("example.com").await?;
16//! # Ok(())
17//! # }
18//! ```
19//!
20//! ## Fallbacks
21//!
22//! **System Resolver --** This resolver supports an optional fallback mechanism where, should the
23//! DNS-over-TLS resolution fail, a followup resolution will be done using the hosts configured
24//! default (e.g. `/etc/resolv.conf` on linux).
25//!
26//! This is disabled by default and can be enabled using [`HickoryDnsResolver::use_system_resolver`].
27//!
28//! **Static Table --**  There is also a second optional fallback mechanism that allows a static map
29//! to be used as a last resort. This can help when DNS encounters errors due to blocked resolvers
30//! or unknown conditions. This is enabled by default, and can be customized if building a new
31//! resolver.
32//!
33//! ## IPv4 / IPv6
34//!
35//! By default the resolver uses only IPv4 nameservers, and is configured to do `A` lookups first,
36//! and only do `AAAA` if no `A` record is available.
37//!
38//! ## Connection provider
39//!
40//! [`HickoryDnsResolver`] is generic over the [`ConnectionProvider`] used by the underlying
41//! `hickory-resolver` crate, defaulting to [`TokioRuntimeProvider`] so that it behaves exactly
42//! like a `TokioResolver` wrapper out of the box. A different connection provider can be supplied
43//! by naming it explicitly, e.g. `HickoryDnsResolver::<MyProvider>::default()`, though process-wide
44//! sharing (see [`HickoryDnsResolver::shared`]) is only available for the default provider.
45//!
46//! ---
47//!
48//! Requires the `https-ring`, `tls-ring`, `webpki-roots` features for the `hickory-resolver` crate
49#![deny(missing_docs)]
50
51use crate::ClientBuilder;
52
53use std::{
54    collections::HashMap,
55    net::{IpAddr, SocketAddr},
56    str::FromStr,
57    sync::{
58        Arc, LazyLock,
59        atomic::{AtomicBool, Ordering::Relaxed},
60    },
61    time::Duration,
62};
63
64use arc_swap::ArcSwap;
65use hickory_resolver::{
66    ConnectionProvider, Resolver,
67    config::{CLOUDFLARE, NameServerConfig, QUAD9, ResolverConfig, ResolverOpts},
68    net::{NetError, runtime::TokioRuntimeProvider},
69};
70use once_cell::sync::OnceCell;
71use reqwest::dns::{Addrs, Name, Resolve, Resolving};
72use tracing::*;
73
74mod constants;
75mod static_resolver;
76mod trial;
77pub(crate) use static_resolver::*;
78
79pub(crate) const DEFAULT_POSITIVE_LOOKUP_CACHE_TTL: Duration = Duration::from_secs(1800);
80pub(crate) const DEFAULT_OVERALL_LOOKUP_TIMEOUT: Duration = Duration::from_secs(10);
81pub(crate) const DEFAULT_QUERY_TIMEOUT: Duration = Duration::from_secs(5);
82
83impl ClientBuilder {
84    /// Override the DNS resolver implementation used by the underlying http client.
85    /// This forces the use of an independent request executor (via [`Self::non_shared`]).
86    pub fn dns_resolver<R: Resolve + 'static>(mut self, resolver: Arc<R>) -> Self {
87        self = self.non_shared();
88        // because of the call to non-shared this conditional should always run.
89        if let Some(rb) = self.reqwest_client_builder {
90            self.reqwest_client_builder = Some(rb.dns_resolver(resolver));
91        }
92        self.use_secure_dns = false;
93        self
94    }
95
96    /// Disables the hickory-dns async resolver in favor of the `reqwest` default threadpool using
97    /// `getaddrinfo`.
98    ///
99    /// If [`Self::dns_resolver`] is called, there is no need to call this as well.
100    ///
101    /// This forces the use of an independent request executor (via [`Self::non_shared`]).
102    pub fn no_hickory_dns(mut self) -> Self {
103        self = self.non_shared();
104        self.use_secure_dns = false;
105        self
106    }
107}
108
109// n.b. static items do not call [`Drop`] on program termination, so this won't be deallocated.
110// this is fine, as the OS can deallocate the terminated program faster than we can free memory
111// but tools like valgrind might report "memory leaks" as it isn't obvious this is intentional.
112static SHARED_RESOLVER: LazyLock<HickoryDnsResolver> = LazyLock::new(|| {
113    tracing::debug!("Initializing shared DNS resolver");
114    HickoryDnsResolver {
115        use_shared: false, // prevent infinite recursion
116        ..Default::default()
117    }
118});
119
120/// Associates a [`ConnectionProvider`] with the process-wide shared resolver instance backing
121/// [`HickoryDnsResolver::shared`], if one exists for that provider.
122///
123/// [`TokioRuntimeProvider`] is the only connection provider with shared state; every other
124/// connection provider always builds an independent resolver, so `use_shared`-style sharing has
125/// no effect for them.
126pub trait SharedResolverState: ConnectionProvider + Default {
127    /// The process-wide shared resolver instance for this connection provider, if one exists.
128    fn shared_resolver() -> Option<&'static HickoryDnsResolver<Self>> {
129        None
130    }
131}
132
133impl SharedResolverState for TokioRuntimeProvider {
134    fn shared_resolver() -> Option<&'static HickoryDnsResolver<Self>> {
135        Some(&SHARED_RESOLVER)
136    }
137}
138
139#[derive(Debug, thiserror::Error)]
140#[allow(missing_docs)]
141/// Error occurring while resolving a hostname into an IP address.
142pub enum ResolveError {
143    #[error("invalid name: {0}")]
144    InvalidNameError(String),
145    #[error("hickory-dns resolver error: {0}")]
146    ResolveError(#[from] NetError),
147    #[error("high level lookup timed out")]
148    Timeout,
149    #[error("hostname not found in static lookup table")]
150    StaticLookupMiss,
151}
152
153impl ResolveError {
154    /// Returns true if the error is a timeout.
155    pub fn is_timeout(&self) -> bool {
156        matches!(
157            self,
158            ResolveError::Timeout | ResolveError::ResolveError(NetError::Timeout)
159        )
160    }
161}
162
163/// Wrapper around a `hickory-resolver` [`Resolver`], which implements the `Resolve` trait.
164///
165/// Generic over the [`ConnectionProvider`] used by the underlying resolver, defaulting to
166/// [`TokioRuntimeProvider`] (i.e. a `TokioResolver`) so existing callers are unaffected. See the
167/// [module docs](self#connection-provider) for details on using a different provider.
168///
169/// Typical use involves instantiating using the `Default` implementation and then resolving using
170/// methods or trait implementations.
171///
172/// The default initialization uses a shared underlying resolver. If a thread local resolver is
173/// required use `thread_resolver()` to build a resolver with an independently instantiated
174/// internal resolver.
175#[derive(Debug, Clone)]
176pub struct HickoryDnsResolver<C: ConnectionProvider = TokioRuntimeProvider> {
177    // Since we might not have been called in the context of a
178    // Tokio Runtime during initialization, we must delay the actual
179    // construction of the resolver. This is swappable (rather than a plain `OnceCell`) so that
180    // [`Self::set_name_servers`] can invalidate it and force a rebuild against the new
181    // nameserver group on the next lookup.
182    state: Arc<ArcSwap<OnceCell<Resolver<C>>>>,
183    use_system: Arc<AtomicBool>,
184    system_resolver: Arc<OnceCell<Resolver<C>>>,
185    static_base: Option<Arc<OnceCell<StaticResolver>>>,
186    /// Nameserver group used to build `state` when it needs (re)constructing.
187    name_servers: Arc<ArcSwap<Vec<NameServerConfig>>>,
188    use_shared: bool,
189    /// Overall timeout for dns lookup associated with any individual host resolution. For example,
190    /// use of retries, server_ordering_strategy, etc. ends absolutely if this timeout is reached.
191    overall_dns_timeout: Duration,
192}
193
194impl<C: ConnectionProvider> Default for HickoryDnsResolver<C> {
195    fn default() -> Self {
196        Self {
197            state: Default::default(),
198            use_system: Arc::new(AtomicBool::new(false)),
199            system_resolver: Default::default(),
200            static_base: Some(Default::default()),
201            name_servers: Arc::new(ArcSwap::from_pointee(default_nameserver_group_ipv4_only())),
202            use_shared: true,
203            overall_dns_timeout: DEFAULT_OVERALL_LOOKUP_TIMEOUT,
204        }
205    }
206}
207
208impl HickoryDnsResolver<TokioRuntimeProvider> {
209    /// Construct the default, Tokio-backed resolver.
210    ///
211    /// Prefer this over `Default::default()` / `HickoryDnsResolver::default()`: since
212    /// [`HickoryDnsResolver`] is generic over its connection provider, `Default` is implemented for
213    /// every valid provider. Building a `HickoryDnsResolver` through the `Default` trait therefore
214    /// requires a type annotation, e.g. `let resolver: HickoryDnsResolver<TokioRuntimeProvider> =
215    /// HickoryDnsResolver::default();`. `new` is inherent to [`TokioRuntimeProvider`] specifically,
216    /// so it resolves without an explicit type annotation.
217    pub fn new() -> Self {
218        Self::default()
219    }
220}
221
222impl<C: SharedResolverState> Resolve for HickoryDnsResolver<C> {
223    fn resolve(&self, name: Name) -> Resolving {
224        let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
225        let use_shared = self.use_shared;
226        let result: Result<Resolver<C>, ResolveError> = if use_system {
227            self.system_resolver
228                .get_or_try_init(|| Self::new_resolver_system(use_shared))
229                .cloned()
230        } else {
231            self.build_configured_resolver()
232        };
233
234        let resolver = match result {
235            Ok(r) => r,
236            Err(err) => return Box::pin(return_err(err)),
237        };
238
239        let maybe_static = self.static_base.clone();
240        let overall_dns_timeout = self.overall_dns_timeout;
241        Box::pin(async move {
242            resolve(
243                name,
244                resolver,
245                maybe_static,
246                use_shared,
247                overall_dns_timeout,
248            )
249            .await
250            .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
251        })
252    }
253}
254
255async fn return_err(e: ResolveError) -> Result<Addrs, Box<dyn std::error::Error + Send + Sync>> {
256    Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
257}
258
259async fn resolve<C: SharedResolverState>(
260    name: Name,
261    resolver: Resolver<C>,
262    maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
263    independent: bool,
264    overall_dns_timeout: Duration,
265) -> Result<Addrs, ResolveError> {
266    // try checking the static table to see if any of the addresses in the table have been
267    // looked up previously within the timeout to where we are not yet ready to try the
268    // default resolver yet again.
269    if let Some(ref static_resolver) = maybe_static {
270        let resolver = static_resolver
271            .get_or_init(|| HickoryDnsResolver::<C>::new_static_fallback(independent));
272
273        if let Some(addrs) = resolver.pre_resolve(name.as_str()) {
274            let addrs: Addrs =
275                Box::new(addrs.into_iter().map(|ip_addr| SocketAddr::new(ip_addr, 0)));
276            return Ok(addrs);
277        }
278    }
279
280    // Attempt a lookup using the primary resolver
281    let resolve_fut = tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
282    let primary_err = match resolve_fut.await {
283        Err(_) => ResolveError::Timeout,
284        Ok(Ok(lookup)) => {
285            // Shuffle so that successive connection attempts cycle through all
286            // returned IPs rather than always hitting the same first address.
287            let mut ips = Vec::from_iter(lookup.iter());
288            fastrand::shuffle(&mut ips);
289            let addrs: Addrs = Box::new(ips.into_iter().map(|ip| SocketAddr::new(ip, 0)));
290            return Ok(addrs);
291        }
292        Ok(Err(e)) => {
293            // on failure use the fall back system configured DNS resolver
294            if !e.is_no_records_found() {
295                warn!("primary DNS failed w/ error: {e}");
296            }
297            e.into()
298        }
299    };
300
301    // If no record has been found and a static map of fallback addresses is configured
302    // check the table for our entry
303    if let Some(ref static_resolver) = maybe_static {
304        debug!("checking static");
305        let resolver = static_resolver
306            .get_or_init(|| HickoryDnsResolver::<C>::new_static_fallback(independent));
307
308        if let Ok(addrs) = resolver.resolve(name).await {
309            return Ok(addrs);
310        }
311    }
312
313    Err(primary_err)
314}
315
316impl<C: SharedResolverState> HickoryDnsResolver<C> {
317    /// Returns an instance of the process-wide shared resolver for this connection provider, if
318    /// one exists (see [`SharedResolverState`]). Falls back to an independent instance (as if
319    /// built via [`Self::default`]) for connection providers without shared state.
320    pub fn shared() -> Self {
321        C::shared_resolver().cloned().unwrap_or_default()
322    }
323
324    /// Attempt to resolve a domain name to a set of [`IpAddr`]s
325    pub async fn resolve_str(
326        &self,
327        name: &str,
328    ) -> Result<impl Iterator<Item = IpAddr> + use<C>, ResolveError> {
329        let n =
330            Name::from_str(name).map_err(|_| ResolveError::InvalidNameError(name.to_string()))?;
331        let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
332        let resolver = if use_system {
333            self.system_resolver
334                .get_or_try_init(|| Self::new_resolver_system(self.use_shared))?
335                .clone()
336        } else {
337            self.build_configured_resolver()?
338        };
339
340        resolve(
341            n,
342            resolver,
343            self.static_base.clone(),
344            self.use_shared,
345            self.overall_dns_timeout,
346        )
347        .await
348        .map(|addrs| addrs.map(|socket_addr| socket_addr.ip()))
349    }
350
351    /// Create a (lazy-initialized) resolver that is not shared across threads.
352    pub fn thread_resolver() -> Self {
353        Self {
354            use_shared: false,
355            ..Default::default()
356        }
357    }
358
359    /// Build (or fetch the already-built) configured resolver using the currently configured
360    /// nameserver group.
361    ///
362    /// When `use_shared` is set and shared state is available for `C` (see
363    /// [`SharedResolverState`]), every call consults the shared resolver's own cache directly
364    /// (`shared.state`) rather than copying the result into this instance's local cache: this
365    /// ensures a [`Self::set_name_servers`] reset made through any instance is visible on this
366    /// instance's very next lookup, instead of this instance being stuck with whatever it cached
367    /// the first time it resolved anything. Otherwise this instance's own local cache
368    /// (`self.state`) is used and (re)built independently.
369    fn build_configured_resolver(&self) -> Result<Resolver<C>, ResolveError> {
370        match self.use_shared.then(C::shared_resolver).flatten() {
371            Some(shared) => shared
372                .state
373                .load()
374                .get_or_try_init(|| {
375                    configure_and_build_resolver::<C>(
376                        shared.name_servers.load_full().as_ref().clone(),
377                    )
378                })
379                .cloned(),
380            None => self
381                .state
382                .load()
383                .get_or_try_init(|| {
384                    configure_and_build_resolver::<C>(
385                        self.name_servers.load_full().as_ref().clone(),
386                    )
387                })
388                .cloned(),
389        }
390    }
391
392    fn new_resolver_system(use_shared: bool) -> Result<Resolver<C>, ResolveError> {
393        // using a closure here is slightly gross, but this makes sure that if the
394        // lazy-init returns an error it can be handled by the client
395        match use_shared.then(C::shared_resolver).flatten() {
396            Some(shared) => Ok(shared
397                .system_resolver
398                .get_or_try_init(new_resolver_system::<C>)?
399                .clone()),
400            None => new_resolver_system::<C>(),
401        }
402    }
403
404    fn new_static_fallback(use_shared: bool) -> StaticResolver {
405        match use_shared.then(C::shared_resolver).flatten() {
406            Some(shared) if shared.static_base.is_some() => shared
407                .static_base
408                .as_ref()
409                .unwrap()
410                .get_or_init(new_default_static_fallback)
411                .clone(),
412            _ => new_default_static_fallback(),
413        }
414    }
415
416    /// Swap the primary internal resolver to the system resolver rather than the
417    /// configured custom resolver.
418    pub fn use_system_resolver(&self) {
419        self.use_system.store(true, Relaxed);
420
421        if let Some(shared) = self.use_shared.then(C::shared_resolver).flatten() {
422            shared.use_system_resolver();
423        }
424    }
425
426    /// Swap the primary internal resolver to the configured custom resolver rather than the
427    /// system resolver.
428    pub fn use_configured_resolver(&self) {
429        self.use_system.store(false, Relaxed);
430
431        if let Some(shared) = self.use_shared.then(C::shared_resolver).flatten() {
432            shared.use_configured_resolver();
433        }
434    }
435
436    /// Clear entries from the static table that would return entries during the pre-resolve stage.
437    /// This means that all lookups will attempt to use the network resolver again before the static
438    /// table is consulted.
439    ///
440    /// Entries elevated to pre-resolve from fallback (added from default or using
441    /// [`Self::set_fallback_addrs`]) will have their cache timeout cleared. Entries added directly
442    /// to pre-resolve (using [`Self::set_static_preresolve`]) will be removed.
443    pub fn clear_preresolve(&self) {
444        debug!("clearing pre-resolve table");
445        if let Some(cell) = &self.static_base
446            && let Some(static_base) = cell.get()
447        {
448            static_base.clear_preresolve()
449        }
450    }
451
452    /// Get the current map of hostnames to addresses used in the fallback static lookup stage if one
453    /// exists.
454    pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
455        Some(self.static_base.as_ref()?.get()?.get_fallback_addrs())
456    }
457
458    /// Set (or overwrite) the map of addresses used in the fallback static hostname lookup.
459    pub fn set_fallback_addrs(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
460        debug!("setting fallback entries for {:?}", addrs.keys());
461        if self.static_base.is_none() {
462            let cell = OnceCell::new();
463            self.static_base = Some(Arc::new(cell));
464        }
465        self.static_base
466            .as_ref()
467            .unwrap()
468            .get_or_init(|| Self::new_static_fallback(self.use_shared))
469            .set_fallback(addrs);
470    }
471
472    /// Get the current map of hostnames to addresses used in the preresolve static lookup stage
473    /// if one exists.
474    pub fn get_static_preresolve(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
475        Some(self.static_base.as_ref()?.get()?.get_preresolve_addrs())
476    }
477
478    /// Set (or overwrite) the map of addresses used in the preresolve static hostname lookup.
479    pub fn set_static_preresolve(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
480        debug!("setting pre-resolve entries for {:?}", addrs.keys());
481        if self.static_base.is_none() {
482            let cell = OnceCell::new();
483            self.static_base = Some(Arc::new(cell));
484        }
485        self.static_base
486            .as_ref()
487            .unwrap()
488            .get_or_init(|| Self::new_static_fallback(self.use_shared))
489            .set_preresolve(addrs);
490    }
491
492    /// Get the full default set of known nameserver configs (Cloudflare and Quad9, DoT and DoH,
493    /// IPv4 and IPv6). Unlike [`Self::get_name_servers`], this is independent of any override
494    /// applied via [`Self::set_name_servers`].
495    pub fn default_name_servers(&self) -> Vec<NameServerConfig> {
496        default_nameserver_group()
497    }
498
499    /// Get the nameserver group currently configured for this resolver, reflecting any change
500    /// made via [`Self::set_name_servers`].
501    pub fn get_name_servers(&self) -> Vec<NameServerConfig> {
502        self.name_servers.load_full().as_ref().clone()
503    }
504
505    /// Set (or overwrite) the nameserver group used by this resolver. Since the underlying
506    /// resolver is immutable once built, this invalidates the internal (cached, lazily-built)
507    /// resolver: the next lookup made with this resolver rebuilds it using the new nameservers.
508    ///
509    /// If this resolver uses the shared underlying resolver, the shared nameserver group and
510    /// cached resolver are reset as well, so that other instances backed by the shared resolver
511    /// pick up the change the next time they need to (re)build it.
512    pub fn set_name_servers(&self, name_servers: Vec<NameServerConfig>) {
513        debug!("setting nameserver group to {name_servers:?}");
514        self.name_servers.store(Arc::new(name_servers));
515        self.state.store(Arc::new(OnceCell::new()));
516
517        if let Some(shared) = self.use_shared.then(C::shared_resolver).flatten() {
518            shared.name_servers.store(self.name_servers.load_full());
519            shared.state.store(Arc::new(OnceCell::new()));
520        }
521    }
522}
523
524/// Successfully resolved addresses are cached for a minimum of 30 minutes Individual lookup
525/// timeouts are set to `DEFAULT_QUERY_TIMEOUT` (5 seconds) Retries after lookup failure are
526/// disabled (`attempts = 0`) Lookup order is set to (default) A then AAAA Number or parallel lookup
527/// is set to (default) 2 Nameserver selection uses the (default) EWMA statistics / performance
528/// based strategy
529fn default_options() -> ResolverOpts {
530    let mut opts = ResolverOpts::default();
531    // Always cache successful responses for queries received by this resolver for 30 min minimum.
532    opts.positive_min_ttl = Some(DEFAULT_POSITIVE_LOOKUP_CACHE_TTL);
533    opts.timeout = DEFAULT_QUERY_TIMEOUT;
534    opts.attempts = 0;
535
536    opts
537}
538
539fn configure_and_build_resolver<C: ConnectionProvider + Default>(
540    name_servers: Vec<NameServerConfig>,
541) -> Result<Resolver<C>, ResolveError> {
542    let options = default_options();
543    info!("building new configured resolver");
544    debug!("configuring resolver with {options:?}, {name_servers:?}");
545
546    let config = ResolverConfig::from_parts(None, Vec::new(), name_servers);
547    let mut resolver_builder = Resolver::<C>::builder_with_config(config, C::default());
548
549    resolver_builder = resolver_builder.with_options(options);
550
551    Ok(resolver_builder.build()?)
552}
553
554fn filter_ipv4(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
555    nameservers
556        .into_iter()
557        .filter(|ns| ns.ip.is_ipv4())
558        .collect()
559}
560
561#[allow(unused)]
562fn filter_ipv6(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
563    nameservers
564        .into_iter()
565        .filter(|ns| ns.ip.is_ipv6())
566        .collect()
567}
568
569fn default_nameserver_group() -> Vec<NameServerConfig> {
570    QUAD9
571        .tls()
572        .chain(QUAD9.https())
573        .chain(CLOUDFLARE.tls())
574        .chain(CLOUDFLARE.https())
575        .collect()
576}
577
578fn default_nameserver_group_ipv4_only() -> Vec<NameServerConfig> {
579    filter_ipv4(default_nameserver_group())
580}
581
582#[allow(unused)]
583fn default_nameserver_group_ipv6_only() -> Vec<NameServerConfig> {
584    filter_ipv6(default_nameserver_group())
585}
586
587/// Create a new resolver with the default configuration, which reads from the system DNS config
588/// (i.e. `/etc/resolv.conf` in unix). The options are overridden to look up for both IPv4 and IPv6
589/// addresses to work with "happy eyeballs" algorithm.
590fn new_resolver_system<C: ConnectionProvider + Default>() -> Result<Resolver<C>, ResolveError> {
591    let mut resolver_builder = Resolver::<C>::builder(C::default())?;
592
593    let options = default_options();
594    info!("building new fallback system resolver");
595    debug!("fallback system resolver with {options:?}");
596
597    resolver_builder = resolver_builder.with_options(options);
598
599    Ok(resolver_builder.build()?)
600}
601
602fn new_default_static_fallback() -> StaticResolver {
603    StaticResolver::new().with_fallback(constants::default_static_addrs())
604}
605
606#[cfg(test)]
607mod test;