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::{Duration, Instant};
16
17mod resolver;
18#[cfg(feature = "unbound")]
19pub use resolver::UnboundResolver;
20use resolver::DEFAULT_NEGATIVE_TTL_SECS;
21pub use resolver::{
22 ptr_host, reverse_ip, AggregateResolver, Answer, DnsError, HickoryResolver, IpDisplay,
23 Resolver, TestResolver,
24};
25
26static RESOLVER: LazyLock<ArcSwap<Box<dyn Resolver>>> =
31 LazyLock::new(|| ArcSwap::from_pointee(Box::new(default_resolver())));
32
33declare_cache! {
34static IPV4_CACHE: LruCacheWithTtl<Name, Arc<IpAddresses>>::new("dns_resolver_ipv4", 1024);
36}
37declare_cache! {
38static IPV6_CACHE: LruCacheWithTtl<Name, Arc<IpAddresses>>::new("dns_resolver_ipv6", 1024);
40}
41declare_cache! {
42static IP_CACHE: LruCacheWithTtl<(Name, IpLookupStrategy), Arc<IpAddresses>>::new("dns_resolver_ip", 1024);
45}
46
47fn default_resolver() -> impl Resolver {
48 #[cfg(feature = "default-unbound")]
49 return UnboundResolver::new().unwrap();
50 #[cfg(not(feature = "default-unbound"))]
51 return HickoryResolver::new().expect("Parsing /etc/resolv.conf failed");
52}
53
54pub fn fully_qualify(domain_name: &str) -> Result<Name, ProtoError> {
55 let mut name = Name::from_str_relaxed(domain_name)?.to_lowercase();
56
57 name.set_fqdn(true);
59
60 Ok(name)
61}
62
63pub fn reconfigure_resolver(resolver: impl Resolver) {
64 RESOLVER.store(Arc::new(Box::new(resolver)));
65}
66
67pub fn get_resolver() -> Arc<Box<dyn Resolver>> {
68 RESOLVER.load_full()
69}
70
71#[derive(Debug, Clone, PartialEq)]
74pub enum DaneStatus {
75 NotApplicable,
80 Records(Vec<TLSA>),
83 Unusable,
88 TempFail(String),
93}
94
95fn tlsa_is_usable(tlsa: &TLSA) -> bool {
102 match tlsa.cert_usage {
103 CertUsage::DaneTa | CertUsage::DaneEe => {}
104 _ => return false,
105 }
106 match tlsa.selector {
107 Selector::Full | Selector::Spki => {}
108 _ => return false,
109 }
110 let expected_len = match tlsa.matching {
111 Matching::Raw => None,
112 Matching::Sha256 => Some(32),
113 Matching::Sha512 => Some(64),
114 _ => return false,
115 };
116 match expected_len {
117 Some(len) if tlsa.cert_data.len() != len => false,
118 _ => true,
119 }
120}
121
122pub async fn resolve_dane(mx_host: &str, port: u16) -> anyhow::Result<DaneStatus> {
136 let name = fully_qualify(&format!("_{port}._tcp.{mx_host}"))?;
137 let answer = RESOLVER.load().resolve(name, RecordType::TLSA).await;
138 Ok(classify_tlsa_answer(mx_host, port, answer))
139}
140
141fn classify_tlsa_answer(mx_host: &str, port: u16, answer: Result<Answer, DnsError>) -> DaneStatus {
145 let answer = match answer {
146 Ok(answer) => answer,
147 Err(err) => {
148 return DaneStatus::TempFail(format!("TLSA lookup for {mx_host}:{port} failed: {err}"));
152 }
153 };
154 tracing::debug!("resolve_dane {mx_host}:{port} TLSA answer is: {answer:?}");
155
156 if answer.bogus {
157 return DaneStatus::TempFail(format!(
160 "TLSA records for {mx_host}:{port} are bogus: {}",
161 answer
162 .why_bogus
163 .as_deref()
164 .unwrap_or("DNSSEC validation failed")
165 ));
166 }
167
168 match answer.response_code {
169 ResponseCode::NoError => {}
170 ResponseCode::NXDomain => return DaneStatus::NotApplicable,
173 rcode => {
175 return DaneStatus::TempFail(format!(
176 "TLSA lookup for {mx_host}:{port} returned {rcode}"
177 ));
178 }
179 }
180
181 if !answer.secure {
183 return DaneStatus::NotApplicable;
184 }
185
186 let mut published = vec![];
187 for r in &answer.records {
188 if let RData::TLSA(tlsa) = r {
189 published.push(tlsa.clone());
190 }
191 }
192
193 if published.is_empty() {
194 return DaneStatus::NotApplicable;
196 }
197
198 let mut usable: Vec<TLSA> = published.into_iter().filter(tlsa_is_usable).collect();
199
200 if usable.is_empty() {
201 return DaneStatus::Unusable;
203 }
204
205 usable.sort_by_key(|a| {
209 (
210 u8::from(a.cert_usage),
211 u8::from(a.selector),
212 u8::from(a.matching),
213 a.cert_data.clone(),
214 )
215 });
216
217 tracing::info!("resolve_dane {mx_host}:{port} usable TLSA records: {usable:?}");
218
219 DaneStatus::Records(usable)
220}
221
222#[derive(Debug, PartialEq, Eq)]
230pub enum SecureCnameStatus {
231 SecureAlias,
234 NotSecureAlias,
238 TempFail(String),
242}
243
244pub async fn resolve_secure_cname(mx_host: &str) -> anyhow::Result<SecureCnameStatus> {
255 let name = fully_qualify(mx_host)?;
256 let answer = RESOLVER.load().resolve(name, RecordType::CNAME).await;
257 Ok(classify_cname_answer(mx_host, answer))
258}
259
260fn classify_cname_answer(mx_host: &str, answer: Result<Answer, DnsError>) -> SecureCnameStatus {
264 let answer = match answer {
265 Ok(answer) => answer,
266 Err(err) => {
267 return SecureCnameStatus::TempFail(format!(
268 "CNAME lookup for {mx_host} failed: {err}"
269 ));
270 }
271 };
272
273 if answer.bogus {
274 return SecureCnameStatus::TempFail(format!(
275 "CNAME records for {mx_host} are bogus: {}",
276 answer
277 .why_bogus
278 .as_deref()
279 .unwrap_or("DNSSEC validation failed")
280 ));
281 }
282
283 match answer.response_code {
284 ResponseCode::NoError => {}
285 ResponseCode::NXDomain => return SecureCnameStatus::NotSecureAlias,
288 rcode => {
289 return SecureCnameStatus::TempFail(format!(
290 "CNAME lookup for {mx_host} returned {rcode}"
291 ));
292 }
293 }
294
295 if !answer.secure {
297 return SecureCnameStatus::NotSecureAlias;
298 }
299
300 if answer.records.iter().any(|r| matches!(r, RData::CNAME(_))) {
302 SecureCnameStatus::SecureAlias
303 } else {
304 SecureCnameStatus::NotSecureAlias
305 }
306}
307
308pub fn has_colon_port(a: &str) -> Option<(&str, u16)> {
312 let (label, maybe_port) = a.rsplit_once(':')?;
313
314 if label.contains(':') {
317 return None;
318 }
319
320 let port = maybe_port.parse::<u16>().ok()?;
321 Some((label, port))
322}
323
324pub enum DomainClassification {
330 Domain(Name, Option<u16>),
332 Literal(HostOrSocketAddress),
334}
335
336impl DomainClassification {
337 pub fn classify(domain_name: &str) -> anyhow::Result<Self> {
338 let (domain_name, mut opt_port) = match has_colon_port(domain_name) {
339 Some((domain_name, port)) => (domain_name, Some(port)),
340 None => (domain_name, None),
341 };
342
343 if domain_name.starts_with('[') {
344 if !domain_name.ends_with(']') {
345 anyhow::bail!(
346 "domain_name `{domain_name}` is a malformed literal \
347 domain with no trailing `]`"
348 );
349 }
350
351 let lowered = domain_name.to_ascii_lowercase();
352 let literal = &lowered[1..lowered.len() - 1];
353
354 let literal = match has_colon_port(literal) {
355 Some((_, _)) if opt_port.is_some() => {
356 anyhow::bail!("invalid address: `{domain_name}` specifies a port both inside and outside a literal address enclosed in square brackets");
357 }
358 Some((literal, port)) => {
359 opt_port.replace(port);
360 literal
361 }
362 None => literal,
363 };
364
365 if let Some(v6_literal) = literal.strip_prefix("ipv6:") {
366 match v6_literal.parse::<Ipv6Addr>() {
367 Ok(addr) => {
368 let mut host_addr: HostOrSocketAddress = addr.into();
369 if let Some(port) = opt_port {
370 host_addr.set_port(port);
371 }
372 return Ok(Self::Literal(host_addr));
373 }
374 Err(err) => {
375 anyhow::bail!("invalid ipv6 address: `{v6_literal}`: {err:#}");
376 }
377 }
378 }
379
380 match literal.parse::<IpAddr>() {
384 Ok(ip_addr) => {
385 let mut host_addr: HostOrSocketAddress = ip_addr.into();
386 if let Some(port) = opt_port {
387 host_addr.set_port(port);
388 }
389 return Ok(Self::Literal(host_addr));
390 }
391 Err(err) => {
392 anyhow::bail!("invalid address: `{literal}`: {err:#}");
393 }
394 }
395 }
396
397 let name_fq = fully_qualify(domain_name)?;
398 Ok(Self::Domain(name_fq, opt_port))
399 }
400
401 pub fn has_port(&self) -> bool {
402 match self {
403 Self::Domain(_, Some(_)) => true,
404 Self::Domain(_, None) => false,
405 Self::Literal(addr) => addr.port().is_some(),
406 }
407 }
408}
409
410pub async fn resolve_a_or_aaaa(
411 domain_name: &str,
412 resolver: Option<&dyn Resolver>,
413 strategy: IpLookupStrategy,
414) -> anyhow::Result<Vec<ResolvedAddress>> {
415 if domain_name.starts_with('[') {
416 if !domain_name.ends_with(']') {
419 anyhow::bail!(
420 "domain_name `{domain_name}` is a malformed literal \
421 domain with no trailing `]`"
422 );
423 }
424
425 let lowered = domain_name.to_ascii_lowercase();
426 let literal = &lowered[1..lowered.len() - 1];
427
428 if let Some(v6_literal) = literal.strip_prefix("ipv6:") {
429 match v6_literal.parse::<Ipv6Addr>() {
430 Ok(addr) => {
431 return Ok(vec![ResolvedAddress {
432 name: domain_name.to_string(),
433 addr: std::net::IpAddr::V6(addr).into(),
434 is_secure: false,
435 }]);
436 }
437 Err(err) => {
438 anyhow::bail!("invalid ipv6 address: `{v6_literal}`: {err:#}");
439 }
440 }
441 }
442
443 match literal.parse::<HostAddress>() {
447 Ok(addr) => {
448 return Ok(vec![ResolvedAddress {
449 name: domain_name.to_string(),
450 addr: addr.into(),
451 is_secure: false,
452 }]);
453 }
454 Err(err) => {
455 anyhow::bail!("invalid address: `{literal}`: {err:#}");
456 }
457 }
458 } else {
459 if let Ok(addr) = domain_name.parse::<HostAddress>() {
461 return Ok(vec![ResolvedAddress {
462 name: domain_name.to_string(),
463 addr: addr.into(),
464 is_secure: false,
465 }]);
466 }
467 }
468
469 match ip_lookup(domain_name, resolver, strategy).await {
470 Ok((result, _expires)) => {
471 let addrs = result
472 .addrs
473 .iter()
474 .map(|&addr| ResolvedAddress {
475 name: domain_name.to_string(),
476 addr: addr.into(),
477 is_secure: result.secure,
478 })
479 .collect();
480 Ok(addrs)
481 }
482 Err(err) => anyhow::bail!("{err:#}"),
483 }
484}
485
486pub async fn resolve_socket_addr(
493 addr: &ResolvableSocketAddr,
494 resolver: Option<&dyn Resolver>,
495 strategy: IpLookupStrategy,
496) -> anyhow::Result<Vec<ResolvedAddress>> {
497 match addr {
498 ResolvableSocketAddr::UnixDomain(unix) => {
499 let name = match unix.as_pathname() {
500 Some(path) => path.display().to_string(),
501 None => "<unbound unix domain>".to_string(),
502 };
503 Ok(vec![ResolvedAddress {
504 name,
505 addr: HostOrSocketAddress::UnixDomain(unix.clone()),
506 is_secure: false,
507 }])
508 }
509 ResolvableSocketAddr::V4(sa) => Ok(vec![ResolvedAddress {
510 name: sa.to_string(),
511 addr: HostOrSocketAddress::V4Socket(sa.clone()),
512 is_secure: false,
513 }]),
514 ResolvableSocketAddr::V6(sa) => Ok(vec![ResolvedAddress {
515 name: sa.to_string(),
516 addr: HostOrSocketAddress::V6Socket(sa.clone()),
517 is_secure: false,
518 }]),
519 ResolvableSocketAddr::Hostname { host, port } => {
520 match ip_lookup(host, resolver, strategy).await {
521 Ok((result, _expires)) => Ok(result
522 .addrs
523 .iter()
524 .map(|&ip| {
525 let sa = SocketAddr::new(ip, *port);
526 ResolvedAddress {
527 name: host.clone(),
528 addr: sa.into(),
529 is_secure: result.secure,
530 }
531 })
532 .collect()),
533 Err(err) => anyhow::bail!("{err:#}"),
534 }
535 }
536 }
537}
538
539#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
540#[repr(u8)]
541pub enum IpLookupStrategy {
542 Ipv4Only,
544 Ipv6Only,
546 #[default]
548 Ipv4AndIpv6,
549 Ipv6ThenIpv4,
551 Ipv4ThenIpv6,
553}
554
555#[derive(Clone, Debug)]
560pub struct IpAddresses {
561 pub addrs: Vec<IpAddr>,
562 pub secure: bool,
564}
565
566pub async fn ip_lookup(
567 key: &str,
568 resolver: Option<&dyn Resolver>,
569 strategy: IpLookupStrategy,
570) -> anyhow::Result<(Arc<IpAddresses>, Instant)> {
571 let key_fq = fully_qualify(key)?;
572
573 if resolver.is_none() {
574 if let Some(lookup) = IP_CACHE.lookup(&(key_fq.clone(), strategy)) {
575 return Ok((lookup.item, lookup.expiration.into()));
576 }
577 }
578
579 let (v4, v6) = match strategy {
580 IpLookupStrategy::Ipv4AndIpv6 => {
581 let (v4, v6) = tokio::join!(ipv4_lookup(key, resolver), ipv6_lookup(key, resolver));
582 (Some(v4), Some(v6))
583 }
584 IpLookupStrategy::Ipv4Only => (Some(ipv4_lookup(key, resolver).await), None),
585 IpLookupStrategy::Ipv6Only => (None, Some(ipv6_lookup(key, resolver).await)),
586 IpLookupStrategy::Ipv6ThenIpv4 => {
587 let v6 = ipv6_lookup(key, resolver).await;
588 match v6 {
589 Ok((answer, exp)) if answer.addrs.is_empty() => (
590 Some(ipv4_lookup(key, resolver).await),
591 Some(Ok((answer, exp))),
592 ),
593 Err(err) => (Some(ipv4_lookup(key, resolver).await), Some(Err(err))),
594 Ok(res) => (None, Some(Ok(res))),
595 }
596 }
597 IpLookupStrategy::Ipv4ThenIpv6 => {
598 let v4 = ipv4_lookup(key, resolver).await;
599 match v4 {
600 Ok((answer, exp)) if answer.addrs.is_empty() => (
601 Some(Ok((answer, exp))),
602 Some(ipv6_lookup(key, resolver).await),
603 ),
604 Err(err) => (Some(Err(err)), Some(ipv6_lookup(key, resolver).await)),
605 Ok(res) => (Some(Ok(res)), None),
606 }
607 }
608 };
609
610 let (result, exp) = combine_ip_families([v4, v6], Instant::now())?;
611
612 if resolver.is_none() {
613 IP_CACHE
614 .insert((key_fq, strategy), result.clone(), exp.into())
615 .await;
616 }
617 Ok((result, exp))
618}
619
620fn combine_ip_families(
628 families: [Option<anyhow::Result<(Arc<IpAddresses>, Instant)>>; 2],
629 now: Instant,
630) -> anyhow::Result<(Arc<IpAddresses>, Instant)> {
631 let mut addrs = vec![];
632 let mut any_addr = false;
633 let mut all_secure = true;
634 let mut errors = vec![];
635 let mut expires: Option<Instant> = None;
636
637 for family in families {
638 match family {
639 Some(Ok((answer, exp))) => {
640 expires = Some(match expires {
641 Some(existing) => exp.min(existing),
642 None => exp,
643 });
644 if !answer.addrs.is_empty() {
645 any_addr = true;
646 all_secure &= answer.secure;
647 addrs.extend_from_slice(&answer.addrs);
648 }
649 }
650 Some(Err(err)) => errors.push(err),
651 None => {}
652 }
653 }
654
655 if addrs.is_empty() && !errors.is_empty() {
656 return Err(errors.remove(0));
657 }
658
659 let result = Arc::new(IpAddresses {
660 addrs,
661 secure: any_addr && all_secure,
664 });
665 let mut exp = expires.unwrap_or(now);
666 if !errors.is_empty() {
667 exp = exp.min(now + Duration::from_secs(DEFAULT_NEGATIVE_TTL_SECS as u64));
668 }
669 Ok((result, exp))
670}
671
672fn reject_bogus_answer(name: &str, answer: &Answer) -> anyhow::Result<()> {
681 anyhow::ensure!(
682 !answer.bogus,
683 "address lookup for {name} is bogus: {}",
684 answer
685 .why_bogus
686 .as_deref()
687 .unwrap_or("DNSSEC validation failed")
688 );
689 Ok(())
690}
691
692pub async fn ipv4_lookup(
693 key: &str,
694 resolver: Option<&dyn Resolver>,
695) -> anyhow::Result<(Arc<IpAddresses>, Instant)> {
696 let key_fq = fully_qualify(key)?;
697 if resolver.is_none() {
698 if let Some(lookup) = IPV4_CACHE.lookup(&key_fq) {
699 return Ok((lookup.item, lookup.expiration.into()));
700 }
701 }
702
703 let answer = match resolver {
704 Some(r) => r.resolve(key_fq.clone(), RecordType::A).await?,
705 None => {
706 RESOLVER
707 .load()
708 .resolve(key_fq.clone(), RecordType::A)
709 .await?
710 }
711 };
712 reject_bogus_answer(key, &answer)?;
713 let result = Arc::new(IpAddresses {
714 addrs: answer.as_addr(),
715 secure: answer.secure,
716 });
717 let expires = answer.expires;
718 if resolver.is_none() {
719 IPV4_CACHE
720 .insert(key_fq, result.clone(), expires.into())
721 .await;
722 }
723 Ok((result, expires))
724}
725
726pub async fn ipv6_lookup(
727 key: &str,
728 resolver: Option<&dyn Resolver>,
729) -> anyhow::Result<(Arc<IpAddresses>, Instant)> {
730 let key_fq = fully_qualify(key)?;
731 if resolver.is_none() {
732 if let Some(lookup) = IPV6_CACHE.lookup(&key_fq) {
733 return Ok((lookup.item, lookup.expiration.into()));
734 }
735 }
736
737 let answer = match resolver {
738 Some(r) => r.resolve(key_fq.clone(), RecordType::AAAA).await?,
739 None => {
740 RESOLVER
741 .load()
742 .resolve(key_fq.clone(), RecordType::AAAA)
743 .await?
744 }
745 };
746 reject_bogus_answer(key, &answer)?;
747 let result = Arc::new(IpAddresses {
748 addrs: answer.as_addr(),
749 secure: answer.secure,
750 });
751 let expires = answer.expires;
752 if resolver.is_none() {
753 IPV6_CACHE
754 .insert(key_fq, result.clone(), expires.into())
755 .await;
756 }
757 Ok((result, expires))
758}
759
760#[cfg(test)]
761mod test {
762 use super::*;
763
764 fn answer(records: Vec<RData>, secure: bool, response_code: ResponseCode) -> Answer {
765 Answer {
766 canon_name: None,
767 records,
768 nxdomain: response_code == ResponseCode::NXDomain,
769 secure,
770 bogus: false,
771 why_bogus: None,
772 expires: Instant::now(),
773 response_code,
774 }
775 }
776
777 fn sha256_tlsa(usage: CertUsage, selector: Selector) -> TLSA {
778 TLSA::new(usage, selector, Matching::Sha256, vec![0u8; 32])
779 }
780
781 fn bogus_answer(records: Vec<RData>) -> Answer {
782 let mut answer = answer(records, false, ResponseCode::NoError);
783 answer.bogus = true;
784 answer.why_bogus = Some("invalid signature".into());
785 answer
786 }
787
788 #[test]
789 fn reject_bogus_rejects_only_bogus() {
790 let a = RData::A("192.0.2.1".parse().unwrap());
791 for records in [vec![], vec![a.clone()]] {
795 let err = reject_bogus_answer("mx.example.com", &bogus_answer(records)).unwrap_err();
796 k9::assert_equal!(
797 err.to_string(),
798 "address lookup for mx.example.com is bogus: invalid signature"
799 );
800 }
801 for secure in [false, true] {
804 for code in [ResponseCode::NoError, ResponseCode::NXDomain] {
805 reject_bogus_answer("mx.example.com", &answer(vec![a.clone()], secure, code))
806 .unwrap();
807 }
808 }
809 }
810
811 #[tokio::test]
812 async fn bogus_family_does_not_downgrade_sibling() {
813 struct SplitResolver {
817 all_bogus: bool,
818 }
819 #[async_trait::async_trait]
820 impl Resolver for SplitResolver {
821 async fn resolve_ip(&self, _: &str) -> Result<Vec<IpAddr>, DnsError> {
822 unimplemented!("ip_lookup only calls resolve")
823 }
824 async fn resolve_mx(&self, _: &str) -> Result<Vec<Name>, DnsError> {
825 unimplemented!("ip_lookup only calls resolve")
826 }
827 async fn resolve_ptr(&self, _: IpAddr) -> Result<Vec<Name>, DnsError> {
828 unimplemented!("ip_lookup only calls resolve")
829 }
830 async fn resolve(&self, _: Name, rrtype: RecordType) -> Result<Answer, DnsError> {
831 Ok(match rrtype {
832 RecordType::A if !self.all_bogus => answer(
833 vec![RData::A("192.0.2.1".parse().unwrap())],
834 true,
835 ResponseCode::NoError,
836 ),
837 RecordType::A => bogus_answer(vec![RData::A("192.0.2.9".parse().unwrap())]),
838 _ => bogus_answer(vec![RData::AAAA("2001:db8::1".parse().unwrap())]),
839 })
840 }
841 }
842
843 let (result, _) = ip_lookup(
848 "mx.example.com",
849 Some(&SplitResolver { all_bogus: false }),
850 IpLookupStrategy::Ipv4AndIpv6,
851 )
852 .await
853 .unwrap();
854 k9::assert_equal!(result.addrs, vec!["192.0.2.1".parse::<IpAddr>().unwrap()]);
855 k9::assert_equal!(result.secure, true);
856
857 let err = ip_lookup(
861 "mx.example.com",
862 Some(&SplitResolver { all_bogus: true }),
863 IpLookupStrategy::Ipv4AndIpv6,
864 )
865 .await
866 .unwrap_err();
867 k9::assert_equal!(
868 err.to_string(),
869 "address lookup for mx.example.com is bogus: invalid signature"
870 );
871 }
872
873 fn v6_family(
874 ttl: Duration,
875 now: Instant,
876 ) -> Option<anyhow::Result<(Arc<IpAddresses>, Instant)>> {
877 Some(Ok((
878 Arc::new(IpAddresses {
879 addrs: vec!["2001:db8::1".parse().unwrap()],
880 secure: false,
881 }),
882 now + ttl,
883 )))
884 }
885
886 #[test]
887 fn failed_family_caps_sibling_ttl() {
888 let now = Instant::now();
889 let v6_ttl = Duration::from_secs(86400);
890
891 let v4 = Some(Err(anyhow::anyhow!("timed out")));
897 let (result, exp) = combine_ip_families([v4, v6_family(v6_ttl, now)], now).unwrap();
898 k9::assert_equal!(result.addrs, vec!["2001:db8::1".parse::<IpAddr>().unwrap()]);
899 k9::assert_equal!(
900 exp,
901 now + Duration::from_secs(DEFAULT_NEGATIVE_TTL_SECS as u64)
902 );
903
904 let v4_ttl = Duration::from_secs(300);
907 let v4 = Some(Ok((
908 Arc::new(IpAddresses {
909 addrs: vec!["192.0.2.1".parse().unwrap()],
910 secure: false,
911 }),
912 now + v4_ttl,
913 )));
914 let (_, exp) = combine_ip_families([v4, v6_family(v6_ttl, now)], now).unwrap();
915 k9::assert_equal!(exp, now + v4_ttl);
916
917 let err = combine_ip_families(
920 [
921 Some(Err(anyhow::anyhow!("a failed"))),
922 Some(Err(anyhow::anyhow!("aaaa failed"))),
923 ],
924 now,
925 )
926 .unwrap_err();
927 k9::assert_equal!(err.to_string(), "a failed");
928 }
929
930 fn dane_ee_record() -> TLSA {
931 sha256_tlsa(CertUsage::DaneEe, Selector::Spki)
932 }
933
934 fn cname_record() -> RData {
935 use hickory_resolver::proto::rr::rdata::CNAME;
936 RData::CNAME(CNAME(Name::from_ascii("target.unsigned.example.").unwrap()))
937 }
938
939 #[test]
940 fn cname_answer_classification() {
941 k9::assert_equal!(
943 classify_cname_answer(
944 "mx.example.com",
945 Ok(answer(vec![cname_record()], true, ResponseCode::NoError))
946 ),
947 SecureCnameStatus::SecureAlias
948 );
949 k9::assert_equal!(
951 classify_cname_answer(
952 "mx.example.com",
953 Ok(answer(vec![], true, ResponseCode::NoError))
954 ),
955 SecureCnameStatus::NotSecureAlias
956 );
957 k9::assert_equal!(
959 classify_cname_answer(
960 "mx.example.com",
961 Ok(answer(vec![cname_record()], false, ResponseCode::NoError))
962 ),
963 SecureCnameStatus::NotSecureAlias
964 );
965 k9::assert_equal!(
967 classify_cname_answer(
968 "mx.example.com",
969 Ok(answer(vec![], true, ResponseCode::NXDomain))
970 ),
971 SecureCnameStatus::NotSecureAlias
972 );
973 assert!(matches!(
975 classify_cname_answer(
976 "mx.example.com",
977 Ok(answer(vec![], false, ResponseCode::ServFail))
978 ),
979 SecureCnameStatus::TempFail(_)
980 ));
981 assert!(matches!(
983 classify_cname_answer(
984 "mx.example.com",
985 Err(DnsError::ResolveFailed("boom".to_string()))
986 ),
987 SecureCnameStatus::TempFail(_)
988 ));
989 }
990
991 #[test]
992 fn tlsa_usability() {
993 assert!(tlsa_is_usable(&sha256_tlsa(
995 CertUsage::DaneTa,
996 Selector::Full
997 )));
998 assert!(tlsa_is_usable(&sha256_tlsa(
999 CertUsage::DaneEe,
1000 Selector::Spki
1001 )));
1002 assert!(!tlsa_is_usable(&sha256_tlsa(
1004 CertUsage::PkixTa,
1005 Selector::Full
1006 )));
1007 assert!(!tlsa_is_usable(&sha256_tlsa(
1008 CertUsage::PkixEe,
1009 Selector::Spki
1010 )));
1011 assert!(!tlsa_is_usable(&sha256_tlsa(
1013 CertUsage::Private,
1014 Selector::Spki
1015 )));
1016 assert!(!tlsa_is_usable(&sha256_tlsa(
1017 CertUsage::Unassigned(200),
1018 Selector::Spki
1019 )));
1020 assert!(!tlsa_is_usable(&sha256_tlsa(
1022 CertUsage::DaneEe,
1023 Selector::Unassigned(7)
1024 )));
1025 assert!(!tlsa_is_usable(&TLSA::new(
1026 CertUsage::DaneEe,
1027 Selector::Spki,
1028 Matching::Unassigned(9),
1029 vec![0u8; 32]
1030 )));
1031 assert!(!tlsa_is_usable(&TLSA::new(
1032 CertUsage::DaneEe,
1033 Selector::Spki,
1034 Matching::Sha256,
1035 vec![0u8; 31]
1036 )));
1037 assert!(tlsa_is_usable(&TLSA::new(
1039 CertUsage::DaneEe,
1040 Selector::Full,
1041 Matching::Raw,
1042 vec![0u8; 100]
1043 )));
1044 }
1045
1046 #[test]
1047 fn tlsa_answer_classification() {
1048 k9::assert_equal!(
1050 classify_tlsa_answer(
1051 "mx.example.com",
1052 25,
1053 Ok(answer(
1054 vec![RData::TLSA(dane_ee_record())],
1055 true,
1056 ResponseCode::NoError
1057 ))
1058 ),
1059 DaneStatus::Records(vec![dane_ee_record()])
1060 );
1061 k9::assert_equal!(
1063 classify_tlsa_answer(
1064 "mx.example.com",
1065 25,
1066 Ok(answer(vec![], true, ResponseCode::NoError))
1067 ),
1068 DaneStatus::NotApplicable
1069 );
1070 k9::assert_equal!(
1072 classify_tlsa_answer(
1073 "mx.example.com",
1074 25,
1075 Ok(answer(vec![], true, ResponseCode::NXDomain))
1076 ),
1077 DaneStatus::NotApplicable
1078 );
1079 k9::assert_equal!(
1081 classify_tlsa_answer(
1082 "mx.example.com",
1083 25,
1084 Ok(answer(
1085 vec![RData::TLSA(dane_ee_record())],
1086 false,
1087 ResponseCode::NoError
1088 ))
1089 ),
1090 DaneStatus::NotApplicable
1091 );
1092 k9::assert_equal!(
1095 classify_tlsa_answer(
1096 "mx.example.com",
1097 25,
1098 Ok(answer(
1099 vec![RData::TLSA(sha256_tlsa(CertUsage::PkixEe, Selector::Spki))],
1100 true,
1101 ResponseCode::NoError
1102 ))
1103 ),
1104 DaneStatus::Unusable
1105 );
1106 assert!(matches!(
1108 classify_tlsa_answer(
1109 "mx.example.com",
1110 25,
1111 Ok(answer(vec![], false, ResponseCode::ServFail))
1112 ),
1113 DaneStatus::TempFail(_)
1114 ));
1115 assert!(matches!(
1117 classify_tlsa_answer(
1118 "mx.example.com",
1119 25,
1120 Err(DnsError::ResolveFailed("boom".to_string()))
1121 ),
1122 DaneStatus::TempFail(_)
1123 ));
1124 }
1125
1126 #[cfg(feature = "live-dns-tests")]
1130 fn validating_hickory(server: &str) -> HickoryResolver {
1131 use hickory_resolver::config::{
1132 ConnectionConfig, NameServerConfig, ProtocolConfig, ResolverConfig,
1133 };
1134 use hickory_resolver::net::runtime::TokioRuntimeProvider;
1135 use hickory_resolver::TokioResolver;
1136
1137 let mut udp = ConnectionConfig::new(ProtocolConfig::Udp);
1138 udp.port = 53;
1139 let mut tcp = ConnectionConfig::new(ProtocolConfig::Tcp);
1140 tcp.port = 53;
1141 let config = ResolverConfig::from_parts(
1142 None,
1143 vec![],
1144 vec![NameServerConfig::new(
1145 server.parse().unwrap(),
1146 true,
1147 vec![udp, tcp],
1148 )],
1149 );
1150 let mut builder =
1151 TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
1152 builder.options_mut().validate = true;
1153 HickoryResolver::from(builder.build().unwrap())
1154 }
1155
1156 #[cfg(feature = "live-dns-tests")]
1161 #[tokio::test]
1162 async fn hickory_dnssec_validation() {
1163 let resolver = validating_hickory("1.1.1.1");
1164
1165 let tlsa = resolver
1166 .resolve(
1167 fully_qualify("_25._tcp.do.havedane.net").unwrap(),
1168 RecordType::TLSA,
1169 )
1170 .await
1171 .unwrap();
1172 assert!(tlsa.secure, "signed TLSA should validate as secure");
1173 assert!(!tlsa.bogus);
1174 assert!(!tlsa.records.is_empty());
1175
1176 let unsigned = resolver
1177 .resolve(fully_qualify("google.com").unwrap(), RecordType::A)
1178 .await
1179 .unwrap();
1180 assert!(!unsigned.secure, "unsigned zone is not secure");
1181 assert!(!unsigned.bogus);
1182 assert!(
1183 !unsigned.records.is_empty(),
1184 "unsigned zone still resolves successfully"
1185 );
1186 }
1187
1188 #[cfg(feature = "live-dns-tests")]
1194 #[tokio::test]
1195 async fn hickory_negative_answer_secure_bit() {
1196 let resolver = validating_hickory("1.1.1.1");
1197
1198 let secure_nodata = resolver
1200 .resolve(fully_qualify("cloudflare.com").unwrap(), RecordType::TLSA)
1201 .await
1202 .unwrap();
1203 assert!(secure_nodata.records.is_empty());
1204 assert!(!secure_nodata.bogus);
1205 assert!(
1206 secure_nodata.secure,
1207 "securely denied NODATA in a signed zone should be secure"
1208 );
1209
1210 let insecure_nodata = resolver
1212 .resolve(fully_qualify("mail.anoebis.be").unwrap(), RecordType::AAAA)
1213 .await
1214 .unwrap();
1215 assert!(insecure_nodata.records.is_empty());
1216 assert!(!insecure_nodata.bogus);
1217 assert!(
1218 !insecure_nodata.secure,
1219 "NODATA in an unsigned zone must not be secure"
1220 );
1221 }
1222
1223 #[cfg(all(feature = "live-dns-tests", feature = "default-unbound"))]
1226 #[tokio::test]
1227 async fn tlsa_have_dane() {
1228 let DaneStatus::Records(tlsa) = resolve_dane("do.havedane.net", 25).await.unwrap() else {
1229 panic!("expected usable DANE records");
1230 };
1231 k9::snapshot!(
1232 tlsa,
1233 "
1234[
1235 TLSA {
1236 cert_usage: TrustAnchor,
1237 selector: Spki,
1238 matching: Sha256,
1239 cert_data: [
1240 39,
1241 182,
1242 148,
1243 181,
1244 29,
1245 31,
1246 239,
1247 136,
1248 133,
1249 55,
1250 42,
1251 207,
1252 179,
1253 145,
1254 147,
1255 117,
1256 151,
1257 34,
1258 183,
1259 54,
1260 176,
1261 66,
1262 104,
1263 100,
1264 220,
1265 28,
1266 121,
1267 208,
1268 101,
1269 31,
1270 239,
1271 115,
1272 ],
1273 },
1274 TLSA {
1275 cert_usage: DomainIssued,
1276 selector: Spki,
1277 matching: Sha256,
1278 cert_data: [
1279 85,
1280 58,
1281 207,
1282 136,
1283 249,
1284 238,
1285 24,
1286 204,
1287 170,
1288 230,
1289 53,
1290 202,
1291 84,
1292 15,
1293 50,
1294 203,
1295 132,
1296 172,
1297 167,
1298 124,
1299 71,
1300 145,
1301 102,
1302 130,
1303 188,
1304 181,
1305 66,
1306 213,
1307 29,
1308 170,
1309 135,
1310 31,
1311 ],
1312 },
1313]
1314"
1315 );
1316 }
1317
1318 #[tokio::test]
1319 async fn txt_lookup_gmail() {
1320 let resolver =
1321 TestResolver::default().with_txt("_mta-sts.gmail.com", "v=STSv1; id=20190429T010101;");
1322 let name = Name::from_str_relaxed("_mta-sts.gmail.com").unwrap();
1323 let answer = resolver.resolve(name, RecordType::TXT).await.unwrap();
1324 k9::snapshot!(
1325 answer.as_txt(),
1326 r#"
1327[
1328 "v=STSv1; id=20190429T010101;",
1329]
1330"#
1331 );
1332 }
1333}