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