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