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