hickory_resolver/
resolver.rs

1// Copyright 2015-2019 Benjamin Fry <benjaminfry@me.com>
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// https://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! Structs for creating and using a Resolver
9use std::fmt;
10use std::future::Future;
11use std::net::IpAddr;
12use std::pin::Pin;
13use std::sync::Arc;
14use std::task::{Context, Poll};
15
16use futures_util::{FutureExt, future};
17use tracing::debug;
18
19use crate::caching_client::CachingClient;
20use crate::config::{ResolveHosts, ResolverConfig, ResolverOpts};
21use crate::dns_lru::{self, DnsLru, TtlConfig};
22use crate::error::{ResolveError, ResolveErrorKind};
23use crate::hosts::Hosts;
24use crate::lookup::{self, Lookup, LookupEither};
25use crate::lookup_ip::{LookupIp, LookupIpFuture};
26#[cfg(feature = "tokio")]
27use crate::name_server::TokioConnectionProvider;
28use crate::name_server::{ConnectionProvider, NameServerPool};
29#[cfg(feature = "__dnssec")]
30use crate::proto::dnssec::{DnssecDnsHandle, TrustAnchors};
31use crate::proto::op::Query;
32use crate::proto::rr::domain::usage::ONION;
33use crate::proto::rr::{IntoName, Name, RData, Record, RecordType};
34use crate::proto::xfer::{DnsHandle, DnsRequestOptions, RetryDnsHandle};
35
36/// A builder to construct a [`Resolver`].
37///
38/// Created by [`Resolver::builder`].
39pub struct ResolverBuilder<P> {
40    config: ResolverConfig,
41    options: ResolverOpts,
42    provider: P,
43
44    #[cfg(feature = "__dnssec")]
45    trust_anchor: Option<Arc<TrustAnchors>>,
46}
47
48impl<P> ResolverBuilder<P>
49where
50    P: ConnectionProvider,
51{
52    /// Sets the [`ResolverOpts`] to be used by the resolver.
53    ///
54    /// NB: A [`ResolverBuilder<P>`] will use the system configuration e.g., `resolv.conf`, by
55    /// default. Usage of this method will overwrite any options set by the system configuration.
56    ///
57    /// See [`system_conf`][crate::system_conf] for functions that can parse a [`ResolverOpts`]
58    /// from the system configuration, or use [`options_mut()`][ResolverBuilder::with_options] to
59    /// acquire a muitable reference to the existing [`ResolverOpts`].
60    pub fn with_options(mut self, options: ResolverOpts) -> Self {
61        self.options = options;
62        self
63    }
64
65    /// Returns a mutable reference to the [`ResolverOpts`].
66    pub fn options_mut(&mut self) -> &mut ResolverOpts {
67        &mut self.options
68    }
69
70    /// Set the DNSSEC trust anchors to be used by the resolver.
71    #[cfg(feature = "__dnssec")]
72    pub fn with_trust_anchor(mut self, trust_anchor: Arc<TrustAnchors>) -> Self {
73        self.trust_anchor = Some(trust_anchor);
74        self
75    }
76
77    /// Construct the resolver.
78    pub fn build(self) -> Resolver<P> {
79        let Self {
80            config,
81            options,
82            provider,
83            #[cfg(feature = "__dnssec")]
84            trust_anchor,
85        } = self;
86
87        let pool = NameServerPool::from_config_with_provider(&config, options.clone(), provider);
88        let client = RetryDnsHandle::new(pool, options.attempts);
89        let either;
90
91        if options.validate {
92            #[cfg(feature = "__dnssec")]
93            {
94                let trust_anchor =
95                    trust_anchor.unwrap_or_else(|| Arc::new(TrustAnchors::default()));
96                either =
97                    LookupEither::Secure(DnssecDnsHandle::with_trust_anchor(client, trust_anchor));
98            }
99
100            #[cfg(not(feature = "__dnssec"))]
101            {
102                tracing::warn!("validate option is only available with dnssec features");
103                either = LookupEither::Retry(client);
104            }
105        } else {
106            either = LookupEither::Retry(client);
107        }
108
109        let lru = DnsLru::new(options.cache_size, TtlConfig::from_opts(&options));
110        let client_cache = CachingClient::with_cache(lru, either, options.preserve_intermediates);
111
112        let hosts = Arc::new(match options.use_hosts_file {
113            ResolveHosts::Always | ResolveHosts::Auto => Hosts::from_system().unwrap_or_default(),
114            ResolveHosts::Never => Hosts::default(),
115        });
116
117        Resolver {
118            config,
119            options,
120            client_cache,
121            hosts,
122        }
123    }
124}
125
126/// An asynchronous resolver for DNS generic over async Runtimes.
127///
128/// The lookup methods on `Resolver` spawn background tasks to perform
129/// queries. The futures returned by a `Resolver` and the corresponding
130/// background tasks need not be spawned on the same executor, or be in the
131/// same thread.
132///
133/// *NOTE* If lookup futures returned by a `Resolver` and the background
134/// tasks are spawned on two separate `CurrentThread` executors, one thread
135/// cannot run both executors simultaneously, so the `run` or `block_on`
136/// functions will cause the thread to deadlock. If both the background work
137/// and the lookup futures are intended to be run on the same thread, they
138/// should be spawned on the same executor.
139#[derive(Clone)]
140pub struct Resolver<P: ConnectionProvider> {
141    config: ResolverConfig,
142    options: ResolverOpts,
143    client_cache: CachingClient<LookupEither<P>>,
144    hosts: Arc<Hosts>,
145}
146
147/// A Resolver used with Tokio
148#[cfg(feature = "tokio")]
149pub type TokioResolver = Resolver<TokioConnectionProvider>;
150
151macro_rules! lookup_fn {
152    ($p:ident, $l:ty, $r:path) => {
153        /// Performs a lookup for the associated type.
154        ///
155        /// *hint* queries that end with a '.' are fully qualified names and are cheaper lookups
156        ///
157        /// # Arguments
158        ///
159        /// * `query` - a string which parses to a domain name, failure to parse will return an error
160        pub async fn $p<N: IntoName>(&self, query: N) -> Result<$l, ResolveError> {
161            let name = match query.into_name() {
162                Ok(name) => name,
163                Err(err) => {
164                    return Err(err.into());
165                }
166            };
167
168            self.inner_lookup(name, $r, self.request_options()).await
169        }
170    };
171    ($p:ident, $l:ty, $r:path, $t:ty) => {
172        /// Performs a lookup for the associated type.
173        ///
174        /// # Arguments
175        ///
176        /// * `query` - a type which can be converted to `Name` via `From`.
177        pub async fn $p(&self, query: $t) -> Result<$l, ResolveError> {
178            let name = Name::from(query);
179            self.inner_lookup(name, $r, self.request_options()).await
180        }
181    };
182}
183
184#[cfg(feature = "tokio")]
185impl TokioResolver {
186    /// Constructs a new Tokio based Resolver with the system configuration.
187    ///
188    /// This will use `/etc/resolv.conf` on Unix OSes and the registry on Windows.
189    #[cfg(any(unix, target_os = "windows"))]
190    #[cfg(feature = "system-config")]
191    pub fn builder_tokio() -> Result<ResolverBuilder<TokioConnectionProvider>, ResolveError> {
192        Self::builder(TokioConnectionProvider::default())
193    }
194}
195
196impl<R: ConnectionProvider> Resolver<R> {
197    /// Constructs a new [`Resolver`] via [`ResolverBuilder`] with the operating system's
198    /// configuration.
199    ///
200    /// To use this with Tokio, see [TokioResolver::builder_tokio] instead.
201    ///
202    /// This will use `/etc/resolv.conf` on Unix OSes and the registry on Windows.
203    #[cfg(any(unix, target_os = "windows"))]
204    #[cfg(feature = "system-config")]
205    pub fn builder(provider: R) -> Result<ResolverBuilder<R>, ResolveError> {
206        let (config, options) = super::system_conf::read_system_conf()?;
207        let mut builder = Self::builder_with_config(config, provider);
208        *builder.options_mut() = options;
209        Ok(builder)
210    }
211
212    /// Construct a new [`Resolver`] via [`ResolverBuilder`] with the provided configuration.
213    pub fn builder_with_config(config: ResolverConfig, provider: R) -> ResolverBuilder<R> {
214        ResolverBuilder {
215            config,
216            options: ResolverOpts::default(),
217            provider,
218            #[cfg(feature = "__dnssec")]
219            trust_anchor: None,
220        }
221    }
222
223    /// Flushes/Removes all entries from the cache
224    pub fn clear_cache(&self) {
225        self.client_cache.clear_cache();
226    }
227
228    /// Read the config for this resolver.
229    pub fn config(&self) -> &ResolverConfig {
230        &self.config
231    }
232
233    /// Read the options for this resolver.
234    pub fn options(&self) -> &ResolverOpts {
235        &self.options
236    }
237}
238
239impl<P: ConnectionProvider> Resolver<P> {
240    /// Per request options based on the ResolverOpts
241    pub(crate) fn request_options(&self) -> DnsRequestOptions {
242        let mut request_opts = DnsRequestOptions::default();
243        request_opts.recursion_desired = self.options.recursion_desired;
244        request_opts.use_edns = self.options.edns0;
245        request_opts.case_randomization = self.options.case_randomization;
246
247        request_opts
248    }
249
250    /// Generic lookup for any RecordType
251    ///
252    /// *WARNING* this interface may change in the future, see if one of the specializations would be better.
253    ///
254    /// # Arguments
255    ///
256    /// * `name` - name of the record to lookup, if name is not a valid domain name, an error will be returned
257    /// * `record_type` - type of record to lookup, all RecordData responses will be filtered to this type
258    ///
259    /// # Returns
260    ///
261    //  A future for the returned Lookup RData
262    pub async fn lookup<N: IntoName>(
263        &self,
264        name: N,
265        record_type: RecordType,
266    ) -> Result<Lookup, ResolveError> {
267        let name = match name.into_name() {
268            Ok(name) => name,
269            Err(err) => return Err(err.into()),
270        };
271
272        self.inner_lookup(name, record_type, self.request_options())
273            .await
274    }
275
276    fn push_name(name: Name, names: &mut Vec<Name>) {
277        if !names.contains(&name) {
278            names.push(name);
279        }
280    }
281
282    fn build_names(&self, name: Name) -> Vec<Name> {
283        // if it's fully qualified, we can short circuit the lookup logic
284        if name.is_fqdn()
285            || ONION.zone_of(&name)
286                && name
287                    .trim_to(2)
288                    .iter()
289                    .next()
290                    .map(|name| name.len() == 56) // size of onion v3 address
291                    .unwrap_or(false)
292        {
293            // if already fully qualified, or if onion address, don't assume it might be a
294            // sub-domain
295            vec![name]
296        } else {
297            // Otherwise we have to build the search list
298            // Note: the vec is built in reverse order of precedence, for stack semantics
299            let mut names =
300                Vec::<Name>::with_capacity(1 /*FQDN*/ + 1 /*DOMAIN*/ + self.config.search().len());
301
302            // if not meeting ndots, we always do the raw name in the final lookup, or it's a localhost...
303            let raw_name_first: bool =
304                name.num_labels() as usize > self.options.ndots || name.is_localhost();
305
306            // if not meeting ndots, we always do the raw name in the final lookup
307            if !raw_name_first {
308                let mut fqdn = name.clone();
309                fqdn.set_fqdn(true);
310                names.push(fqdn);
311            }
312
313            for search in self.config.search().iter().rev() {
314                let name_search = name.clone().append_domain(search);
315
316                match name_search {
317                    Ok(name_search) => Self::push_name(name_search, &mut names),
318                    Err(e) => debug!(
319                        "Not adding {} to {} for search due to error: {}",
320                        search, name, e
321                    ),
322                }
323            }
324
325            if let Some(domain) = self.config.domain() {
326                let name_search = name.clone().append_domain(domain);
327
328                match name_search {
329                    Ok(name_search) => Self::push_name(name_search, &mut names),
330                    Err(e) => debug!(
331                        "Not adding {} to {} for search due to error: {}",
332                        domain, name, e
333                    ),
334                }
335            }
336
337            // this is the direct name lookup
338            if raw_name_first {
339                // adding the name as though it's an FQDN for lookup
340                let mut fqdn = name.clone();
341                fqdn.set_fqdn(true);
342                names.push(fqdn);
343            }
344
345            names
346        }
347    }
348
349    pub(crate) async fn inner_lookup<L>(
350        &self,
351        name: Name,
352        record_type: RecordType,
353        options: DnsRequestOptions,
354    ) -> Result<L, ResolveError>
355    where
356        L: From<Lookup> + Send + Sync + 'static,
357    {
358        let names = self.build_names(name);
359        LookupFuture::lookup_with_hosts(
360            names,
361            record_type,
362            options,
363            self.client_cache.clone(),
364            self.hosts.clone(),
365        )
366        .await
367        .map(L::from)
368    }
369
370    /// Performs a dual-stack DNS lookup for the IP for the given hostname.
371    ///
372    /// See the configuration and options parameters for controlling the way in which A(Ipv4) and AAAA(Ipv6) lookups will be performed. For the least expensive query a fully-qualified-domain-name, FQDN, which ends in a final `.`, e.g. `www.example.com.`, will only issue one query. Anything else will always incur the cost of querying the `ResolverConfig::domain` and `ResolverConfig::search`.
373    ///
374    /// # Arguments
375    /// * `host` - string hostname, if this is an invalid hostname, an error will be returned.
376    pub async fn lookup_ip(&self, host: impl IntoName) -> Result<LookupIp, ResolveError> {
377        let mut finally_ip_addr = None;
378        let maybe_ip = host.to_ip().map(RData::from);
379        let maybe_name = host.into_name();
380
381        // if host is a ip address, return directly.
382        if let Some(ip_addr) = maybe_ip {
383            let name = maybe_name.clone().unwrap_or_default();
384            let record = Record::from_rdata(name.clone(), dns_lru::MAX_TTL, ip_addr.clone());
385
386            // if ndots are greater than 4, then we can't assume the name is an IpAddr
387            //   this accepts IPv6 as well, b/c IPv6 can take the form: 2001:db8::198.51.100.35
388            //   but `:` is not a valid DNS character, so technically this will fail parsing.
389            //   TODO: should we always do search before returning this?
390            if self.options.ndots > 4 {
391                finally_ip_addr = Some(record);
392            } else {
393                let query = Query::query(name, ip_addr.record_type());
394                let lookup = Lookup::new_with_max_ttl(query, Arc::from([record]));
395                return Ok(lookup.into());
396            }
397        }
398
399        let name = match (maybe_name, finally_ip_addr.as_ref()) {
400            (Ok(name), _) => name,
401            (Err(_), Some(ip_addr)) => {
402                // it was a valid IP, return that...
403                let query = Query::query(ip_addr.name().clone(), ip_addr.record_type());
404                let lookup = Lookup::new_with_max_ttl(query, Arc::from([ip_addr.clone()]));
405                return Ok(lookup.into());
406            }
407            (Err(err), None) => {
408                return Err(err.into());
409            }
410        };
411
412        let names = self.build_names(name);
413        let hosts = self.hosts.clone();
414
415        LookupIpFuture::lookup(
416            names,
417            self.options.ip_strategy,
418            self.client_cache.clone(),
419            self.request_options(),
420            hosts,
421            finally_ip_addr.map(Record::into_data),
422        )
423        .await
424    }
425
426    /// Customizes the static hosts used in this resolver.
427    pub fn set_hosts(&mut self, hosts: Arc<Hosts>) {
428        self.hosts = hosts;
429    }
430
431    lookup_fn!(
432        reverse_lookup,
433        lookup::ReverseLookup,
434        RecordType::PTR,
435        IpAddr
436    );
437    lookup_fn!(ipv4_lookup, lookup::Ipv4Lookup, RecordType::A);
438    lookup_fn!(ipv6_lookup, lookup::Ipv6Lookup, RecordType::AAAA);
439    lookup_fn!(mx_lookup, lookup::MxLookup, RecordType::MX);
440    lookup_fn!(ns_lookup, lookup::NsLookup, RecordType::NS);
441    lookup_fn!(soa_lookup, lookup::SoaLookup, RecordType::SOA);
442    lookup_fn!(srv_lookup, lookup::SrvLookup, RecordType::SRV);
443    lookup_fn!(tlsa_lookup, lookup::TlsaLookup, RecordType::TLSA);
444    lookup_fn!(txt_lookup, lookup::TxtLookup, RecordType::TXT);
445    lookup_fn!(cert_lookup, lookup::CertLookup, RecordType::CERT);
446}
447
448impl<P: ConnectionProvider> fmt::Debug for Resolver<P> {
449    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
450        f.debug_struct("Resolver").finish()
451    }
452}
453
454/// The Future returned from [`Resolver`] when performing a lookup.
455#[doc(hidden)]
456pub struct LookupFuture<C>
457where
458    C: DnsHandle + 'static,
459{
460    client_cache: CachingClient<C>,
461    names: Vec<Name>,
462    record_type: RecordType,
463    options: DnsRequestOptions,
464    query: Pin<Box<dyn Future<Output = Result<Lookup, ResolveError>> + Send>>,
465}
466
467impl<C> LookupFuture<C>
468where
469    C: DnsHandle + 'static,
470{
471    /// Perform a lookup from a name and type to a set of RDatas
472    ///
473    /// # Arguments
474    ///
475    /// * `names` - a set of DNS names to attempt to resolve, they will be attempted in queue order, i.e. the first is `names.pop()`. Upon each failure, the next will be attempted.
476    /// * `record_type` - type of record being sought
477    /// * `client_cache` - cache with a connection to use for performing all lookups
478    #[doc(hidden)]
479    pub fn lookup(
480        names: Vec<Name>,
481        record_type: RecordType,
482        options: DnsRequestOptions,
483        client_cache: CachingClient<C>,
484    ) -> Self {
485        Self::lookup_with_hosts(
486            names,
487            record_type,
488            options,
489            client_cache,
490            Arc::new(Hosts::default()),
491        )
492    }
493
494    /// Perform a lookup from a name and type to a set of RDatas, taking the local
495    /// hosts file into account.
496    ///
497    /// # Arguments
498    ///
499    /// * `names` - a set of DNS names to attempt to resolve, they will be attempted in queue order, i.e. the first is `names.pop()`. Upon each failure, the next will be attempted.
500    /// * `record_type` - type of record being sought
501    /// * `client_cache` - cache with a connection to use for performing all lookups
502    /// * `hosts` - the local host file, the records inside it will be prioritized over the upstream DNS server
503    #[doc(hidden)]
504    pub fn lookup_with_hosts(
505        mut names: Vec<Name>,
506        record_type: RecordType,
507        options: DnsRequestOptions,
508        client_cache: CachingClient<C>,
509        hosts: Arc<Hosts>,
510    ) -> Self {
511        let name = names.pop().ok_or_else(|| {
512            ResolveError::from(ResolveErrorKind::Message("can not lookup for no names"))
513        });
514
515        let query: Pin<Box<dyn Future<Output = Result<Lookup, ResolveError>> + Send>> = match name {
516            Ok(name) => {
517                let query = Query::query(name, record_type);
518
519                if let Some(lookup) = hosts.lookup_static_host(&query) {
520                    future::ok(lookup).boxed()
521                } else {
522                    client_cache.lookup(query, options).boxed()
523                }
524            }
525            Err(err) => future::err(err).boxed(),
526        };
527
528        Self {
529            client_cache,
530            names,
531            record_type,
532            options,
533            query,
534        }
535    }
536}
537
538impl<C> Future for LookupFuture<C>
539where
540    C: DnsHandle + 'static,
541{
542    type Output = Result<Lookup, ResolveError>;
543
544    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
545        loop {
546            // Try polling the underlying DNS query.
547            let query = self.query.as_mut().poll_unpin(cx);
548
549            // Determine whether or not we will attempt to retry the query.
550            let should_retry = match &query {
551                // If the query is NotReady, yield immediately.
552                Poll::Pending => return Poll::Pending,
553                // If the query returned a successful lookup, we will attempt
554                // to retry if the lookup is empty. Otherwise, we will return
555                // that lookup.
556                Poll::Ready(Ok(lookup)) => lookup.records().is_empty(),
557                // If the query failed, we will attempt to retry.
558                Poll::Ready(Err(_)) => true,
559            };
560
561            if should_retry {
562                if let Some(name) = self.names.pop() {
563                    let record_type = self.record_type;
564                    let options = self.options;
565
566                    // If there's another name left to try, build a new query
567                    // for that next name and continue looping.
568                    self.query = self
569                        .client_cache
570                        .lookup(Query::query(name, record_type), options);
571                    // Continue looping with the new query. It will be polled
572                    // on the next iteration of the loop.
573                    continue;
574                }
575            }
576            // If we didn't have to retry the query, or we weren't able to
577            // retry because we've exhausted the names to search, return the
578            // current query.
579            return query;
580            // If we skipped retrying the  query, this will return the
581            // successful lookup, otherwise, if the retry failed, this will
582            // return the last  query result --- either an empty lookup or the
583            // last error we saw.
584        }
585    }
586}
587
588/// Unit tests compatible with different runtime.
589#[cfg(all(test, feature = "tokio"))]
590pub(crate) mod testing {
591    use std::{net::*, str::FromStr};
592
593    use crate::Resolver;
594    use crate::config::{LookupIpStrategy, NameServerConfig, ResolverConfig};
595    use crate::name_server::ConnectionProvider;
596    use crate::proto::{rr::Name, runtime::Executor};
597
598    /// Test IP lookup from URLs.
599    pub(crate) async fn lookup_test<R: ConnectionProvider>(config: ResolverConfig, handle: R) {
600        let resolver = Resolver::<R>::builder_with_config(config, handle).build();
601
602        let response = resolver
603            .lookup_ip("www.example.com.")
604            .await
605            .expect("failed to run lookup");
606
607        assert_ne!(response.iter().count(), 0);
608    }
609
610    /// Test IP lookup from IP literals.
611    pub(crate) async fn ip_lookup_test<R: ConnectionProvider>(handle: R) {
612        let resolver =
613            Resolver::<R>::builder_with_config(ResolverConfig::default(), handle).build();
614
615        let response = resolver
616            .lookup_ip("10.1.0.2")
617            .await
618            .expect("failed to run lookup");
619
620        assert_eq!(
621            Some(IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2))),
622            response.iter().next()
623        );
624
625        let response = resolver
626            .lookup_ip("2606:2800:21f:cb07:6820:80da:af6b:8b2c")
627            .await
628            .expect("failed to run lookup");
629
630        assert_eq!(
631            Some(IpAddr::V6(Ipv6Addr::new(
632                0x2606, 0x2800, 0x21f, 0xcb07, 0x6820, 0x80da, 0xaf6b, 0x8b2c,
633            ))),
634            response.iter().next()
635        );
636    }
637
638    /// Test IP lookup from IP literals across threads.
639    pub(crate) fn ip_lookup_across_threads_test<E: Executor, R: ConnectionProvider>(handle: R) {
640        // Test ensuring that running the background task on a separate
641        // executor in a separate thread from the futures returned by the
642        // Resolver works correctly.
643        use std::thread;
644        let resolver =
645            Resolver::<R>::builder_with_config(ResolverConfig::default(), handle).build();
646
647        let resolver_one = resolver.clone();
648        let resolver_two = resolver;
649
650        let test_fn = |resolver: Resolver<R>| {
651            let mut exec = E::new();
652
653            let response = exec
654                .block_on(resolver.lookup_ip("10.1.0.2"))
655                .expect("failed to run lookup");
656
657            assert_eq!(
658                Some(IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2))),
659                response.iter().next()
660            );
661
662            let response = exec
663                .block_on(resolver.lookup_ip("2606:2800:21f:cb07:6820:80da:af6b:8b2c"))
664                .expect("failed to run lookup");
665
666            assert_eq!(
667                Some(IpAddr::V6(Ipv6Addr::new(
668                    0x2606, 0x2800, 0x21f, 0xcb07, 0x6820, 0x80da, 0xaf6b, 0x8b2c,
669                ))),
670                response.iter().next()
671            );
672        };
673
674        let thread_one = thread::spawn(move || {
675            test_fn(resolver_one);
676        });
677
678        let thread_two = thread::spawn(move || {
679            test_fn(resolver_two);
680        });
681
682        thread_one.join().expect("thread_one failed");
683        thread_two.join().expect("thread_two failed");
684    }
685
686    /// Test IP lookup from URLs with DNSSEC validation.
687    #[cfg(feature = "__dnssec")]
688    pub(crate) async fn sec_lookup_test<R: ConnectionProvider>(handle: R) {
689        let mut resolver_builder = Resolver::builder_with_config(ResolverConfig::default(), handle);
690        resolver_builder.options_mut().validate = true;
691        resolver_builder.options_mut().try_tcp_on_error = true;
692        let resolver = resolver_builder.build();
693
694        let response = resolver
695            .lookup_ip("www.example.com.")
696            .await
697            .expect("failed to run lookup");
698
699        assert_ne!(response.iter().count(), 0);
700        assert!(
701            response
702                .as_lookup()
703                .record_iter()
704                .any(|record| record.proof().is_secure())
705        );
706    }
707
708    /// Test IP lookup from domains that exist but unsigned with DNSSEC validation.
709    #[allow(deprecated)]
710    #[cfg(feature = "__dnssec")]
711    pub(crate) async fn sec_lookup_fails_test<R: ConnectionProvider>(handle: R) {
712        let mut resolver_builder = Resolver::builder_with_config(ResolverConfig::default(), handle);
713        resolver_builder.options_mut().validate = true;
714        resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
715        let resolver = resolver_builder.build();
716
717        // needs to be a domain that exists, but is not signed (eventually this will be)
718        let response = resolver.lookup_ip("hickory-dns.org.").await;
719
720        let lookup_ip = response.unwrap();
721        for record in lookup_ip.as_lookup().record_iter() {
722            assert!(record.proof().is_insecure());
723        }
724    }
725
726    /// Test Resolver created from system configuration with IP lookup.
727    #[cfg(feature = "system-config")]
728    pub(crate) async fn system_lookup_test<R: ConnectionProvider>(handle: R) {
729        let resolver = Resolver::<R>::builder(handle)
730            .expect("failed to create resolver")
731            .build();
732
733        let response = resolver
734            .lookup_ip("www.example.com.")
735            .await
736            .expect("failed to run lookup");
737
738        assert_eq!(response.iter().count(), 2);
739        for address in response.iter() {
740            if address.is_ipv4() {
741                assert_eq!(address, IpAddr::V4(Ipv4Addr::new(93, 184, 215, 14)));
742            } else {
743                assert_eq!(
744                    address,
745                    IpAddr::V6(Ipv6Addr::new(
746                        0x2606, 0x2800, 0x21f, 0xcb07, 0x6820, 0x80da, 0xaf6b, 0x8b2c,
747                    ))
748                );
749            }
750        }
751    }
752
753    /// Test Resolver created from system configuration with host lookups.
754    #[cfg(feature = "system-config")]
755    pub(crate) async fn hosts_lookup_test<R: ConnectionProvider>(handle: R) {
756        let resolver = Resolver::<R>::builder(handle)
757            .expect("failed to create resolver")
758            .build();
759
760        let response = resolver
761            .lookup_ip("a.com")
762            .await
763            .expect("failed to run lookup");
764
765        assert_eq!(response.iter().count(), 1);
766        for address in response.iter() {
767            if address.is_ipv4() {
768                assert_eq!(address, IpAddr::V4(Ipv4Addr::new(10, 1, 0, 104)));
769            } else {
770                panic!("failed to run lookup");
771            }
772        }
773    }
774
775    /// Test fqdn.
776    pub(crate) async fn fqdn_test<R: ConnectionProvider>(handle: R) {
777        let domain = Name::from_str("incorrect.example.com.").unwrap();
778        let search = vec![
779            Name::from_str("bad.example.com.").unwrap(),
780            Name::from_str("wrong.example.com.").unwrap(),
781        ];
782        let name_servers: Vec<NameServerConfig> =
783            ResolverConfig::default().name_servers().to_owned();
784
785        let mut resolver_builder = Resolver::<R>::builder_with_config(
786            ResolverConfig::from_parts(Some(domain), search, name_servers),
787            handle,
788        );
789        resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
790        let resolver = resolver_builder.build();
791
792        let response = resolver
793            .lookup_ip("www.example.com.")
794            .await
795            .expect("failed to run lookup");
796
797        assert_ne!(response.iter().count(), 0);
798        for address in response.iter() {
799            assert!(address.is_ipv4(), "should only be looking up IPv4");
800        }
801    }
802
803    /// Test ndots with non-fqdn.
804    pub(crate) async fn ndots_test<R: ConnectionProvider>(handle: R) {
805        let domain = Name::from_str("incorrect.example.com.").unwrap();
806        let search = vec![
807            Name::from_str("bad.example.com.").unwrap(),
808            Name::from_str("wrong.example.com.").unwrap(),
809        ];
810        let name_servers: Vec<NameServerConfig> =
811            ResolverConfig::default().name_servers().to_owned();
812
813        let mut resolver_builder = Resolver::<R>::builder_with_config(
814            ResolverConfig::from_parts(Some(domain), search, name_servers),
815            handle,
816        );
817        // our name does have 2, the default should be fine, let's just narrow the test criteria a bit.
818        resolver_builder.options_mut().ndots = 2;
819        resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
820        let resolver = resolver_builder.build();
821
822        // notice this is not a FQDN, no trailing dot.
823        let response = resolver
824            .lookup_ip("www.example.com")
825            .await
826            .expect("failed to run lookup");
827
828        assert_ne!(response.iter().count(), 0);
829        for address in response.iter() {
830            assert!(address.is_ipv4(), "should only be looking up IPv4");
831        }
832    }
833
834    /// Test large ndots with non-fqdn.
835    pub(crate) async fn large_ndots_test<R: ConnectionProvider>(handle: R) {
836        let domain = Name::from_str("incorrect.example.com.").unwrap();
837        let search = vec![
838            Name::from_str("bad.example.com.").unwrap(),
839            Name::from_str("wrong.example.com.").unwrap(),
840        ];
841        let name_servers: Vec<NameServerConfig> =
842            ResolverConfig::default().name_servers().to_owned();
843
844        let mut resolver_builder = Resolver::<R>::builder_with_config(
845            ResolverConfig::from_parts(Some(domain), search, name_servers),
846            handle,
847        );
848        // matches kubernetes default
849        resolver_builder.options_mut().ndots = 5;
850        resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
851        let resolver = resolver_builder.build();
852
853        // notice this is not a FQDN, no trailing dot.
854        let response = resolver
855            .lookup_ip("www.example.com")
856            .await
857            .expect("failed to run lookup");
858
859        assert_ne!(response.iter().count(), 0);
860        for address in response.iter() {
861            assert!(address.is_ipv4(), "should only be looking up IPv4");
862        }
863    }
864
865    /// Test domain search.
866    pub(crate) async fn domain_search_test<R: ConnectionProvider>(handle: R) {
867        // domain is good now, should be combined with the name to form www.example.com
868        let domain = Name::from_str("example.com.").unwrap();
869        let search = vec![
870            Name::from_str("bad.example.com.").unwrap(),
871            Name::from_str("wrong.example.com.").unwrap(),
872        ];
873        let name_servers: Vec<NameServerConfig> =
874            ResolverConfig::default().name_servers().to_owned();
875
876        let mut resolver_builder = Resolver::<R>::builder_with_config(
877            ResolverConfig::from_parts(Some(domain), search, name_servers),
878            handle,
879        );
880        resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
881        let resolver = resolver_builder.build();
882
883        // notice no dots, should not trigger ndots rule
884        let response = resolver
885            .lookup_ip("www")
886            .await
887            .expect("failed to run lookup");
888
889        assert_ne!(response.iter().count(), 0);
890        for address in response.iter() {
891            assert!(address.is_ipv4(), "should only be looking up IPv4");
892        }
893    }
894
895    /// Test search lists.
896    pub(crate) async fn search_list_test<R: ConnectionProvider>(handle: R) {
897        let domain = Name::from_str("incorrect.example.com.").unwrap();
898        let search = vec![
899            // let's skip one search domain to test the loop...
900            Name::from_str("bad.example.com.").unwrap(),
901            // this should combine with the search name to form www.example.com
902            Name::from_str("example.com.").unwrap(),
903        ];
904        let name_servers: Vec<NameServerConfig> =
905            ResolverConfig::default().name_servers().to_owned();
906
907        let mut resolver_builder = Resolver::<R>::builder_with_config(
908            ResolverConfig::from_parts(Some(domain), search, name_servers),
909            handle,
910        );
911        resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
912        let resolver = resolver_builder.build();
913
914        // notice no dots, should not trigger ndots rule
915        let response = resolver
916            .lookup_ip("www")
917            .await
918            .expect("failed to run lookup");
919
920        assert_ne!(response.iter().count(), 0);
921        for address in response.iter() {
922            assert!(address.is_ipv4(), "should only be looking up IPv4");
923        }
924    }
925
926    /// Test idna.
927    pub(crate) async fn idna_test<R: ConnectionProvider>(handle: R) {
928        let resolver =
929            Resolver::<R>::builder_with_config(ResolverConfig::default(), handle).build();
930
931        let response = resolver
932            .lookup_ip("中国.icom.museum.")
933            .await
934            .expect("failed to run lookup");
935
936        // we just care that the request succeeded, not about the actual content
937        //   it's not certain that the ip won't change.
938        assert!(response.iter().next().is_some());
939    }
940
941    /// Test ipv4 localhost.
942    pub(crate) async fn localhost_ipv4_test<R: ConnectionProvider>(handle: R) {
943        let mut resolver_builder =
944            Resolver::<R>::builder_with_config(ResolverConfig::default(), handle);
945        resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4thenIpv6;
946        let resolver = resolver_builder.build();
947
948        let response = resolver
949            .lookup_ip("localhost")
950            .await
951            .expect("failed to run lookup");
952
953        let mut iter = response.iter();
954        assert_eq!(iter.next().expect("no A"), IpAddr::V4(Ipv4Addr::LOCALHOST));
955    }
956
957    /// Test ipv6 localhost.
958    pub(crate) async fn localhost_ipv6_test<R: ConnectionProvider>(handle: R) {
959        let mut resolver_builder =
960            Resolver::<R>::builder_with_config(ResolverConfig::default(), handle);
961        resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv6thenIpv4;
962        let resolver = resolver_builder.build();
963
964        let response = resolver
965            .lookup_ip("localhost")
966            .await
967            .expect("failed to run lookup");
968
969        let mut iter = response.iter();
970        assert_eq!(
971            iter.next().expect("no AAAA"),
972            IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1,))
973        );
974    }
975
976    /// Test ipv4 search with large ndots.
977    pub(crate) async fn search_ipv4_large_ndots_test<R: ConnectionProvider>(handle: R) {
978        let mut config = ResolverConfig::default();
979        config.add_search(Name::from_str("example.com").unwrap());
980
981        let mut resolver_builder = Resolver::<R>::builder_with_config(config, handle);
982        resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
983        resolver_builder.options_mut().ndots = 5;
984        let resolver = resolver_builder.build();
985
986        let response = resolver
987            .lookup_ip("198.51.100.35")
988            .await
989            .expect("failed to run lookup");
990
991        let mut iter = response.iter();
992        assert_eq!(
993            iter.next().expect("no rdatas"),
994            IpAddr::V4(Ipv4Addr::new(198, 51, 100, 35))
995        );
996    }
997
998    /// Test ipv6 search with large ndots.
999    pub(crate) async fn search_ipv6_large_ndots_test<R: ConnectionProvider>(handle: R) {
1000        let mut config = ResolverConfig::default();
1001        config.add_search(Name::from_str("example.com").unwrap());
1002
1003        let mut resolver_builder = Resolver::<R>::builder_with_config(config, handle);
1004        resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
1005        resolver_builder.options_mut().ndots = 5;
1006        let resolver = resolver_builder.build();
1007
1008        let response = resolver
1009            .lookup_ip("2001:db8::c633:6423")
1010            .await
1011            .expect("failed to run lookup");
1012
1013        let mut iter = response.iter();
1014        assert_eq!(
1015            iter.next().expect("no rdatas"),
1016            IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0xc633, 0x6423))
1017        );
1018    }
1019
1020    /// Test ipv6 name parse fails.
1021    pub(crate) async fn search_ipv6_name_parse_fails_test<R: ConnectionProvider>(handle: R) {
1022        let mut config = ResolverConfig::default();
1023        config.add_search(Name::from_str("example.com").unwrap());
1024
1025        let mut resolver_builder = Resolver::<R>::builder_with_config(config, handle);
1026        resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
1027        resolver_builder.options_mut().ndots = 5;
1028        let resolver = resolver_builder.build();
1029
1030        let response = resolver
1031            .lookup_ip("2001:db8::198.51.100.35")
1032            .await
1033            .expect("failed to run lookup");
1034
1035        let mut iter = response.iter();
1036        assert_eq!(
1037            iter.next().expect("no rdatas"),
1038            IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0xc633, 0x6423))
1039        );
1040    }
1041}
1042
1043#[cfg(test)]
1044#[cfg(feature = "tokio")]
1045#[allow(clippy::extra_unused_type_parameters)]
1046mod tests {
1047    use std::net::{IpAddr, Ipv4Addr};
1048    use std::sync::Mutex;
1049
1050    use futures_util::stream::once;
1051    use futures_util::{Stream, future};
1052    use test_support::subscribe;
1053    use tokio::runtime::Runtime;
1054
1055    use super::testing::{
1056        domain_search_test, fqdn_test, idna_test, ip_lookup_across_threads_test, ip_lookup_test,
1057        large_ndots_test, localhost_ipv4_test, localhost_ipv6_test, lookup_test, ndots_test,
1058        search_ipv4_large_ndots_test, search_ipv6_large_ndots_test,
1059        search_ipv6_name_parse_fails_test, search_list_test,
1060    };
1061    #[cfg(feature = "system-config")]
1062    use super::testing::{hosts_lookup_test, system_lookup_test};
1063    #[cfg(feature = "__dnssec")]
1064    use super::testing::{sec_lookup_fails_test, sec_lookup_test};
1065    use super::*;
1066    use crate::config::{ResolverConfig, ResolverOpts};
1067    use crate::name_server::GenericConnection;
1068    use crate::proto::op::Message;
1069    use crate::proto::rr::rdata::A;
1070    use crate::proto::xfer::{DnsRequest, DnsResponse};
1071    use crate::proto::{ProtoError, ProtoErrorKind};
1072
1073    fn is_send_t<T: Send>() -> bool {
1074        true
1075    }
1076
1077    fn is_sync_t<T: Sync>() -> bool {
1078        true
1079    }
1080
1081    #[test]
1082    fn test_send_sync() {
1083        assert!(is_send_t::<ResolverConfig>());
1084        assert!(is_sync_t::<ResolverConfig>());
1085        assert!(is_send_t::<ResolverOpts>());
1086        assert!(is_sync_t::<ResolverOpts>());
1087
1088        assert!(is_send_t::<Resolver<TokioConnectionProvider>>());
1089        assert!(is_sync_t::<Resolver<TokioConnectionProvider>>());
1090
1091        assert!(is_send_t::<DnsRequest>());
1092        assert!(is_send_t::<LookupIpFuture<GenericConnection>>());
1093        assert!(is_send_t::<LookupFuture<GenericConnection>>());
1094    }
1095
1096    #[tokio::test]
1097    async fn test_lookup_google() {
1098        subscribe();
1099        let handle = TokioConnectionProvider::default();
1100        lookup_test(ResolverConfig::google(), handle).await;
1101    }
1102
1103    #[tokio::test]
1104    async fn test_lookup_cloudflare() {
1105        subscribe();
1106        let handle = TokioConnectionProvider::default();
1107        lookup_test(ResolverConfig::cloudflare(), handle).await;
1108    }
1109
1110    #[tokio::test]
1111    async fn test_ip_lookup() {
1112        subscribe();
1113        let handle = TokioConnectionProvider::default();
1114        ip_lookup_test(handle).await;
1115    }
1116
1117    #[test]
1118    fn test_ip_lookup_across_threads() {
1119        subscribe();
1120        let handle = TokioConnectionProvider::default();
1121        ip_lookup_across_threads_test::<Runtime, _>(handle);
1122    }
1123
1124    #[tokio::test]
1125    #[cfg(feature = "__dnssec")]
1126    async fn test_sec_lookup() {
1127        subscribe();
1128        let handle = TokioConnectionProvider::default();
1129        sec_lookup_test(handle).await;
1130    }
1131
1132    #[tokio::test]
1133    #[cfg(feature = "__dnssec")]
1134    async fn test_sec_lookup_fails() {
1135        subscribe();
1136        let handle = TokioConnectionProvider::default();
1137        sec_lookup_fails_test(handle).await;
1138    }
1139
1140    #[tokio::test]
1141    #[ignore]
1142    #[cfg(any(unix, target_os = "windows"))]
1143    #[cfg(feature = "system-config")]
1144    async fn test_system_lookup() {
1145        subscribe();
1146        let handle = TokioConnectionProvider::default();
1147        system_lookup_test(handle).await;
1148    }
1149
1150    #[tokio::test]
1151    #[ignore]
1152    // these appear to not work on CI, test on macos with `10.1.0.104  a.com`
1153    #[cfg(unix)]
1154    #[cfg(feature = "system-config")]
1155    async fn test_hosts_lookup() {
1156        subscribe();
1157        let handle = TokioConnectionProvider::default();
1158        hosts_lookup_test(handle).await;
1159    }
1160
1161    #[tokio::test]
1162    async fn test_fqdn() {
1163        subscribe();
1164        let handle = TokioConnectionProvider::default();
1165        fqdn_test(handle).await;
1166    }
1167
1168    #[tokio::test]
1169    async fn test_ndots() {
1170        subscribe();
1171        let handle = TokioConnectionProvider::default();
1172        ndots_test(handle).await;
1173    }
1174
1175    #[tokio::test]
1176    async fn test_large_ndots() {
1177        subscribe();
1178        let handle = TokioConnectionProvider::default();
1179        large_ndots_test(handle).await;
1180    }
1181
1182    #[tokio::test]
1183    async fn test_domain_search() {
1184        subscribe();
1185        let handle = TokioConnectionProvider::default();
1186        domain_search_test(handle).await;
1187    }
1188
1189    #[tokio::test]
1190    async fn test_search_list() {
1191        subscribe();
1192        let handle = TokioConnectionProvider::default();
1193        search_list_test(handle).await;
1194    }
1195
1196    #[tokio::test]
1197    async fn test_idna() {
1198        subscribe();
1199        let handle = TokioConnectionProvider::default();
1200        idna_test(handle).await;
1201    }
1202
1203    #[tokio::test]
1204    async fn test_localhost_ipv4() {
1205        subscribe();
1206        let handle = TokioConnectionProvider::default();
1207        localhost_ipv4_test(handle).await;
1208    }
1209
1210    #[tokio::test]
1211    async fn test_localhost_ipv6() {
1212        subscribe();
1213        let handle = TokioConnectionProvider::default();
1214        localhost_ipv6_test(handle).await;
1215    }
1216
1217    #[tokio::test]
1218    async fn test_search_ipv4_large_ndots() {
1219        subscribe();
1220        let handle = TokioConnectionProvider::default();
1221        search_ipv4_large_ndots_test(handle).await;
1222    }
1223
1224    #[tokio::test]
1225    async fn test_search_ipv6_large_ndots() {
1226        subscribe();
1227        let handle = TokioConnectionProvider::default();
1228        search_ipv6_large_ndots_test(handle).await;
1229    }
1230
1231    #[tokio::test]
1232    async fn test_search_ipv6_name_parse_fails() {
1233        subscribe();
1234        let handle = TokioConnectionProvider::default();
1235        search_ipv6_name_parse_fails_test(handle).await;
1236    }
1237
1238    #[test]
1239    fn test_build_names() {
1240        use std::str::FromStr;
1241
1242        let handle = TokioConnectionProvider::default();
1243        let mut config = ResolverConfig::default();
1244        config.add_search(Name::from_ascii("example.com.").unwrap());
1245        let resolver = Resolver::builder_with_config(config, handle).build();
1246
1247        assert_eq!(resolver.build_names(Name::from_str("").unwrap()).len(), 2);
1248        assert_eq!(resolver.build_names(Name::from_str(".").unwrap()).len(), 1);
1249
1250        let fqdn = Name::from_str("foo.example.com.").unwrap();
1251        let name_list = resolver.build_names(Name::from_str("foo").unwrap());
1252        assert!(name_list.contains(&fqdn));
1253
1254        let name_list = resolver.build_names(fqdn.clone());
1255        assert_eq!(name_list.len(), 1);
1256        assert_eq!(name_list.first(), Some(&fqdn));
1257    }
1258
1259    #[test]
1260    fn test_build_names_onion() {
1261        let handle = TokioConnectionProvider::default();
1262        let mut config = ResolverConfig::default();
1263        config.add_search(Name::from_ascii("example.com.").unwrap());
1264        let resolver = Resolver::builder_with_config(config, handle).build();
1265        let tor_address = [
1266            Name::from_ascii("2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion")
1267                .unwrap(),
1268            Name::from_ascii("www.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion")
1269                .unwrap(), // subdomain are allowed too
1270        ];
1271        let not_tor_address = [
1272            Name::from_ascii("onion").unwrap(),
1273            Name::from_ascii("www.onion").unwrap(),
1274            Name::from_ascii("2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.www.onion")
1275                .unwrap(), // www before key
1276            Name::from_ascii("2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion.to")
1277                .unwrap(), // Tor2web
1278        ];
1279        for name in &tor_address {
1280            assert_eq!(resolver.build_names(name.clone()).len(), 1);
1281        }
1282        for name in &not_tor_address {
1283            assert_eq!(resolver.build_names(name.clone()).len(), 2);
1284        }
1285    }
1286
1287    #[tokio::test]
1288    async fn test_lookup() {
1289        assert_eq!(
1290            LookupFuture::lookup(
1291                vec![Name::root()],
1292                RecordType::A,
1293                DnsRequestOptions::default(),
1294                CachingClient::new(0, mock(vec![v4_message()]), false),
1295            )
1296            .await
1297            .unwrap()
1298            .iter()
1299            .map(|r| r.ip_addr().unwrap())
1300            .collect::<Vec<IpAddr>>(),
1301            vec![Ipv4Addr::LOCALHOST]
1302        );
1303    }
1304
1305    #[tokio::test]
1306    async fn test_lookup_slice() {
1307        assert_eq!(
1308            Record::data(
1309                &LookupFuture::lookup(
1310                    vec![Name::root()],
1311                    RecordType::A,
1312                    DnsRequestOptions::default(),
1313                    CachingClient::new(0, mock(vec![v4_message()]), false),
1314                )
1315                .await
1316                .unwrap()
1317                .records()[0]
1318            )
1319            .ip_addr()
1320            .unwrap(),
1321            Ipv4Addr::LOCALHOST
1322        );
1323    }
1324
1325    #[tokio::test]
1326    async fn test_lookup_into_iter() {
1327        assert_eq!(
1328            LookupFuture::lookup(
1329                vec![Name::root()],
1330                RecordType::A,
1331                DnsRequestOptions::default(),
1332                CachingClient::new(0, mock(vec![v4_message()]), false),
1333            )
1334            .await
1335            .unwrap()
1336            .into_iter()
1337            .map(|r| r.ip_addr().unwrap())
1338            .collect::<Vec<IpAddr>>(),
1339            vec![Ipv4Addr::LOCALHOST]
1340        );
1341    }
1342
1343    #[tokio::test]
1344    async fn test_error() {
1345        assert!(
1346            LookupFuture::lookup(
1347                vec![Name::root()],
1348                RecordType::A,
1349                DnsRequestOptions::default(),
1350                CachingClient::new(0, mock(vec![error()]), false),
1351            )
1352            .await
1353            .is_err()
1354        );
1355    }
1356
1357    #[tokio::test]
1358    async fn test_empty_no_response() {
1359        if let ProtoErrorKind::NoRecordsFound {
1360            query,
1361            negative_ttl,
1362            ..
1363        } = LookupFuture::lookup(
1364            vec![Name::root()],
1365            RecordType::A,
1366            DnsRequestOptions::default(),
1367            CachingClient::new(0, mock(vec![empty()]), false),
1368        )
1369        .await
1370        .expect_err("this should have been a NoRecordsFound")
1371        .proto()
1372        .expect("it should have been a ProtoError")
1373        .kind()
1374        {
1375            assert_eq!(**query, Query::query(Name::root(), RecordType::A));
1376            assert_eq!(*negative_ttl, None);
1377        } else {
1378            panic!("wrong error received");
1379        }
1380    }
1381
1382    #[derive(Clone)]
1383    struct MockDnsHandle {
1384        messages: Arc<Mutex<Vec<Result<DnsResponse, ProtoError>>>>,
1385    }
1386
1387    impl DnsHandle for MockDnsHandle {
1388        type Response = Pin<Box<dyn Stream<Item = Result<DnsResponse, ProtoError>> + Send>>;
1389
1390        fn send<R: Into<DnsRequest>>(&self, _: R) -> Self::Response {
1391            Box::pin(once(
1392                future::ready(self.messages.lock().unwrap().pop().unwrap_or_else(empty)).boxed(),
1393            ))
1394        }
1395    }
1396
1397    fn v4_message() -> Result<DnsResponse, ProtoError> {
1398        let mut message = Message::new();
1399        message.add_query(Query::query(Name::root(), RecordType::A));
1400        message.insert_answers(vec![Record::from_rdata(
1401            Name::root(),
1402            86400,
1403            RData::A(A::new(127, 0, 0, 1)),
1404        )]);
1405
1406        let resp = DnsResponse::from_message(message).unwrap();
1407        assert!(resp.contains_answer());
1408        Ok(resp)
1409    }
1410
1411    fn empty() -> Result<DnsResponse, ProtoError> {
1412        Ok(DnsResponse::from_message(Message::new()).unwrap())
1413    }
1414
1415    fn error() -> Result<DnsResponse, ProtoError> {
1416        Err(ProtoError::from(std::io::Error::from(
1417            std::io::ErrorKind::Other,
1418        )))
1419    }
1420
1421    fn mock(messages: Vec<Result<DnsResponse, ProtoError>>) -> MockDnsHandle {
1422        MockDnsHandle {
1423            messages: Arc::new(Mutex::new(messages)),
1424        }
1425    }
1426}