1#![deny(missing_docs)]
42
43use crate::ClientBuilder;
44
45use std::{
46 collections::HashMap,
47 net::{IpAddr, SocketAddr},
48 str::FromStr,
49 sync::{
50 Arc, LazyLock,
51 atomic::{AtomicBool, Ordering::Relaxed},
52 },
53 time::Duration,
54};
55
56use hickory_resolver::{
57 TokioResolver,
58 config::{NameServerConfig, NameServerConfigGroup, ResolverConfig, ResolverOpts},
59 lookup_ip::LookupIpIntoIter,
60 name_server::TokioConnectionProvider,
61};
62use once_cell::sync::OnceCell;
63use reqwest::dns::{Addrs, Name, Resolve, Resolving};
64use tracing::*;
65
66mod constants;
67mod static_resolver;
68pub(crate) use static_resolver::*;
69
70pub(crate) const DEFAULT_POSITIVE_LOOKUP_CACHE_TTL: Duration = Duration::from_secs(1800);
71pub(crate) const DEFAULT_OVERALL_LOOKUP_TIMEOUT: Duration = Duration::from_secs(10);
72pub(crate) const DEFAULT_QUERY_TIMEOUT: Duration = Duration::from_secs(5);
73
74impl ClientBuilder {
75 pub fn dns_resolver<R: Resolve + 'static>(mut self, resolver: Arc<R>) -> Self {
78 self = self.non_shared();
79 if let Some(rb) = self.reqwest_client_builder {
81 self.reqwest_client_builder = Some(rb.dns_resolver(resolver));
82 }
83 self.use_secure_dns = false;
84 self
85 }
86
87 pub fn no_hickory_dns(mut self) -> Self {
92 self = self.non_shared();
93 self.use_secure_dns = false;
94 self
95 }
96}
97
98static SHARED_RESOLVER: LazyLock<HickoryDnsResolver> = LazyLock::new(|| {
102 tracing::debug!("Initializing shared DNS resolver");
103 HickoryDnsResolver {
104 use_shared: false, ..Default::default()
106 }
107});
108
109#[derive(Debug, thiserror::Error)]
110#[allow(missing_docs)]
111pub enum ResolveError {
113 #[error("invalid name: {0}")]
114 InvalidNameError(String),
115 #[error("hickory-dns resolver error: {0}")]
116 ResolveError(#[from] hickory_resolver::ResolveError),
117 #[error("high level lookup timed out")]
118 Timeout,
119 #[error("hostname not found in static lookup table")]
120 StaticLookupMiss,
121}
122
123impl ResolveError {
124 pub fn is_timeout(&self) -> bool {
126 matches!(self, ResolveError::Timeout)
127 }
128}
129
130#[derive(Debug, Clone)]
139pub struct HickoryDnsResolver {
140 state: Arc<OnceCell<TokioResolver>>,
144 use_system: Arc<AtomicBool>,
145 system_resolver: Arc<OnceCell<TokioResolver>>,
146 static_base: Option<Arc<OnceCell<StaticResolver>>>,
147 use_shared: bool,
148 overall_dns_timeout: Duration,
151}
152
153impl Default for HickoryDnsResolver {
154 fn default() -> Self {
155 Self {
156 state: Default::default(),
157 use_system: Arc::new(AtomicBool::new(false)),
158 system_resolver: Default::default(),
159 static_base: Some(Default::default()),
160 use_shared: true,
161 overall_dns_timeout: DEFAULT_OVERALL_LOOKUP_TIMEOUT,
162 }
163 }
164}
165
166impl Resolve for HickoryDnsResolver {
167 fn resolve(&self, name: Name) -> Resolving {
168 let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
169 let use_shared = self.use_shared;
170 let resolver = if use_system {
171 match self
172 .system_resolver
173 .get_or_try_init(|| HickoryDnsResolver::new_resolver_system(use_shared))
174 {
175 Ok(r) => r.clone(),
176 Err(e) => return Box::pin(return_err(e)),
177 }
178 } else {
179 self.state
180 .get_or_init(|| HickoryDnsResolver::new_resolver(use_shared))
181 .clone()
182 };
183
184 let maybe_static = self.static_base.clone();
185 let overall_dns_timeout = self.overall_dns_timeout;
186 Box::pin(async move {
187 resolve(
188 name,
189 resolver,
190 maybe_static,
191 use_shared,
192 overall_dns_timeout,
193 )
194 .await
195 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
196 })
197 }
198}
199
200async fn return_err(e: ResolveError) -> Result<Addrs, Box<dyn std::error::Error + Send + Sync>> {
201 Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
202}
203
204async fn resolve(
205 name: Name,
206 resolver: TokioResolver,
207 maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
208 independent: bool,
209 overall_dns_timeout: Duration,
210) -> Result<Addrs, ResolveError> {
211 if let Some(ref static_resolver) = maybe_static {
215 let resolver =
216 static_resolver.get_or_init(|| HickoryDnsResolver::new_static_fallback(independent));
217
218 if let Some(addrs) = resolver.pre_resolve(name.as_str()) {
219 let addrs: Addrs =
220 Box::new(addrs.into_iter().map(|ip_addr| SocketAddr::new(ip_addr, 0)));
221 return Ok(addrs);
222 }
223 }
224
225 let resolve_fut = tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
227 let primary_err = match resolve_fut.await {
228 Err(_) => ResolveError::Timeout,
229 Ok(Ok(lookup)) => {
230 let addrs: Addrs = Box::new(SocketAddrs {
231 iter: lookup.into_iter(),
232 });
233 return Ok(addrs);
234 }
235 Ok(Err(e)) => {
236 if !e.is_no_records_found() {
238 warn!("primary DNS failed w/ error: {e}");
239 }
240 e.into()
241 }
242 };
243
244 if let Some(ref static_resolver) = maybe_static {
247 debug!("checking static");
248 let resolver =
249 static_resolver.get_or_init(|| HickoryDnsResolver::new_static_fallback(independent));
250
251 if let Ok(addrs) = resolver.resolve(name).await {
252 return Ok(addrs);
253 }
254 }
255
256 Err(primary_err)
257}
258
259struct SocketAddrs {
260 iter: LookupIpIntoIter,
261}
262
263impl Iterator for SocketAddrs {
264 type Item = SocketAddr;
265
266 fn next(&mut self) -> Option<Self::Item> {
267 self.iter.next().map(|ip_addr| SocketAddr::new(ip_addr, 0))
268 }
269}
270
271impl HickoryDnsResolver {
272 pub fn shared() -> Self {
274 SHARED_RESOLVER.clone()
275 }
276
277 pub async fn resolve_str(
279 &self,
280 name: &str,
281 ) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> {
282 let n =
283 Name::from_str(name).map_err(|_| ResolveError::InvalidNameError(name.to_string()))?;
284 let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
285 let resolver = if use_system {
286 self.system_resolver
287 .get_or_try_init(|| HickoryDnsResolver::new_resolver_system(self.use_shared))?
288 .clone()
289 } else {
290 self.state
291 .get_or_init(|| HickoryDnsResolver::new_resolver(self.use_shared))
292 .clone()
293 };
294
295 resolve(
296 n,
297 resolver,
298 self.static_base.clone(),
299 self.use_shared,
300 self.overall_dns_timeout,
301 )
302 .await
303 .map(|addrs| addrs.map(|socket_addr| socket_addr.ip()))
304 }
305
306 pub fn thread_resolver() -> Self {
308 Self {
309 use_shared: false,
310 ..Default::default()
311 }
312 }
313
314 fn new_resolver(use_shared: bool) -> TokioResolver {
315 if use_shared {
318 SHARED_RESOLVER.state.get_or_init(new_resolver).clone()
319 } else {
320 new_resolver()
321 }
322 }
323
324 fn new_resolver_system(use_shared: bool) -> Result<TokioResolver, ResolveError> {
325 if !use_shared {
328 new_resolver_system()
329 } else {
330 Ok(SHARED_RESOLVER
331 .system_resolver
332 .get_or_try_init(new_resolver_system)?
333 .clone())
334 }
335 }
336
337 fn new_static_fallback(use_shared: bool) -> StaticResolver {
338 if use_shared && let Some(ref shared_resolver) = SHARED_RESOLVER.static_base {
339 shared_resolver
340 .get_or_init(new_default_static_fallback)
341 .clone()
342 } else {
343 new_default_static_fallback()
344 }
345 }
346
347 pub fn use_system_resolver(&self) {
350 self.use_system.store(true, Relaxed);
351
352 if self.use_shared {
353 SHARED_RESOLVER.use_system_resolver();
354 }
355 }
356
357 pub fn use_configured_resolver(&self) {
360 self.use_system.store(false, Relaxed);
361
362 if self.use_shared {
363 SHARED_RESOLVER.use_configured_resolver();
364 }
365 }
366
367 pub fn clear_preresolve(&self) {
375 debug!("clearing pre-resolve table");
376 if let Some(cell) = &self.static_base
377 && let Some(static_base) = cell.get()
378 {
379 static_base.clear_preresolve()
380 }
381 }
382
383 pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
386 Some(self.static_base.as_ref()?.get()?.get_fallback_addrs())
387 }
388
389 pub fn set_fallback_addrs(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
391 debug!("setting fallback entries for {:?}", addrs.keys());
392 if self.static_base.is_none() {
393 let cell = OnceCell::new();
394 self.static_base = Some(Arc::new(cell));
395 }
396 self.static_base
397 .as_ref()
398 .unwrap()
399 .get_or_init(|| Self::new_static_fallback(self.use_shared))
400 .set_fallback(addrs);
401 }
402
403 pub fn get_static_preresolve(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
406 Some(self.static_base.as_ref()?.get()?.get_preresolve_addrs())
407 }
408
409 pub fn set_static_preresolve(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
411 debug!("setting pre-resolve entries for {:?}", addrs.keys());
412 if self.static_base.is_none() {
413 let cell = OnceCell::new();
414 self.static_base = Some(Arc::new(cell));
415 }
416 self.static_base
417 .as_ref()
418 .unwrap()
419 .get_or_init(|| Self::new_static_fallback(self.use_shared))
420 .set_preresolve(addrs);
421 }
422
423 fn default_options() -> ResolverOpts {
430 let mut opts = ResolverOpts::default();
431 opts.positive_min_ttl = Some(DEFAULT_POSITIVE_LOOKUP_CACHE_TTL);
433 opts.timeout = DEFAULT_QUERY_TIMEOUT;
434 opts.attempts = 0;
435
436 opts
437 }
438
439 pub fn all_configured_name_servers(&self) -> Vec<NameServerConfig> {
441 default_nameserver_group().to_vec()
442 }
443
444 pub fn active_name_servers(&self) -> Vec<NameServerConfig> {
446 if !self.use_shared {
447 return self
448 .state
449 .get()
450 .map(|r| r.config().name_servers().to_vec())
451 .unwrap_or(self.all_configured_name_servers());
452 }
453
454 SHARED_RESOLVER.active_name_servers()
455 }
456
457 pub async fn trial_nameservers(&self) {
460 let nameservers = default_nameserver_group();
461 for (ns, result) in trial_nameservers_inner(&nameservers).await {
462 if let Err(e) = result {
463 warn!("trial {ns:?} errored: {e}");
464 } else {
465 info!("trial {ns:?} succeeded");
466 }
467 }
468 }
469}
470
471fn new_resolver() -> TokioResolver {
481 let name_servers = default_nameserver_group_ipv4_only();
482
483 configure_and_build_resolver(name_servers)
484}
485
486fn configure_and_build_resolver<G>(name_servers: G) -> TokioResolver
487where
488 G: Into<NameServerConfigGroup>,
489{
490 let options = HickoryDnsResolver::default_options();
491 let name_servers: NameServerConfigGroup = name_servers.into();
492 info!("building new configured resolver");
493 debug!("configuring resolver with {options:?}, {name_servers:?}");
494
495 let config = ResolverConfig::from_parts(None, Vec::new(), name_servers);
496 let mut resolver_builder =
497 TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
498
499 resolver_builder = resolver_builder.with_options(options);
500
501 resolver_builder.build()
502}
503
504fn filter_ipv4(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
505 nameservers
506 .as_ref()
507 .iter()
508 .filter(|ns| ns.socket_addr.is_ipv4())
509 .cloned()
510 .collect()
511}
512
513#[allow(unused)]
514fn filter_ipv6(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
515 nameservers
516 .as_ref()
517 .iter()
518 .filter(|ns| ns.socket_addr.is_ipv6())
519 .cloned()
520 .collect()
521}
522
523#[allow(unused)]
524fn default_nameserver_group() -> NameServerConfigGroup {
525 let mut name_servers = NameServerConfigGroup::quad9_tls();
526 name_servers.merge(NameServerConfigGroup::quad9_https());
527 name_servers.merge(NameServerConfigGroup::cloudflare_tls());
528 name_servers.merge(NameServerConfigGroup::cloudflare_https());
529 name_servers
530}
531
532fn default_nameserver_group_ipv4_only() -> NameServerConfigGroup {
533 filter_ipv4(&default_nameserver_group() as &[NameServerConfig]).into()
534}
535
536#[allow(unused)]
537fn default_nameserver_group_ipv6_only() -> NameServerConfigGroup {
538 filter_ipv6(&default_nameserver_group() as &[NameServerConfig]).into()
539}
540
541fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
545 let mut resolver_builder = TokioResolver::builder_tokio()?;
546
547 let options = HickoryDnsResolver::default_options();
548 info!("building new fallback system resolver");
549 debug!("fallback system resolver with {options:?}");
550
551 resolver_builder = resolver_builder.with_options(options);
552
553 Ok(resolver_builder.build())
554}
555
556fn new_default_static_fallback() -> StaticResolver {
557 StaticResolver::new().with_fallback(constants::default_static_addrs())
558}
559
560async fn trial_nameservers_inner(
563 name_servers: &[NameServerConfig],
564) -> Vec<(NameServerConfig, Result<(), ResolveError>)> {
565 let mut trial_lookups = tokio::task::JoinSet::new();
566
567 for name_server in name_servers {
568 let ns = name_server.clone();
569 trial_lookups.spawn(async { (ns.clone(), trial_lookup(ns, "example.com").await) });
570 }
571
572 trial_lookups.join_all().await
573}
574
575async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(), ResolveError> {
578 debug!("running ns trial {name_server:?} query={query}");
579
580 let resolver = configure_and_build_resolver(vec![name_server]);
581
582 match tokio::time::timeout(DEFAULT_OVERALL_LOOKUP_TIMEOUT, resolver.ipv4_lookup(query)).await {
583 Ok(Ok(_)) => Ok(()),
584 Ok(Err(e)) => Err(e.into()),
585 Err(_) => Err(ResolveError::Timeout),
586 }
587}
588
589#[cfg(test)]
590mod test {
591 use super::*;
592 use itertools::Itertools;
593 use std::collections::HashMap;
594 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
595
596 const GUARANTEED_BROKEN_IPS_1: &[IpAddr] = &[
600 IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
601 IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)),
602 IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1111)),
603 IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1001)),
604 ];
605
606 #[tokio::test]
607 async fn reqwest_with_custom_dns() {
608 let var_name = HickoryDnsResolver::default();
609 let resolver = var_name;
610 let client = reqwest::ClientBuilder::new()
611 .dns_resolver(resolver)
612 .build()
613 .unwrap();
614
615 let resp = client
616 .get("http://ifconfig.me:80")
617 .send()
618 .await
619 .unwrap()
620 .bytes()
621 .await
622 .unwrap();
623
624 assert!(!resp.is_empty());
625 }
626
627 #[tokio::test]
628 async fn dns_lookup() -> Result<(), ResolveError> {
629 let resolver = HickoryDnsResolver::default();
630
631 let domain = "ifconfig.me";
632 let addrs = resolver.resolve_str(domain).await?;
633
634 assert!(addrs.into_iter().next().is_some());
635
636 Ok(())
637 }
638
639 #[tokio::test]
640 async fn static_resolver_as_fallback() -> Result<(), ResolveError> {
641 let example_domain = "non-existent.nymvpn.com";
642 let mut resolver = HickoryDnsResolver {
643 ..Default::default()
644 };
645
646 let result = resolver.resolve_str(example_domain).await;
647 assert!(result.is_err()); resolver.static_base = Some(Default::default());
650
651 let mut addr_map = HashMap::new();
652 let example_ip4: IpAddr = "10.10.10.10".parse().unwrap();
653 let example_ip6: IpAddr = "dead::beef".parse().unwrap();
654 addr_map.insert(example_domain.to_string(), vec![example_ip4, example_ip6]);
655
656 resolver.set_fallback_addrs(addr_map);
657
658 let mut addrs = resolver.resolve_str(example_domain).await?;
659 assert!(addrs.contains(&example_ip4));
660 assert!(addrs.contains(&example_ip6));
661 Ok(())
662 }
663
664 #[tokio::test]
667 async fn trial_nameservers() {
668 let good_cf_ip = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
669
670 let mut ns_ips = GUARANTEED_BROKEN_IPS_1.to_vec();
671 ns_ips.push(good_cf_ip);
672
673 let broken_ns_https = NameServerConfigGroup::from_ips_https(
674 &ns_ips,
675 443,
676 "cloudflare-dns.com".to_string(),
677 true,
678 );
679
680 let inner = configure_and_build_resolver(broken_ns_https);
681
682 let resolver = HickoryDnsResolver {
684 use_shared: false,
685 state: Arc::new(OnceCell::with_value(inner)),
686 static_base: Some(Default::default()),
687 ..Default::default()
688 };
689
690 let name_servers = resolver.state.get().unwrap().config().name_servers();
691 for (ns, result) in trial_nameservers_inner(name_servers).await {
692 if ns.socket_addr.ip() == good_cf_ip {
693 assert!(result.is_ok())
694 } else {
695 assert!(result.is_err())
696 }
697 }
698 }
699
700 mod failure_test {
701 use super::*;
702
703 fn build_broken_resolver() -> Result<TokioResolver, ResolveError> {
706 info!("building new faulty resolver");
707
708 let mut broken_ns_group = NameServerConfigGroup::from_ips_tls(
709 GUARANTEED_BROKEN_IPS_1,
710 853,
711 "cloudflare-dns.com".to_string(),
712 true,
713 );
714 let broken_ns_https = NameServerConfigGroup::from_ips_https(
715 GUARANTEED_BROKEN_IPS_1,
716 443,
717 "cloudflare-dns.com".to_string(),
718 true,
719 );
720 broken_ns_group.merge(broken_ns_https);
721
722 Ok(configure_and_build_resolver(broken_ns_group))
723 }
724
725 #[tokio::test]
726 async fn dns_lookup_failures() -> Result<(), ResolveError> {
727 let time_start = std::time::Instant::now();
728
729 let r = OnceCell::new();
730 r.set(build_broken_resolver().expect("failed to build resolver"))
731 .expect("broken resolver init error");
732
733 let resolver = HickoryDnsResolver {
735 use_shared: false,
736 state: Arc::new(r),
737 overall_dns_timeout: Duration::from_secs(5),
738 ..Default::default()
739 };
740 build_broken_resolver()?;
741 let domain = "ifconfig.me";
742 let result = resolver.resolve_str(domain).await;
743 assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
744
745 let duration = time_start.elapsed();
746 assert!(duration < resolver.overall_dns_timeout + Duration::from_secs(1));
747
748 Ok(())
749 }
750
751 #[tokio::test]
752 async fn fallback_to_static() -> Result<(), ResolveError> {
753 let r = OnceCell::new();
754 r.set(build_broken_resolver().expect("failed to build resolver"))
755 .expect("broken resolver init error");
756
757 let resolver = HickoryDnsResolver {
759 use_shared: false,
760 state: Arc::new(r),
761 static_base: Some(Default::default()),
762 overall_dns_timeout: Duration::from_secs(5),
763 ..Default::default()
764 };
765 build_broken_resolver()?;
766
767 let domain = "nymvpn.com";
769 let _ = resolver
770 .resolve_str(domain)
771 .await
772 .expect("failed to resolve address in static lookup");
773
774 let domain = "non-existent.nymtech.net";
776 let result = resolver.resolve_str(domain).await;
777 assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
778
779 Ok(())
780 }
781
782 #[test]
783 fn default_resolver_uses_ipv4_only_nameservers() {
784 let resolver = HickoryDnsResolver::thread_resolver();
785 resolver
786 .active_name_servers()
787 .iter()
788 .all(|cfg| cfg.socket_addr.is_ipv4());
789
790 SHARED_RESOLVER
791 .active_name_servers()
792 .iter()
793 .all(|cfg| cfg.socket_addr.is_ipv4());
794 }
795
796 #[tokio::test]
797 #[cfg(any())] async fn dns_lookup_failure_on_shared() -> Result<(), ResolveError> {
805 let resolver1 = HickoryDnsResolver::shared();
806
807 let time_start = std::time::Instant::now();
808 let resolver = HickoryDnsResolver::shared();
810
811 let domain = "rpc.nymtech.net";
813 let _ = resolver
814 .resolve_str(domain)
815 .await
816 .expect("failed to resolve address in static lookup");
817
818 let lookup_dur = Instant::now() - time_start;
819 assert!(
820 lookup_dur > resolver.overall_dns_timeout,
821 "expected lookup timeout - took {}ms",
822 (lookup_dur).as_millis()
823 );
824
825 let time_start = std::time::Instant::now();
826 let domain = "rpc.nymtech.net";
828 let _ = resolver1
829 .resolve_str(domain)
830 .await
831 .expect("domain expected to be in pre-resolve");
832
833 let lookup_dur = std::time::Instant::now() - time_start;
835 assert!(
836 lookup_dur < Duration::from_millis(10),
837 "expected instant - took {}ms",
838 (lookup_dur).as_millis()
839 );
840
841 let domain = "non-existent.nymtech.net";
843 let result = resolver.resolve_str(domain).await;
844 assert!(result.is_err());
845 Ok(())
848 }
849
850 #[tokio::test]
851 #[cfg(any())] async fn setting_dns_fallbacks_with_shared_resolver() -> Result<(), ResolveError> {
855 let resolver1 = HickoryDnsResolver::shared();
856
857 let mut resolver = HickoryDnsResolver::shared();
859
860 let example_domains = [
861 String::from("static1.nymvpn.com"),
862 String::from("static2.nymvpn.com"),
863 ];
864 let mut addr_map1 = HashMap::new();
865 addr_map1.insert(
866 example_domains[0].clone(),
867 vec![Ipv4Addr::new(10, 10, 10, 10).into()],
868 );
869 addr_map1.insert(
870 example_domains[1].clone(),
871 vec![Ipv4Addr::new(1, 1, 1, 1).into()],
872 );
873
874 resolver.set_static_preresolve(addr_map1);
875
876 let time_start = std::time::Instant::now();
877 let _ = resolver1
879 .resolve_str(&example_domains[0])
880 .await
881 .expect("domain expected to be in pre-resolve");
882
883 let lookup_dur = std::time::Instant::now() - time_start;
885 assert!(
886 lookup_dur < Duration::from_millis(10),
887 "expected instant - took {}ms",
888 (lookup_dur).as_millis()
889 );
890
891 resolver.clear_preresolve();
893
894 let prereslve_lookup = resolver1
896 .static_base
897 .as_ref()
898 .unwrap()
899 .get()
900 .unwrap()
901 .pre_resolve(&example_domains[0]);
902 assert!(prereslve_lookup.is_none());
903
904 Ok(())
905 }
906 }
907}