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 kumo_prometheus::declare_metric;
11use lruttl::declare_cache;
12use rand::prelude::SliceRandom;
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeMap;
15use std::net::{IpAddr, Ipv6Addr};
16use std::sync::atomic::{AtomicUsize, Ordering};
17use std::sync::{Arc, LazyLock};
18use std::time::{Duration, Instant};
19use tokio::sync::Semaphore;
20use tokio::time::timeout;
21
22mod resolver;
23#[cfg(feature = "unbound")]
24pub use resolver::UnboundResolver;
25pub use resolver::{
26 ptr_host, reverse_ip, AggregateResolver, DnsError, HickoryResolver, IpDisplay, Resolver,
27 TestResolver,
28};
29
30static RESOLVER: LazyLock<ArcSwap<Box<dyn Resolver>>> =
35 LazyLock::new(|| ArcSwap::from_pointee(Box::new(default_resolver())));
36
37declare_cache! {
38static MX_CACHE: LruCacheWithTtl<(Name, Option<u16>), Result<Arc<MailExchanger>, String>>::new("dns_resolver_mx", 64 * 1024);
40}
41declare_cache! {
42static IPV4_CACHE: LruCacheWithTtl<Name, Arc<Vec<IpAddr>>>::new("dns_resolver_ipv4", 1024);
44}
45declare_cache! {
46static IPV6_CACHE: LruCacheWithTtl<Name, Arc<Vec<IpAddr>>>::new("dns_resolver_ipv6", 1024);
48}
49declare_cache! {
50static IP_CACHE: LruCacheWithTtl<(Name, IpLookupStrategy), Arc<Vec<IpAddr>>>::new("dns_resolver_ip", 1024);
52}
53
54static MX_MAX_CONCURRENCY: AtomicUsize = AtomicUsize::new(128);
56static MX_CONCURRENCY_SEMA: LazyLock<Semaphore> =
57 LazyLock::new(|| Semaphore::new(MX_MAX_CONCURRENCY.load(Ordering::SeqCst)));
58
59static MX_TIMEOUT_MS: AtomicUsize = AtomicUsize::new(5000);
61
62static MX_NEGATIVE_TTL: AtomicUsize = AtomicUsize::new(300 * 1000);
64
65declare_metric! {
66static MX_IN_PROGRESS: IntGauge("dns_mx_resolve_in_progress");
68}
69
70declare_metric! {
71static MX_SUCCESS: IntCounter(
73 "dns_mx_resolve_status_ok");
74}
75
76declare_metric! {
77static MX_FAIL: IntCounter("dns_mx_resolve_status_fail");
83}
84
85declare_metric! {
86static MX_CACHED: IntCounter("dns_mx_resolve_cache_hit");
91}
92
93declare_metric! {
94static MX_QUERIES: IntCounter("dns_mx_resolve_cache_miss");
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 strategy: IpLookupStrategy,
318) -> anyhow::Result<Vec<ResolvedAddress>> {
319 if domain_name.starts_with('[') {
320 if !domain_name.ends_with(']') {
323 anyhow::bail!(
324 "domain_name `{domain_name}` is a malformed literal \
325 domain with no trailing `]`"
326 );
327 }
328
329 let lowered = domain_name.to_ascii_lowercase();
330 let literal = &lowered[1..lowered.len() - 1];
331
332 if let Some(v6_literal) = literal.strip_prefix("ipv6:") {
333 match v6_literal.parse::<Ipv6Addr>() {
334 Ok(addr) => {
335 return Ok(vec![ResolvedAddress {
336 name: domain_name.to_string(),
337 addr: std::net::IpAddr::V6(addr).into(),
338 }]);
339 }
340 Err(err) => {
341 anyhow::bail!("invalid ipv6 address: `{v6_literal}`: {err:#}");
342 }
343 }
344 }
345
346 match literal.parse::<HostAddress>() {
350 Ok(addr) => {
351 return Ok(vec![ResolvedAddress {
352 name: domain_name.to_string(),
353 addr: addr.into(),
354 }]);
355 }
356 Err(err) => {
357 anyhow::bail!("invalid address: `{literal}`: {err:#}");
358 }
359 }
360 } else {
361 if let Ok(addr) = domain_name.parse::<HostAddress>() {
363 return Ok(vec![ResolvedAddress {
364 name: domain_name.to_string(),
365 addr: addr.into(),
366 }]);
367 }
368 }
369
370 match ip_lookup(domain_name, resolver, strategy).await {
371 Ok((addrs, _expires)) => {
372 let addrs = addrs
373 .iter()
374 .map(|&addr| ResolvedAddress {
375 name: domain_name.to_string(),
376 addr: addr.into(),
377 })
378 .collect();
379 Ok(addrs)
380 }
381 Err(err) => anyhow::bail!("{err:#}"),
382 }
383}
384
385impl MailExchanger {
386 pub async fn resolve(domain_name: &str) -> anyhow::Result<Arc<Self>> {
387 MX_IN_PROGRESS.inc();
388 let result = Self::resolve_impl(domain_name).await;
389 MX_IN_PROGRESS.dec();
390 if result.is_ok() {
391 MX_SUCCESS.inc();
392 } else {
393 MX_FAIL.inc();
394 }
395 result
396 }
397
398 async fn resolve_impl(domain_name: &str) -> anyhow::Result<Arc<Self>> {
399 let (name_fq, opt_port) = match DomainClassification::classify(domain_name)? {
400 DomainClassification::Literal(addr) => {
401 let mut by_pref = BTreeMap::new();
402 by_pref.insert(1, vec![addr.to_string()]);
403 return Ok(Arc::new(Self {
404 domain_name: domain_name.to_string(),
405 hosts: vec![addr.to_string()],
406 site_name: addr.to_string(),
407 by_pref,
408 is_domain_literal: true,
409 is_secure: false,
410 is_mx: false,
411 expires: None,
412 }));
413 }
414 DomainClassification::Domain(name_fq, opt_port) => (name_fq, opt_port),
415 };
416
417 let lookup_result = MX_CACHE
418 .get_or_try_insert(
419 &(name_fq.clone(), opt_port),
420 |mx_result| {
421 if let Ok(mx) = mx_result {
422 if let Some(exp) = mx.expires {
423 return exp
424 .checked_duration_since(std::time::Instant::now())
425 .unwrap_or_else(|| Duration::from_secs(10));
426 }
427 }
428 get_mx_negative_ttl()
429 },
430 async {
431 MX_QUERIES.inc();
432 let start = Instant::now();
433 let (mut by_pref, expires) = match lookup_mx_record(&name_fq).await {
434 Ok((by_pref, expires)) => (by_pref, expires),
435 Err(err) => {
436 let error = format!(
437 "MX lookup for {domain_name} failed after {elapsed:?}: {err:#}",
438 elapsed = start.elapsed()
439 );
440 return Ok::<Result<Arc<MailExchanger>, String>, anyhow::Error>(Err(
441 error,
442 ));
443 }
444 };
445
446 let mut hosts = vec![];
447 for pref in &mut by_pref {
448 for host in &mut pref.hosts {
449 if let Some(port) = opt_port {
450 *host = format!("{host}:{port}");
451 };
452 hosts.push(host.to_string());
453 }
454 }
455
456 let is_secure = by_pref.iter().all(|p| p.is_secure);
457 let is_mx = by_pref.iter().all(|p| p.is_mx);
458
459 let by_pref = by_pref
460 .into_iter()
461 .map(|pref| (pref.pref, pref.hosts))
462 .collect();
463
464 let site_name = factor_names(&hosts);
465 let mx = Self {
466 hosts,
467 domain_name: name_fq.to_ascii(),
468 site_name,
469 by_pref,
470 is_domain_literal: false,
471 is_secure,
472 is_mx,
473 expires: Some(expires),
474 };
475
476 Ok(Ok(Arc::new(mx)))
477 },
478 )
479 .await
480 .map_err(|err| anyhow::anyhow!("{err}"))?;
481
482 if !lookup_result.is_fresh {
483 MX_CACHED.inc();
484 }
485
486 lookup_result.item.map_err(|err| anyhow::anyhow!("{err}"))
487 }
488
489 pub fn has_expired(&self) -> bool {
490 match self.expires {
491 Some(deadline) => deadline <= Instant::now(),
492 None => false,
493 }
494 }
495
496 pub async fn resolve_addresses(&self, strategy: IpLookupStrategy) -> ResolvedMxAddresses {
501 let mut result = vec![];
502
503 for hosts in self.by_pref.values().rev() {
504 let mut by_pref = vec![];
505
506 for mx_host in hosts {
507 if mx_host == "." {
509 return ResolvedMxAddresses::NullMx;
510 }
511
512 let (mx_host, opt_port) = match has_colon_port(mx_host) {
514 Some((domain_name, port)) => (domain_name, Some(port)),
515 None => (mx_host.as_str(), None),
516 };
517 if let Ok(addr) = mx_host.parse::<IpAddr>() {
518 let mut addr: HostOrSocketAddress = addr.into();
519 if let Some(port) = opt_port {
520 addr.set_port(port);
521 }
522 by_pref.push(ResolvedAddress {
523 name: mx_host.to_string(),
524 addr: addr.into(),
525 });
526 continue;
527 }
528
529 match ip_lookup(mx_host, None, strategy).await {
530 Err(err) => {
531 tracing::error!("failed to resolve {mx_host}: {err:#}");
532 continue;
533 }
534 Ok((addresses, _expires)) => {
535 for addr in addresses.iter() {
536 let mut addr: HostOrSocketAddress = (*addr).into();
537 if let Some(port) = opt_port {
538 addr.set_port(port);
539 }
540 by_pref.push(ResolvedAddress {
541 name: mx_host.to_string(),
542 addr,
543 });
544 }
545 }
546 }
547 }
548
549 let mut rng = rand::thread_rng();
553 by_pref.shuffle(&mut rng);
554 result.append(&mut by_pref);
555 }
556 ResolvedMxAddresses::Addresses(result)
557 }
558}
559
560#[derive(Debug, Clone, Serialize)]
561pub enum ResolvedMxAddresses {
562 NullMx,
563 Addresses(Vec<ResolvedAddress>),
566}
567
568struct ByPreference {
569 hosts: Vec<String>,
570 pref: u16,
571 is_secure: bool,
572 is_mx: bool,
573}
574
575async fn lookup_mx_record(domain_name: &Name) -> anyhow::Result<(Vec<ByPreference>, Instant)> {
576 let mx_lookup = timeout(get_mx_timeout(), async {
577 let _permit = MX_CONCURRENCY_SEMA.acquire().await;
578 RESOLVER
579 .load()
580 .resolve(domain_name.clone(), RecordType::MX)
581 .await
582 })
583 .await??;
584 let mx_records = mx_lookup.records;
585
586 if mx_records.is_empty() {
587 if mx_lookup.nxdomain {
588 anyhow::bail!("NXDOMAIN");
589 }
590
591 return Ok((
592 vec![ByPreference {
593 hosts: vec![domain_name.to_lowercase().to_ascii()],
594 pref: 1,
595 is_secure: false,
596 is_mx: false,
597 }],
598 mx_lookup.expires,
599 ));
600 }
601
602 let mut records: Vec<ByPreference> = Vec::with_capacity(mx_records.len());
603
604 for mx_record in mx_records {
605 if let Some(mx) = mx_record.as_mx() {
606 let pref = mx.preference();
607 let host = mx.exchange().to_lowercase().to_string();
608
609 if let Some(record) = records.iter_mut().find(|r| r.pref == pref) {
610 record.hosts.push(host);
611 } else {
612 records.push(ByPreference {
613 hosts: vec![host],
614 pref,
615 is_secure: mx_lookup.secure,
616 is_mx: true,
617 });
618 }
619 }
620 }
621
622 records.sort_unstable_by(|a, b| a.pref.cmp(&b.pref));
624
625 for mx in &mut records {
628 mx.hosts.sort();
629 }
630
631 Ok((records, mx_lookup.expires))
632}
633
634#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
635#[repr(u8)]
636pub enum IpLookupStrategy {
637 Ipv4Only,
639 Ipv6Only,
641 #[default]
643 Ipv4AndIpv6,
644 Ipv6ThenIpv4,
646 Ipv4ThenIpv6,
648}
649
650pub async fn ip_lookup(
651 key: &str,
652 resolver: Option<&dyn Resolver>,
653 strategy: IpLookupStrategy,
654) -> anyhow::Result<(Arc<Vec<IpAddr>>, Instant)> {
655 let key_fq = fully_qualify(key)?;
656
657 if resolver.is_none() {
658 if let Some(lookup) = IP_CACHE.lookup(&(key_fq.clone(), strategy)) {
659 return Ok((lookup.item, lookup.expiration.into()));
660 }
661 }
662
663 let (v4, v6) = match strategy {
664 IpLookupStrategy::Ipv4AndIpv6 => {
665 let (v4, v6) = tokio::join!(ipv4_lookup(key, resolver), ipv6_lookup(key, resolver));
666 (Some(v4), Some(v6))
667 }
668 IpLookupStrategy::Ipv4Only => (Some(ipv4_lookup(key, resolver).await), None),
669 IpLookupStrategy::Ipv6Only => (None, Some(ipv6_lookup(key, resolver).await)),
670 IpLookupStrategy::Ipv6ThenIpv4 => {
671 let v6 = ipv6_lookup(key, resolver).await;
672 match v6 {
673 Ok((addrs, exp)) if addrs.is_empty() => (
674 Some(ipv4_lookup(key, resolver).await),
675 Some(Ok((addrs, exp))),
676 ),
677 Err(err) => (Some(ipv4_lookup(key, resolver).await), Some(Err(err))),
678 Ok(res) => (None, Some(Ok(res))),
679 }
680 }
681 IpLookupStrategy::Ipv4ThenIpv6 => {
682 let v4 = ipv4_lookup(key, resolver).await;
683 match v4 {
684 Ok((addrs, exp)) if addrs.is_empty() => (
685 Some(Ok((addrs, exp))),
686 Some(ipv6_lookup(key, resolver).await),
687 ),
688 Err(err) => (Some(Err(err)), Some(ipv6_lookup(key, resolver).await)),
689 Ok(res) => (Some(Ok(res)), None),
690 }
691 }
692 };
693
694 let mut results = vec![];
695 let mut errors = vec![];
696 let mut expires = None;
697
698 match v4 {
699 Some(Ok((addrs, exp))) => {
700 expires.replace(exp);
701 for a in addrs.iter() {
702 results.push(*a);
703 }
704 }
705 Some(Err(err)) => errors.push(err),
706 None => {}
707 }
708
709 match v6 {
710 Some(Ok((addrs, exp))) => {
711 let exp = match expires.take() {
712 Some(existing) => exp.min(existing),
713 None => exp,
714 };
715 expires.replace(exp);
716
717 for a in addrs.iter() {
718 results.push(*a);
719 }
720 }
721 Some(Err(err)) => errors.push(err),
722 None => {}
723 }
724
725 if results.is_empty() && !errors.is_empty() {
726 return Err(errors.remove(0));
727 }
728
729 let addr = Arc::new(results);
730 let exp = expires.take().unwrap_or_else(Instant::now);
731
732 if resolver.is_none() {
733 IP_CACHE
734 .insert((key_fq, strategy), addr.clone(), exp.into())
735 .await;
736 }
737 Ok((addr, exp))
738}
739
740pub async fn ipv4_lookup(
741 key: &str,
742 resolver: Option<&dyn Resolver>,
743) -> anyhow::Result<(Arc<Vec<IpAddr>>, Instant)> {
744 let key_fq = fully_qualify(key)?;
745 if resolver.is_none() {
746 if let Some(lookup) = IPV4_CACHE.lookup(&key_fq) {
747 return Ok((lookup.item, lookup.expiration.into()));
748 }
749 }
750
751 let answer = match resolver {
752 Some(r) => r.resolve(key_fq.clone(), RecordType::A).await?,
753 None => {
754 RESOLVER
755 .load()
756 .resolve(key_fq.clone(), RecordType::A)
757 .await?
758 }
759 };
760 let ips = answer.as_addr();
761
762 let ips = Arc::new(ips);
763 let expires = answer.expires;
764 if resolver.is_none() {
765 IPV4_CACHE.insert(key_fq, ips.clone(), expires.into()).await;
766 }
767 Ok((ips, expires))
768}
769
770pub async fn ipv6_lookup(
771 key: &str,
772 resolver: Option<&dyn Resolver>,
773) -> anyhow::Result<(Arc<Vec<IpAddr>>, Instant)> {
774 let key_fq = fully_qualify(key)?;
775 if resolver.is_none() {
776 if let Some(lookup) = IPV6_CACHE.lookup(&key_fq) {
777 return Ok((lookup.item, lookup.expiration.into()));
778 }
779 }
780
781 let answer = match resolver {
782 Some(r) => r.resolve(key_fq.clone(), RecordType::AAAA).await?,
783 None => {
784 RESOLVER
785 .load()
786 .resolve(key_fq.clone(), RecordType::AAAA)
787 .await?
788 }
789 };
790 let ips = answer.as_addr();
791
792 let ips = Arc::new(ips);
793 let expires = answer.expires;
794 if resolver.is_none() {
795 IPV6_CACHE.insert(key_fq, ips.clone(), expires.into()).await;
796 }
797 Ok((ips, expires))
798}
799
800fn factor_names<S: AsRef<str>>(name_strings: &[S]) -> String {
805 let mut max_element_count = 0;
806
807 let mut names = vec![];
808
809 for name in name_strings {
810 let (name, opt_port) = match has_colon_port(name.as_ref()) {
811 Some((name, port)) => (name, Some(port)),
812 None => (name.as_ref(), None),
813 };
814 if let Ok(name) = fully_qualify(name) {
815 names.push((name.to_lowercase(), opt_port));
816 }
817 }
818
819 let mut elements: Vec<Vec<&str>> = vec![];
820
821 let mut split_names = vec![];
822 for (name, opt_port) in names {
823 let mut fields: Vec<_> = name
824 .iter()
825 .map(|s| String::from_utf8_lossy(s).to_string())
826 .collect();
827 if let Some(port) = opt_port {
828 fields.last_mut().map(|s| {
829 s.push_str(&format!(":{port}"));
830 });
831 }
832 fields.reverse();
833 max_element_count = max_element_count.max(fields.len());
834 split_names.push(fields);
835 }
836
837 fn add_element<'a>(elements: &mut Vec<Vec<&'a str>>, field: &'a str, i: usize) {
838 match elements.get_mut(i) {
839 Some(ele) => {
840 if !ele.contains(&field) {
841 ele.push(field);
842 }
843 }
844 None => {
845 elements.push(vec![field]);
846 }
847 }
848 }
849
850 for fields in &split_names {
851 for (i, field) in fields.iter().enumerate() {
852 add_element(&mut elements, field, i);
853 }
854 for i in fields.len()..max_element_count {
855 add_element(&mut elements, "?", i);
856 }
857 }
858
859 let mut result = vec![];
860 for mut ele in elements {
861 let has_q = ele.contains(&"?");
862 ele.retain(|&e| e != "?");
863 let mut item_text = if ele.len() == 1 {
864 ele[0].to_string()
865 } else {
866 format!("({})", ele.join("|"))
867 };
868 if has_q {
869 item_text.push('?');
870 }
871 result.push(item_text);
872 }
873 result.reverse();
874
875 result.join(".")
876}
877
878#[cfg(test)]
879mod test {
880 use super::*;
881
882 #[tokio::test]
883 async fn literal_resolve() {
884 let v4_loopback = MailExchanger::resolve("[127.0.0.1]").await.unwrap();
885 k9::snapshot!(
886 &v4_loopback,
887 r#"
888MailExchanger {
889 domain_name: "[127.0.0.1]",
890 hosts: [
891 "127.0.0.1",
892 ],
893 site_name: "127.0.0.1",
894 by_pref: {
895 1: [
896 "127.0.0.1",
897 ],
898 },
899 is_domain_literal: true,
900 is_secure: false,
901 is_mx: false,
902 expires: None,
903}
904"#
905 );
906 k9::snapshot!(
907 v4_loopback
908 .resolve_addresses(IpLookupStrategy::default())
909 .await,
910 r#"
911Addresses(
912 [
913 ResolvedAddress {
914 name: "127.0.0.1",
915 addr: 127.0.0.1,
916 },
917 ],
918)
919"#
920 );
921
922 let v6_loopback_non_conforming = MailExchanger::resolve("[::1]").await.unwrap();
923 k9::snapshot!(
924 &v6_loopback_non_conforming,
925 r#"
926MailExchanger {
927 domain_name: "[::1]",
928 hosts: [
929 "::1",
930 ],
931 site_name: "::1",
932 by_pref: {
933 1: [
934 "::1",
935 ],
936 },
937 is_domain_literal: true,
938 is_secure: false,
939 is_mx: false,
940 expires: None,
941}
942"#
943 );
944 k9::snapshot!(
945 v6_loopback_non_conforming
946 .resolve_addresses(IpLookupStrategy::default())
947 .await,
948 r#"
949Addresses(
950 [
951 ResolvedAddress {
952 name: "::1",
953 addr: ::1,
954 },
955 ],
956)
957"#
958 );
959
960 let v6_loopback = MailExchanger::resolve("[IPv6:::1]").await.unwrap();
961 k9::snapshot!(
962 &v6_loopback,
963 r#"
964MailExchanger {
965 domain_name: "[IPv6:::1]",
966 hosts: [
967 "::1",
968 ],
969 site_name: "::1",
970 by_pref: {
971 1: [
972 "::1",
973 ],
974 },
975 is_domain_literal: true,
976 is_secure: false,
977 is_mx: false,
978 expires: None,
979}
980"#
981 );
982 k9::snapshot!(
983 v6_loopback
984 .resolve_addresses(IpLookupStrategy::default())
985 .await,
986 r#"
987Addresses(
988 [
989 ResolvedAddress {
990 name: "::1",
991 addr: ::1,
992 },
993 ],
994)
995"#
996 );
997 }
998
999 #[test]
1000 fn name_factoring() {
1001 assert_eq!(
1002 factor_names(&[
1003 "mta5.am0.yahoodns.net",
1004 "mta6.am0.yahoodns.net",
1005 "mta7.am0.yahoodns.net"
1006 ]),
1007 "(mta5|mta6|mta7).am0.yahoodns.net".to_string()
1008 );
1009
1010 assert_eq!(
1012 factor_names(&[
1013 "mta5.AM0.yahoodns.net",
1014 "mta6.am0.yAHOodns.net",
1015 "mta7.am0.yahoodns.net"
1016 ]),
1017 "(mta5|mta6|mta7).am0.yahoodns.net".to_string()
1018 );
1019
1020 assert_eq!(
1023 factor_names(&[
1024 "gmail-smtp-in.l.google.com",
1025 "alt1.gmail-smtp-in.l.google.com",
1026 "alt2.gmail-smtp-in.l.google.com",
1027 "alt3.gmail-smtp-in.l.google.com",
1028 "alt4.gmail-smtp-in.l.google.com",
1029 ]),
1030 "(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com".to_string()
1031 );
1032
1033 assert_eq!(
1034 factor_names(&[
1035 "mta5.am0.yahoodns.net:123",
1036 "mta6.am0.yahoodns.net:123",
1037 "mta7.am0.yahoodns.net:123"
1038 ]),
1039 "(mta5|mta6|mta7).am0.yahoodns.net:123".to_string()
1040 );
1041 assert_eq!(
1042 factor_names(&[
1043 "mta5.am0.yahoodns.net:123",
1044 "mta6.am0.yahoodns.net:456",
1045 "mta7.am0.yahoodns.net:123"
1046 ]),
1047 "(mta5|mta6|mta7).am0.yahoodns.(net:123|net:456)".to_string()
1048 );
1049 }
1050
1051 #[test]
1055 fn mx_order_name_factor() {
1056 assert_eq!(
1057 factor_names(&[
1058 "example-com.mail.protection.outlook.com.",
1059 "mx-biz.mail.am0.yahoodns.net.",
1060 "mx-biz.mail.am0.yahoodns.net.",
1061 ]),
1062 "(example-com|mx-biz).mail.(protection|am0).(outlook|yahoodns).(com|net)".to_string()
1063 );
1064 assert_eq!(
1065 factor_names(&[
1066 "mx-biz.mail.am0.yahoodns.net.",
1067 "mx-biz.mail.am0.yahoodns.net.",
1068 "example-com.mail.protection.outlook.com.",
1069 ]),
1070 "(mx-biz|example-com).mail.(am0|protection).(yahoodns|outlook).(net|com)".to_string()
1071 );
1072 }
1073
1074 #[cfg(feature = "live-dns-tests")]
1075 #[tokio::test]
1076 async fn lookup_gmail_mx() {
1077 let mut gmail = (*MailExchanger::resolve("gmail.com").await.unwrap()).clone();
1078 gmail.expires.take();
1079 k9::snapshot!(
1080 &gmail,
1081 r#"
1082MailExchanger {
1083 domain_name: "gmail.com.",
1084 hosts: [
1085 "gmail-smtp-in.l.google.com.",
1086 "alt1.gmail-smtp-in.l.google.com.",
1087 "alt2.gmail-smtp-in.l.google.com.",
1088 "alt3.gmail-smtp-in.l.google.com.",
1089 "alt4.gmail-smtp-in.l.google.com.",
1090 ],
1091 site_name: "(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com",
1092 by_pref: {
1093 5: [
1094 "gmail-smtp-in.l.google.com.",
1095 ],
1096 10: [
1097 "alt1.gmail-smtp-in.l.google.com.",
1098 ],
1099 20: [
1100 "alt2.gmail-smtp-in.l.google.com.",
1101 ],
1102 30: [
1103 "alt3.gmail-smtp-in.l.google.com.",
1104 ],
1105 40: [
1106 "alt4.gmail-smtp-in.l.google.com.",
1107 ],
1108 },
1109 is_domain_literal: false,
1110 is_secure: false,
1111 is_mx: true,
1112 expires: None,
1113}
1114"#
1115 );
1116
1117 k9::snapshot!(
1126 gmail.resolve_addresses(IpLookupStrategy::default()).await,
1127 r#"
1128Addresses(
1129 [
1130 ResolvedAddress {
1131 name: "alt4.gmail-smtp-in.l.google.com.",
1132 addr: 2607:f8b0:4023:401::1b,
1133 },
1134 ResolvedAddress {
1135 name: "alt4.gmail-smtp-in.l.google.com.",
1136 addr: 173.194.77.27,
1137 },
1138 ResolvedAddress {
1139 name: "alt3.gmail-smtp-in.l.google.com.",
1140 addr: 2607:f8b0:4023:1::1a,
1141 },
1142 ResolvedAddress {
1143 name: "alt3.gmail-smtp-in.l.google.com.",
1144 addr: 172.253.113.26,
1145 },
1146 ResolvedAddress {
1147 name: "alt2.gmail-smtp-in.l.google.com.",
1148 addr: 2607:f8b0:4001:c1d::1b,
1149 },
1150 ResolvedAddress {
1151 name: "alt2.gmail-smtp-in.l.google.com.",
1152 addr: 74.125.126.27,
1153 },
1154 ResolvedAddress {
1155 name: "alt1.gmail-smtp-in.l.google.com.",
1156 addr: 2607:f8b0:4003:c04::1b,
1157 },
1158 ResolvedAddress {
1159 name: "alt1.gmail-smtp-in.l.google.com.",
1160 addr: 108.177.104.27,
1161 },
1162 ResolvedAddress {
1163 name: "gmail-smtp-in.l.google.com.",
1164 addr: 2607:f8b0:4023:c06::1b,
1165 },
1166 ResolvedAddress {
1167 name: "gmail-smtp-in.l.google.com.",
1168 addr: 142.251.2.26,
1169 },
1170 ],
1171)
1172"#
1173 );
1174 }
1175
1176 #[cfg(feature = "live-dns-tests")]
1177 #[tokio::test]
1178 async fn lookup_punycode_no_mx_only_a() {
1179 let mx = MailExchanger::resolve("xn--bb-eka.at").await.unwrap();
1180 assert_eq!(mx.domain_name, "xn--bb-eka.at.");
1181 assert_eq!(mx.hosts[0], "xn--bb-eka.at.");
1182 }
1183
1184 #[cfg(feature = "live-dns-tests")]
1185 #[tokio::test]
1186 async fn lookup_bogus_aasland() {
1187 let err = MailExchanger::resolve("not-mairs.aasland.com")
1188 .await
1189 .unwrap_err();
1190 k9::snapshot!(err, "MX lookup for not-mairs.aasland.com failed: NXDOMAIN");
1191 }
1192
1193 #[cfg(feature = "live-dns-tests")]
1194 #[tokio::test]
1195 async fn lookup_example_com() {
1196 let mx = MailExchanger::resolve("example.com").await.unwrap();
1198 k9::snapshot!(
1199 mx,
1200 r#"
1201MailExchanger {
1202 domain_name: "example.com.",
1203 hosts: [
1204 ".",
1205 ],
1206 site_name: "",
1207 by_pref: {
1208 0: [
1209 ".",
1210 ],
1211 },
1212 is_domain_literal: false,
1213 is_secure: true,
1214 is_mx: true,
1215}
1216"#
1217 );
1218 }
1219
1220 #[cfg(feature = "live-dns-tests")]
1221 #[tokio::test]
1222 async fn lookup_have_dane() {
1223 let mx = MailExchanger::resolve("do.havedane.net").await.unwrap();
1224 k9::snapshot!(
1225 mx,
1226 r#"
1227MailExchanger {
1228 domain_name: "do.havedane.net.",
1229 hosts: [
1230 "do.havedane.net.",
1231 ],
1232 site_name: "do.havedane.net",
1233 by_pref: {
1234 10: [
1235 "do.havedane.net.",
1236 ],
1237 },
1238 is_domain_literal: false,
1239 is_secure: true,
1240 is_mx: true,
1241}
1242"#
1243 );
1244 }
1245
1246 #[cfg(feature = "live-dns-tests")]
1247 #[tokio::test]
1248 async fn tlsa_have_dane() {
1249 let tlsa = resolve_dane("do.havedane.net", 25).await.unwrap();
1250 k9::snapshot!(
1251 tlsa,
1252 "
1253[
1254 TLSA {
1255 cert_usage: TrustAnchor,
1256 selector: Spki,
1257 matching: Sha256,
1258 cert_data: [
1259 39,
1260 182,
1261 148,
1262 181,
1263 29,
1264 31,
1265 239,
1266 136,
1267 133,
1268 55,
1269 42,
1270 207,
1271 179,
1272 145,
1273 147,
1274 117,
1275 151,
1276 34,
1277 183,
1278 54,
1279 176,
1280 66,
1281 104,
1282 100,
1283 220,
1284 28,
1285 121,
1286 208,
1287 101,
1288 31,
1289 239,
1290 115,
1291 ],
1292 },
1293 TLSA {
1294 cert_usage: DomainIssued,
1295 selector: Spki,
1296 matching: Sha256,
1297 cert_data: [
1298 85,
1299 58,
1300 207,
1301 136,
1302 249,
1303 238,
1304 24,
1305 204,
1306 170,
1307 230,
1308 53,
1309 202,
1310 84,
1311 15,
1312 50,
1313 203,
1314 132,
1315 172,
1316 167,
1317 124,
1318 71,
1319 145,
1320 102,
1321 130,
1322 188,
1323 181,
1324 66,
1325 213,
1326 29,
1327 170,
1328 135,
1329 31,
1330 ],
1331 },
1332]
1333"
1334 );
1335 }
1336
1337 #[cfg(feature = "live-dns-tests")]
1338 #[tokio::test]
1339 async fn mx_lookup_www_example_com() {
1340 let mx = MailExchanger::resolve("www.example.com").await.unwrap();
1342 k9::snapshot!(
1343 mx,
1344 r#"
1345MailExchanger {
1346 domain_name: "www.example.com.",
1347 hosts: [
1348 "www.example.com.",
1349 ],
1350 site_name: "www.example.com",
1351 by_pref: {
1352 1: [
1353 "www.example.com.",
1354 ],
1355 },
1356 is_domain_literal: false,
1357 is_secure: false,
1358 is_mx: false,
1359}
1360"#
1361 );
1362 }
1363
1364 #[cfg(feature = "live-dns-tests")]
1365 #[tokio::test]
1366 async fn txt_lookup_gmail() {
1367 let name = Name::from_str_relaxed("_mta-sts.gmail.com").unwrap();
1368 let answer = get_resolver().resolve(name, RecordType::TXT).await.unwrap();
1369 k9::snapshot!(
1370 answer.as_txt(),
1371 r#"
1372[
1373 "v=STSv1; id=20190429T010101;",
1374]
1375"#
1376 );
1377 }
1378}