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