1#![deny(missing_docs)]
42
43use crate::ClientBuilder;
44
45use std::{
46 collections::HashMap,
47 net::{IpAddr, SocketAddr},
48 str::FromStr,
49 sync::{Arc, LazyLock},
50 time::Duration,
51};
52
53use hickory_resolver::{
54 TokioResolver,
55 config::{NameServerConfig, NameServerConfigGroup, ResolverConfig, ResolverOpts},
56 lookup_ip::LookupIpIntoIter,
57 name_server::TokioConnectionProvider,
58};
59use once_cell::sync::OnceCell;
60use reqwest::dns::{Addrs, Name, Resolve, Resolving};
61use tracing::*;
62
63mod constants;
64mod static_resolver;
65pub(crate) use static_resolver::*;
66
67pub(crate) const DEFAULT_POSITIVE_LOOKUP_CACHE_TTL: Duration = Duration::from_secs(1800);
68pub(crate) const DEFAULT_OVERALL_LOOKUP_TIMEOUT: Duration = Duration::from_secs(10);
69pub(crate) const DEFAULT_QUERY_TIMEOUT: Duration = Duration::from_secs(5);
70
71impl ClientBuilder {
72 pub fn dns_resolver<R: Resolve + 'static>(mut self, resolver: Arc<R>) -> Self {
74 self.reqwest_client_builder = self.reqwest_client_builder.dns_resolver(resolver);
75 self.use_secure_dns = false;
76 self
77 }
78
79 pub fn no_hickory_dns(mut self) -> Self {
81 self.use_secure_dns = false;
82 self
83 }
84}
85
86static SHARED_RESOLVER: LazyLock<HickoryDnsResolver> = LazyLock::new(|| {
90 tracing::debug!("Initializing shared DNS resolver");
91 HickoryDnsResolver {
92 use_shared: false, ..Default::default()
94 }
95});
96
97#[derive(Debug, thiserror::Error)]
98#[allow(missing_docs)]
99pub enum ResolveError {
101 #[error("invalid name: {0}")]
102 InvalidNameError(String),
103 #[error("hickory-dns resolver error: {0}")]
104 ResolveError(#[from] hickory_resolver::ResolveError),
105 #[error("high level lookup timed out")]
106 Timeout,
107 #[error("hostname not found in static lookup table")]
108 StaticLookupMiss,
109}
110
111impl ResolveError {
112 pub fn is_timeout(&self) -> bool {
114 matches!(self, ResolveError::Timeout)
115 }
116}
117
118#[derive(Debug, Clone)]
127pub struct HickoryDnsResolver {
128 state: Arc<OnceCell<TokioResolver>>,
132 fallback: Option<Arc<OnceCell<TokioResolver>>>,
133 static_base: Option<Arc<OnceCell<StaticResolver>>>,
134 use_shared: bool,
135 overall_dns_timeout: Duration,
138}
139
140impl Default for HickoryDnsResolver {
141 fn default() -> Self {
142 Self {
143 state: Default::default(),
144 fallback: Default::default(),
145 static_base: Some(Default::default()),
146 use_shared: true,
147 overall_dns_timeout: DEFAULT_OVERALL_LOOKUP_TIMEOUT,
148 }
149 }
150}
151
152impl Resolve for HickoryDnsResolver {
153 fn resolve(&self, name: Name) -> Resolving {
154 let resolver = self.state.clone();
155 let maybe_fallback = self.fallback.clone();
156 let maybe_static = self.static_base.clone();
157 let use_shared = self.use_shared;
158 let overall_dns_timeout = self.overall_dns_timeout;
159 Box::pin(async move {
160 resolve(
161 name,
162 resolver,
163 maybe_fallback,
164 maybe_static,
165 use_shared,
166 overall_dns_timeout,
167 )
168 .await
169 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
170 })
171 }
172}
173
174async fn resolve(
175 name: Name,
176 resolver: Arc<OnceCell<TokioResolver>>,
177 maybe_fallback: Option<Arc<OnceCell<TokioResolver>>>,
178 maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
179 independent: bool,
180 overall_dns_timeout: Duration,
181) -> Result<Addrs, ResolveError> {
182 let resolver = resolver.get_or_init(|| HickoryDnsResolver::new_resolver(independent));
183
184 if let Some(ref static_resolver) = maybe_static {
188 let resolver =
189 static_resolver.get_or_init(|| HickoryDnsResolver::new_static_fallback(independent));
190
191 if let Some(addrs) = resolver.pre_resolve(name.as_str()) {
192 let addrs: Addrs =
193 Box::new(addrs.into_iter().map(|ip_addr| SocketAddr::new(ip_addr, 0)));
194 return Ok(addrs);
195 }
196 }
197
198 let resolve_fut = tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
200 let primary_err = match resolve_fut.await {
201 Err(_) => ResolveError::Timeout,
202 Ok(Ok(lookup)) => {
203 let addrs: Addrs = Box::new(SocketAddrs {
204 iter: lookup.into_iter(),
205 });
206 return Ok(addrs);
207 }
208 Ok(Err(e)) => {
209 if !e.is_no_records_found() {
211 warn!("primary DNS failed w/ error: {e}");
212 }
213 e.into()
214 }
215 };
216
217 if let Some(ref fallback) = maybe_fallback {
220 let resolver =
221 fallback.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(independent))?;
222
223 let resolve_fut =
224 tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
225 if let Ok(Ok(lookup)) = resolve_fut.await {
226 let addrs: Addrs = Box::new(SocketAddrs {
227 iter: lookup.into_iter(),
228 });
229 return Ok(addrs);
230 }
231 }
232
233 if let Some(ref static_resolver) = maybe_static {
236 debug!("checking static");
237 let resolver =
238 static_resolver.get_or_init(|| HickoryDnsResolver::new_static_fallback(independent));
239
240 if let Ok(addrs) = resolver.resolve(name).await {
241 return Ok(addrs);
242 }
243 }
244
245 Err(primary_err)
246}
247
248struct SocketAddrs {
249 iter: LookupIpIntoIter,
250}
251
252impl Iterator for SocketAddrs {
253 type Item = SocketAddr;
254
255 fn next(&mut self) -> Option<Self::Item> {
256 self.iter.next().map(|ip_addr| SocketAddr::new(ip_addr, 0))
257 }
258}
259
260impl HickoryDnsResolver {
261 pub async fn resolve_str(
263 &self,
264 name: &str,
265 ) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> {
266 let n =
267 Name::from_str(name).map_err(|_| ResolveError::InvalidNameError(name.to_string()))?;
268 resolve(
269 n,
270 self.state.clone(),
271 self.fallback.clone(),
272 self.static_base.clone(),
273 self.use_shared,
274 self.overall_dns_timeout,
275 )
276 .await
277 .map(|addrs| addrs.map(|socket_addr| socket_addr.ip()))
278 }
279
280 pub fn thread_resolver() -> Self {
282 Self {
283 use_shared: false,
284 ..Default::default()
285 }
286 }
287
288 fn new_resolver(use_shared: bool) -> TokioResolver {
289 if use_shared {
292 SHARED_RESOLVER.state.get_or_init(new_resolver).clone()
293 } else {
294 new_resolver()
295 }
296 }
297
298 fn new_resolver_system(use_shared: bool) -> Result<TokioResolver, ResolveError> {
299 if !use_shared || SHARED_RESOLVER.fallback.is_none() {
302 new_resolver_system()
303 } else {
304 Ok(SHARED_RESOLVER
305 .fallback
306 .as_ref()
307 .unwrap()
308 .get_or_try_init(new_resolver_system)?
309 .clone())
310 }
311 }
312
313 fn new_static_fallback(use_shared: bool) -> StaticResolver {
314 if use_shared && let Some(ref shared_resolver) = SHARED_RESOLVER.static_base {
315 shared_resolver
316 .get_or_init(new_default_static_fallback)
317 .clone()
318 } else {
319 new_default_static_fallback()
320 }
321 }
322
323 pub fn enable_system_fallback(&mut self) -> Result<(), ResolveError> {
325 self.fallback = Some(Default::default());
326 let _ = self
327 .fallback
328 .as_ref()
329 .unwrap()
330 .get_or_try_init(new_resolver_system)?;
331
332 Ok(())
337 }
338
339 pub fn disable_system_fallback(&mut self) {
342 self.fallback = None;
343
344 }
349
350 pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
353 Some(self.static_base.as_ref()?.get()?.get_addrs())
354 }
355
356 pub fn set_static_fallbacks(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
358 let cell = OnceCell::new();
359 cell.set(StaticResolver::new(addrs))
360 .expect("infallible assign");
361 self.static_base = Some(Arc::new(cell));
362 }
363
364 fn default_options() -> ResolverOpts {
371 let mut opts = ResolverOpts::default();
372 opts.positive_min_ttl = Some(DEFAULT_POSITIVE_LOOKUP_CACHE_TTL);
374 opts.timeout = DEFAULT_QUERY_TIMEOUT;
375 opts.attempts = 0;
376
377 opts
378 }
379
380 pub fn all_configured_name_servers(&self) -> Vec<NameServerConfig> {
382 default_nameserver_group().to_vec()
383 }
384
385 pub fn active_name_servers(&self) -> Vec<NameServerConfig> {
387 if !self.use_shared {
388 return self
389 .state
390 .get()
391 .map(|r| r.config().name_servers().to_vec())
392 .unwrap_or(self.all_configured_name_servers());
393 }
394
395 SHARED_RESOLVER.active_name_servers()
396 }
397
398 pub async fn trial_nameservers(&self) {
401 let nameservers = default_nameserver_group();
402 for (ns, result) in trial_nameservers_inner(&nameservers).await {
403 if let Err(e) = result {
404 warn!("trial {ns:?} errored: {e}");
405 } else {
406 info!("trial {ns:?} succeeded");
407 }
408 }
409 }
410}
411
412fn new_resolver() -> TokioResolver {
422 let name_servers = default_nameserver_group_ipv4_only();
423
424 configure_and_build_resolver(name_servers)
425}
426
427fn configure_and_build_resolver<G>(name_servers: G) -> TokioResolver
428where
429 G: Into<NameServerConfigGroup>,
430{
431 let options = HickoryDnsResolver::default_options();
432 let name_servers: NameServerConfigGroup = name_servers.into();
433 info!("building new configured resolver");
434 debug!("configuring resolver with {options:?}, {name_servers:?}");
435
436 let config = ResolverConfig::from_parts(None, Vec::new(), name_servers);
437 let mut resolver_builder =
438 TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
439
440 resolver_builder = resolver_builder.with_options(options);
441
442 resolver_builder.build()
443}
444
445fn filter_ipv4(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
446 nameservers
447 .as_ref()
448 .iter()
449 .filter(|ns| ns.socket_addr.is_ipv4())
450 .cloned()
451 .collect()
452}
453
454#[allow(unused)]
455fn filter_ipv6(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
456 nameservers
457 .as_ref()
458 .iter()
459 .filter(|ns| ns.socket_addr.is_ipv6())
460 .cloned()
461 .collect()
462}
463
464#[allow(unused)]
465fn default_nameserver_group() -> NameServerConfigGroup {
466 let mut name_servers = NameServerConfigGroup::quad9_tls();
467 name_servers.merge(NameServerConfigGroup::quad9_https());
468 name_servers.merge(NameServerConfigGroup::cloudflare_tls());
469 name_servers.merge(NameServerConfigGroup::cloudflare_https());
470 name_servers
471}
472
473fn default_nameserver_group_ipv4_only() -> NameServerConfigGroup {
474 filter_ipv4(&default_nameserver_group() as &[NameServerConfig]).into()
475}
476
477#[allow(unused)]
478fn default_nameserver_group_ipv6_only() -> NameServerConfigGroup {
479 filter_ipv6(&default_nameserver_group() as &[NameServerConfig]).into()
480}
481
482fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
486 let mut resolver_builder = TokioResolver::builder_tokio()?;
487
488 let options = HickoryDnsResolver::default_options();
489 info!("building new fallback system resolver");
490 debug!("fallback system resolver with {options:?}");
491
492 resolver_builder = resolver_builder.with_options(options);
493
494 Ok(resolver_builder.build())
495}
496
497fn new_default_static_fallback() -> StaticResolver {
498 StaticResolver::new(constants::default_static_addrs())
499}
500
501async fn trial_nameservers_inner(
504 name_servers: &[NameServerConfig],
505) -> Vec<(NameServerConfig, Result<(), ResolveError>)> {
506 let mut trial_lookups = tokio::task::JoinSet::new();
507
508 for name_server in name_servers {
509 let ns = name_server.clone();
510 trial_lookups.spawn(async { (ns.clone(), trial_lookup(ns, "example.com").await) });
511 }
512
513 trial_lookups.join_all().await
514}
515
516async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(), ResolveError> {
519 debug!("running ns trial {name_server:?} query={query}");
520
521 let resolver = configure_and_build_resolver(vec![name_server]);
522
523 match tokio::time::timeout(DEFAULT_OVERALL_LOOKUP_TIMEOUT, resolver.ipv4_lookup(query)).await {
524 Ok(Ok(_)) => Ok(()),
525 Ok(Err(e)) => Err(e.into()),
526 Err(_) => Err(ResolveError::Timeout),
527 }
528}
529
530#[cfg(test)]
531mod test {
532 use super::*;
533 use itertools::Itertools;
534 use std::collections::HashMap;
535 use std::{
536 net::{IpAddr, Ipv4Addr, Ipv6Addr},
537 time::Instant,
538 };
539
540 const GUARANTEED_BROKEN_IPS_1: &[IpAddr] = &[
544 IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
545 IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)),
546 IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1111)),
547 IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1001)),
548 ];
549
550 #[tokio::test]
551 async fn reqwest_with_custom_dns() {
552 let var_name = HickoryDnsResolver::default();
553 let resolver = var_name;
554 let client = reqwest::ClientBuilder::new()
555 .dns_resolver(resolver.into())
556 .build()
557 .unwrap();
558
559 let resp = client
560 .get("http://ifconfig.me:80")
561 .send()
562 .await
563 .unwrap()
564 .bytes()
565 .await
566 .unwrap();
567
568 assert!(!resp.is_empty());
569 }
570
571 #[tokio::test]
572 async fn dns_lookup() -> Result<(), ResolveError> {
573 let resolver = HickoryDnsResolver::default();
574
575 let domain = "ifconfig.me";
576 let addrs = resolver.resolve_str(domain).await?;
577
578 assert!(addrs.into_iter().next().is_some());
579
580 Ok(())
581 }
582
583 #[tokio::test]
584 async fn static_resolver_as_fallback() -> Result<(), ResolveError> {
585 let example_domain = "non-existent.nymvpn.com";
586 let mut resolver = HickoryDnsResolver {
587 ..Default::default()
588 };
589
590 let result = resolver.resolve_str(example_domain).await;
591 assert!(result.is_err()); resolver.static_base = Some(Default::default());
594
595 let mut addr_map = HashMap::new();
596 let example_ip4: IpAddr = "10.10.10.10".parse().unwrap();
597 let example_ip6: IpAddr = "dead::beef".parse().unwrap();
598 addr_map.insert(example_domain.to_string(), vec![example_ip4, example_ip6]);
599
600 resolver.set_static_fallbacks(addr_map);
601
602 let mut addrs = resolver.resolve_str(example_domain).await?;
603 assert!(addrs.contains(&example_ip4));
604 assert!(addrs.contains(&example_ip6));
605 Ok(())
606 }
607
608 #[tokio::test]
611 async fn trial_nameservers() {
612 let good_cf_ip = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
613
614 let mut ns_ips = GUARANTEED_BROKEN_IPS_1.to_vec();
615 ns_ips.push(good_cf_ip);
616
617 let broken_ns_https = NameServerConfigGroup::from_ips_https(
618 &ns_ips,
619 443,
620 "cloudflare-dns.com".to_string(),
621 true,
622 );
623
624 let inner = configure_and_build_resolver(broken_ns_https);
625
626 let resolver = HickoryDnsResolver {
628 use_shared: false,
629 state: Arc::new(OnceCell::with_value(inner)),
630 static_base: Some(Default::default()),
631 ..Default::default()
632 };
633
634 let name_servers = resolver.state.get().unwrap().config().name_servers();
635 for (ns, result) in trial_nameservers_inner(name_servers).await {
636 if ns.socket_addr.ip() == good_cf_ip {
637 assert!(result.is_ok())
638 } else {
639 assert!(result.is_err())
640 }
641 }
642 }
643
644 mod failure_test {
645 use super::*;
646
647 fn build_broken_resolver() -> Result<TokioResolver, ResolveError> {
650 info!("building new faulty resolver");
651
652 let mut broken_ns_group = NameServerConfigGroup::from_ips_tls(
653 GUARANTEED_BROKEN_IPS_1,
654 853,
655 "cloudflare-dns.com".to_string(),
656 true,
657 );
658 let broken_ns_https = NameServerConfigGroup::from_ips_https(
659 GUARANTEED_BROKEN_IPS_1,
660 443,
661 "cloudflare-dns.com".to_string(),
662 true,
663 );
664 broken_ns_group.merge(broken_ns_https);
665
666 Ok(configure_and_build_resolver(broken_ns_group))
667 }
668
669 #[tokio::test]
670 async fn dns_lookup_failures() -> Result<(), ResolveError> {
671 let time_start = std::time::Instant::now();
672
673 let r = OnceCell::new();
674 r.set(build_broken_resolver().expect("failed to build resolver"))
675 .expect("broken resolver init error");
676
677 let resolver = HickoryDnsResolver {
679 use_shared: false,
680 state: Arc::new(r),
681 overall_dns_timeout: Duration::from_secs(5),
682 ..Default::default()
683 };
684 build_broken_resolver()?;
685 let domain = "ifconfig.me";
686 let result = resolver.resolve_str(domain).await;
687 assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
688
689 let duration = time_start.elapsed();
690 assert!(duration < resolver.overall_dns_timeout + Duration::from_secs(1));
691
692 Ok(())
693 }
694
695 #[tokio::test]
696 async fn fallback_to_static() -> Result<(), ResolveError> {
697 let r = OnceCell::new();
698 r.set(build_broken_resolver().expect("failed to build resolver"))
699 .expect("broken resolver init error");
700
701 let resolver = HickoryDnsResolver {
703 use_shared: false,
704 state: Arc::new(r),
705 static_base: Some(Default::default()),
706 overall_dns_timeout: Duration::from_secs(5),
707 ..Default::default()
708 };
709 build_broken_resolver()?;
710
711 let domain = "nymvpn.com";
713 let _ = resolver
714 .resolve_str(domain)
715 .await
716 .expect("failed to resolve address in static lookup");
717
718 let domain = "non-existent.nymtech.net";
720 let result = resolver.resolve_str(domain).await;
721 assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
722
723 Ok(())
724 }
725
726 #[test]
727 fn default_resolver_uses_ipv4_only_nameservers() {
728 let resolver = HickoryDnsResolver::thread_resolver();
729 resolver
730 .active_name_servers()
731 .iter()
732 .all(|cfg| cfg.socket_addr.is_ipv4());
733
734 SHARED_RESOLVER
735 .active_name_servers()
736 .iter()
737 .all(|cfg| cfg.socket_addr.is_ipv4());
738 }
739
740 #[tokio::test]
741 #[ignore]
742 async fn dns_lookup_failure_on_shared() -> Result<(), ResolveError> {
746 let time_start = Instant::now();
747 let r = OnceCell::new();
748 r.set(build_broken_resolver().expect("failed to build resolver"))
749 .expect("broken resolver init error");
750
751 let resolver = HickoryDnsResolver::default();
753
754 let domain = "rpc.nymtech.net";
756 let _ = resolver
757 .resolve_str(domain)
758 .await
759 .expect("failed to resolve address in static lookup");
760
761 println!(
762 "{}ms resolved {domain}",
763 (Instant::now() - time_start).as_millis()
764 );
765
766 let domain = "non-existent.nymtech.net";
768 let result = resolver.resolve_str(domain).await;
769 assert!(result.is_err());
770 Ok(())
773 }
774 }
775}