1use arc_swap::ArcSwap;
2use hickory_resolver::proto::op::ResponseCode;
3pub use hickory_resolver::proto::rr::rdata::tlsa::{CertUsage, Matching, Selector, TLSA};
4pub use hickory_resolver::proto::rr::Name;
5use hickory_resolver::proto::rr::{RData, RecordType};
6use hickory_resolver::proto::ProtoError;
7use kumo_address::host::HostAddress;
8use kumo_address::host_or_socket::HostOrSocketAddress;
9use kumo_address::resolvable::ResolvableSocketAddr;
10use kumo_log_types::ResolvedAddress;
11use lruttl::declare_cache;
12use serde::{Deserialize, Serialize};
13use std::net::{IpAddr, Ipv6Addr, SocketAddr};
14use std::sync::{Arc, LazyLock};
15use std::time::Instant;
16
17mod resolver;
18#[cfg(feature = "unbound")]
19pub use resolver::UnboundResolver;
20pub use resolver::{
21 ptr_host, reverse_ip, AggregateResolver, Answer, DnsError, HickoryResolver, IpDisplay,
22 Resolver, TestResolver,
23};
24
25static RESOLVER: LazyLock<ArcSwap<Box<dyn Resolver>>> =
30 LazyLock::new(|| ArcSwap::from_pointee(Box::new(default_resolver())));
31
32declare_cache! {
33static IPV4_CACHE: LruCacheWithTtl<Name, Arc<IpAddresses>>::new("dns_resolver_ipv4", 1024);
35}
36declare_cache! {
37static IPV6_CACHE: LruCacheWithTtl<Name, Arc<IpAddresses>>::new("dns_resolver_ipv6", 1024);
39}
40declare_cache! {
41static IP_CACHE: LruCacheWithTtl<(Name, IpLookupStrategy), Arc<IpAddresses>>::new("dns_resolver_ip", 1024);
44}
45
46fn default_resolver() -> impl Resolver {
47 #[cfg(feature = "default-unbound")]
48 return UnboundResolver::new().unwrap();
49 #[cfg(not(feature = "default-unbound"))]
50 return HickoryResolver::new().expect("Parsing /etc/resolv.conf failed");
51}
52
53pub fn fully_qualify(domain_name: &str) -> Result<Name, ProtoError> {
54 let mut name = Name::from_str_relaxed(domain_name)?.to_lowercase();
55
56 name.set_fqdn(true);
58
59 Ok(name)
60}
61
62pub fn reconfigure_resolver(resolver: impl Resolver) {
63 RESOLVER.store(Arc::new(Box::new(resolver)));
64}
65
66pub fn get_resolver() -> Arc<Box<dyn Resolver>> {
67 RESOLVER.load_full()
68}
69
70#[derive(Debug, Clone, PartialEq)]
73pub enum DaneStatus {
74 NotApplicable,
79 Records(Vec<TLSA>),
82 Unusable,
87 TempFail(String),
92}
93
94fn tlsa_is_usable(tlsa: &TLSA) -> bool {
101 match tlsa.cert_usage {
102 CertUsage::DaneTa | CertUsage::DaneEe => {}
103 _ => return false,
104 }
105 match tlsa.selector {
106 Selector::Full | Selector::Spki => {}
107 _ => return false,
108 }
109 let expected_len = match tlsa.matching {
110 Matching::Raw => None,
111 Matching::Sha256 => Some(32),
112 Matching::Sha512 => Some(64),
113 _ => return false,
114 };
115 match expected_len {
116 Some(len) if tlsa.cert_data.len() != len => false,
117 _ => true,
118 }
119}
120
121pub async fn resolve_dane(mx_host: &str, port: u16) -> anyhow::Result<DaneStatus> {
135 let name = fully_qualify(&format!("_{port}._tcp.{mx_host}"))?;
136 let answer = RESOLVER.load().resolve(name, RecordType::TLSA).await;
137 Ok(classify_tlsa_answer(mx_host, port, answer))
138}
139
140fn classify_tlsa_answer(mx_host: &str, port: u16, answer: Result<Answer, DnsError>) -> DaneStatus {
144 let answer = match answer {
145 Ok(answer) => answer,
146 Err(err) => {
147 return DaneStatus::TempFail(format!("TLSA lookup for {mx_host}:{port} failed: {err}"));
151 }
152 };
153 tracing::debug!("resolve_dane {mx_host}:{port} TLSA answer is: {answer:?}");
154
155 if answer.bogus {
156 return DaneStatus::TempFail(format!(
159 "TLSA records for {mx_host}:{port} are bogus: {}",
160 answer
161 .why_bogus
162 .as_deref()
163 .unwrap_or("DNSSEC validation failed")
164 ));
165 }
166
167 match answer.response_code {
168 ResponseCode::NoError => {}
169 ResponseCode::NXDomain => return DaneStatus::NotApplicable,
172 rcode => {
174 return DaneStatus::TempFail(format!(
175 "TLSA lookup for {mx_host}:{port} returned {rcode}"
176 ));
177 }
178 }
179
180 if !answer.secure {
182 return DaneStatus::NotApplicable;
183 }
184
185 let mut published = vec![];
186 for r in &answer.records {
187 if let RData::TLSA(tlsa) = r {
188 published.push(tlsa.clone());
189 }
190 }
191
192 if published.is_empty() {
193 return DaneStatus::NotApplicable;
195 }
196
197 let mut usable: Vec<TLSA> = published.into_iter().filter(tlsa_is_usable).collect();
198
199 if usable.is_empty() {
200 return DaneStatus::Unusable;
202 }
203
204 usable.sort_by_key(|a| {
208 (
209 u8::from(a.cert_usage),
210 u8::from(a.selector),
211 u8::from(a.matching),
212 a.cert_data.clone(),
213 )
214 });
215
216 tracing::info!("resolve_dane {mx_host}:{port} usable TLSA records: {usable:?}");
217
218 DaneStatus::Records(usable)
219}
220
221#[derive(Debug, PartialEq, Eq)]
229pub enum SecureCnameStatus {
230 SecureAlias,
233 NotSecureAlias,
237 TempFail(String),
241}
242
243pub async fn resolve_secure_cname(mx_host: &str) -> anyhow::Result<SecureCnameStatus> {
254 let name = fully_qualify(mx_host)?;
255 let answer = RESOLVER.load().resolve(name, RecordType::CNAME).await;
256 Ok(classify_cname_answer(mx_host, answer))
257}
258
259fn classify_cname_answer(mx_host: &str, answer: Result<Answer, DnsError>) -> SecureCnameStatus {
263 let answer = match answer {
264 Ok(answer) => answer,
265 Err(err) => {
266 return SecureCnameStatus::TempFail(format!(
267 "CNAME lookup for {mx_host} failed: {err}"
268 ));
269 }
270 };
271
272 if answer.bogus {
273 return SecureCnameStatus::TempFail(format!(
274 "CNAME records for {mx_host} are bogus: {}",
275 answer
276 .why_bogus
277 .as_deref()
278 .unwrap_or("DNSSEC validation failed")
279 ));
280 }
281
282 match answer.response_code {
283 ResponseCode::NoError => {}
284 ResponseCode::NXDomain => return SecureCnameStatus::NotSecureAlias,
287 rcode => {
288 return SecureCnameStatus::TempFail(format!(
289 "CNAME lookup for {mx_host} returned {rcode}"
290 ));
291 }
292 }
293
294 if !answer.secure {
296 return SecureCnameStatus::NotSecureAlias;
297 }
298
299 if answer.records.iter().any(|r| matches!(r, RData::CNAME(_))) {
301 SecureCnameStatus::SecureAlias
302 } else {
303 SecureCnameStatus::NotSecureAlias
304 }
305}
306
307pub fn has_colon_port(a: &str) -> Option<(&str, u16)> {
311 let (label, maybe_port) = a.rsplit_once(':')?;
312
313 if label.contains(':') {
316 return None;
317 }
318
319 let port = maybe_port.parse::<u16>().ok()?;
320 Some((label, port))
321}
322
323pub enum DomainClassification {
329 Domain(Name, Option<u16>),
331 Literal(HostOrSocketAddress),
333}
334
335impl DomainClassification {
336 pub fn classify(domain_name: &str) -> anyhow::Result<Self> {
337 let (domain_name, mut opt_port) = match has_colon_port(domain_name) {
338 Some((domain_name, port)) => (domain_name, Some(port)),
339 None => (domain_name, None),
340 };
341
342 if domain_name.starts_with('[') {
343 if !domain_name.ends_with(']') {
344 anyhow::bail!(
345 "domain_name `{domain_name}` is a malformed literal \
346 domain with no trailing `]`"
347 );
348 }
349
350 let lowered = domain_name.to_ascii_lowercase();
351 let literal = &lowered[1..lowered.len() - 1];
352
353 let literal = match has_colon_port(literal) {
354 Some((_, _)) if opt_port.is_some() => {
355 anyhow::bail!("invalid address: `{domain_name}` specifies a port both inside and outside a literal address enclosed in square brackets");
356 }
357 Some((literal, port)) => {
358 opt_port.replace(port);
359 literal
360 }
361 None => literal,
362 };
363
364 if let Some(v6_literal) = literal.strip_prefix("ipv6:") {
365 match v6_literal.parse::<Ipv6Addr>() {
366 Ok(addr) => {
367 let mut host_addr: HostOrSocketAddress = addr.into();
368 if let Some(port) = opt_port {
369 host_addr.set_port(port);
370 }
371 return Ok(Self::Literal(host_addr));
372 }
373 Err(err) => {
374 anyhow::bail!("invalid ipv6 address: `{v6_literal}`: {err:#}");
375 }
376 }
377 }
378
379 match literal.parse::<IpAddr>() {
383 Ok(ip_addr) => {
384 let mut host_addr: HostOrSocketAddress = ip_addr.into();
385 if let Some(port) = opt_port {
386 host_addr.set_port(port);
387 }
388 return Ok(Self::Literal(host_addr));
389 }
390 Err(err) => {
391 anyhow::bail!("invalid address: `{literal}`: {err:#}");
392 }
393 }
394 }
395
396 let name_fq = fully_qualify(domain_name)?;
397 Ok(Self::Domain(name_fq, opt_port))
398 }
399
400 pub fn has_port(&self) -> bool {
401 match self {
402 Self::Domain(_, Some(_)) => true,
403 Self::Domain(_, None) => false,
404 Self::Literal(addr) => addr.port().is_some(),
405 }
406 }
407}
408
409pub async fn resolve_a_or_aaaa(
410 domain_name: &str,
411 resolver: Option<&dyn Resolver>,
412 strategy: IpLookupStrategy,
413) -> anyhow::Result<Vec<ResolvedAddress>> {
414 if domain_name.starts_with('[') {
415 if !domain_name.ends_with(']') {
418 anyhow::bail!(
419 "domain_name `{domain_name}` is a malformed literal \
420 domain with no trailing `]`"
421 );
422 }
423
424 let lowered = domain_name.to_ascii_lowercase();
425 let literal = &lowered[1..lowered.len() - 1];
426
427 if let Some(v6_literal) = literal.strip_prefix("ipv6:") {
428 match v6_literal.parse::<Ipv6Addr>() {
429 Ok(addr) => {
430 return Ok(vec![ResolvedAddress {
431 name: domain_name.to_string(),
432 addr: std::net::IpAddr::V6(addr).into(),
433 is_secure: false,
434 }]);
435 }
436 Err(err) => {
437 anyhow::bail!("invalid ipv6 address: `{v6_literal}`: {err:#}");
438 }
439 }
440 }
441
442 match literal.parse::<HostAddress>() {
446 Ok(addr) => {
447 return Ok(vec![ResolvedAddress {
448 name: domain_name.to_string(),
449 addr: addr.into(),
450 is_secure: false,
451 }]);
452 }
453 Err(err) => {
454 anyhow::bail!("invalid address: `{literal}`: {err:#}");
455 }
456 }
457 } else {
458 if let Ok(addr) = domain_name.parse::<HostAddress>() {
460 return Ok(vec![ResolvedAddress {
461 name: domain_name.to_string(),
462 addr: addr.into(),
463 is_secure: false,
464 }]);
465 }
466 }
467
468 match ip_lookup(domain_name, resolver, strategy).await {
469 Ok((result, _expires)) => {
470 let addrs = result
471 .addrs
472 .iter()
473 .map(|&addr| ResolvedAddress {
474 name: domain_name.to_string(),
475 addr: addr.into(),
476 is_secure: result.secure,
477 })
478 .collect();
479 Ok(addrs)
480 }
481 Err(err) => anyhow::bail!("{err:#}"),
482 }
483}
484
485pub async fn resolve_socket_addr(
492 addr: &ResolvableSocketAddr,
493 resolver: Option<&dyn Resolver>,
494 strategy: IpLookupStrategy,
495) -> anyhow::Result<Vec<ResolvedAddress>> {
496 match addr {
497 ResolvableSocketAddr::UnixDomain(unix) => {
498 let name = match unix.as_pathname() {
499 Some(path) => path.display().to_string(),
500 None => "<unbound unix domain>".to_string(),
501 };
502 Ok(vec![ResolvedAddress {
503 name,
504 addr: HostOrSocketAddress::UnixDomain(unix.clone()),
505 is_secure: false,
506 }])
507 }
508 ResolvableSocketAddr::V4(sa) => Ok(vec![ResolvedAddress {
509 name: sa.to_string(),
510 addr: HostOrSocketAddress::V4Socket(sa.clone()),
511 is_secure: false,
512 }]),
513 ResolvableSocketAddr::V6(sa) => Ok(vec![ResolvedAddress {
514 name: sa.to_string(),
515 addr: HostOrSocketAddress::V6Socket(sa.clone()),
516 is_secure: false,
517 }]),
518 ResolvableSocketAddr::Hostname { host, port } => {
519 match ip_lookup(host, resolver, strategy).await {
520 Ok((result, _expires)) => Ok(result
521 .addrs
522 .iter()
523 .map(|&ip| {
524 let sa = SocketAddr::new(ip, *port);
525 ResolvedAddress {
526 name: host.clone(),
527 addr: sa.into(),
528 is_secure: result.secure,
529 }
530 })
531 .collect()),
532 Err(err) => anyhow::bail!("{err:#}"),
533 }
534 }
535 }
536}
537
538#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
539#[repr(u8)]
540pub enum IpLookupStrategy {
541 Ipv4Only,
543 Ipv6Only,
545 #[default]
547 Ipv4AndIpv6,
548 Ipv6ThenIpv4,
550 Ipv4ThenIpv6,
552}
553
554#[derive(Clone, Debug)]
559pub struct IpAddresses {
560 pub addrs: Vec<IpAddr>,
561 pub secure: bool,
563}
564
565pub async fn ip_lookup(
566 key: &str,
567 resolver: Option<&dyn Resolver>,
568 strategy: IpLookupStrategy,
569) -> anyhow::Result<(Arc<IpAddresses>, Instant)> {
570 let key_fq = fully_qualify(key)?;
571
572 if resolver.is_none() {
573 if let Some(lookup) = IP_CACHE.lookup(&(key_fq.clone(), strategy)) {
574 return Ok((lookup.item, lookup.expiration.into()));
575 }
576 }
577
578 let (v4, v6) = match strategy {
579 IpLookupStrategy::Ipv4AndIpv6 => {
580 let (v4, v6) = tokio::join!(ipv4_lookup(key, resolver), ipv6_lookup(key, resolver));
581 (Some(v4), Some(v6))
582 }
583 IpLookupStrategy::Ipv4Only => (Some(ipv4_lookup(key, resolver).await), None),
584 IpLookupStrategy::Ipv6Only => (None, Some(ipv6_lookup(key, resolver).await)),
585 IpLookupStrategy::Ipv6ThenIpv4 => {
586 let v6 = ipv6_lookup(key, resolver).await;
587 match v6 {
588 Ok((answer, exp)) if answer.addrs.is_empty() => (
589 Some(ipv4_lookup(key, resolver).await),
590 Some(Ok((answer, exp))),
591 ),
592 Err(err) => (Some(ipv4_lookup(key, resolver).await), Some(Err(err))),
593 Ok(res) => (None, Some(Ok(res))),
594 }
595 }
596 IpLookupStrategy::Ipv4ThenIpv6 => {
597 let v4 = ipv4_lookup(key, resolver).await;
598 match v4 {
599 Ok((answer, exp)) if answer.addrs.is_empty() => (
600 Some(Ok((answer, exp))),
601 Some(ipv6_lookup(key, resolver).await),
602 ),
603 Err(err) => (Some(Err(err)), Some(ipv6_lookup(key, resolver).await)),
604 Ok(res) => (Some(Ok(res)), None),
605 }
606 }
607 };
608
609 let mut addrs = vec![];
610 let mut any_addr = false;
611 let mut all_secure = true;
612 let mut errors = vec![];
613 let mut expires: Option<Instant> = None;
614
615 for family in [v4, v6] {
616 match family {
617 Some(Ok((answer, exp))) => {
618 expires = Some(match expires {
619 Some(existing) => exp.min(existing),
620 None => exp,
621 });
622 if !answer.addrs.is_empty() {
623 any_addr = true;
624 all_secure &= answer.secure;
625 addrs.extend_from_slice(&answer.addrs);
626 }
627 }
628 Some(Err(err)) => errors.push(err),
629 None => {}
630 }
631 }
632
633 if addrs.is_empty() && !errors.is_empty() {
634 return Err(errors.remove(0));
635 }
636
637 let result = Arc::new(IpAddresses {
638 addrs,
639 secure: any_addr && all_secure,
642 });
643 let exp = expires.unwrap_or_else(Instant::now);
644
645 if resolver.is_none() {
646 IP_CACHE
647 .insert((key_fq, strategy), result.clone(), exp.into())
648 .await;
649 }
650 Ok((result, exp))
651}
652
653pub async fn ipv4_lookup(
654 key: &str,
655 resolver: Option<&dyn Resolver>,
656) -> anyhow::Result<(Arc<IpAddresses>, Instant)> {
657 let key_fq = fully_qualify(key)?;
658 if resolver.is_none() {
659 if let Some(lookup) = IPV4_CACHE.lookup(&key_fq) {
660 return Ok((lookup.item, lookup.expiration.into()));
661 }
662 }
663
664 let answer = match resolver {
665 Some(r) => r.resolve(key_fq.clone(), RecordType::A).await?,
666 None => {
667 RESOLVER
668 .load()
669 .resolve(key_fq.clone(), RecordType::A)
670 .await?
671 }
672 };
673 let result = Arc::new(IpAddresses {
674 addrs: answer.as_addr(),
675 secure: answer.secure,
676 });
677 let expires = answer.expires;
678 if resolver.is_none() {
679 IPV4_CACHE
680 .insert(key_fq, result.clone(), expires.into())
681 .await;
682 }
683 Ok((result, expires))
684}
685
686pub async fn ipv6_lookup(
687 key: &str,
688 resolver: Option<&dyn Resolver>,
689) -> anyhow::Result<(Arc<IpAddresses>, Instant)> {
690 let key_fq = fully_qualify(key)?;
691 if resolver.is_none() {
692 if let Some(lookup) = IPV6_CACHE.lookup(&key_fq) {
693 return Ok((lookup.item, lookup.expiration.into()));
694 }
695 }
696
697 let answer = match resolver {
698 Some(r) => r.resolve(key_fq.clone(), RecordType::AAAA).await?,
699 None => {
700 RESOLVER
701 .load()
702 .resolve(key_fq.clone(), RecordType::AAAA)
703 .await?
704 }
705 };
706 let result = Arc::new(IpAddresses {
707 addrs: answer.as_addr(),
708 secure: answer.secure,
709 });
710 let expires = answer.expires;
711 if resolver.is_none() {
712 IPV6_CACHE
713 .insert(key_fq, result.clone(), expires.into())
714 .await;
715 }
716 Ok((result, expires))
717}
718
719#[cfg(test)]
720mod test {
721 use super::*;
722
723 fn answer(records: Vec<RData>, secure: bool, response_code: ResponseCode) -> Answer {
724 Answer {
725 canon_name: None,
726 records,
727 nxdomain: response_code == ResponseCode::NXDomain,
728 secure,
729 bogus: false,
730 why_bogus: None,
731 expires: Instant::now(),
732 response_code,
733 }
734 }
735
736 fn sha256_tlsa(usage: CertUsage, selector: Selector) -> TLSA {
737 TLSA::new(usage, selector, Matching::Sha256, vec![0u8; 32])
738 }
739
740 fn dane_ee_record() -> TLSA {
741 sha256_tlsa(CertUsage::DaneEe, Selector::Spki)
742 }
743
744 fn cname_record() -> RData {
745 use hickory_resolver::proto::rr::rdata::CNAME;
746 RData::CNAME(CNAME(Name::from_ascii("target.unsigned.example.").unwrap()))
747 }
748
749 #[test]
750 fn cname_answer_classification() {
751 k9::assert_equal!(
753 classify_cname_answer(
754 "mx.example.com",
755 Ok(answer(vec![cname_record()], true, ResponseCode::NoError))
756 ),
757 SecureCnameStatus::SecureAlias
758 );
759 k9::assert_equal!(
761 classify_cname_answer(
762 "mx.example.com",
763 Ok(answer(vec![], true, ResponseCode::NoError))
764 ),
765 SecureCnameStatus::NotSecureAlias
766 );
767 k9::assert_equal!(
769 classify_cname_answer(
770 "mx.example.com",
771 Ok(answer(vec![cname_record()], false, ResponseCode::NoError))
772 ),
773 SecureCnameStatus::NotSecureAlias
774 );
775 k9::assert_equal!(
777 classify_cname_answer(
778 "mx.example.com",
779 Ok(answer(vec![], true, ResponseCode::NXDomain))
780 ),
781 SecureCnameStatus::NotSecureAlias
782 );
783 assert!(matches!(
785 classify_cname_answer(
786 "mx.example.com",
787 Ok(answer(vec![], false, ResponseCode::ServFail))
788 ),
789 SecureCnameStatus::TempFail(_)
790 ));
791 assert!(matches!(
793 classify_cname_answer(
794 "mx.example.com",
795 Err(DnsError::ResolveFailed("boom".to_string()))
796 ),
797 SecureCnameStatus::TempFail(_)
798 ));
799 }
800
801 #[test]
802 fn tlsa_usability() {
803 assert!(tlsa_is_usable(&sha256_tlsa(
805 CertUsage::DaneTa,
806 Selector::Full
807 )));
808 assert!(tlsa_is_usable(&sha256_tlsa(
809 CertUsage::DaneEe,
810 Selector::Spki
811 )));
812 assert!(!tlsa_is_usable(&sha256_tlsa(
814 CertUsage::PkixTa,
815 Selector::Full
816 )));
817 assert!(!tlsa_is_usable(&sha256_tlsa(
818 CertUsage::PkixEe,
819 Selector::Spki
820 )));
821 assert!(!tlsa_is_usable(&sha256_tlsa(
823 CertUsage::Private,
824 Selector::Spki
825 )));
826 assert!(!tlsa_is_usable(&sha256_tlsa(
827 CertUsage::Unassigned(200),
828 Selector::Spki
829 )));
830 assert!(!tlsa_is_usable(&sha256_tlsa(
832 CertUsage::DaneEe,
833 Selector::Unassigned(7)
834 )));
835 assert!(!tlsa_is_usable(&TLSA::new(
836 CertUsage::DaneEe,
837 Selector::Spki,
838 Matching::Unassigned(9),
839 vec![0u8; 32]
840 )));
841 assert!(!tlsa_is_usable(&TLSA::new(
842 CertUsage::DaneEe,
843 Selector::Spki,
844 Matching::Sha256,
845 vec![0u8; 31]
846 )));
847 assert!(tlsa_is_usable(&TLSA::new(
849 CertUsage::DaneEe,
850 Selector::Full,
851 Matching::Raw,
852 vec![0u8; 100]
853 )));
854 }
855
856 #[test]
857 fn tlsa_answer_classification() {
858 k9::assert_equal!(
860 classify_tlsa_answer(
861 "mx.example.com",
862 25,
863 Ok(answer(
864 vec![RData::TLSA(dane_ee_record())],
865 true,
866 ResponseCode::NoError
867 ))
868 ),
869 DaneStatus::Records(vec![dane_ee_record()])
870 );
871 k9::assert_equal!(
873 classify_tlsa_answer(
874 "mx.example.com",
875 25,
876 Ok(answer(vec![], true, ResponseCode::NoError))
877 ),
878 DaneStatus::NotApplicable
879 );
880 k9::assert_equal!(
882 classify_tlsa_answer(
883 "mx.example.com",
884 25,
885 Ok(answer(vec![], true, ResponseCode::NXDomain))
886 ),
887 DaneStatus::NotApplicable
888 );
889 k9::assert_equal!(
891 classify_tlsa_answer(
892 "mx.example.com",
893 25,
894 Ok(answer(
895 vec![RData::TLSA(dane_ee_record())],
896 false,
897 ResponseCode::NoError
898 ))
899 ),
900 DaneStatus::NotApplicable
901 );
902 k9::assert_equal!(
905 classify_tlsa_answer(
906 "mx.example.com",
907 25,
908 Ok(answer(
909 vec![RData::TLSA(sha256_tlsa(CertUsage::PkixEe, Selector::Spki))],
910 true,
911 ResponseCode::NoError
912 ))
913 ),
914 DaneStatus::Unusable
915 );
916 assert!(matches!(
918 classify_tlsa_answer(
919 "mx.example.com",
920 25,
921 Ok(answer(vec![], false, ResponseCode::ServFail))
922 ),
923 DaneStatus::TempFail(_)
924 ));
925 assert!(matches!(
927 classify_tlsa_answer(
928 "mx.example.com",
929 25,
930 Err(DnsError::ResolveFailed("boom".to_string()))
931 ),
932 DaneStatus::TempFail(_)
933 ));
934 }
935
936 #[cfg(feature = "live-dns-tests")]
941 #[tokio::test]
942 async fn hickory_dnssec_validation() {
943 use hickory_resolver::config::{
944 ConnectionConfig, NameServerConfig, ProtocolConfig, ResolverConfig,
945 };
946 use hickory_resolver::net::runtime::TokioRuntimeProvider;
947 use hickory_resolver::TokioResolver;
948
949 let mut udp = ConnectionConfig::new(ProtocolConfig::Udp);
950 udp.port = 53;
951 let mut tcp = ConnectionConfig::new(ProtocolConfig::Tcp);
952 tcp.port = 53;
953 let config = ResolverConfig::from_parts(
954 None,
955 vec![],
956 vec![NameServerConfig::new(
957 "1.1.1.1".parse().unwrap(),
958 true,
959 vec![udp, tcp],
960 )],
961 );
962 let mut builder =
963 TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
964 builder.options_mut().validate = true;
965 let resolver = HickoryResolver::from(builder.build().unwrap());
966
967 let tlsa = resolver
968 .resolve(
969 fully_qualify("_25._tcp.do.havedane.net").unwrap(),
970 RecordType::TLSA,
971 )
972 .await
973 .unwrap();
974 assert!(tlsa.secure, "signed TLSA should validate as secure");
975 assert!(!tlsa.bogus);
976 assert!(!tlsa.records.is_empty());
977
978 let unsigned = resolver
979 .resolve(fully_qualify("google.com").unwrap(), RecordType::A)
980 .await
981 .unwrap();
982 assert!(!unsigned.secure, "unsigned zone is not secure");
983 assert!(!unsigned.bogus);
984 assert!(
985 !unsigned.records.is_empty(),
986 "unsigned zone still resolves successfully"
987 );
988 }
989
990 #[cfg(feature = "live-dns-tests")]
996 #[tokio::test]
997 async fn hickory_negative_answer_secure_bit() {
998 use hickory_resolver::config::{
999 ConnectionConfig, NameServerConfig, ProtocolConfig, ResolverConfig,
1000 };
1001 use hickory_resolver::net::runtime::TokioRuntimeProvider;
1002 use hickory_resolver::TokioResolver;
1003
1004 let mut udp = ConnectionConfig::new(ProtocolConfig::Udp);
1005 udp.port = 53;
1006 let mut tcp = ConnectionConfig::new(ProtocolConfig::Tcp);
1007 tcp.port = 53;
1008 let config = ResolverConfig::from_parts(
1009 None,
1010 vec![],
1011 vec![NameServerConfig::new(
1012 "1.1.1.1".parse().unwrap(),
1013 true,
1014 vec![udp, tcp],
1015 )],
1016 );
1017 let mut builder =
1018 TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
1019 builder.options_mut().validate = true;
1020 let resolver = HickoryResolver::from(builder.build().unwrap());
1021
1022 let secure_nodata = resolver
1024 .resolve(fully_qualify("cloudflare.com").unwrap(), RecordType::TLSA)
1025 .await
1026 .unwrap();
1027 assert!(secure_nodata.records.is_empty());
1028 assert!(!secure_nodata.bogus);
1029 assert!(
1030 secure_nodata.secure,
1031 "securely denied NODATA in a signed zone should be secure"
1032 );
1033
1034 let insecure_nodata = resolver
1036 .resolve(fully_qualify("mail.anoebis.be").unwrap(), RecordType::AAAA)
1037 .await
1038 .unwrap();
1039 assert!(insecure_nodata.records.is_empty());
1040 assert!(!insecure_nodata.bogus);
1041 assert!(
1042 !insecure_nodata.secure,
1043 "NODATA in an unsigned zone must not be secure"
1044 );
1045 }
1046
1047 #[cfg(all(feature = "live-dns-tests", feature = "default-unbound"))]
1050 #[tokio::test]
1051 async fn tlsa_have_dane() {
1052 let DaneStatus::Records(tlsa) = resolve_dane("do.havedane.net", 25).await.unwrap() else {
1053 panic!("expected usable DANE records");
1054 };
1055 k9::snapshot!(
1056 tlsa,
1057 "
1058[
1059 TLSA {
1060 cert_usage: TrustAnchor,
1061 selector: Spki,
1062 matching: Sha256,
1063 cert_data: [
1064 39,
1065 182,
1066 148,
1067 181,
1068 29,
1069 31,
1070 239,
1071 136,
1072 133,
1073 55,
1074 42,
1075 207,
1076 179,
1077 145,
1078 147,
1079 117,
1080 151,
1081 34,
1082 183,
1083 54,
1084 176,
1085 66,
1086 104,
1087 100,
1088 220,
1089 28,
1090 121,
1091 208,
1092 101,
1093 31,
1094 239,
1095 115,
1096 ],
1097 },
1098 TLSA {
1099 cert_usage: DomainIssued,
1100 selector: Spki,
1101 matching: Sha256,
1102 cert_data: [
1103 85,
1104 58,
1105 207,
1106 136,
1107 249,
1108 238,
1109 24,
1110 204,
1111 170,
1112 230,
1113 53,
1114 202,
1115 84,
1116 15,
1117 50,
1118 203,
1119 132,
1120 172,
1121 167,
1122 124,
1123 71,
1124 145,
1125 102,
1126 130,
1127 188,
1128 181,
1129 66,
1130 213,
1131 29,
1132 170,
1133 135,
1134 31,
1135 ],
1136 },
1137]
1138"
1139 );
1140 }
1141
1142 #[tokio::test]
1143 async fn txt_lookup_gmail() {
1144 let resolver =
1145 TestResolver::default().with_txt("_mta-sts.gmail.com", "v=STSv1; id=20190429T010101;");
1146 let name = Name::from_str_relaxed("_mta-sts.gmail.com").unwrap();
1147 let answer = resolver.resolve(name, RecordType::TXT).await.unwrap();
1148 k9::snapshot!(
1149 answer.as_txt(),
1150 r#"
1151[
1152 "v=STSv1; id=20190429T010101;",
1153]
1154"#
1155 );
1156 }
1157}