1use anyhow::Context;
2use arc_swap::ArcSwap;
3use hickory_resolver::proto::op::ResponseCode;
4pub use hickory_resolver::proto::rr::rdata::tlsa::{CertUsage, Matching, Selector, TLSA};
5pub use hickory_resolver::proto::rr::Name;
6use hickory_resolver::proto::rr::{RData, RecordType};
7use hickory_resolver::proto::ProtoError;
8use kumo_address::host::HostAddress;
9use kumo_address::host_or_socket::HostOrSocketAddress;
10use kumo_address::resolvable::ResolvableSocketAddr;
11use kumo_log_types::ResolvedAddress;
12use kumo_prometheus::declare_metric;
13use lruttl::declare_cache;
14use rand::prelude::SliceRandom;
15use serde::{Deserialize, Serialize};
16use std::collections::BTreeMap;
17use std::net::{IpAddr, Ipv6Addr, SocketAddr};
18use std::sync::atomic::{AtomicUsize, Ordering};
19use std::sync::{Arc, LazyLock};
20use std::time::{Duration, Instant};
21use tokio::sync::Semaphore;
22use tokio::time::timeout;
23
24mod resolver;
25#[cfg(feature = "unbound")]
26pub use resolver::UnboundResolver;
27pub use resolver::{
28 ptr_host, reverse_ip, AggregateResolver, Answer, DnsError, HickoryResolver, IpDisplay,
29 Resolver, TestResolver,
30};
31
32static RESOLVER: LazyLock<ArcSwap<Box<dyn Resolver>>> =
37 LazyLock::new(|| ArcSwap::from_pointee(Box::new(default_resolver())));
38
39declare_cache! {
40static MX_CACHE: LruCacheWithTtl<(Name, Option<u16>), Result<Arc<MailExchanger>, String>>::new("dns_resolver_mx", 64 * 1024);
42}
43declare_cache! {
44static IPV4_CACHE: LruCacheWithTtl<Name, Arc<IpAddresses>>::new("dns_resolver_ipv4", 1024);
46}
47declare_cache! {
48static IPV6_CACHE: LruCacheWithTtl<Name, Arc<IpAddresses>>::new("dns_resolver_ipv6", 1024);
50}
51declare_cache! {
52static IP_CACHE: LruCacheWithTtl<(Name, IpLookupStrategy), Arc<IpAddresses>>::new("dns_resolver_ip", 1024);
55}
56
57static MX_MAX_CONCURRENCY: AtomicUsize = AtomicUsize::new(128);
59static MX_CONCURRENCY_SEMA: LazyLock<Semaphore> =
60 LazyLock::new(|| Semaphore::new(MX_MAX_CONCURRENCY.load(Ordering::SeqCst)));
61
62static MX_TIMEOUT_MS: AtomicUsize = AtomicUsize::new(5000);
64
65static MX_NEGATIVE_TTL: AtomicUsize = AtomicUsize::new(300 * 1000);
67
68declare_metric! {
69static MX_IN_PROGRESS: IntGauge("dns_mx_resolve_in_progress");
71}
72
73declare_metric! {
74static MX_SUCCESS: IntCounter(
76 "dns_mx_resolve_status_ok");
77}
78
79declare_metric! {
80static MX_FAIL: IntCounter("dns_mx_resolve_status_fail");
86}
87
88declare_metric! {
89static MX_CACHED: IntCounter("dns_mx_resolve_cache_hit");
94}
95
96declare_metric! {
97static MX_QUERIES: IntCounter("dns_mx_resolve_cache_miss");
102}
103
104fn default_resolver() -> impl Resolver {
105 #[cfg(feature = "default-unbound")]
106 return UnboundResolver::new().unwrap();
107 #[cfg(not(feature = "default-unbound"))]
108 return HickoryResolver::new().expect("Parsing /etc/resolv.conf failed");
109}
110
111pub fn set_mx_concurrency_limit(n: usize) {
112 MX_MAX_CONCURRENCY.store(n, Ordering::SeqCst);
113}
114
115pub fn set_mx_timeout(duration: Duration) -> anyhow::Result<()> {
116 let ms = duration
117 .as_millis()
118 .try_into()
119 .context("set_mx_timeout: duration is too large")?;
120 MX_TIMEOUT_MS.store(ms, Ordering::Relaxed);
121 Ok(())
122}
123
124pub fn get_mx_timeout() -> Duration {
125 Duration::from_millis(MX_TIMEOUT_MS.load(Ordering::Relaxed) as u64)
126}
127
128pub fn set_mx_negative_cache_ttl(duration: Duration) -> anyhow::Result<()> {
129 let ms = duration
130 .as_millis()
131 .try_into()
132 .context("set_mx_negative_cache_ttl: duration is too large")?;
133 MX_NEGATIVE_TTL.store(ms, Ordering::Relaxed);
134 Ok(())
135}
136
137pub fn get_mx_negative_ttl() -> Duration {
138 Duration::from_millis(MX_NEGATIVE_TTL.load(Ordering::Relaxed) as u64)
139}
140
141#[derive(Clone, Debug, Serialize)]
142pub struct MailExchanger {
143 pub domain_name: String,
144 pub hosts: Vec<String>,
145 pub site_name: String,
146 pub by_pref: BTreeMap<u16, Vec<String>>,
147 pub is_domain_literal: bool,
148 pub is_secure: bool,
150 pub is_mx: bool,
151 #[serde(skip)]
152 expires: Option<Instant>,
153}
154
155pub fn fully_qualify(domain_name: &str) -> Result<Name, ProtoError> {
156 let mut name = Name::from_str_relaxed(domain_name)?.to_lowercase();
157
158 name.set_fqdn(true);
160
161 Ok(name)
162}
163
164pub fn reconfigure_resolver(resolver: impl Resolver) {
165 RESOLVER.store(Arc::new(Box::new(resolver)));
166}
167
168pub fn get_resolver() -> Arc<Box<dyn Resolver>> {
169 RESOLVER.load_full()
170}
171
172#[derive(Debug, Clone, PartialEq)]
175pub enum DaneStatus {
176 NotApplicable,
181 Records(Vec<TLSA>),
184 Unusable,
189 TempFail(String),
194}
195
196fn tlsa_is_usable(tlsa: &TLSA) -> bool {
203 match tlsa.cert_usage {
204 CertUsage::DaneTa | CertUsage::DaneEe => {}
205 _ => return false,
206 }
207 match tlsa.selector {
208 Selector::Full | Selector::Spki => {}
209 _ => return false,
210 }
211 let expected_len = match tlsa.matching {
212 Matching::Raw => None,
213 Matching::Sha256 => Some(32),
214 Matching::Sha512 => Some(64),
215 _ => return false,
216 };
217 match expected_len {
218 Some(len) if tlsa.cert_data.len() != len => false,
219 _ => true,
220 }
221}
222
223pub async fn resolve_dane(mx_host: &str, port: u16) -> anyhow::Result<DaneStatus> {
237 let name = fully_qualify(&format!("_{port}._tcp.{mx_host}"))?;
238 let answer = RESOLVER.load().resolve(name, RecordType::TLSA).await;
239 Ok(classify_tlsa_answer(mx_host, port, answer))
240}
241
242fn classify_tlsa_answer(mx_host: &str, port: u16, answer: Result<Answer, DnsError>) -> DaneStatus {
246 let answer = match answer {
247 Ok(answer) => answer,
248 Err(err) => {
249 return DaneStatus::TempFail(format!("TLSA lookup for {mx_host}:{port} failed: {err}"));
253 }
254 };
255 tracing::debug!("resolve_dane {mx_host}:{port} TLSA answer is: {answer:?}");
256
257 if answer.bogus {
258 return DaneStatus::TempFail(format!(
261 "TLSA records for {mx_host}:{port} are bogus: {}",
262 answer
263 .why_bogus
264 .as_deref()
265 .unwrap_or("DNSSEC validation failed")
266 ));
267 }
268
269 match answer.response_code {
270 ResponseCode::NoError => {}
271 ResponseCode::NXDomain => return DaneStatus::NotApplicable,
274 rcode => {
276 return DaneStatus::TempFail(format!(
277 "TLSA lookup for {mx_host}:{port} returned {rcode}"
278 ));
279 }
280 }
281
282 if !answer.secure {
284 return DaneStatus::NotApplicable;
285 }
286
287 let mut published = vec![];
288 for r in &answer.records {
289 if let RData::TLSA(tlsa) = r {
290 published.push(tlsa.clone());
291 }
292 }
293
294 if published.is_empty() {
295 return DaneStatus::NotApplicable;
297 }
298
299 let mut usable: Vec<TLSA> = published.into_iter().filter(tlsa_is_usable).collect();
300
301 if usable.is_empty() {
302 return DaneStatus::Unusable;
304 }
305
306 usable.sort_by_key(|a| {
310 (
311 u8::from(a.cert_usage),
312 u8::from(a.selector),
313 u8::from(a.matching),
314 a.cert_data.clone(),
315 )
316 });
317
318 tracing::info!("resolve_dane {mx_host}:{port} usable TLSA records: {usable:?}");
319
320 DaneStatus::Records(usable)
321}
322
323#[derive(Debug, PartialEq, Eq)]
331pub enum SecureCnameStatus {
332 SecureAlias,
335 NotSecureAlias,
339 TempFail(String),
343}
344
345pub async fn resolve_secure_cname(mx_host: &str) -> anyhow::Result<SecureCnameStatus> {
356 let name = fully_qualify(mx_host)?;
357 let answer = RESOLVER.load().resolve(name, RecordType::CNAME).await;
358 Ok(classify_cname_answer(mx_host, answer))
359}
360
361fn classify_cname_answer(mx_host: &str, answer: Result<Answer, DnsError>) -> SecureCnameStatus {
365 let answer = match answer {
366 Ok(answer) => answer,
367 Err(err) => {
368 return SecureCnameStatus::TempFail(format!(
369 "CNAME lookup for {mx_host} failed: {err}"
370 ));
371 }
372 };
373
374 if answer.bogus {
375 return SecureCnameStatus::TempFail(format!(
376 "CNAME records for {mx_host} are bogus: {}",
377 answer
378 .why_bogus
379 .as_deref()
380 .unwrap_or("DNSSEC validation failed")
381 ));
382 }
383
384 match answer.response_code {
385 ResponseCode::NoError => {}
386 ResponseCode::NXDomain => return SecureCnameStatus::NotSecureAlias,
389 rcode => {
390 return SecureCnameStatus::TempFail(format!(
391 "CNAME lookup for {mx_host} returned {rcode}"
392 ));
393 }
394 }
395
396 if !answer.secure {
398 return SecureCnameStatus::NotSecureAlias;
399 }
400
401 if answer.records.iter().any(|r| matches!(r, RData::CNAME(_))) {
403 SecureCnameStatus::SecureAlias
404 } else {
405 SecureCnameStatus::NotSecureAlias
406 }
407}
408
409pub fn has_colon_port(a: &str) -> Option<(&str, u16)> {
413 let (label, maybe_port) = a.rsplit_once(':')?;
414
415 if label.contains(':') {
418 return None;
419 }
420
421 let port = maybe_port.parse::<u16>().ok()?;
422 Some((label, port))
423}
424
425pub enum DomainClassification {
431 Domain(Name, Option<u16>),
433 Literal(HostOrSocketAddress),
435}
436
437impl DomainClassification {
438 pub fn classify(domain_name: &str) -> anyhow::Result<Self> {
439 let (domain_name, mut opt_port) = match has_colon_port(domain_name) {
440 Some((domain_name, port)) => (domain_name, Some(port)),
441 None => (domain_name, None),
442 };
443
444 if domain_name.starts_with('[') {
445 if !domain_name.ends_with(']') {
446 anyhow::bail!(
447 "domain_name `{domain_name}` is a malformed literal \
448 domain with no trailing `]`"
449 );
450 }
451
452 let lowered = domain_name.to_ascii_lowercase();
453 let literal = &lowered[1..lowered.len() - 1];
454
455 let literal = match has_colon_port(literal) {
456 Some((_, _)) if opt_port.is_some() => {
457 anyhow::bail!("invalid address: `{domain_name}` specifies a port both inside and outside a literal address enclosed in square brackets");
458 }
459 Some((literal, port)) => {
460 opt_port.replace(port);
461 literal
462 }
463 None => literal,
464 };
465
466 if let Some(v6_literal) = literal.strip_prefix("ipv6:") {
467 match v6_literal.parse::<Ipv6Addr>() {
468 Ok(addr) => {
469 let mut host_addr: HostOrSocketAddress = addr.into();
470 if let Some(port) = opt_port {
471 host_addr.set_port(port);
472 }
473 return Ok(Self::Literal(host_addr));
474 }
475 Err(err) => {
476 anyhow::bail!("invalid ipv6 address: `{v6_literal}`: {err:#}");
477 }
478 }
479 }
480
481 match literal.parse::<IpAddr>() {
485 Ok(ip_addr) => {
486 let mut host_addr: HostOrSocketAddress = ip_addr.into();
487 if let Some(port) = opt_port {
488 host_addr.set_port(port);
489 }
490 return Ok(Self::Literal(host_addr));
491 }
492 Err(err) => {
493 anyhow::bail!("invalid address: `{literal}`: {err:#}");
494 }
495 }
496 }
497
498 let name_fq = fully_qualify(domain_name)?;
499 Ok(Self::Domain(name_fq, opt_port))
500 }
501
502 pub fn has_port(&self) -> bool {
503 match self {
504 Self::Domain(_, Some(_)) => true,
505 Self::Domain(_, None) => false,
506 Self::Literal(addr) => addr.port().is_some(),
507 }
508 }
509}
510
511pub async fn resolve_a_or_aaaa(
512 domain_name: &str,
513 resolver: Option<&dyn Resolver>,
514 strategy: IpLookupStrategy,
515) -> anyhow::Result<Vec<ResolvedAddress>> {
516 if domain_name.starts_with('[') {
517 if !domain_name.ends_with(']') {
520 anyhow::bail!(
521 "domain_name `{domain_name}` is a malformed literal \
522 domain with no trailing `]`"
523 );
524 }
525
526 let lowered = domain_name.to_ascii_lowercase();
527 let literal = &lowered[1..lowered.len() - 1];
528
529 if let Some(v6_literal) = literal.strip_prefix("ipv6:") {
530 match v6_literal.parse::<Ipv6Addr>() {
531 Ok(addr) => {
532 return Ok(vec![ResolvedAddress {
533 name: domain_name.to_string(),
534 addr: std::net::IpAddr::V6(addr).into(),
535 is_secure: false,
536 }]);
537 }
538 Err(err) => {
539 anyhow::bail!("invalid ipv6 address: `{v6_literal}`: {err:#}");
540 }
541 }
542 }
543
544 match literal.parse::<HostAddress>() {
548 Ok(addr) => {
549 return Ok(vec![ResolvedAddress {
550 name: domain_name.to_string(),
551 addr: addr.into(),
552 is_secure: false,
553 }]);
554 }
555 Err(err) => {
556 anyhow::bail!("invalid address: `{literal}`: {err:#}");
557 }
558 }
559 } else {
560 if let Ok(addr) = domain_name.parse::<HostAddress>() {
562 return Ok(vec![ResolvedAddress {
563 name: domain_name.to_string(),
564 addr: addr.into(),
565 is_secure: false,
566 }]);
567 }
568 }
569
570 match ip_lookup(domain_name, resolver, strategy).await {
571 Ok((result, _expires)) => {
572 let addrs = result
573 .addrs
574 .iter()
575 .map(|&addr| ResolvedAddress {
576 name: domain_name.to_string(),
577 addr: addr.into(),
578 is_secure: result.secure,
579 })
580 .collect();
581 Ok(addrs)
582 }
583 Err(err) => anyhow::bail!("{err:#}"),
584 }
585}
586
587pub async fn resolve_socket_addr(
594 addr: &ResolvableSocketAddr,
595 resolver: Option<&dyn Resolver>,
596 strategy: IpLookupStrategy,
597) -> anyhow::Result<Vec<ResolvedAddress>> {
598 match addr {
599 ResolvableSocketAddr::UnixDomain(unix) => {
600 let name = match unix.as_pathname() {
601 Some(path) => path.display().to_string(),
602 None => "<unbound unix domain>".to_string(),
603 };
604 Ok(vec![ResolvedAddress {
605 name,
606 addr: HostOrSocketAddress::UnixDomain(unix.clone()),
607 is_secure: false,
608 }])
609 }
610 ResolvableSocketAddr::V4(sa) => Ok(vec![ResolvedAddress {
611 name: sa.to_string(),
612 addr: HostOrSocketAddress::V4Socket(sa.clone()),
613 is_secure: false,
614 }]),
615 ResolvableSocketAddr::V6(sa) => Ok(vec![ResolvedAddress {
616 name: sa.to_string(),
617 addr: HostOrSocketAddress::V6Socket(sa.clone()),
618 is_secure: false,
619 }]),
620 ResolvableSocketAddr::Hostname { host, port } => {
621 match ip_lookup(host, resolver, strategy).await {
622 Ok((result, _expires)) => Ok(result
623 .addrs
624 .iter()
625 .map(|&ip| {
626 let sa = SocketAddr::new(ip, *port);
627 ResolvedAddress {
628 name: host.clone(),
629 addr: sa.into(),
630 is_secure: result.secure,
631 }
632 })
633 .collect()),
634 Err(err) => anyhow::bail!("{err:#}"),
635 }
636 }
637 }
638}
639
640impl MailExchanger {
641 pub async fn resolve(domain_name: &str) -> anyhow::Result<Arc<Self>> {
642 MX_IN_PROGRESS.inc();
643 let result = Self::resolve_impl(domain_name).await;
644 MX_IN_PROGRESS.dec();
645 if result.is_ok() {
646 MX_SUCCESS.inc();
647 } else {
648 MX_FAIL.inc();
649 }
650 result
651 }
652
653 async fn resolve_impl(domain_name: &str) -> anyhow::Result<Arc<Self>> {
654 let (name_fq, opt_port) = match DomainClassification::classify(domain_name)? {
655 DomainClassification::Literal(addr) => {
656 let mut by_pref = BTreeMap::new();
657 by_pref.insert(1, vec![addr.to_string()]);
658 return Ok(Arc::new(Self {
659 domain_name: domain_name.to_string(),
660 hosts: vec![addr.to_string()],
661 site_name: addr.to_string(),
662 by_pref,
663 is_domain_literal: true,
664 is_secure: false,
665 is_mx: false,
666 expires: None,
667 }));
668 }
669 DomainClassification::Domain(name_fq, opt_port) => (name_fq, opt_port),
670 };
671
672 let lookup_result = MX_CACHE
673 .get_or_try_insert(
674 &(name_fq.clone(), opt_port),
675 |mx_result| {
676 if let Ok(mx) = mx_result {
677 if let Some(exp) = mx.expires {
678 return exp
679 .checked_duration_since(std::time::Instant::now())
680 .unwrap_or_else(|| Duration::from_secs(10));
681 }
682 }
683 get_mx_negative_ttl()
684 },
685 async {
686 MX_QUERIES.inc();
687 let start = Instant::now();
688 let (mut by_pref, expires) = match lookup_mx_record(&name_fq).await {
689 Ok((by_pref, expires)) => (by_pref, expires),
690 Err(err) => {
691 let error = format!(
692 "MX lookup for {domain_name} failed after {elapsed:?}: {err:#}",
693 elapsed = start.elapsed()
694 );
695 return Ok::<Result<Arc<MailExchanger>, String>, anyhow::Error>(Err(
696 error,
697 ));
698 }
699 };
700
701 let mut hosts = vec![];
702 for pref in &mut by_pref {
703 for host in &mut pref.hosts {
704 if let Some(port) = opt_port {
705 *host = format!("{host}:{port}");
706 };
707 hosts.push(host.to_string());
708 }
709 }
710
711 let is_secure = by_pref.iter().all(|p| p.is_secure);
712 let is_mx = by_pref.iter().all(|p| p.is_mx);
713
714 let by_pref = by_pref
715 .into_iter()
716 .map(|pref| (pref.pref, pref.hosts))
717 .collect();
718
719 let site_name = factor_names(&hosts);
720 let mx = Self {
721 hosts,
722 domain_name: name_fq.to_ascii(),
723 site_name,
724 by_pref,
725 is_domain_literal: false,
726 is_secure,
727 is_mx,
728 expires: Some(expires),
729 };
730
731 Ok(Ok(Arc::new(mx)))
732 },
733 )
734 .await
735 .map_err(|err| anyhow::anyhow!("{err}"))?;
736
737 if !lookup_result.is_fresh {
738 MX_CACHED.inc();
739 }
740
741 lookup_result.item.map_err(|err| anyhow::anyhow!("{err}"))
742 }
743
744 pub fn has_expired(&self) -> bool {
745 match self.expires {
746 Some(deadline) => deadline <= Instant::now(),
747 None => false,
748 }
749 }
750
751 pub async fn resolve_addresses(&self, strategy: IpLookupStrategy) -> ResolvedMxAddresses {
756 let mut result = vec![];
757
758 for hosts in self.by_pref.values().rev() {
759 let mut by_pref = vec![];
760
761 for mx_host in hosts {
762 if mx_host == "." {
764 return ResolvedMxAddresses::NullMx;
765 }
766
767 let (mx_host, opt_port) = match has_colon_port(mx_host) {
769 Some((domain_name, port)) => (domain_name, Some(port)),
770 None => (mx_host.as_str(), None),
771 };
772 if let Ok(addr) = mx_host.parse::<IpAddr>() {
773 let mut addr: HostOrSocketAddress = addr.into();
774 if let Some(port) = opt_port {
775 addr.set_port(port);
776 }
777 by_pref.push(ResolvedAddress {
778 name: mx_host.to_string(),
779 addr: addr.into(),
780 is_secure: false,
781 });
782 continue;
783 }
784
785 match ip_lookup(mx_host, None, strategy).await {
786 Err(err) => {
787 tracing::error!("failed to resolve {mx_host}: {err:#}");
788 continue;
789 }
790 Ok((result, _expires)) => {
791 for addr in result.addrs.iter() {
792 let mut addr: HostOrSocketAddress = (*addr).into();
793 if let Some(port) = opt_port {
794 addr.set_port(port);
795 }
796 by_pref.push(ResolvedAddress {
797 name: mx_host.to_string(),
798 addr,
799 is_secure: result.secure,
800 });
801 }
802 }
803 }
804 }
805
806 let mut rng = rand::thread_rng();
810 by_pref.shuffle(&mut rng);
811 result.append(&mut by_pref);
812 }
813 ResolvedMxAddresses::Addresses(result)
814 }
815}
816
817#[derive(Debug, Clone, Serialize)]
818pub enum ResolvedMxAddresses {
819 NullMx,
820 Addresses(Vec<ResolvedAddress>),
823}
824
825struct ByPreference {
826 hosts: Vec<String>,
827 pref: u16,
828 is_secure: bool,
829 is_mx: bool,
830}
831
832async fn lookup_mx_record(domain_name: &Name) -> anyhow::Result<(Vec<ByPreference>, Instant)> {
833 let mx_lookup = timeout(get_mx_timeout(), async {
834 let _permit = MX_CONCURRENCY_SEMA.acquire().await;
835 RESOLVER
836 .load()
837 .resolve(domain_name.clone(), RecordType::MX)
838 .await
839 })
840 .await??;
841 let mx_records = mx_lookup.records;
842
843 if mx_records.is_empty() {
844 if mx_lookup.nxdomain {
845 anyhow::bail!("NXDOMAIN");
846 }
847
848 return Ok((
853 vec![ByPreference {
854 hosts: vec![domain_name.to_lowercase().to_ascii()],
855 pref: 1,
856 is_secure: mx_lookup.secure,
857 is_mx: false,
858 }],
859 mx_lookup.expires,
860 ));
861 }
862
863 let mut records: Vec<ByPreference> = Vec::with_capacity(mx_records.len());
864
865 for mx_record in mx_records {
866 if let RData::MX(mx) = mx_record {
867 let pref = mx.preference;
868 let host = mx.exchange.to_lowercase().to_string();
869
870 if let Some(record) = records.iter_mut().find(|r| r.pref == pref) {
871 record.hosts.push(host);
872 } else {
873 records.push(ByPreference {
874 hosts: vec![host],
875 pref,
876 is_secure: mx_lookup.secure,
877 is_mx: true,
878 });
879 }
880 }
881 }
882
883 records.sort_unstable_by(|a, b| a.pref.cmp(&b.pref));
885
886 for mx in &mut records {
889 mx.hosts.sort();
890 }
891
892 Ok((records, mx_lookup.expires))
893}
894
895#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
896#[repr(u8)]
897pub enum IpLookupStrategy {
898 Ipv4Only,
900 Ipv6Only,
902 #[default]
904 Ipv4AndIpv6,
905 Ipv6ThenIpv4,
907 Ipv4ThenIpv6,
909}
910
911#[derive(Clone, Debug)]
916pub struct IpAddresses {
917 pub addrs: Vec<IpAddr>,
918 pub secure: bool,
920}
921
922pub async fn ip_lookup(
923 key: &str,
924 resolver: Option<&dyn Resolver>,
925 strategy: IpLookupStrategy,
926) -> anyhow::Result<(Arc<IpAddresses>, Instant)> {
927 let key_fq = fully_qualify(key)?;
928
929 if resolver.is_none() {
930 if let Some(lookup) = IP_CACHE.lookup(&(key_fq.clone(), strategy)) {
931 return Ok((lookup.item, lookup.expiration.into()));
932 }
933 }
934
935 let (v4, v6) = match strategy {
936 IpLookupStrategy::Ipv4AndIpv6 => {
937 let (v4, v6) = tokio::join!(ipv4_lookup(key, resolver), ipv6_lookup(key, resolver));
938 (Some(v4), Some(v6))
939 }
940 IpLookupStrategy::Ipv4Only => (Some(ipv4_lookup(key, resolver).await), None),
941 IpLookupStrategy::Ipv6Only => (None, Some(ipv6_lookup(key, resolver).await)),
942 IpLookupStrategy::Ipv6ThenIpv4 => {
943 let v6 = ipv6_lookup(key, resolver).await;
944 match v6 {
945 Ok((answer, exp)) if answer.addrs.is_empty() => (
946 Some(ipv4_lookup(key, resolver).await),
947 Some(Ok((answer, exp))),
948 ),
949 Err(err) => (Some(ipv4_lookup(key, resolver).await), Some(Err(err))),
950 Ok(res) => (None, Some(Ok(res))),
951 }
952 }
953 IpLookupStrategy::Ipv4ThenIpv6 => {
954 let v4 = ipv4_lookup(key, resolver).await;
955 match v4 {
956 Ok((answer, exp)) if answer.addrs.is_empty() => (
957 Some(Ok((answer, exp))),
958 Some(ipv6_lookup(key, resolver).await),
959 ),
960 Err(err) => (Some(Err(err)), Some(ipv6_lookup(key, resolver).await)),
961 Ok(res) => (Some(Ok(res)), None),
962 }
963 }
964 };
965
966 let mut addrs = vec![];
967 let mut any_addr = false;
968 let mut all_secure = true;
969 let mut errors = vec![];
970 let mut expires: Option<Instant> = None;
971
972 for family in [v4, v6] {
973 match family {
974 Some(Ok((answer, exp))) => {
975 expires = Some(match expires {
976 Some(existing) => exp.min(existing),
977 None => exp,
978 });
979 if !answer.addrs.is_empty() {
980 any_addr = true;
981 all_secure &= answer.secure;
982 addrs.extend_from_slice(&answer.addrs);
983 }
984 }
985 Some(Err(err)) => errors.push(err),
986 None => {}
987 }
988 }
989
990 if addrs.is_empty() && !errors.is_empty() {
991 return Err(errors.remove(0));
992 }
993
994 let result = Arc::new(IpAddresses {
995 addrs,
996 secure: any_addr && all_secure,
999 });
1000 let exp = expires.unwrap_or_else(Instant::now);
1001
1002 if resolver.is_none() {
1003 IP_CACHE
1004 .insert((key_fq, strategy), result.clone(), exp.into())
1005 .await;
1006 }
1007 Ok((result, exp))
1008}
1009
1010pub async fn ipv4_lookup(
1011 key: &str,
1012 resolver: Option<&dyn Resolver>,
1013) -> anyhow::Result<(Arc<IpAddresses>, Instant)> {
1014 let key_fq = fully_qualify(key)?;
1015 if resolver.is_none() {
1016 if let Some(lookup) = IPV4_CACHE.lookup(&key_fq) {
1017 return Ok((lookup.item, lookup.expiration.into()));
1018 }
1019 }
1020
1021 let answer = match resolver {
1022 Some(r) => r.resolve(key_fq.clone(), RecordType::A).await?,
1023 None => {
1024 RESOLVER
1025 .load()
1026 .resolve(key_fq.clone(), RecordType::A)
1027 .await?
1028 }
1029 };
1030 let result = Arc::new(IpAddresses {
1031 addrs: answer.as_addr(),
1032 secure: answer.secure,
1033 });
1034 let expires = answer.expires;
1035 if resolver.is_none() {
1036 IPV4_CACHE
1037 .insert(key_fq, result.clone(), expires.into())
1038 .await;
1039 }
1040 Ok((result, expires))
1041}
1042
1043pub async fn ipv6_lookup(
1044 key: &str,
1045 resolver: Option<&dyn Resolver>,
1046) -> anyhow::Result<(Arc<IpAddresses>, Instant)> {
1047 let key_fq = fully_qualify(key)?;
1048 if resolver.is_none() {
1049 if let Some(lookup) = IPV6_CACHE.lookup(&key_fq) {
1050 return Ok((lookup.item, lookup.expiration.into()));
1051 }
1052 }
1053
1054 let answer = match resolver {
1055 Some(r) => r.resolve(key_fq.clone(), RecordType::AAAA).await?,
1056 None => {
1057 RESOLVER
1058 .load()
1059 .resolve(key_fq.clone(), RecordType::AAAA)
1060 .await?
1061 }
1062 };
1063 let result = Arc::new(IpAddresses {
1064 addrs: answer.as_addr(),
1065 secure: answer.secure,
1066 });
1067 let expires = answer.expires;
1068 if resolver.is_none() {
1069 IPV6_CACHE
1070 .insert(key_fq, result.clone(), expires.into())
1071 .await;
1072 }
1073 Ok((result, expires))
1074}
1075
1076fn factor_names<S: AsRef<str>>(name_strings: &[S]) -> String {
1081 let mut max_element_count = 0;
1082
1083 let mut names = vec![];
1084
1085 for name in name_strings {
1086 let (name, opt_port) = match has_colon_port(name.as_ref()) {
1087 Some((name, port)) => (name, Some(port)),
1088 None => (name.as_ref(), None),
1089 };
1090 if let Ok(name) = fully_qualify(name) {
1091 names.push((name.to_lowercase(), opt_port));
1092 }
1093 }
1094
1095 let mut elements: Vec<Vec<&str>> = vec![];
1096
1097 let mut split_names = vec![];
1098 for (name, opt_port) in names {
1099 let mut fields: Vec<_> = name
1100 .iter()
1101 .map(|s| String::from_utf8_lossy(s).to_string())
1102 .collect();
1103 if let Some(port) = opt_port {
1104 fields.last_mut().map(|s| {
1105 s.push_str(&format!(":{port}"));
1106 });
1107 }
1108 fields.reverse();
1109 max_element_count = max_element_count.max(fields.len());
1110 split_names.push(fields);
1111 }
1112
1113 fn add_element<'a>(elements: &mut Vec<Vec<&'a str>>, field: &'a str, i: usize) {
1114 match elements.get_mut(i) {
1115 Some(ele) => {
1116 if !ele.contains(&field) {
1117 ele.push(field);
1118 }
1119 }
1120 None => {
1121 elements.push(vec![field]);
1122 }
1123 }
1124 }
1125
1126 for fields in &split_names {
1127 for (i, field) in fields.iter().enumerate() {
1128 add_element(&mut elements, field, i);
1129 }
1130 for i in fields.len()..max_element_count {
1131 add_element(&mut elements, "?", i);
1132 }
1133 }
1134
1135 let mut result = vec![];
1136 for mut ele in elements {
1137 let has_q = ele.contains(&"?");
1138 ele.retain(|&e| e != "?");
1139 let mut item_text = if ele.len() == 1 {
1140 ele[0].to_string()
1141 } else {
1142 format!("({})", ele.join("|"))
1143 };
1144 if has_q {
1145 item_text.push('?');
1146 }
1147 result.push(item_text);
1148 }
1149 result.reverse();
1150
1151 result.join(".")
1152}
1153
1154#[cfg(test)]
1155mod test {
1156 use super::*;
1157
1158 fn answer(records: Vec<RData>, secure: bool, response_code: ResponseCode) -> Answer {
1159 Answer {
1160 canon_name: None,
1161 records,
1162 nxdomain: response_code == ResponseCode::NXDomain,
1163 secure,
1164 bogus: false,
1165 why_bogus: None,
1166 expires: Instant::now(),
1167 response_code,
1168 }
1169 }
1170
1171 fn sha256_tlsa(usage: CertUsage, selector: Selector) -> TLSA {
1172 TLSA::new(usage, selector, Matching::Sha256, vec![0u8; 32])
1173 }
1174
1175 fn dane_ee_record() -> TLSA {
1176 sha256_tlsa(CertUsage::DaneEe, Selector::Spki)
1177 }
1178
1179 fn cname_record() -> RData {
1180 use hickory_resolver::proto::rr::rdata::CNAME;
1181 RData::CNAME(CNAME(Name::from_ascii("target.unsigned.example.").unwrap()))
1182 }
1183
1184 #[test]
1185 fn cname_answer_classification() {
1186 k9::assert_equal!(
1188 classify_cname_answer(
1189 "mx.example.com",
1190 Ok(answer(vec![cname_record()], true, ResponseCode::NoError))
1191 ),
1192 SecureCnameStatus::SecureAlias
1193 );
1194 k9::assert_equal!(
1196 classify_cname_answer(
1197 "mx.example.com",
1198 Ok(answer(vec![], true, ResponseCode::NoError))
1199 ),
1200 SecureCnameStatus::NotSecureAlias
1201 );
1202 k9::assert_equal!(
1204 classify_cname_answer(
1205 "mx.example.com",
1206 Ok(answer(vec![cname_record()], false, ResponseCode::NoError))
1207 ),
1208 SecureCnameStatus::NotSecureAlias
1209 );
1210 k9::assert_equal!(
1212 classify_cname_answer(
1213 "mx.example.com",
1214 Ok(answer(vec![], true, ResponseCode::NXDomain))
1215 ),
1216 SecureCnameStatus::NotSecureAlias
1217 );
1218 assert!(matches!(
1220 classify_cname_answer(
1221 "mx.example.com",
1222 Ok(answer(vec![], false, ResponseCode::ServFail))
1223 ),
1224 SecureCnameStatus::TempFail(_)
1225 ));
1226 assert!(matches!(
1228 classify_cname_answer(
1229 "mx.example.com",
1230 Err(DnsError::ResolveFailed("boom".to_string()))
1231 ),
1232 SecureCnameStatus::TempFail(_)
1233 ));
1234 }
1235
1236 #[test]
1237 fn tlsa_usability() {
1238 assert!(tlsa_is_usable(&sha256_tlsa(
1240 CertUsage::DaneTa,
1241 Selector::Full
1242 )));
1243 assert!(tlsa_is_usable(&sha256_tlsa(
1244 CertUsage::DaneEe,
1245 Selector::Spki
1246 )));
1247 assert!(!tlsa_is_usable(&sha256_tlsa(
1249 CertUsage::PkixTa,
1250 Selector::Full
1251 )));
1252 assert!(!tlsa_is_usable(&sha256_tlsa(
1253 CertUsage::PkixEe,
1254 Selector::Spki
1255 )));
1256 assert!(!tlsa_is_usable(&sha256_tlsa(
1258 CertUsage::Private,
1259 Selector::Spki
1260 )));
1261 assert!(!tlsa_is_usable(&sha256_tlsa(
1262 CertUsage::Unassigned(200),
1263 Selector::Spki
1264 )));
1265 assert!(!tlsa_is_usable(&sha256_tlsa(
1267 CertUsage::DaneEe,
1268 Selector::Unassigned(7)
1269 )));
1270 assert!(!tlsa_is_usable(&TLSA::new(
1271 CertUsage::DaneEe,
1272 Selector::Spki,
1273 Matching::Unassigned(9),
1274 vec![0u8; 32]
1275 )));
1276 assert!(!tlsa_is_usable(&TLSA::new(
1277 CertUsage::DaneEe,
1278 Selector::Spki,
1279 Matching::Sha256,
1280 vec![0u8; 31]
1281 )));
1282 assert!(tlsa_is_usable(&TLSA::new(
1284 CertUsage::DaneEe,
1285 Selector::Full,
1286 Matching::Raw,
1287 vec![0u8; 100]
1288 )));
1289 }
1290
1291 #[test]
1292 fn tlsa_answer_classification() {
1293 k9::assert_equal!(
1295 classify_tlsa_answer(
1296 "mx.example.com",
1297 25,
1298 Ok(answer(
1299 vec![RData::TLSA(dane_ee_record())],
1300 true,
1301 ResponseCode::NoError
1302 ))
1303 ),
1304 DaneStatus::Records(vec![dane_ee_record()])
1305 );
1306 k9::assert_equal!(
1308 classify_tlsa_answer(
1309 "mx.example.com",
1310 25,
1311 Ok(answer(vec![], true, ResponseCode::NoError))
1312 ),
1313 DaneStatus::NotApplicable
1314 );
1315 k9::assert_equal!(
1317 classify_tlsa_answer(
1318 "mx.example.com",
1319 25,
1320 Ok(answer(vec![], true, ResponseCode::NXDomain))
1321 ),
1322 DaneStatus::NotApplicable
1323 );
1324 k9::assert_equal!(
1326 classify_tlsa_answer(
1327 "mx.example.com",
1328 25,
1329 Ok(answer(
1330 vec![RData::TLSA(dane_ee_record())],
1331 false,
1332 ResponseCode::NoError
1333 ))
1334 ),
1335 DaneStatus::NotApplicable
1336 );
1337 k9::assert_equal!(
1340 classify_tlsa_answer(
1341 "mx.example.com",
1342 25,
1343 Ok(answer(
1344 vec![RData::TLSA(sha256_tlsa(CertUsage::PkixEe, Selector::Spki))],
1345 true,
1346 ResponseCode::NoError
1347 ))
1348 ),
1349 DaneStatus::Unusable
1350 );
1351 assert!(matches!(
1353 classify_tlsa_answer(
1354 "mx.example.com",
1355 25,
1356 Ok(answer(vec![], false, ResponseCode::ServFail))
1357 ),
1358 DaneStatus::TempFail(_)
1359 ));
1360 assert!(matches!(
1362 classify_tlsa_answer(
1363 "mx.example.com",
1364 25,
1365 Err(DnsError::ResolveFailed("boom".to_string()))
1366 ),
1367 DaneStatus::TempFail(_)
1368 ));
1369 }
1370
1371 #[cfg(feature = "live-dns-tests")]
1376 #[tokio::test]
1377 async fn hickory_dnssec_validation() {
1378 use hickory_resolver::config::{
1379 ConnectionConfig, NameServerConfig, ProtocolConfig, ResolverConfig,
1380 };
1381 use hickory_resolver::net::runtime::TokioRuntimeProvider;
1382 use hickory_resolver::TokioResolver;
1383
1384 let mut udp = ConnectionConfig::new(ProtocolConfig::Udp);
1385 udp.port = 53;
1386 let mut tcp = ConnectionConfig::new(ProtocolConfig::Tcp);
1387 tcp.port = 53;
1388 let config = ResolverConfig::from_parts(
1389 None,
1390 vec![],
1391 vec![NameServerConfig::new(
1392 "1.1.1.1".parse().unwrap(),
1393 true,
1394 vec![udp, tcp],
1395 )],
1396 );
1397 let mut builder =
1398 TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
1399 builder.options_mut().validate = true;
1400 let resolver = HickoryResolver::from(builder.build().unwrap());
1401
1402 let tlsa = resolver
1403 .resolve(
1404 fully_qualify("_25._tcp.do.havedane.net").unwrap(),
1405 RecordType::TLSA,
1406 )
1407 .await
1408 .unwrap();
1409 assert!(tlsa.secure, "signed TLSA should validate as secure");
1410 assert!(!tlsa.bogus);
1411 assert!(!tlsa.records.is_empty());
1412
1413 let unsigned = resolver
1414 .resolve(fully_qualify("google.com").unwrap(), RecordType::A)
1415 .await
1416 .unwrap();
1417 assert!(!unsigned.secure, "unsigned zone is not secure");
1418 assert!(!unsigned.bogus);
1419 assert!(
1420 !unsigned.records.is_empty(),
1421 "unsigned zone still resolves successfully"
1422 );
1423 }
1424
1425 #[cfg(feature = "live-dns-tests")]
1431 #[tokio::test]
1432 async fn hickory_negative_answer_secure_bit() {
1433 use hickory_resolver::config::{
1434 ConnectionConfig, NameServerConfig, ProtocolConfig, ResolverConfig,
1435 };
1436 use hickory_resolver::net::runtime::TokioRuntimeProvider;
1437 use hickory_resolver::TokioResolver;
1438
1439 let mut udp = ConnectionConfig::new(ProtocolConfig::Udp);
1440 udp.port = 53;
1441 let mut tcp = ConnectionConfig::new(ProtocolConfig::Tcp);
1442 tcp.port = 53;
1443 let config = ResolverConfig::from_parts(
1444 None,
1445 vec![],
1446 vec![NameServerConfig::new(
1447 "1.1.1.1".parse().unwrap(),
1448 true,
1449 vec![udp, tcp],
1450 )],
1451 );
1452 let mut builder =
1453 TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
1454 builder.options_mut().validate = true;
1455 let resolver = HickoryResolver::from(builder.build().unwrap());
1456
1457 let secure_nodata = resolver
1459 .resolve(fully_qualify("cloudflare.com").unwrap(), RecordType::TLSA)
1460 .await
1461 .unwrap();
1462 assert!(secure_nodata.records.is_empty());
1463 assert!(!secure_nodata.bogus);
1464 assert!(
1465 secure_nodata.secure,
1466 "securely denied NODATA in a signed zone should be secure"
1467 );
1468
1469 let insecure_nodata = resolver
1471 .resolve(fully_qualify("mail.anoebis.be").unwrap(), RecordType::AAAA)
1472 .await
1473 .unwrap();
1474 assert!(insecure_nodata.records.is_empty());
1475 assert!(!insecure_nodata.bogus);
1476 assert!(
1477 !insecure_nodata.secure,
1478 "NODATA in an unsigned zone must not be secure"
1479 );
1480 }
1481
1482 #[tokio::test]
1483 async fn literal_resolve() {
1484 let v4_loopback = MailExchanger::resolve("[127.0.0.1]").await.unwrap();
1485 k9::snapshot!(
1486 &v4_loopback,
1487 r#"
1488MailExchanger {
1489 domain_name: "[127.0.0.1]",
1490 hosts: [
1491 "127.0.0.1",
1492 ],
1493 site_name: "127.0.0.1",
1494 by_pref: {
1495 1: [
1496 "127.0.0.1",
1497 ],
1498 },
1499 is_domain_literal: true,
1500 is_secure: false,
1501 is_mx: false,
1502 expires: None,
1503}
1504"#
1505 );
1506 k9::snapshot!(
1507 v4_loopback
1508 .resolve_addresses(IpLookupStrategy::default())
1509 .await,
1510 r#"
1511Addresses(
1512 [
1513 ResolvedAddress {
1514 name: "127.0.0.1",
1515 addr: 127.0.0.1,
1516 is_secure: false,
1517 },
1518 ],
1519)
1520"#
1521 );
1522
1523 let v6_loopback_non_conforming = MailExchanger::resolve("[::1]").await.unwrap();
1524 k9::snapshot!(
1525 &v6_loopback_non_conforming,
1526 r#"
1527MailExchanger {
1528 domain_name: "[::1]",
1529 hosts: [
1530 "::1",
1531 ],
1532 site_name: "::1",
1533 by_pref: {
1534 1: [
1535 "::1",
1536 ],
1537 },
1538 is_domain_literal: true,
1539 is_secure: false,
1540 is_mx: false,
1541 expires: None,
1542}
1543"#
1544 );
1545 k9::snapshot!(
1546 v6_loopback_non_conforming
1547 .resolve_addresses(IpLookupStrategy::default())
1548 .await,
1549 r#"
1550Addresses(
1551 [
1552 ResolvedAddress {
1553 name: "::1",
1554 addr: ::1,
1555 is_secure: false,
1556 },
1557 ],
1558)
1559"#
1560 );
1561
1562 let v6_loopback = MailExchanger::resolve("[IPv6:::1]").await.unwrap();
1563 k9::snapshot!(
1564 &v6_loopback,
1565 r#"
1566MailExchanger {
1567 domain_name: "[IPv6:::1]",
1568 hosts: [
1569 "::1",
1570 ],
1571 site_name: "::1",
1572 by_pref: {
1573 1: [
1574 "::1",
1575 ],
1576 },
1577 is_domain_literal: true,
1578 is_secure: false,
1579 is_mx: false,
1580 expires: None,
1581}
1582"#
1583 );
1584 k9::snapshot!(
1585 v6_loopback
1586 .resolve_addresses(IpLookupStrategy::default())
1587 .await,
1588 r#"
1589Addresses(
1590 [
1591 ResolvedAddress {
1592 name: "::1",
1593 addr: ::1,
1594 is_secure: false,
1595 },
1596 ],
1597)
1598"#
1599 );
1600 }
1601
1602 #[test]
1603 fn name_factoring() {
1604 assert_eq!(
1605 factor_names(&[
1606 "mta5.am0.yahoodns.net",
1607 "mta6.am0.yahoodns.net",
1608 "mta7.am0.yahoodns.net"
1609 ]),
1610 "(mta5|mta6|mta7).am0.yahoodns.net".to_string()
1611 );
1612
1613 assert_eq!(
1615 factor_names(&[
1616 "mta5.AM0.yahoodns.net",
1617 "mta6.am0.yAHOodns.net",
1618 "mta7.am0.yahoodns.net"
1619 ]),
1620 "(mta5|mta6|mta7).am0.yahoodns.net".to_string()
1621 );
1622
1623 assert_eq!(
1626 factor_names(&[
1627 "gmail-smtp-in.l.google.com",
1628 "alt1.gmail-smtp-in.l.google.com",
1629 "alt2.gmail-smtp-in.l.google.com",
1630 "alt3.gmail-smtp-in.l.google.com",
1631 "alt4.gmail-smtp-in.l.google.com",
1632 ]),
1633 "(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com".to_string()
1634 );
1635
1636 assert_eq!(
1637 factor_names(&[
1638 "mta5.am0.yahoodns.net:123",
1639 "mta6.am0.yahoodns.net:123",
1640 "mta7.am0.yahoodns.net:123"
1641 ]),
1642 "(mta5|mta6|mta7).am0.yahoodns.net:123".to_string()
1643 );
1644 assert_eq!(
1645 factor_names(&[
1646 "mta5.am0.yahoodns.net:123",
1647 "mta6.am0.yahoodns.net:456",
1648 "mta7.am0.yahoodns.net:123"
1649 ]),
1650 "(mta5|mta6|mta7).am0.yahoodns.(net:123|net:456)".to_string()
1651 );
1652 }
1653
1654 #[test]
1658 fn mx_order_name_factor() {
1659 assert_eq!(
1660 factor_names(&[
1661 "example-com.mail.protection.outlook.com.",
1662 "mx-biz.mail.am0.yahoodns.net.",
1663 "mx-biz.mail.am0.yahoodns.net.",
1664 ]),
1665 "(example-com|mx-biz).mail.(protection|am0).(outlook|yahoodns).(com|net)".to_string()
1666 );
1667 assert_eq!(
1668 factor_names(&[
1669 "mx-biz.mail.am0.yahoodns.net.",
1670 "mx-biz.mail.am0.yahoodns.net.",
1671 "example-com.mail.protection.outlook.com.",
1672 ]),
1673 "(mx-biz|example-com).mail.(am0|protection).(yahoodns|outlook).(net|com)".to_string()
1674 );
1675 }
1676
1677 #[cfg(feature = "live-dns-tests")]
1678 #[tokio::test]
1679 async fn lookup_gmail_mx() {
1680 let mut gmail = (*MailExchanger::resolve("gmail.com").await.unwrap()).clone();
1681 gmail.expires.take();
1682 k9::snapshot!(
1683 &gmail,
1684 r#"
1685MailExchanger {
1686 domain_name: "gmail.com.",
1687 hosts: [
1688 "gmail-smtp-in.l.google.com.",
1689 "alt1.gmail-smtp-in.l.google.com.",
1690 "alt2.gmail-smtp-in.l.google.com.",
1691 "alt3.gmail-smtp-in.l.google.com.",
1692 "alt4.gmail-smtp-in.l.google.com.",
1693 ],
1694 site_name: "(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com",
1695 by_pref: {
1696 5: [
1697 "gmail-smtp-in.l.google.com.",
1698 ],
1699 10: [
1700 "alt1.gmail-smtp-in.l.google.com.",
1701 ],
1702 20: [
1703 "alt2.gmail-smtp-in.l.google.com.",
1704 ],
1705 30: [
1706 "alt3.gmail-smtp-in.l.google.com.",
1707 ],
1708 40: [
1709 "alt4.gmail-smtp-in.l.google.com.",
1710 ],
1711 },
1712 is_domain_literal: false,
1713 is_secure: false,
1714 is_mx: true,
1715 expires: None,
1716}
1717"#
1718 );
1719
1720 k9::snapshot!(
1729 gmail.resolve_addresses(IpLookupStrategy::default()).await,
1730 r#"
1731Addresses(
1732 [
1733 ResolvedAddress {
1734 name: "alt4.gmail-smtp-in.l.google.com.",
1735 addr: 2607:f8b0:4023:401::1b,
1736 },
1737 ResolvedAddress {
1738 name: "alt4.gmail-smtp-in.l.google.com.",
1739 addr: 173.194.77.27,
1740 },
1741 ResolvedAddress {
1742 name: "alt3.gmail-smtp-in.l.google.com.",
1743 addr: 2607:f8b0:4023:1::1a,
1744 },
1745 ResolvedAddress {
1746 name: "alt3.gmail-smtp-in.l.google.com.",
1747 addr: 172.253.113.26,
1748 },
1749 ResolvedAddress {
1750 name: "alt2.gmail-smtp-in.l.google.com.",
1751 addr: 2607:f8b0:4001:c1d::1b,
1752 },
1753 ResolvedAddress {
1754 name: "alt2.gmail-smtp-in.l.google.com.",
1755 addr: 74.125.126.27,
1756 },
1757 ResolvedAddress {
1758 name: "alt1.gmail-smtp-in.l.google.com.",
1759 addr: 2607:f8b0:4003:c04::1b,
1760 },
1761 ResolvedAddress {
1762 name: "alt1.gmail-smtp-in.l.google.com.",
1763 addr: 108.177.104.27,
1764 },
1765 ResolvedAddress {
1766 name: "gmail-smtp-in.l.google.com.",
1767 addr: 2607:f8b0:4023:c06::1b,
1768 },
1769 ResolvedAddress {
1770 name: "gmail-smtp-in.l.google.com.",
1771 addr: 142.251.2.26,
1772 },
1773 ],
1774)
1775"#
1776 );
1777 }
1778
1779 #[cfg(feature = "live-dns-tests")]
1780 #[tokio::test]
1781 async fn lookup_punycode_no_mx_only_a() {
1782 let mx = MailExchanger::resolve("xn--bb-eka.at").await.unwrap();
1783 assert_eq!(mx.domain_name, "xn--bb-eka.at.");
1784 assert_eq!(mx.hosts[0], "xn--bb-eka.at.");
1785 }
1786
1787 #[cfg(feature = "live-dns-tests")]
1788 #[tokio::test]
1789 async fn lookup_bogus_aasland() {
1790 let err = MailExchanger::resolve("not-mairs.aasland.com")
1791 .await
1792 .unwrap_err();
1793 k9::snapshot!(err, "MX lookup for not-mairs.aasland.com failed: NXDOMAIN");
1794 }
1795
1796 #[cfg(all(feature = "live-dns-tests", feature = "default-unbound"))]
1799 #[tokio::test]
1800 async fn lookup_example_com() {
1801 let mx = MailExchanger::resolve("example.com").await.unwrap();
1803 k9::snapshot!(
1804 mx,
1805 r#"
1806MailExchanger {
1807 domain_name: "example.com.",
1808 hosts: [
1809 ".",
1810 ],
1811 site_name: "",
1812 by_pref: {
1813 0: [
1814 ".",
1815 ],
1816 },
1817 is_domain_literal: false,
1818 is_secure: true,
1819 is_mx: true,
1820}
1821"#
1822 );
1823 }
1824
1825 #[cfg(all(feature = "live-dns-tests", feature = "default-unbound"))]
1828 #[tokio::test]
1829 async fn lookup_have_dane() {
1830 let mx = MailExchanger::resolve("do.havedane.net").await.unwrap();
1831 k9::snapshot!(
1832 mx,
1833 r#"
1834MailExchanger {
1835 domain_name: "do.havedane.net.",
1836 hosts: [
1837 "do.havedane.net.",
1838 ],
1839 site_name: "do.havedane.net",
1840 by_pref: {
1841 10: [
1842 "do.havedane.net.",
1843 ],
1844 },
1845 is_domain_literal: false,
1846 is_secure: true,
1847 is_mx: true,
1848}
1849"#
1850 );
1851 }
1852
1853 #[cfg(all(feature = "live-dns-tests", feature = "default-unbound"))]
1856 #[tokio::test]
1857 async fn tlsa_have_dane() {
1858 let DaneStatus::Records(tlsa) = resolve_dane("do.havedane.net", 25).await.unwrap() else {
1859 panic!("expected usable DANE records");
1860 };
1861 k9::snapshot!(
1862 tlsa,
1863 "
1864[
1865 TLSA {
1866 cert_usage: TrustAnchor,
1867 selector: Spki,
1868 matching: Sha256,
1869 cert_data: [
1870 39,
1871 182,
1872 148,
1873 181,
1874 29,
1875 31,
1876 239,
1877 136,
1878 133,
1879 55,
1880 42,
1881 207,
1882 179,
1883 145,
1884 147,
1885 117,
1886 151,
1887 34,
1888 183,
1889 54,
1890 176,
1891 66,
1892 104,
1893 100,
1894 220,
1895 28,
1896 121,
1897 208,
1898 101,
1899 31,
1900 239,
1901 115,
1902 ],
1903 },
1904 TLSA {
1905 cert_usage: DomainIssued,
1906 selector: Spki,
1907 matching: Sha256,
1908 cert_data: [
1909 85,
1910 58,
1911 207,
1912 136,
1913 249,
1914 238,
1915 24,
1916 204,
1917 170,
1918 230,
1919 53,
1920 202,
1921 84,
1922 15,
1923 50,
1924 203,
1925 132,
1926 172,
1927 167,
1928 124,
1929 71,
1930 145,
1931 102,
1932 130,
1933 188,
1934 181,
1935 66,
1936 213,
1937 29,
1938 170,
1939 135,
1940 31,
1941 ],
1942 },
1943]
1944"
1945 );
1946 }
1947
1948 #[cfg(feature = "live-dns-tests")]
1949 #[tokio::test]
1950 async fn mx_lookup_www_example_com() {
1951 let mx = MailExchanger::resolve("www.example.com").await.unwrap();
1953 k9::snapshot!(
1954 mx,
1955 r#"
1956MailExchanger {
1957 domain_name: "www.example.com.",
1958 hosts: [
1959 "www.example.com.",
1960 ],
1961 site_name: "www.example.com",
1962 by_pref: {
1963 1: [
1964 "www.example.com.",
1965 ],
1966 },
1967 is_domain_literal: false,
1968 is_secure: false,
1969 is_mx: false,
1970}
1971"#
1972 );
1973 }
1974
1975 #[cfg(feature = "live-dns-tests")]
1976 #[tokio::test]
1977 async fn txt_lookup_gmail() {
1978 let name = Name::from_str_relaxed("_mta-sts.gmail.com").unwrap();
1979 let answer = get_resolver().resolve(name, RecordType::TXT).await.unwrap();
1980 k9::snapshot!(
1981 answer.as_txt(),
1982 r#"
1983[
1984 "v=STSv1; id=20190429T010101;",
1985]
1986"#
1987 );
1988 }
1989}