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::{CLOUDFLARE, NameServerConfig, QUAD9, ResolverConfig, ResolverOpts},
59 net::{NetError, runtime::TokioRuntimeProvider},
60};
61use once_cell::sync::OnceCell;
62use reqwest::dns::{Addrs, Name, Resolve, Resolving};
63use tracing::*;
64
65mod constants;
66mod static_resolver;
67pub(crate) use static_resolver::*;
68
69pub(crate) const DEFAULT_POSITIVE_LOOKUP_CACHE_TTL: Duration = Duration::from_secs(1800);
70pub(crate) const DEFAULT_OVERALL_LOOKUP_TIMEOUT: Duration = Duration::from_secs(10);
71pub(crate) const DEFAULT_QUERY_TIMEOUT: Duration = Duration::from_secs(5);
72
73impl ClientBuilder {
74 pub fn dns_resolver<R: Resolve + 'static>(mut self, resolver: Arc<R>) -> Self {
77 self = self.non_shared();
78 if let Some(rb) = self.reqwest_client_builder {
80 self.reqwest_client_builder = Some(rb.dns_resolver(resolver));
81 }
82 self.use_secure_dns = false;
83 self
84 }
85
86 pub fn no_hickory_dns(mut self) -> Self {
91 self = self.non_shared();
92 self.use_secure_dns = false;
93 self
94 }
95}
96
97static SHARED_RESOLVER: LazyLock<HickoryDnsResolver> = LazyLock::new(|| {
101 tracing::debug!("Initializing shared DNS resolver");
102 HickoryDnsResolver {
103 use_shared: false, ..Default::default()
105 }
106});
107
108#[derive(Debug, thiserror::Error)]
109#[allow(missing_docs)]
110pub enum ResolveError {
112 #[error("invalid name: {0}")]
113 InvalidNameError(String),
114 #[error("hickory-dns resolver error: {0}")]
115 ResolveError(#[from] NetError),
116 #[error("high level lookup timed out")]
117 Timeout,
118 #[error("hostname not found in static lookup table")]
119 StaticLookupMiss,
120}
121
122impl ResolveError {
123 pub fn is_timeout(&self) -> bool {
125 matches!(
126 self,
127 ResolveError::Timeout | ResolveError::ResolveError(NetError::Timeout)
128 )
129 }
130}
131
132#[derive(Debug, Clone)]
141pub struct HickoryDnsResolver {
142 state: Arc<OnceCell<TokioResolver>>,
146 use_system: Arc<AtomicBool>,
147 system_resolver: Arc<OnceCell<TokioResolver>>,
148 static_base: Option<Arc<OnceCell<StaticResolver>>>,
149 use_shared: bool,
150 overall_dns_timeout: Duration,
153}
154
155impl Default for HickoryDnsResolver {
156 fn default() -> Self {
157 Self {
158 state: Default::default(),
159 use_system: Arc::new(AtomicBool::new(false)),
160 system_resolver: Default::default(),
161 static_base: Some(Default::default()),
162 use_shared: true,
163 overall_dns_timeout: DEFAULT_OVERALL_LOOKUP_TIMEOUT,
164 }
165 }
166}
167
168impl Resolve for HickoryDnsResolver {
169 fn resolve(&self, name: Name) -> Resolving {
170 let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
171 let use_shared = self.use_shared;
172 let result = if use_system {
173 self.system_resolver
174 .get_or_try_init(|| HickoryDnsResolver::new_resolver_system(use_shared))
175 } else {
176 self.state
177 .get_or_try_init(|| HickoryDnsResolver::new_resolver(use_shared))
178 };
179
180 let resolver = match result {
181 Ok(r) => r.clone(),
182 Err(err) => return Box::pin(return_err(err)),
183 };
184
185 let maybe_static = self.static_base.clone();
186 let overall_dns_timeout = self.overall_dns_timeout;
187 Box::pin(async move {
188 resolve(
189 name,
190 resolver,
191 maybe_static,
192 use_shared,
193 overall_dns_timeout,
194 )
195 .await
196 .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
197 })
198 }
199}
200
201async fn return_err(e: ResolveError) -> Result<Addrs, Box<dyn std::error::Error + Send + Sync>> {
202 Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
203}
204
205async fn resolve(
206 name: Name,
207 resolver: TokioResolver,
208 maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
209 independent: bool,
210 overall_dns_timeout: Duration,
211) -> Result<Addrs, ResolveError> {
212 if let Some(ref static_resolver) = maybe_static {
216 let resolver =
217 static_resolver.get_or_init(|| HickoryDnsResolver::new_static_fallback(independent));
218
219 if let Some(addrs) = resolver.pre_resolve(name.as_str()) {
220 let addrs: Addrs =
221 Box::new(addrs.into_iter().map(|ip_addr| SocketAddr::new(ip_addr, 0)));
222 return Ok(addrs);
223 }
224 }
225
226 let resolve_fut = tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
228 let primary_err = match resolve_fut.await {
229 Err(_) => ResolveError::Timeout,
230 Ok(Ok(lookup)) => {
231 let mut ips = Vec::from_iter(lookup.iter());
234 fastrand::shuffle(&mut ips);
235 let addrs: Addrs = Box::new(ips.into_iter().map(|ip| SocketAddr::new(ip, 0)));
236 return Ok(addrs);
237 }
238 Ok(Err(e)) => {
239 if !e.is_no_records_found() {
241 warn!("primary DNS failed w/ error: {e}");
242 }
243 e.into()
244 }
245 };
246
247 if let Some(ref static_resolver) = maybe_static {
250 debug!("checking static");
251 let resolver =
252 static_resolver.get_or_init(|| HickoryDnsResolver::new_static_fallback(independent));
253
254 if let Ok(addrs) = resolver.resolve(name).await {
255 return Ok(addrs);
256 }
257 }
258
259 Err(primary_err)
260}
261
262impl HickoryDnsResolver {
263 pub fn shared() -> Self {
265 SHARED_RESOLVER.clone()
266 }
267
268 pub async fn resolve_str(
270 &self,
271 name: &str,
272 ) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> {
273 let n =
274 Name::from_str(name).map_err(|_| ResolveError::InvalidNameError(name.to_string()))?;
275 let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
276 let resolver = if use_system {
277 self.system_resolver
278 .get_or_try_init(|| HickoryDnsResolver::new_resolver_system(self.use_shared))?
279 .clone()
280 } else {
281 self.state
282 .get_or_try_init(|| HickoryDnsResolver::new_resolver(self.use_shared))?
283 .clone()
284 };
285
286 resolve(
287 n,
288 resolver,
289 self.static_base.clone(),
290 self.use_shared,
291 self.overall_dns_timeout,
292 )
293 .await
294 .map(|addrs| addrs.map(|socket_addr| socket_addr.ip()))
295 }
296
297 pub fn thread_resolver() -> Self {
299 Self {
300 use_shared: false,
301 ..Default::default()
302 }
303 }
304
305 fn new_resolver(use_shared: bool) -> Result<TokioResolver, ResolveError> {
306 if use_shared {
309 SHARED_RESOLVER.state.get_or_try_init(new_resolver).cloned()
310 } else {
311 new_resolver()
312 }
313 }
314
315 fn new_resolver_system(use_shared: bool) -> Result<TokioResolver, ResolveError> {
316 if !use_shared {
319 new_resolver_system()
320 } else {
321 Ok(SHARED_RESOLVER
322 .system_resolver
323 .get_or_try_init(new_resolver_system)?
324 .clone())
325 }
326 }
327
328 fn new_static_fallback(use_shared: bool) -> StaticResolver {
329 if use_shared && let Some(ref shared_resolver) = SHARED_RESOLVER.static_base {
330 shared_resolver
331 .get_or_init(new_default_static_fallback)
332 .clone()
333 } else {
334 new_default_static_fallback()
335 }
336 }
337
338 pub fn use_system_resolver(&self) {
341 self.use_system.store(true, Relaxed);
342
343 if self.use_shared {
344 SHARED_RESOLVER.use_system_resolver();
345 }
346 }
347
348 pub fn use_configured_resolver(&self) {
351 self.use_system.store(false, Relaxed);
352
353 if self.use_shared {
354 SHARED_RESOLVER.use_configured_resolver();
355 }
356 }
357
358 pub fn clear_preresolve(&self) {
366 debug!("clearing pre-resolve table");
367 if let Some(cell) = &self.static_base
368 && let Some(static_base) = cell.get()
369 {
370 static_base.clear_preresolve()
371 }
372 }
373
374 pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
377 Some(self.static_base.as_ref()?.get()?.get_fallback_addrs())
378 }
379
380 pub fn set_fallback_addrs(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
382 debug!("setting fallback entries for {:?}", addrs.keys());
383 if self.static_base.is_none() {
384 let cell = OnceCell::new();
385 self.static_base = Some(Arc::new(cell));
386 }
387 self.static_base
388 .as_ref()
389 .unwrap()
390 .get_or_init(|| Self::new_static_fallback(self.use_shared))
391 .set_fallback(addrs);
392 }
393
394 pub fn get_static_preresolve(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
397 Some(self.static_base.as_ref()?.get()?.get_preresolve_addrs())
398 }
399
400 pub fn set_static_preresolve(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
402 debug!("setting pre-resolve entries for {:?}", addrs.keys());
403 if self.static_base.is_none() {
404 let cell = OnceCell::new();
405 self.static_base = Some(Arc::new(cell));
406 }
407 self.static_base
408 .as_ref()
409 .unwrap()
410 .get_or_init(|| Self::new_static_fallback(self.use_shared))
411 .set_preresolve(addrs);
412 }
413
414 fn default_options() -> ResolverOpts {
421 let mut opts = ResolverOpts::default();
422 opts.positive_min_ttl = Some(DEFAULT_POSITIVE_LOOKUP_CACHE_TTL);
424 opts.timeout = DEFAULT_QUERY_TIMEOUT;
425 opts.attempts = 0;
426
427 opts
428 }
429
430 pub fn all_configured_name_servers(&self) -> Vec<NameServerConfig> {
432 default_nameserver_group()
433 }
434
435 pub async fn trial_nameservers(&self) {
438 let nameservers = default_nameserver_group();
439 for (ns, result) in trial_nameservers_inner(&nameservers).await {
440 if let Err(e) = result {
441 warn!("trial {ns:?} errored: {e}");
442 } else {
443 info!("trial {ns:?} succeeded");
444 }
445 }
446 }
447}
448
449fn new_resolver() -> Result<TokioResolver, ResolveError> {
459 let name_servers = default_nameserver_group_ipv4_only();
460
461 configure_and_build_resolver(name_servers)
462}
463
464fn configure_and_build_resolver(
465 name_servers: Vec<NameServerConfig>,
466) -> Result<TokioResolver, ResolveError> {
467 let options = HickoryDnsResolver::default_options();
468 info!("building new configured resolver");
469 debug!("configuring resolver with {options:?}, {name_servers:?}");
470
471 let config = ResolverConfig::from_parts(None, Vec::new(), name_servers);
472 let mut resolver_builder =
473 TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
474
475 resolver_builder = resolver_builder.with_options(options);
476
477 Ok(resolver_builder.build()?)
478}
479
480fn filter_ipv4(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
481 nameservers
482 .into_iter()
483 .filter(|ns| ns.ip.is_ipv4())
484 .collect()
485}
486
487#[allow(unused)]
488fn filter_ipv6(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
489 nameservers
490 .into_iter()
491 .filter(|ns| ns.ip.is_ipv6())
492 .collect()
493}
494
495#[allow(unused)]
496fn default_nameserver_group() -> Vec<NameServerConfig> {
497 QUAD9
498 .tls()
499 .chain(QUAD9.https())
500 .chain(CLOUDFLARE.tls())
501 .chain(CLOUDFLARE.https())
502 .collect()
503}
504
505fn default_nameserver_group_ipv4_only() -> Vec<NameServerConfig> {
506 filter_ipv4(default_nameserver_group())
507}
508
509#[allow(unused)]
510fn default_nameserver_group_ipv6_only() -> Vec<NameServerConfig> {
511 filter_ipv6(default_nameserver_group())
512}
513
514fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
518 let mut resolver_builder = TokioResolver::builder_tokio()?;
519
520 let options = HickoryDnsResolver::default_options();
521 info!("building new fallback system resolver");
522 debug!("fallback system resolver with {options:?}");
523
524 resolver_builder = resolver_builder.with_options(options);
525
526 Ok(resolver_builder.build()?)
527}
528
529fn new_default_static_fallback() -> StaticResolver {
530 StaticResolver::new().with_fallback(constants::default_static_addrs())
531}
532
533async fn trial_nameservers_inner(
536 name_servers: &[NameServerConfig],
537) -> Vec<(NameServerConfig, Result<(), ResolveError>)> {
538 let mut trial_lookups = tokio::task::JoinSet::new();
539
540 for name_server in name_servers {
541 let ns = name_server.clone();
542 trial_lookups.spawn(async { (ns.clone(), trial_lookup(ns, "example.com").await) });
543 }
544
545 trial_lookups.join_all().await
546}
547
548async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(), ResolveError> {
551 debug!("running ns trial {name_server:?} query={query}");
552
553 let resolver = configure_and_build_resolver(vec![name_server])?;
554
555 match tokio::time::timeout(DEFAULT_OVERALL_LOOKUP_TIMEOUT, resolver.ipv4_lookup(query)).await {
556 Ok(Ok(_)) => Ok(()),
557 Ok(Err(e)) => Err(e.into()),
558 Err(_) => Err(ResolveError::Timeout),
559 }
560}
561
562#[cfg(test)]
563mod test {
564 use super::*;
565 use itertools::Itertools;
566 use std::{
567 collections::HashMap,
568 net::{IpAddr, Ipv4Addr, Ipv6Addr},
569 };
570
571 const GUARANTEED_BROKEN_IPS_1: &[IpAddr] = &[
575 IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
576 IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)),
577 IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1111)),
578 IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1001)),
579 ];
580
581 #[tokio::test]
582 async fn reqwest_with_custom_dns() {
583 let var_name = HickoryDnsResolver::default();
584 let resolver = var_name;
585 let client = reqwest::ClientBuilder::new()
586 .dns_resolver(resolver)
587 .build()
588 .unwrap();
589
590 let resp = client
591 .get("http://ifconfig.me:80")
592 .send()
593 .await
594 .unwrap()
595 .bytes()
596 .await
597 .unwrap();
598
599 assert!(!resp.is_empty());
600 }
601
602 #[tokio::test]
603 async fn dns_lookup() -> Result<(), ResolveError> {
604 let resolver = HickoryDnsResolver::default();
605
606 let domain = "ifconfig.me";
607 let addrs = resolver.resolve_str(domain).await?;
608
609 assert!(addrs.into_iter().next().is_some());
610
611 Ok(())
612 }
613
614 #[tokio::test]
615 async fn static_resolver_as_fallback() -> Result<(), ResolveError> {
616 let example_domain = "non-existent.nymvpn.com";
617 let mut resolver = HickoryDnsResolver {
618 ..Default::default()
619 };
620
621 let result = resolver.resolve_str(example_domain).await;
622 assert!(result.is_err()); resolver.static_base = Some(Default::default());
625
626 let mut addr_map = HashMap::new();
627 let example_ip4: IpAddr = "10.10.10.10".parse().unwrap();
628 let example_ip6: IpAddr = "dead::beef".parse().unwrap();
629 addr_map.insert(example_domain.to_string(), vec![example_ip4, example_ip6]);
630
631 resolver.set_fallback_addrs(addr_map);
632
633 let mut addrs = resolver.resolve_str(example_domain).await?;
634 assert!(addrs.contains(&example_ip4));
635 assert!(addrs.contains(&example_ip6));
636 Ok(())
637 }
638
639 #[tokio::test]
642 async fn trial_nameservers() {
643 let good_cf_ip = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
644
645 let mut ns_ips = GUARANTEED_BROKEN_IPS_1.to_vec();
646 ns_ips.push(good_cf_ip);
647
648 let domain = Arc::<str>::from("cloudflare-dns.com");
649 let path = Arc::<str>::from("/dns-query");
650 let broken_ns_https = GUARANTEED_BROKEN_IPS_1
651 .iter()
652 .chain([&good_cf_ip])
653 .map(|ip| NameServerConfig::https(*ip, domain.clone(), Some(path.clone())))
654 .collect::<Vec<_>>();
655
656 for (ns, result) in trial_nameservers_inner(&broken_ns_https).await {
657 if ns.ip == good_cf_ip {
658 assert!(result.is_ok())
659 } else {
660 assert!(result.is_err())
661 }
662 }
663 }
664
665 mod failure_test {
666 use super::*;
667
668 fn build_broken_resolver() -> Result<TokioResolver, ResolveError> {
671 info!("building new faulty resolver");
672
673 let domain = Arc::<str>::from("cloudflare-dns.com");
674 let path = Arc::<str>::from("/dns-query");
675 let broken_ns_group = GUARANTEED_BROKEN_IPS_1
676 .iter()
677 .map(|ip| NameServerConfig::tls(*ip, domain.clone()))
678 .chain(
679 GUARANTEED_BROKEN_IPS_1
680 .iter()
681 .map(|ip| NameServerConfig::https(*ip, domain.clone(), Some(path.clone())))
682 .collect::<Vec<_>>(),
683 )
684 .collect::<Vec<_>>();
685
686 configure_and_build_resolver(broken_ns_group)
687 }
688
689 #[tokio::test]
690 async fn dns_lookup_failures() -> Result<(), ResolveError> {
691 let time_start = std::time::Instant::now();
692
693 let r = OnceCell::new();
694 r.set(build_broken_resolver().expect("failed to build resolver"))
695 .expect("broken resolver init error");
696
697 let resolver = HickoryDnsResolver {
699 use_shared: false,
700 state: Arc::new(r),
701 overall_dns_timeout: Duration::from_secs(5),
702 ..Default::default()
703 };
704 build_broken_resolver()?;
705 let domain = "ifconfig.me";
706 let result = resolver.resolve_str(domain).await;
707 assert!(result.is_err_and(|e| e.is_timeout()));
708
709 let duration = time_start.elapsed();
710 assert!(duration < resolver.overall_dns_timeout + Duration::from_secs(1));
711
712 Ok(())
713 }
714
715 #[tokio::test]
716 async fn fallback_to_static() -> Result<(), ResolveError> {
717 let r = OnceCell::new();
718 r.set(build_broken_resolver().expect("failed to build resolver"))
719 .expect("broken resolver init error");
720
721 let resolver = HickoryDnsResolver {
723 use_shared: false,
724 state: Arc::new(r),
725 static_base: Some(Default::default()),
726 overall_dns_timeout: Duration::from_secs(5),
727 ..Default::default()
728 };
729 build_broken_resolver()?;
730
731 let domain = "nymvpn.com";
733 let _ = resolver
734 .resolve_str(domain)
735 .await
736 .expect("failed to resolve address in static lookup");
737
738 let domain = "non-existent.nymtech.net";
740 let result = resolver.resolve_str(domain).await;
741 assert!(result.is_err_and(|e| e.is_timeout()));
742
743 Ok(())
744 }
745
746 #[tokio::test]
747 #[cfg(any())] async fn dns_lookup_failure_on_shared() -> Result<(), ResolveError> {
755 let resolver1 = HickoryDnsResolver::shared();
756
757 let time_start = std::time::Instant::now();
758 let resolver = HickoryDnsResolver::shared();
760
761 let domain = "rpc.nymtech.net";
763 let _ = resolver
764 .resolve_str(domain)
765 .await
766 .expect("failed to resolve address in static lookup");
767
768 let lookup_dur = Instant::now() - time_start;
769 assert!(
770 lookup_dur > resolver.overall_dns_timeout,
771 "expected lookup timeout - took {}ms",
772 (lookup_dur).as_millis()
773 );
774
775 let time_start = std::time::Instant::now();
776 let domain = "rpc.nymtech.net";
778 let _ = resolver1
779 .resolve_str(domain)
780 .await
781 .expect("domain expected to be in pre-resolve");
782
783 let lookup_dur = std::time::Instant::now() - time_start;
785 assert!(
786 lookup_dur < Duration::from_millis(10),
787 "expected instant - took {}ms",
788 (lookup_dur).as_millis()
789 );
790
791 let domain = "non-existent.nymtech.net";
793 let result = resolver.resolve_str(domain).await;
794 assert!(result.is_err());
795 Ok(())
798 }
799
800 #[tokio::test]
801 #[cfg(any())] async fn setting_dns_fallbacks_with_shared_resolver() -> Result<(), ResolveError> {
805 let resolver1 = HickoryDnsResolver::shared();
806
807 let mut resolver = HickoryDnsResolver::shared();
809
810 let example_domains = [
811 String::from("static1.nymvpn.com"),
812 String::from("static2.nymvpn.com"),
813 ];
814 let mut addr_map1 = HashMap::new();
815 addr_map1.insert(
816 example_domains[0].clone(),
817 vec![Ipv4Addr::new(10, 10, 10, 10).into()],
818 );
819 addr_map1.insert(
820 example_domains[1].clone(),
821 vec![Ipv4Addr::new(1, 1, 1, 1).into()],
822 );
823
824 resolver.set_static_preresolve(addr_map1);
825
826 let time_start = std::time::Instant::now();
827 let _ = resolver1
829 .resolve_str(&example_domains[0])
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 resolver.clear_preresolve();
843
844 let prereslve_lookup = resolver1
846 .static_base
847 .as_ref()
848 .unwrap()
849 .get()
850 .unwrap()
851 .pre_resolve(&example_domains[0]);
852 assert!(prereslve_lookup.is_none());
853
854 Ok(())
855 }
856 }
857}