dns_resolver/
lib.rs

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
25// An `ArcSwap` can only hold `Sized` types, so we cannot stuff a `dyn Resolver` directly into it.
26// Instead, the documentation recommends adding a level of indirection, so we wrap the `Resolver`
27// trait object in a `Box`. In the context of DNS requests, the additional pointer chasing should
28// not be a significant performance concern.
29static RESOLVER: LazyLock<ArcSwap<Box<dyn Resolver>>> =
30    LazyLock::new(|| ArcSwap::from_pointee(Box::new(default_resolver())));
31
32declare_cache! {
33/// Caches domain name to ipv4 records and their DNSSEC secure status
34static IPV4_CACHE: LruCacheWithTtl<Name, Arc<IpAddresses>>::new("dns_resolver_ipv4", 1024);
35}
36declare_cache! {
37/// Caches domain name to ipv6 records and their DNSSEC secure status
38static IPV6_CACHE: LruCacheWithTtl<Name, Arc<IpAddresses>>::new("dns_resolver_ipv6", 1024);
39}
40declare_cache! {
41/// Caches domain name to the combined set of ipv4 and ipv6 records and their
42/// DNSSEC secure status
43static 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    // Treat it as fully qualified
57    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/// The outcome of attempting to resolve DANE TLSA records for an MX host,
71/// per <https://datatracker.ietf.org/doc/html/rfc7672>.
72#[derive(Debug, Clone, PartialEq)]
73pub enum DaneStatus {
74    /// The host is not DANE-eligible: either the MX host's address (A/AAAA)
75    /// RRset was not securely (DNSSEC) resolved, or there was a secure proof
76    /// that no TLSA records exist (NODATA/NXDOMAIN). Delivery should continue
77    /// using the normal (opportunistic / configured) TLS policy.
78    NotApplicable,
79    /// Secure, usable DANE-TA(2)/DANE-EE(3) TLSA records were found. The
80    /// connection must use TLS and authenticate the peer against these records.
81    Records(Vec<TLSA>),
82    /// Secure TLSA records were published, but none are usable for DANE SMTP
83    /// (e.g. only PKIX-TA(0)/PKIX-EE(1), private/unassigned usages, or
84    /// unsupported selector/matching types). Per RFC 7672 section 4.1 the
85    /// client must still require STARTTLS but cannot authenticate the peer.
86    Unusable,
87    /// The secure status of the TLSA records (or of the MX host's address
88    /// records) could not be determined, e.g. due to SERVFAIL, a timeout, or a
89    /// DNSSEC validation failure (bogus). To preserve downgrade resistance the
90    /// delivery must be deferred rather than continuing without authentication.
91    TempFail(String),
92}
93
94/// Decides whether a single TLSA record is usable for DANE SMTP.
95///
96/// RFC 7672 section 3.1.3 restricts SMTP DANE to the DANE-TA(2) and DANE-EE(3)
97/// certificate usages; PKIX-TA(0), PKIX-EE(1), and private/unassigned usages
98/// MUST be treated as unusable. We also require a selector and matching type
99/// that we (and OpenSSL) understand, and a digest of the correct length.
100fn 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
121/// Resolves DANE TLSA records for the given MX host and port, applying the
122/// RFC 7672 rules required for downgrade-resistant DANE SMTP.
123///
124/// `mx_host` must be the MX hostname (RFC 7672 section 3.2.2: the MX hostname is
125/// the only reference identifier), not the envelope/routing domain.
126///
127/// The caller is responsible for only invoking this when the chain to the MX
128/// host was securely (DNSSEC) resolved: both the MX RRset (see
129/// [`MailExchanger::is_secure`]) and the MX host's address records (see
130/// [`ResolvedAddress::is_secure`](kumo_log_types::ResolvedAddress)) must be
131/// secure. RFC 7672 section 2.2 requires the address records to be secure
132/// before it is safe to rely on TLSA records; we satisfy that by gating on the
133/// secure status of the very address we are about to connect to.
134pub 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
140/// Maps the result of the TLSA lookup onto a [`DaneStatus`], applying the
141/// RFC 7672 rules. Split out from [`resolve_dane`] so that it can be unit
142/// tested without a live (DNSSEC validating) resolver.
143fn 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            // No answer at all (a timeout or other communication/resource
148            // failure); the TLSA status is unknown, so we must not downgrade.
149            // Failure RCODEs such as SERVFAIL are handled in the match below.
150            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        // Bogus records are either tampered with, or due to misconfiguration
157        // of the local resolver.
158        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        // A secure (or insecure) denial of existence means there are no TLSA
170        // records; the host is simply not a DANE host.
171        ResponseCode::NXDomain => return DaneStatus::NotApplicable,
172        // SERVFAIL, REFUSED, NOTIMP, etc. leave the status unknown.
173        rcode => {
174            return DaneStatus::TempFail(format!(
175                "TLSA lookup for {mx_host}:{port} returned {rcode}"
176            ));
177        }
178    }
179
180    // We can only trust TLSA records that were DNSSEC validated.
181    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        // Secure proof that no TLSA records exist (NODATA).
194        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        // TLSA records exist but none are usable for DANE SMTP.
201        return DaneStatus::Unusable;
202    }
203
204    // DNS results are unordered; sort for stable behavior and tests. The TLSA
205    // type is an upstream type and does not implement Ord, so sort on its
206    // component fields directly.
207    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/// The outcome of an explicit `CNAME` lookup used to decide DANE eligibility
222/// for an MX host whose address chain was not fully DNSSEC-secure.
223///
224/// RFC 7672 section 2.2.2 treats an MX host that is a securely published CNAME
225/// alias as DANE-eligible at the original (unexpanded) name, even when the
226/// alias target lands in an insecure (unsigned) zone: it is the securely
227/// published TLSA RRset, not the address records, that authenticates the peer.
228#[derive(Debug, PartialEq, Eq)]
229pub enum SecureCnameStatus {
230    /// The name is a CNAME alias whose alias record was DNSSEC validated, so the
231    /// host remains DANE-eligible at its original name.
232    SecureAlias,
233    /// The name is not a securely published CNAME alias (it is not an alias at
234    /// all, or the alias was not DNSSEC validated); it is not the secure-CNAME
235    /// case and DANE must not be engaged off the back of it.
236    NotSecureAlias,
237    /// The secure status of the alias could not be determined (SERVFAIL,
238    /// timeout, or a DNSSEC validation failure). To preserve downgrade
239    /// resistance the caller must defer rather than continue.
240    TempFail(String),
241}
242
243/// Performs an explicit `CNAME` lookup for `mx_host` to determine whether it is
244/// a securely published alias.
245///
246/// This is a narrow fallback in the DANE path: when the MX host's address
247/// (A/AAAA) records did not resolve securely we may still be looking at a secure
248/// CNAME whose target merely lives in an unsigned zone. Querying the `CNAME`
249/// type explicitly isolates the alias's own DNSSEC status (a CNAME-type query is
250/// answered by the alias RRset and is not chased into the insecure target), and
251/// works uniformly across resolver backends because it relies only on the
252/// per-answer secure bit rather than per-record validation proofs.
253pub 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
259/// Maps the result of the explicit `CNAME` lookup onto a [`SecureCnameStatus`].
260/// Split out from [`resolve_secure_cname`] so it can be unit tested without a
261/// live (DNSSEC validating) resolver.
262fn 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        // A denial of existence means there is no CNAME (and possibly no such
285        // name); either way it is not a secure alias.
286        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    // We can only rely on a CNAME that was DNSSEC validated.
295    if !answer.secure {
296        return SecureCnameStatus::NotSecureAlias;
297    }
298
299    // A secure NODATA answer (no CNAME record) means the host is not an alias.
300    if answer.records.iter().any(|r| matches!(r, RData::CNAME(_))) {
301        SecureCnameStatus::SecureAlias
302    } else {
303        SecureCnameStatus::NotSecureAlias
304    }
305}
306
307/// If the provided parameter ends with `:PORT` and `PORT` is a valid u16,
308/// then crack apart and return the LABEL and PORT number portions.
309/// Otherwise, returns None
310pub fn has_colon_port(a: &str) -> Option<(&str, u16)> {
311    let (label, maybe_port) = a.rsplit_once(':')?;
312
313    // v6 addresses can look like `::1` and confuse us. Try not
314    // to be confused here
315    if label.contains(':') {
316        return None;
317    }
318
319    let port = maybe_port.parse::<u16>().ok()?;
320    Some((label, port))
321}
322
323/// Helper to reason about a domain name string.
324/// It can either be name that needs to be resolved, or some kind
325/// of IP literal.
326/// We also allow for an optional port number to be present in
327/// the domain name string.
328pub enum DomainClassification {
329    /// A DNS Name pending resolution, plus an optional port number
330    Domain(Name, Option<u16>),
331    /// A literal IP address (no port), or socket address (with port)
332    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            // Try to interpret the literal as either an IPv4 or IPv6 address.
380            // Note that RFC5321 doesn't actually permit using an untagged
381            // IPv6 address, so this is non-conforming behavior.
382            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        // It's a literal address, no DNS lookup necessary
416
417        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        // Try to interpret the literal as either an IPv4 or IPv6 address.
443        // Note that RFC5321 doesn't actually permit using an untagged
444        // IPv6 address, so this is non-conforming behavior.
445        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        // Maybe its a unix domain socket path
459        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
485/// Resolve a [`ResolvableSocketAddr`] to a list of concrete [`ResolvedAddress`]es.
486///
487/// For `UnixDomain`, `V4` and `V6` arms, no DNS lookup is performed; a single
488/// `ResolvedAddress` is returned whose `addr` carries the supplied port (for
489/// the IP arms). For the `Hostname` arm, an A/AAAA lookup is performed via
490/// [`ip_lookup`] and each returned IP is paired with the supplied port.
491pub 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    /// Only query for A (Ipv4) records
542    Ipv4Only,
543    /// Only query for AAAA (Ipv6) records
544    Ipv6Only,
545    /// Query for A and AAAA in parallel
546    #[default]
547    Ipv4AndIpv6,
548    /// Query for Ipv6 if that fails, query for Ipv4
549    Ipv6ThenIpv4,
550    /// Query for Ipv4 if that fails, query for Ipv6 (default)
551    Ipv4ThenIpv6,
552}
553
554/// A set of resolved IP addresses together with whether the DNS lookup that
555/// produced them was DNSSEC validated (secure). The secure flag is what lets
556/// the delivery path decide DANE eligibility without performing a second
557/// lookup; see [`ResolvedAddress::is_secure`](kumo_log_types::ResolvedAddress).
558#[derive(Clone, Debug)]
559pub struct IpAddresses {
560    pub addrs: Vec<IpAddr>,
561    /// True only when every address here came from a DNSSEC-validated lookup.
562    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        // Only claim secure if we actually have addresses and every family that
640        // contributed one was DNSSEC validated.
641        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        // Secure CNAME alias: DANE-eligible at the original name.
752        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        // Secure NODATA (not an alias): not the secure-CNAME case.
760        k9::assert_equal!(
761            classify_cname_answer(
762                "mx.example.com",
763                Ok(answer(vec![], true, ResponseCode::NoError))
764            ),
765            SecureCnameStatus::NotSecureAlias
766        );
767        // A CNAME that was not DNSSEC validated cannot be trusted.
768        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        // NXDOMAIN: not an alias.
776        k9::assert_equal!(
777            classify_cname_answer(
778                "mx.example.com",
779                Ok(answer(vec![], true, ResponseCode::NXDomain))
780            ),
781            SecureCnameStatus::NotSecureAlias
782        );
783        // SERVFAIL leaves the status unknown: defer, do not downgrade.
784        assert!(matches!(
785            classify_cname_answer(
786                "mx.example.com",
787                Ok(answer(vec![], false, ResponseCode::ServFail))
788            ),
789            SecureCnameStatus::TempFail(_)
790        ));
791        // A resolver error is also a temporary failure.
792        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        // DANE-TA(2) and DANE-EE(3) are usable for DANE SMTP.
804        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        // PKIX-TA(0)/PKIX-EE(1) are not in scope for DANE SMTP (RFC 7672 3.1.3).
813        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        // Private and unassigned usages are unusable.
822        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        // Unknown selector/matching types and bad digest lengths are unusable.
831        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        // Raw (full value, matching type 0) has no fixed length.
848        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        // Usable, secure DANE-EE record.
859        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        // Secure NODATA: no TLSA records, not a DANE host.
872        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        // Secure NXDOMAIN: not a DANE host.
881        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        // TLSA records present but unvalidated must not be trusted.
890        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        // Secure records present, but only disallowed usages: unusable, so
903        // STARTTLS is required without authentication.
904        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        // SERVFAIL leaves the status unknown: defer, do not downgrade.
917        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        // A resolver error is also a temporary failure.
926        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    /// Confirms that a DNSSEC-validating hickory resolver, via our adapter,
937    /// reports signed records as secure and resolves unsigned records as
938    /// insecure (rather than failing). Validation requires a DNSSEC-capable
939    /// upstream reachable over TCP, since DNSKEY/RRSIG responses are large.
940    #[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    /// A securely denied answer (NODATA/NXDOMAIN in a signed zone) must be
991    /// reported as secure so that, for example, a securely proven "no MX" can
992    /// engage DANE for the implicit MX. An insecure (unsigned) denial must
993    /// remain insecure. Exercises the authority-section proof handling on the
994    /// hickory backend.
995    #[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        // Signed zone with no TLSA at the apex: a securely proven NODATA.
1023        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        // Unsigned zone NODATA: must not be reported as secure.
1035        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    // Requires DNSSEC-validated TLSA records, so it only holds when the default
1048    // resolver validates, i.e. the unbound backend.
1049    #[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}