1use anyhow::Context;
2use arc_swap::ArcSwap;
3pub use hickory_resolver::proto::rr::rdata::tlsa::TLSA;
4pub use hickory_resolver::proto::rr::Name;
5use hickory_resolver::proto::rr::{RData, RecordType};
6use hickory_resolver::proto::ProtoError;
7use kumo_address::host::HostAddress;
8use kumo_address::host_or_socket::HostOrSocketAddress;
9use kumo_address::resolvable::ResolvableSocketAddr;
10use kumo_log_types::ResolvedAddress;
11use kumo_prometheus::declare_metric;
12use lruttl::declare_cache;
13use rand::prelude::SliceRandom;
14use serde::{Deserialize, Serialize};
15use std::collections::BTreeMap;
16use std::net::{IpAddr, Ipv6Addr, SocketAddr};
17use std::sync::atomic::{AtomicUsize, Ordering};
18use std::sync::{Arc, LazyLock};
19use std::time::{Duration, Instant};
20use tokio::sync::Semaphore;
21use tokio::time::timeout;
22
23mod resolver;
24#[cfg(feature = "unbound")]
25pub use resolver::UnboundResolver;
26pub use resolver::{
27 ptr_host, reverse_ip, AggregateResolver, DnsError, HickoryResolver, IpDisplay, Resolver,
28 TestResolver,
29};
30
31static RESOLVER: LazyLock<ArcSwap<Box<dyn Resolver>>> =
36 LazyLock::new(|| ArcSwap::from_pointee(Box::new(default_resolver())));
37
38declare_cache! {
39static MX_CACHE: LruCacheWithTtl<(Name, Option<u16>), Result<Arc<MailExchanger>, String>>::new("dns_resolver_mx", 64 * 1024);
41}
42declare_cache! {
43static IPV4_CACHE: LruCacheWithTtl<Name, Arc<Vec<IpAddr>>>::new("dns_resolver_ipv4", 1024);
45}
46declare_cache! {
47static IPV6_CACHE: LruCacheWithTtl<Name, Arc<Vec<IpAddr>>>::new("dns_resolver_ipv6", 1024);
49}
50declare_cache! {
51static IP_CACHE: LruCacheWithTtl<(Name, IpLookupStrategy), Arc<Vec<IpAddr>>>::new("dns_resolver_ip", 1024);
53}
54
55static MX_MAX_CONCURRENCY: AtomicUsize = AtomicUsize::new(128);
57static MX_CONCURRENCY_SEMA: LazyLock<Semaphore> =
58 LazyLock::new(|| Semaphore::new(MX_MAX_CONCURRENCY.load(Ordering::SeqCst)));
59
60static MX_TIMEOUT_MS: AtomicUsize = AtomicUsize::new(5000);
62
63static MX_NEGATIVE_TTL: AtomicUsize = AtomicUsize::new(300 * 1000);
65
66declare_metric! {
67static MX_IN_PROGRESS: IntGauge("dns_mx_resolve_in_progress");
69}
70
71declare_metric! {
72static MX_SUCCESS: IntCounter(
74 "dns_mx_resolve_status_ok");
75}
76
77declare_metric! {
78static MX_FAIL: IntCounter("dns_mx_resolve_status_fail");
84}
85
86declare_metric! {
87static MX_CACHED: IntCounter("dns_mx_resolve_cache_hit");
92}
93
94declare_metric! {
95static MX_QUERIES: IntCounter("dns_mx_resolve_cache_miss");
100}
101
102fn default_resolver() -> impl Resolver {
103 #[cfg(feature = "default-unbound")]
104 return UnboundResolver::new().unwrap();
105 #[cfg(not(feature = "default-unbound"))]
106 return HickoryResolver::new().expect("Parsing /etc/resolv.conf failed");
107}
108
109pub fn set_mx_concurrency_limit(n: usize) {
110 MX_MAX_CONCURRENCY.store(n, Ordering::SeqCst);
111}
112
113pub fn set_mx_timeout(duration: Duration) -> anyhow::Result<()> {
114 let ms = duration
115 .as_millis()
116 .try_into()
117 .context("set_mx_timeout: duration is too large")?;
118 MX_TIMEOUT_MS.store(ms, Ordering::Relaxed);
119 Ok(())
120}
121
122pub fn get_mx_timeout() -> Duration {
123 Duration::from_millis(MX_TIMEOUT_MS.load(Ordering::Relaxed) as u64)
124}
125
126pub fn set_mx_negative_cache_ttl(duration: Duration) -> anyhow::Result<()> {
127 let ms = duration
128 .as_millis()
129 .try_into()
130 .context("set_mx_negative_cache_ttl: duration is too large")?;
131 MX_NEGATIVE_TTL.store(ms, Ordering::Relaxed);
132 Ok(())
133}
134
135pub fn get_mx_negative_ttl() -> Duration {
136 Duration::from_millis(MX_NEGATIVE_TTL.load(Ordering::Relaxed) as u64)
137}
138
139#[derive(Clone, Debug, Serialize)]
140pub struct MailExchanger {
141 pub domain_name: String,
142 pub hosts: Vec<String>,
143 pub site_name: String,
144 pub by_pref: BTreeMap<u16, Vec<String>>,
145 pub is_domain_literal: bool,
146 pub is_secure: bool,
148 pub is_mx: bool,
149 #[serde(skip)]
150 expires: Option<Instant>,
151}
152
153pub fn fully_qualify(domain_name: &str) -> Result<Name, ProtoError> {
154 let mut name = Name::from_str_relaxed(domain_name)?.to_lowercase();
155
156 name.set_fqdn(true);
158
159 Ok(name)
160}
161
162pub fn reconfigure_resolver(resolver: impl Resolver) {
163 RESOLVER.store(Arc::new(Box::new(resolver)));
164}
165
166pub fn get_resolver() -> Arc<Box<dyn Resolver>> {
167 RESOLVER.load_full()
168}
169
170pub async fn resolve_dane(hostname: &str, port: u16) -> anyhow::Result<Vec<TLSA>> {
173 let name = fully_qualify(&format!("_{port}._tcp.{hostname}"))?;
174 let answer = RESOLVER.load().resolve(name, RecordType::TLSA).await?;
175 tracing::debug!("resolve_dane {hostname}:{port} TLSA answer is: {answer:?}");
176
177 if answer.bogus {
178 anyhow::bail!(
181 "DANE result for {hostname}:{port} unusable because: {}",
182 answer
183 .why_bogus
184 .as_deref()
185 .unwrap_or("DNSSEC validation failed")
186 );
187 }
188
189 let mut result = vec![];
190 if answer.secure {
194 for r in &answer.records {
195 if let RData::TLSA(tlsa) = r {
196 result.push(tlsa.clone());
197 }
198 }
199 result.sort_by_key(|a| a.to_string());
206 }
207
208 tracing::info!("resolve_dane {hostname}:{port} result is: {result:?}");
209
210 Ok(result)
211}
212
213pub fn has_colon_port(a: &str) -> Option<(&str, u16)> {
217 let (label, maybe_port) = a.rsplit_once(':')?;
218
219 if label.contains(':') {
222 return None;
223 }
224
225 let port = maybe_port.parse::<u16>().ok()?;
226 Some((label, port))
227}
228
229pub enum DomainClassification {
235 Domain(Name, Option<u16>),
237 Literal(HostOrSocketAddress),
239}
240
241impl DomainClassification {
242 pub fn classify(domain_name: &str) -> anyhow::Result<Self> {
243 let (domain_name, mut opt_port) = match has_colon_port(domain_name) {
244 Some((domain_name, port)) => (domain_name, Some(port)),
245 None => (domain_name, None),
246 };
247
248 if domain_name.starts_with('[') {
249 if !domain_name.ends_with(']') {
250 anyhow::bail!(
251 "domain_name `{domain_name}` is a malformed literal \
252 domain with no trailing `]`"
253 );
254 }
255
256 let lowered = domain_name.to_ascii_lowercase();
257 let literal = &lowered[1..lowered.len() - 1];
258
259 let literal = match has_colon_port(literal) {
260 Some((_, _)) if opt_port.is_some() => {
261 anyhow::bail!("invalid address: `{domain_name}` specifies a port both inside and outside a literal address enclosed in square brackets");
262 }
263 Some((literal, port)) => {
264 opt_port.replace(port);
265 literal
266 }
267 None => literal,
268 };
269
270 if let Some(v6_literal) = literal.strip_prefix("ipv6:") {
271 match v6_literal.parse::<Ipv6Addr>() {
272 Ok(addr) => {
273 let mut host_addr: HostOrSocketAddress = addr.into();
274 if let Some(port) = opt_port {
275 host_addr.set_port(port);
276 }
277 return Ok(Self::Literal(host_addr));
278 }
279 Err(err) => {
280 anyhow::bail!("invalid ipv6 address: `{v6_literal}`: {err:#}");
281 }
282 }
283 }
284
285 match literal.parse::<IpAddr>() {
289 Ok(ip_addr) => {
290 let mut host_addr: HostOrSocketAddress = ip_addr.into();
291 if let Some(port) = opt_port {
292 host_addr.set_port(port);
293 }
294 return Ok(Self::Literal(host_addr));
295 }
296 Err(err) => {
297 anyhow::bail!("invalid address: `{literal}`: {err:#}");
298 }
299 }
300 }
301
302 let name_fq = fully_qualify(domain_name)?;
303 Ok(Self::Domain(name_fq, opt_port))
304 }
305
306 pub fn has_port(&self) -> bool {
307 match self {
308 Self::Domain(_, Some(_)) => true,
309 Self::Domain(_, None) => false,
310 Self::Literal(addr) => addr.port().is_some(),
311 }
312 }
313}
314
315pub async fn resolve_a_or_aaaa(
316 domain_name: &str,
317 resolver: Option<&dyn Resolver>,
318 strategy: IpLookupStrategy,
319) -> anyhow::Result<Vec<ResolvedAddress>> {
320 if domain_name.starts_with('[') {
321 if !domain_name.ends_with(']') {
324 anyhow::bail!(
325 "domain_name `{domain_name}` is a malformed literal \
326 domain with no trailing `]`"
327 );
328 }
329
330 let lowered = domain_name.to_ascii_lowercase();
331 let literal = &lowered[1..lowered.len() - 1];
332
333 if let Some(v6_literal) = literal.strip_prefix("ipv6:") {
334 match v6_literal.parse::<Ipv6Addr>() {
335 Ok(addr) => {
336 return Ok(vec![ResolvedAddress {
337 name: domain_name.to_string(),
338 addr: std::net::IpAddr::V6(addr).into(),
339 }]);
340 }
341 Err(err) => {
342 anyhow::bail!("invalid ipv6 address: `{v6_literal}`: {err:#}");
343 }
344 }
345 }
346
347 match literal.parse::<HostAddress>() {
351 Ok(addr) => {
352 return Ok(vec![ResolvedAddress {
353 name: domain_name.to_string(),
354 addr: addr.into(),
355 }]);
356 }
357 Err(err) => {
358 anyhow::bail!("invalid address: `{literal}`: {err:#}");
359 }
360 }
361 } else {
362 if let Ok(addr) = domain_name.parse::<HostAddress>() {
364 return Ok(vec![ResolvedAddress {
365 name: domain_name.to_string(),
366 addr: addr.into(),
367 }]);
368 }
369 }
370
371 match ip_lookup(domain_name, resolver, strategy).await {
372 Ok((addrs, _expires)) => {
373 let addrs = addrs
374 .iter()
375 .map(|&addr| ResolvedAddress {
376 name: domain_name.to_string(),
377 addr: addr.into(),
378 })
379 .collect();
380 Ok(addrs)
381 }
382 Err(err) => anyhow::bail!("{err:#}"),
383 }
384}
385
386pub async fn resolve_socket_addr(
393 addr: &ResolvableSocketAddr,
394 resolver: Option<&dyn Resolver>,
395 strategy: IpLookupStrategy,
396) -> anyhow::Result<Vec<ResolvedAddress>> {
397 match addr {
398 ResolvableSocketAddr::UnixDomain(unix) => {
399 let name = match unix.as_pathname() {
400 Some(path) => path.display().to_string(),
401 None => "<unbound unix domain>".to_string(),
402 };
403 Ok(vec![ResolvedAddress {
404 name,
405 addr: HostOrSocketAddress::UnixDomain(unix.clone()),
406 }])
407 }
408 ResolvableSocketAddr::V4(sa) => Ok(vec![ResolvedAddress {
409 name: sa.to_string(),
410 addr: HostOrSocketAddress::V4Socket(sa.clone()),
411 }]),
412 ResolvableSocketAddr::V6(sa) => Ok(vec![ResolvedAddress {
413 name: sa.to_string(),
414 addr: HostOrSocketAddress::V6Socket(sa.clone()),
415 }]),
416 ResolvableSocketAddr::Hostname { host, port } => {
417 match ip_lookup(host, resolver, strategy).await {
418 Ok((addrs, _expires)) => Ok(addrs
419 .iter()
420 .map(|&ip| {
421 let sa = SocketAddr::new(ip, *port);
422 ResolvedAddress {
423 name: host.clone(),
424 addr: sa.into(),
425 }
426 })
427 .collect()),
428 Err(err) => anyhow::bail!("{err:#}"),
429 }
430 }
431 }
432}
433
434impl MailExchanger {
435 pub async fn resolve(domain_name: &str) -> anyhow::Result<Arc<Self>> {
436 MX_IN_PROGRESS.inc();
437 let result = Self::resolve_impl(domain_name).await;
438 MX_IN_PROGRESS.dec();
439 if result.is_ok() {
440 MX_SUCCESS.inc();
441 } else {
442 MX_FAIL.inc();
443 }
444 result
445 }
446
447 async fn resolve_impl(domain_name: &str) -> anyhow::Result<Arc<Self>> {
448 let (name_fq, opt_port) = match DomainClassification::classify(domain_name)? {
449 DomainClassification::Literal(addr) => {
450 let mut by_pref = BTreeMap::new();
451 by_pref.insert(1, vec![addr.to_string()]);
452 return Ok(Arc::new(Self {
453 domain_name: domain_name.to_string(),
454 hosts: vec![addr.to_string()],
455 site_name: addr.to_string(),
456 by_pref,
457 is_domain_literal: true,
458 is_secure: false,
459 is_mx: false,
460 expires: None,
461 }));
462 }
463 DomainClassification::Domain(name_fq, opt_port) => (name_fq, opt_port),
464 };
465
466 let lookup_result = MX_CACHE
467 .get_or_try_insert(
468 &(name_fq.clone(), opt_port),
469 |mx_result| {
470 if let Ok(mx) = mx_result {
471 if let Some(exp) = mx.expires {
472 return exp
473 .checked_duration_since(std::time::Instant::now())
474 .unwrap_or_else(|| Duration::from_secs(10));
475 }
476 }
477 get_mx_negative_ttl()
478 },
479 async {
480 MX_QUERIES.inc();
481 let start = Instant::now();
482 let (mut by_pref, expires) = match lookup_mx_record(&name_fq).await {
483 Ok((by_pref, expires)) => (by_pref, expires),
484 Err(err) => {
485 let error = format!(
486 "MX lookup for {domain_name} failed after {elapsed:?}: {err:#}",
487 elapsed = start.elapsed()
488 );
489 return Ok::<Result<Arc<MailExchanger>, String>, anyhow::Error>(Err(
490 error,
491 ));
492 }
493 };
494
495 let mut hosts = vec![];
496 for pref in &mut by_pref {
497 for host in &mut pref.hosts {
498 if let Some(port) = opt_port {
499 *host = format!("{host}:{port}");
500 };
501 hosts.push(host.to_string());
502 }
503 }
504
505 let is_secure = by_pref.iter().all(|p| p.is_secure);
506 let is_mx = by_pref.iter().all(|p| p.is_mx);
507
508 let by_pref = by_pref
509 .into_iter()
510 .map(|pref| (pref.pref, pref.hosts))
511 .collect();
512
513 let site_name = factor_names(&hosts);
514 let mx = Self {
515 hosts,
516 domain_name: name_fq.to_ascii(),
517 site_name,
518 by_pref,
519 is_domain_literal: false,
520 is_secure,
521 is_mx,
522 expires: Some(expires),
523 };
524
525 Ok(Ok(Arc::new(mx)))
526 },
527 )
528 .await
529 .map_err(|err| anyhow::anyhow!("{err}"))?;
530
531 if !lookup_result.is_fresh {
532 MX_CACHED.inc();
533 }
534
535 lookup_result.item.map_err(|err| anyhow::anyhow!("{err}"))
536 }
537
538 pub fn has_expired(&self) -> bool {
539 match self.expires {
540 Some(deadline) => deadline <= Instant::now(),
541 None => false,
542 }
543 }
544
545 pub async fn resolve_addresses(&self, strategy: IpLookupStrategy) -> ResolvedMxAddresses {
550 let mut result = vec![];
551
552 for hosts in self.by_pref.values().rev() {
553 let mut by_pref = vec![];
554
555 for mx_host in hosts {
556 if mx_host == "." {
558 return ResolvedMxAddresses::NullMx;
559 }
560
561 let (mx_host, opt_port) = match has_colon_port(mx_host) {
563 Some((domain_name, port)) => (domain_name, Some(port)),
564 None => (mx_host.as_str(), None),
565 };
566 if let Ok(addr) = mx_host.parse::<IpAddr>() {
567 let mut addr: HostOrSocketAddress = addr.into();
568 if let Some(port) = opt_port {
569 addr.set_port(port);
570 }
571 by_pref.push(ResolvedAddress {
572 name: mx_host.to_string(),
573 addr: addr.into(),
574 });
575 continue;
576 }
577
578 match ip_lookup(mx_host, None, strategy).await {
579 Err(err) => {
580 tracing::error!("failed to resolve {mx_host}: {err:#}");
581 continue;
582 }
583 Ok((addresses, _expires)) => {
584 for addr in addresses.iter() {
585 let mut addr: HostOrSocketAddress = (*addr).into();
586 if let Some(port) = opt_port {
587 addr.set_port(port);
588 }
589 by_pref.push(ResolvedAddress {
590 name: mx_host.to_string(),
591 addr,
592 });
593 }
594 }
595 }
596 }
597
598 let mut rng = rand::thread_rng();
602 by_pref.shuffle(&mut rng);
603 result.append(&mut by_pref);
604 }
605 ResolvedMxAddresses::Addresses(result)
606 }
607}
608
609#[derive(Debug, Clone, Serialize)]
610pub enum ResolvedMxAddresses {
611 NullMx,
612 Addresses(Vec<ResolvedAddress>),
615}
616
617struct ByPreference {
618 hosts: Vec<String>,
619 pref: u16,
620 is_secure: bool,
621 is_mx: bool,
622}
623
624async fn lookup_mx_record(domain_name: &Name) -> anyhow::Result<(Vec<ByPreference>, Instant)> {
625 let mx_lookup = timeout(get_mx_timeout(), async {
626 let _permit = MX_CONCURRENCY_SEMA.acquire().await;
627 RESOLVER
628 .load()
629 .resolve(domain_name.clone(), RecordType::MX)
630 .await
631 })
632 .await??;
633 let mx_records = mx_lookup.records;
634
635 if mx_records.is_empty() {
636 if mx_lookup.nxdomain {
637 anyhow::bail!("NXDOMAIN");
638 }
639
640 return Ok((
641 vec![ByPreference {
642 hosts: vec![domain_name.to_lowercase().to_ascii()],
643 pref: 1,
644 is_secure: false,
645 is_mx: false,
646 }],
647 mx_lookup.expires,
648 ));
649 }
650
651 let mut records: Vec<ByPreference> = Vec::with_capacity(mx_records.len());
652
653 for mx_record in mx_records {
654 if let RData::MX(mx) = mx_record {
655 let pref = mx.preference;
656 let host = mx.exchange.to_lowercase().to_string();
657
658 if let Some(record) = records.iter_mut().find(|r| r.pref == pref) {
659 record.hosts.push(host);
660 } else {
661 records.push(ByPreference {
662 hosts: vec![host],
663 pref,
664 is_secure: mx_lookup.secure,
665 is_mx: true,
666 });
667 }
668 }
669 }
670
671 records.sort_unstable_by(|a, b| a.pref.cmp(&b.pref));
673
674 for mx in &mut records {
677 mx.hosts.sort();
678 }
679
680 Ok((records, mx_lookup.expires))
681}
682
683#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
684#[repr(u8)]
685pub enum IpLookupStrategy {
686 Ipv4Only,
688 Ipv6Only,
690 #[default]
692 Ipv4AndIpv6,
693 Ipv6ThenIpv4,
695 Ipv4ThenIpv6,
697}
698
699pub async fn ip_lookup(
700 key: &str,
701 resolver: Option<&dyn Resolver>,
702 strategy: IpLookupStrategy,
703) -> anyhow::Result<(Arc<Vec<IpAddr>>, Instant)> {
704 let key_fq = fully_qualify(key)?;
705
706 if resolver.is_none() {
707 if let Some(lookup) = IP_CACHE.lookup(&(key_fq.clone(), strategy)) {
708 return Ok((lookup.item, lookup.expiration.into()));
709 }
710 }
711
712 let (v4, v6) = match strategy {
713 IpLookupStrategy::Ipv4AndIpv6 => {
714 let (v4, v6) = tokio::join!(ipv4_lookup(key, resolver), ipv6_lookup(key, resolver));
715 (Some(v4), Some(v6))
716 }
717 IpLookupStrategy::Ipv4Only => (Some(ipv4_lookup(key, resolver).await), None),
718 IpLookupStrategy::Ipv6Only => (None, Some(ipv6_lookup(key, resolver).await)),
719 IpLookupStrategy::Ipv6ThenIpv4 => {
720 let v6 = ipv6_lookup(key, resolver).await;
721 match v6 {
722 Ok((addrs, exp)) if addrs.is_empty() => (
723 Some(ipv4_lookup(key, resolver).await),
724 Some(Ok((addrs, exp))),
725 ),
726 Err(err) => (Some(ipv4_lookup(key, resolver).await), Some(Err(err))),
727 Ok(res) => (None, Some(Ok(res))),
728 }
729 }
730 IpLookupStrategy::Ipv4ThenIpv6 => {
731 let v4 = ipv4_lookup(key, resolver).await;
732 match v4 {
733 Ok((addrs, exp)) if addrs.is_empty() => (
734 Some(Ok((addrs, exp))),
735 Some(ipv6_lookup(key, resolver).await),
736 ),
737 Err(err) => (Some(Err(err)), Some(ipv6_lookup(key, resolver).await)),
738 Ok(res) => (Some(Ok(res)), None),
739 }
740 }
741 };
742
743 let mut results = vec![];
744 let mut errors = vec![];
745 let mut expires = None;
746
747 match v4 {
748 Some(Ok((addrs, exp))) => {
749 expires.replace(exp);
750 for a in addrs.iter() {
751 results.push(*a);
752 }
753 }
754 Some(Err(err)) => errors.push(err),
755 None => {}
756 }
757
758 match v6 {
759 Some(Ok((addrs, exp))) => {
760 let exp = match expires.take() {
761 Some(existing) => exp.min(existing),
762 None => exp,
763 };
764 expires.replace(exp);
765
766 for a in addrs.iter() {
767 results.push(*a);
768 }
769 }
770 Some(Err(err)) => errors.push(err),
771 None => {}
772 }
773
774 if results.is_empty() && !errors.is_empty() {
775 return Err(errors.remove(0));
776 }
777
778 let addr = Arc::new(results);
779 let exp = expires.take().unwrap_or_else(Instant::now);
780
781 if resolver.is_none() {
782 IP_CACHE
783 .insert((key_fq, strategy), addr.clone(), exp.into())
784 .await;
785 }
786 Ok((addr, exp))
787}
788
789pub async fn ipv4_lookup(
790 key: &str,
791 resolver: Option<&dyn Resolver>,
792) -> anyhow::Result<(Arc<Vec<IpAddr>>, Instant)> {
793 let key_fq = fully_qualify(key)?;
794 if resolver.is_none() {
795 if let Some(lookup) = IPV4_CACHE.lookup(&key_fq) {
796 return Ok((lookup.item, lookup.expiration.into()));
797 }
798 }
799
800 let answer = match resolver {
801 Some(r) => r.resolve(key_fq.clone(), RecordType::A).await?,
802 None => {
803 RESOLVER
804 .load()
805 .resolve(key_fq.clone(), RecordType::A)
806 .await?
807 }
808 };
809 let ips = answer.as_addr();
810
811 let ips = Arc::new(ips);
812 let expires = answer.expires;
813 if resolver.is_none() {
814 IPV4_CACHE.insert(key_fq, ips.clone(), expires.into()).await;
815 }
816 Ok((ips, expires))
817}
818
819pub async fn ipv6_lookup(
820 key: &str,
821 resolver: Option<&dyn Resolver>,
822) -> anyhow::Result<(Arc<Vec<IpAddr>>, Instant)> {
823 let key_fq = fully_qualify(key)?;
824 if resolver.is_none() {
825 if let Some(lookup) = IPV6_CACHE.lookup(&key_fq) {
826 return Ok((lookup.item, lookup.expiration.into()));
827 }
828 }
829
830 let answer = match resolver {
831 Some(r) => r.resolve(key_fq.clone(), RecordType::AAAA).await?,
832 None => {
833 RESOLVER
834 .load()
835 .resolve(key_fq.clone(), RecordType::AAAA)
836 .await?
837 }
838 };
839 let ips = answer.as_addr();
840
841 let ips = Arc::new(ips);
842 let expires = answer.expires;
843 if resolver.is_none() {
844 IPV6_CACHE.insert(key_fq, ips.clone(), expires.into()).await;
845 }
846 Ok((ips, expires))
847}
848
849fn factor_names<S: AsRef<str>>(name_strings: &[S]) -> String {
854 let mut max_element_count = 0;
855
856 let mut names = vec![];
857
858 for name in name_strings {
859 let (name, opt_port) = match has_colon_port(name.as_ref()) {
860 Some((name, port)) => (name, Some(port)),
861 None => (name.as_ref(), None),
862 };
863 if let Ok(name) = fully_qualify(name) {
864 names.push((name.to_lowercase(), opt_port));
865 }
866 }
867
868 let mut elements: Vec<Vec<&str>> = vec![];
869
870 let mut split_names = vec![];
871 for (name, opt_port) in names {
872 let mut fields: Vec<_> = name
873 .iter()
874 .map(|s| String::from_utf8_lossy(s).to_string())
875 .collect();
876 if let Some(port) = opt_port {
877 fields.last_mut().map(|s| {
878 s.push_str(&format!(":{port}"));
879 });
880 }
881 fields.reverse();
882 max_element_count = max_element_count.max(fields.len());
883 split_names.push(fields);
884 }
885
886 fn add_element<'a>(elements: &mut Vec<Vec<&'a str>>, field: &'a str, i: usize) {
887 match elements.get_mut(i) {
888 Some(ele) => {
889 if !ele.contains(&field) {
890 ele.push(field);
891 }
892 }
893 None => {
894 elements.push(vec![field]);
895 }
896 }
897 }
898
899 for fields in &split_names {
900 for (i, field) in fields.iter().enumerate() {
901 add_element(&mut elements, field, i);
902 }
903 for i in fields.len()..max_element_count {
904 add_element(&mut elements, "?", i);
905 }
906 }
907
908 let mut result = vec![];
909 for mut ele in elements {
910 let has_q = ele.contains(&"?");
911 ele.retain(|&e| e != "?");
912 let mut item_text = if ele.len() == 1 {
913 ele[0].to_string()
914 } else {
915 format!("({})", ele.join("|"))
916 };
917 if has_q {
918 item_text.push('?');
919 }
920 result.push(item_text);
921 }
922 result.reverse();
923
924 result.join(".")
925}
926
927#[cfg(test)]
928mod test {
929 use super::*;
930
931 #[tokio::test]
932 async fn literal_resolve() {
933 let v4_loopback = MailExchanger::resolve("[127.0.0.1]").await.unwrap();
934 k9::snapshot!(
935 &v4_loopback,
936 r#"
937MailExchanger {
938 domain_name: "[127.0.0.1]",
939 hosts: [
940 "127.0.0.1",
941 ],
942 site_name: "127.0.0.1",
943 by_pref: {
944 1: [
945 "127.0.0.1",
946 ],
947 },
948 is_domain_literal: true,
949 is_secure: false,
950 is_mx: false,
951 expires: None,
952}
953"#
954 );
955 k9::snapshot!(
956 v4_loopback
957 .resolve_addresses(IpLookupStrategy::default())
958 .await,
959 r#"
960Addresses(
961 [
962 ResolvedAddress {
963 name: "127.0.0.1",
964 addr: 127.0.0.1,
965 },
966 ],
967)
968"#
969 );
970
971 let v6_loopback_non_conforming = MailExchanger::resolve("[::1]").await.unwrap();
972 k9::snapshot!(
973 &v6_loopback_non_conforming,
974 r#"
975MailExchanger {
976 domain_name: "[::1]",
977 hosts: [
978 "::1",
979 ],
980 site_name: "::1",
981 by_pref: {
982 1: [
983 "::1",
984 ],
985 },
986 is_domain_literal: true,
987 is_secure: false,
988 is_mx: false,
989 expires: None,
990}
991"#
992 );
993 k9::snapshot!(
994 v6_loopback_non_conforming
995 .resolve_addresses(IpLookupStrategy::default())
996 .await,
997 r#"
998Addresses(
999 [
1000 ResolvedAddress {
1001 name: "::1",
1002 addr: ::1,
1003 },
1004 ],
1005)
1006"#
1007 );
1008
1009 let v6_loopback = MailExchanger::resolve("[IPv6:::1]").await.unwrap();
1010 k9::snapshot!(
1011 &v6_loopback,
1012 r#"
1013MailExchanger {
1014 domain_name: "[IPv6:::1]",
1015 hosts: [
1016 "::1",
1017 ],
1018 site_name: "::1",
1019 by_pref: {
1020 1: [
1021 "::1",
1022 ],
1023 },
1024 is_domain_literal: true,
1025 is_secure: false,
1026 is_mx: false,
1027 expires: None,
1028}
1029"#
1030 );
1031 k9::snapshot!(
1032 v6_loopback
1033 .resolve_addresses(IpLookupStrategy::default())
1034 .await,
1035 r#"
1036Addresses(
1037 [
1038 ResolvedAddress {
1039 name: "::1",
1040 addr: ::1,
1041 },
1042 ],
1043)
1044"#
1045 );
1046 }
1047
1048 #[test]
1049 fn name_factoring() {
1050 assert_eq!(
1051 factor_names(&[
1052 "mta5.am0.yahoodns.net",
1053 "mta6.am0.yahoodns.net",
1054 "mta7.am0.yahoodns.net"
1055 ]),
1056 "(mta5|mta6|mta7).am0.yahoodns.net".to_string()
1057 );
1058
1059 assert_eq!(
1061 factor_names(&[
1062 "mta5.AM0.yahoodns.net",
1063 "mta6.am0.yAHOodns.net",
1064 "mta7.am0.yahoodns.net"
1065 ]),
1066 "(mta5|mta6|mta7).am0.yahoodns.net".to_string()
1067 );
1068
1069 assert_eq!(
1072 factor_names(&[
1073 "gmail-smtp-in.l.google.com",
1074 "alt1.gmail-smtp-in.l.google.com",
1075 "alt2.gmail-smtp-in.l.google.com",
1076 "alt3.gmail-smtp-in.l.google.com",
1077 "alt4.gmail-smtp-in.l.google.com",
1078 ]),
1079 "(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com".to_string()
1080 );
1081
1082 assert_eq!(
1083 factor_names(&[
1084 "mta5.am0.yahoodns.net:123",
1085 "mta6.am0.yahoodns.net:123",
1086 "mta7.am0.yahoodns.net:123"
1087 ]),
1088 "(mta5|mta6|mta7).am0.yahoodns.net:123".to_string()
1089 );
1090 assert_eq!(
1091 factor_names(&[
1092 "mta5.am0.yahoodns.net:123",
1093 "mta6.am0.yahoodns.net:456",
1094 "mta7.am0.yahoodns.net:123"
1095 ]),
1096 "(mta5|mta6|mta7).am0.yahoodns.(net:123|net:456)".to_string()
1097 );
1098 }
1099
1100 #[test]
1104 fn mx_order_name_factor() {
1105 assert_eq!(
1106 factor_names(&[
1107 "example-com.mail.protection.outlook.com.",
1108 "mx-biz.mail.am0.yahoodns.net.",
1109 "mx-biz.mail.am0.yahoodns.net.",
1110 ]),
1111 "(example-com|mx-biz).mail.(protection|am0).(outlook|yahoodns).(com|net)".to_string()
1112 );
1113 assert_eq!(
1114 factor_names(&[
1115 "mx-biz.mail.am0.yahoodns.net.",
1116 "mx-biz.mail.am0.yahoodns.net.",
1117 "example-com.mail.protection.outlook.com.",
1118 ]),
1119 "(mx-biz|example-com).mail.(am0|protection).(yahoodns|outlook).(net|com)".to_string()
1120 );
1121 }
1122
1123 #[cfg(feature = "live-dns-tests")]
1124 #[tokio::test]
1125 async fn lookup_gmail_mx() {
1126 let mut gmail = (*MailExchanger::resolve("gmail.com").await.unwrap()).clone();
1127 gmail.expires.take();
1128 k9::snapshot!(
1129 &gmail,
1130 r#"
1131MailExchanger {
1132 domain_name: "gmail.com.",
1133 hosts: [
1134 "gmail-smtp-in.l.google.com.",
1135 "alt1.gmail-smtp-in.l.google.com.",
1136 "alt2.gmail-smtp-in.l.google.com.",
1137 "alt3.gmail-smtp-in.l.google.com.",
1138 "alt4.gmail-smtp-in.l.google.com.",
1139 ],
1140 site_name: "(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com",
1141 by_pref: {
1142 5: [
1143 "gmail-smtp-in.l.google.com.",
1144 ],
1145 10: [
1146 "alt1.gmail-smtp-in.l.google.com.",
1147 ],
1148 20: [
1149 "alt2.gmail-smtp-in.l.google.com.",
1150 ],
1151 30: [
1152 "alt3.gmail-smtp-in.l.google.com.",
1153 ],
1154 40: [
1155 "alt4.gmail-smtp-in.l.google.com.",
1156 ],
1157 },
1158 is_domain_literal: false,
1159 is_secure: false,
1160 is_mx: true,
1161 expires: None,
1162}
1163"#
1164 );
1165
1166 k9::snapshot!(
1175 gmail.resolve_addresses(IpLookupStrategy::default()).await,
1176 r#"
1177Addresses(
1178 [
1179 ResolvedAddress {
1180 name: "alt4.gmail-smtp-in.l.google.com.",
1181 addr: 2607:f8b0:4023:401::1b,
1182 },
1183 ResolvedAddress {
1184 name: "alt4.gmail-smtp-in.l.google.com.",
1185 addr: 173.194.77.27,
1186 },
1187 ResolvedAddress {
1188 name: "alt3.gmail-smtp-in.l.google.com.",
1189 addr: 2607:f8b0:4023:1::1a,
1190 },
1191 ResolvedAddress {
1192 name: "alt3.gmail-smtp-in.l.google.com.",
1193 addr: 172.253.113.26,
1194 },
1195 ResolvedAddress {
1196 name: "alt2.gmail-smtp-in.l.google.com.",
1197 addr: 2607:f8b0:4001:c1d::1b,
1198 },
1199 ResolvedAddress {
1200 name: "alt2.gmail-smtp-in.l.google.com.",
1201 addr: 74.125.126.27,
1202 },
1203 ResolvedAddress {
1204 name: "alt1.gmail-smtp-in.l.google.com.",
1205 addr: 2607:f8b0:4003:c04::1b,
1206 },
1207 ResolvedAddress {
1208 name: "alt1.gmail-smtp-in.l.google.com.",
1209 addr: 108.177.104.27,
1210 },
1211 ResolvedAddress {
1212 name: "gmail-smtp-in.l.google.com.",
1213 addr: 2607:f8b0:4023:c06::1b,
1214 },
1215 ResolvedAddress {
1216 name: "gmail-smtp-in.l.google.com.",
1217 addr: 142.251.2.26,
1218 },
1219 ],
1220)
1221"#
1222 );
1223 }
1224
1225 #[cfg(feature = "live-dns-tests")]
1226 #[tokio::test]
1227 async fn lookup_punycode_no_mx_only_a() {
1228 let mx = MailExchanger::resolve("xn--bb-eka.at").await.unwrap();
1229 assert_eq!(mx.domain_name, "xn--bb-eka.at.");
1230 assert_eq!(mx.hosts[0], "xn--bb-eka.at.");
1231 }
1232
1233 #[cfg(feature = "live-dns-tests")]
1234 #[tokio::test]
1235 async fn lookup_bogus_aasland() {
1236 let err = MailExchanger::resolve("not-mairs.aasland.com")
1237 .await
1238 .unwrap_err();
1239 k9::snapshot!(err, "MX lookup for not-mairs.aasland.com failed: NXDOMAIN");
1240 }
1241
1242 #[cfg(feature = "live-dns-tests")]
1243 #[tokio::test]
1244 async fn lookup_example_com() {
1245 let mx = MailExchanger::resolve("example.com").await.unwrap();
1247 k9::snapshot!(
1248 mx,
1249 r#"
1250MailExchanger {
1251 domain_name: "example.com.",
1252 hosts: [
1253 ".",
1254 ],
1255 site_name: "",
1256 by_pref: {
1257 0: [
1258 ".",
1259 ],
1260 },
1261 is_domain_literal: false,
1262 is_secure: true,
1263 is_mx: true,
1264}
1265"#
1266 );
1267 }
1268
1269 #[cfg(feature = "live-dns-tests")]
1270 #[tokio::test]
1271 async fn lookup_have_dane() {
1272 let mx = MailExchanger::resolve("do.havedane.net").await.unwrap();
1273 k9::snapshot!(
1274 mx,
1275 r#"
1276MailExchanger {
1277 domain_name: "do.havedane.net.",
1278 hosts: [
1279 "do.havedane.net.",
1280 ],
1281 site_name: "do.havedane.net",
1282 by_pref: {
1283 10: [
1284 "do.havedane.net.",
1285 ],
1286 },
1287 is_domain_literal: false,
1288 is_secure: true,
1289 is_mx: true,
1290}
1291"#
1292 );
1293 }
1294
1295 #[cfg(feature = "live-dns-tests")]
1296 #[tokio::test]
1297 async fn tlsa_have_dane() {
1298 let tlsa = resolve_dane("do.havedane.net", 25).await.unwrap();
1299 k9::snapshot!(
1300 tlsa,
1301 "
1302[
1303 TLSA {
1304 cert_usage: TrustAnchor,
1305 selector: Spki,
1306 matching: Sha256,
1307 cert_data: [
1308 39,
1309 182,
1310 148,
1311 181,
1312 29,
1313 31,
1314 239,
1315 136,
1316 133,
1317 55,
1318 42,
1319 207,
1320 179,
1321 145,
1322 147,
1323 117,
1324 151,
1325 34,
1326 183,
1327 54,
1328 176,
1329 66,
1330 104,
1331 100,
1332 220,
1333 28,
1334 121,
1335 208,
1336 101,
1337 31,
1338 239,
1339 115,
1340 ],
1341 },
1342 TLSA {
1343 cert_usage: DomainIssued,
1344 selector: Spki,
1345 matching: Sha256,
1346 cert_data: [
1347 85,
1348 58,
1349 207,
1350 136,
1351 249,
1352 238,
1353 24,
1354 204,
1355 170,
1356 230,
1357 53,
1358 202,
1359 84,
1360 15,
1361 50,
1362 203,
1363 132,
1364 172,
1365 167,
1366 124,
1367 71,
1368 145,
1369 102,
1370 130,
1371 188,
1372 181,
1373 66,
1374 213,
1375 29,
1376 170,
1377 135,
1378 31,
1379 ],
1380 },
1381]
1382"
1383 );
1384 }
1385
1386 #[cfg(feature = "live-dns-tests")]
1387 #[tokio::test]
1388 async fn mx_lookup_www_example_com() {
1389 let mx = MailExchanger::resolve("www.example.com").await.unwrap();
1391 k9::snapshot!(
1392 mx,
1393 r#"
1394MailExchanger {
1395 domain_name: "www.example.com.",
1396 hosts: [
1397 "www.example.com.",
1398 ],
1399 site_name: "www.example.com",
1400 by_pref: {
1401 1: [
1402 "www.example.com.",
1403 ],
1404 },
1405 is_domain_literal: false,
1406 is_secure: false,
1407 is_mx: false,
1408}
1409"#
1410 );
1411 }
1412
1413 #[cfg(feature = "live-dns-tests")]
1414 #[tokio::test]
1415 async fn txt_lookup_gmail() {
1416 let name = Name::from_str_relaxed("_mta-sts.gmail.com").unwrap();
1417 let answer = get_resolver().resolve(name, RecordType::TXT).await.unwrap();
1418 k9::snapshot!(
1419 answer.as_txt(),
1420 r#"
1421[
1422 "v=STSv1; id=20190429T010101;",
1423]
1424"#
1425 );
1426 }
1427}