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::{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
26// An `ArcSwap` can only hold `Sized` types, so we cannot stuff a `dyn Resolver` directly into it.
27// Instead, the documentation recommends adding a level of indirection, so we wrap the `Resolver`
28// trait object in a `Box`. In the context of DNS requests, the additional pointer chasing should
29// not be a significant performance concern.
30static RESOLVER: LazyLock<ArcSwap<Box<dyn Resolver>>> =
31    LazyLock::new(|| ArcSwap::from_pointee(Box::new(default_resolver())));
32
33declare_cache! {
34/// Caches domain name to ipv4 records and their DNSSEC secure status
35static IPV4_CACHE: LruCacheWithTtl<Name, Arc<IpAddresses>>::new("dns_resolver_ipv4", 1024);
36}
37declare_cache! {
38/// Caches domain name to ipv6 records and their DNSSEC secure status
39static IPV6_CACHE: LruCacheWithTtl<Name, Arc<IpAddresses>>::new("dns_resolver_ipv6", 1024);
40}
41declare_cache! {
42/// Caches domain name to the combined set of ipv4 and ipv6 records and their
43/// DNSSEC secure status
44static 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    // Treat it as fully qualified
58    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/// The outcome of attempting to resolve DANE TLSA records for an MX host,
72/// per <https://datatracker.ietf.org/doc/html/rfc7672>.
73#[derive(Debug, Clone, PartialEq)]
74pub enum DaneStatus {
75    /// The host is not DANE-eligible: either the MX host's address (A/AAAA)
76    /// RRset was not securely (DNSSEC) resolved, or there was a secure proof
77    /// that no TLSA records exist (NODATA/NXDOMAIN). Delivery should continue
78    /// using the normal (opportunistic / configured) TLS policy.
79    NotApplicable,
80    /// Secure, usable DANE-TA(2)/DANE-EE(3) TLSA records were found. The
81    /// connection must use TLS and authenticate the peer against these records.
82    Records(Vec<TLSA>),
83    /// Secure TLSA records were published, but none are usable for DANE SMTP
84    /// (e.g. only PKIX-TA(0)/PKIX-EE(1), private/unassigned usages, or
85    /// unsupported selector/matching types). Per RFC 7672 section 4.1 the
86    /// client must still require STARTTLS but cannot authenticate the peer.
87    Unusable,
88    /// The secure status of the TLSA records (or of the MX host's address
89    /// records) could not be determined, e.g. due to SERVFAIL, a timeout, or a
90    /// DNSSEC validation failure (bogus). To preserve downgrade resistance the
91    /// delivery must be deferred rather than continuing without authentication.
92    TempFail(String),
93}
94
95/// Decides whether a single TLSA record is usable for DANE SMTP.
96///
97/// RFC 7672 section 3.1.3 restricts SMTP DANE to the DANE-TA(2) and DANE-EE(3)
98/// certificate usages; PKIX-TA(0), PKIX-EE(1), and private/unassigned usages
99/// MUST be treated as unusable. We also require a selector and matching type
100/// that we (and OpenSSL) understand, and a digest of the correct length.
101fn 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
122/// Resolves DANE TLSA records for the given MX host and port, applying the
123/// RFC 7672 rules required for downgrade-resistant DANE SMTP.
124///
125/// `mx_host` must be the MX hostname (RFC 7672 section 3.2.2: the MX hostname is
126/// the only reference identifier), not the envelope/routing domain.
127///
128/// The caller is responsible for only invoking this when the chain to the MX
129/// host was securely (DNSSEC) resolved: both the MX RRset (see
130/// [`MailExchanger::is_secure`]) and the MX host's address records (see
131/// [`ResolvedAddress::is_secure`](kumo_log_types::ResolvedAddress)) must be
132/// secure. RFC 7672 section 2.2 requires the address records to be secure
133/// before it is safe to rely on TLSA records; we satisfy that by gating on the
134/// secure status of the very address we are about to connect to.
135pub 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
141/// Maps the result of the TLSA lookup onto a [`DaneStatus`], applying the
142/// RFC 7672 rules. Split out from [`resolve_dane`] so that it can be unit
143/// tested without a live (DNSSEC validating) resolver.
144fn 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            // No answer at all (a timeout or other communication/resource
149            // failure); the TLSA status is unknown, so we must not downgrade.
150            // Failure RCODEs such as SERVFAIL are handled in the match below.
151            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        // Bogus records are either tampered with, or due to misconfiguration
158        // of the local resolver.
159        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        // A secure (or insecure) denial of existence means there are no TLSA
171        // records; the host is simply not a DANE host.
172        ResponseCode::NXDomain => return DaneStatus::NotApplicable,
173        // SERVFAIL, REFUSED, NOTIMP, etc. leave the status unknown.
174        rcode => {
175            return DaneStatus::TempFail(format!(
176                "TLSA lookup for {mx_host}:{port} returned {rcode}"
177            ));
178        }
179    }
180
181    // We can only trust TLSA records that were DNSSEC validated.
182    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        // Secure proof that no TLSA records exist (NODATA).
195        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        // TLSA records exist but none are usable for DANE SMTP.
202        return DaneStatus::Unusable;
203    }
204
205    // DNS results are unordered; sort for stable behavior and tests. The TLSA
206    // type is an upstream type and does not implement Ord, so sort on its
207    // component fields directly.
208    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/// The outcome of an explicit `CNAME` lookup used to decide DANE eligibility
223/// for an MX host whose address chain was not fully DNSSEC-secure.
224///
225/// RFC 7672 section 2.2.2 treats an MX host that is a securely published CNAME
226/// alias as DANE-eligible at the original (unexpanded) name, even when the
227/// alias target lands in an insecure (unsigned) zone: it is the securely
228/// published TLSA RRset, not the address records, that authenticates the peer.
229#[derive(Debug, PartialEq, Eq)]
230pub enum SecureCnameStatus {
231    /// The name is a CNAME alias whose alias record was DNSSEC validated, so the
232    /// host remains DANE-eligible at its original name.
233    SecureAlias,
234    /// The name is not a securely published CNAME alias (it is not an alias at
235    /// all, or the alias was not DNSSEC validated); it is not the secure-CNAME
236    /// case and DANE must not be engaged off the back of it.
237    NotSecureAlias,
238    /// The secure status of the alias could not be determined (SERVFAIL,
239    /// timeout, or a DNSSEC validation failure). To preserve downgrade
240    /// resistance the caller must defer rather than continue.
241    TempFail(String),
242}
243
244/// Performs an explicit `CNAME` lookup for `mx_host` to determine whether it is
245/// a securely published alias.
246///
247/// This is a narrow fallback in the DANE path: when the MX host's address
248/// (A/AAAA) records did not resolve securely we may still be looking at a secure
249/// CNAME whose target merely lives in an unsigned zone. Querying the `CNAME`
250/// type explicitly isolates the alias's own DNSSEC status (a CNAME-type query is
251/// answered by the alias RRset and is not chased into the insecure target), and
252/// works uniformly across resolver backends because it relies only on the
253/// per-answer secure bit rather than per-record validation proofs.
254pub 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
260/// Maps the result of the explicit `CNAME` lookup onto a [`SecureCnameStatus`].
261/// Split out from [`resolve_secure_cname`] so it can be unit tested without a
262/// live (DNSSEC validating) resolver.
263fn 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        // A denial of existence means there is no CNAME (and possibly no such
286        // name); either way it is not a secure alias.
287        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    // We can only rely on a CNAME that was DNSSEC validated.
296    if !answer.secure {
297        return SecureCnameStatus::NotSecureAlias;
298    }
299
300    // A secure NODATA answer (no CNAME record) means the host is not an alias.
301    if answer.records.iter().any(|r| matches!(r, RData::CNAME(_))) {
302        SecureCnameStatus::SecureAlias
303    } else {
304        SecureCnameStatus::NotSecureAlias
305    }
306}
307
308/// If the provided parameter ends with `:PORT` and `PORT` is a valid u16,
309/// then crack apart and return the LABEL and PORT number portions.
310/// Otherwise, returns None
311pub fn has_colon_port(a: &str) -> Option<(&str, u16)> {
312    let (label, maybe_port) = a.rsplit_once(':')?;
313
314    // v6 addresses can look like `::1` and confuse us. Try not
315    // to be confused here
316    if label.contains(':') {
317        return None;
318    }
319
320    let port = maybe_port.parse::<u16>().ok()?;
321    Some((label, port))
322}
323
324/// Helper to reason about a domain name string.
325/// It can either be name that needs to be resolved, or some kind
326/// of IP literal.
327/// We also allow for an optional port number to be present in
328/// the domain name string.
329pub enum DomainClassification {
330    /// A DNS Name pending resolution, plus an optional port number
331    Domain(Name, Option<u16>),
332    /// A literal IP address (no port), or socket address (with port)
333    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            // Try to interpret the literal as either an IPv4 or IPv6 address.
381            // Note that RFC5321 doesn't actually permit using an untagged
382            // IPv6 address, so this is non-conforming behavior.
383            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        // It's a literal address, no DNS lookup necessary
417
418        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        // Try to interpret the literal as either an IPv4 or IPv6 address.
444        // Note that RFC5321 doesn't actually permit using an untagged
445        // IPv6 address, so this is non-conforming behavior.
446        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        // Maybe its a unix domain socket path
460        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
486/// Resolve a [`ResolvableSocketAddr`] to a list of concrete [`ResolvedAddress`]es.
487///
488/// For `UnixDomain`, `V4` and `V6` arms, no DNS lookup is performed; a single
489/// `ResolvedAddress` is returned whose `addr` carries the supplied port (for
490/// the IP arms). For the `Hostname` arm, an A/AAAA lookup is performed via
491/// [`ip_lookup`] and each returned IP is paired with the supplied port.
492pub 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    /// Only query for A (Ipv4) records
543    Ipv4Only,
544    /// Only query for AAAA (Ipv6) records
545    Ipv6Only,
546    /// Query for A and AAAA in parallel
547    #[default]
548    Ipv4AndIpv6,
549    /// Query for Ipv6 if that fails, query for Ipv4
550    Ipv6ThenIpv4,
551    /// Query for Ipv4 if that fails, query for Ipv6 (default)
552    Ipv4ThenIpv6,
553}
554
555/// A set of resolved IP addresses together with whether the DNS lookup that
556/// produced them was DNSSEC validated (secure). The secure flag is what lets
557/// the delivery path decide DANE eligibility without performing a second
558/// lookup; see [`ResolvedAddress::is_secure`](kumo_log_types::ResolvedAddress).
559#[derive(Clone, Debug)]
560pub struct IpAddresses {
561    pub addrs: Vec<IpAddr>,
562    /// True only when every address here came from a DNSSEC-validated lookup.
563    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
620/// Merge the per-family (v4, v6) lookup outcomes into one address set and the
621/// instant the combined entry should expire, measured from `now`.
622///
623/// When one family lookup succeeds and the other fails, the returned
624/// expiry is capped to at most the negative-cache window rather than the
625/// TTL of the surviving family. If every family failed the first error
626/// is returned.
627fn 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        // Only claim secure if we actually have addresses and every family that
662        // contributed one was DNSSEC validated.
663        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
672// A validating resolver can mark an answer bogus (its records are forged or
673// tampered, or the zone is misconfigured) while still returning the addresses
674// from that forged RRset. Without this check, ipv4_lookup and ipv6_lookup
675// would hand those addresses back with secure=false, indistinguishable from
676// an ordinary unsigned answer. classify_tlsa_answer and classify_cname_answer
677// in this file reject bogus the same way, because a caller that decides DANE
678// eligibility from the secure bit would otherwise treat a forged address as
679// merely unauthenticated and connect to it without DANE (RFC 7672 2.1.1-2.1.2).
680fn 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        // A bogus answer is rejected whether or not it contains addresses: the
792        // addresses are forged, and an empty bogus answer must not pass as an
793        // ordinary empty result either.
794        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        // Non-bogus answers pass, regardless of secure status or response code;
802        // their addresses (or absence) are the caller's to interpret.
803        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        // Models a validating resolver that returns a DNSSEC-validated A RRset
814        // and a bogus AAAA RRset that still contains an address, per the family
815        // controlled by `all_bogus`.
816        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        // Secure A, bogus AAAA: ip_lookup excludes the bogus AAAA family from
844        // both the address list and the secure computation, the same as it
845        // excludes a family that errored. The surviving A keeps its own secure
846        // bit rather than being dragged insecure by its bogus sibling.
847        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        // Every family bogus: both per-family lookups error, the address list
858        // stays empty, and ip_lookup raises the first of those errors rather
859        // than returning an empty or unsigned result.
860        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        // A hard-fails (a timeout) while AAAA succeeds with a long TTL: the
892        // surviving address is still returned, and the combined entry expires
893        // at the negative-cache window rather than the AAAA TTL, because we
894        // want the failed A family retried within a minute rather than left
895        // unused for the full AAAA TTL.
896        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        // Both families succeed: the entry keeps the shorter of the two TTLs,
905        // and the negative-cache cap is never applied.
906        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        // Every family failed and the address list is empty: the first error is
918        // surfaced.
919        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        // Secure CNAME alias: DANE-eligible at the original name.
942        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        // Secure NODATA (not an alias): not the secure-CNAME case.
950        k9::assert_equal!(
951            classify_cname_answer(
952                "mx.example.com",
953                Ok(answer(vec![], true, ResponseCode::NoError))
954            ),
955            SecureCnameStatus::NotSecureAlias
956        );
957        // A CNAME that was not DNSSEC validated cannot be trusted.
958        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        // NXDOMAIN: not an alias.
966        k9::assert_equal!(
967            classify_cname_answer(
968                "mx.example.com",
969                Ok(answer(vec![], true, ResponseCode::NXDomain))
970            ),
971            SecureCnameStatus::NotSecureAlias
972        );
973        // SERVFAIL leaves the status unknown: defer, do not downgrade.
974        assert!(matches!(
975            classify_cname_answer(
976                "mx.example.com",
977                Ok(answer(vec![], false, ResponseCode::ServFail))
978            ),
979            SecureCnameStatus::TempFail(_)
980        ));
981        // A resolver error is also a temporary failure.
982        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        // DANE-TA(2) and DANE-EE(3) are usable for DANE SMTP.
994        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        // PKIX-TA(0)/PKIX-EE(1) are not in scope for DANE SMTP (RFC 7672 3.1.3).
1003        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        // Private and unassigned usages are unusable.
1012        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        // Unknown selector/matching types and bad digest lengths are unusable.
1021        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        // Raw (full value, matching type 0) has no fixed length.
1038        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        // Usable, secure DANE-EE record.
1049        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        // Secure NODATA: no TLSA records, not a DANE host.
1062        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        // Secure NXDOMAIN: not a DANE host.
1071        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        // TLSA records present but unvalidated must not be trusted.
1080        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        // Secure records present, but only disallowed usages: unusable, so
1093        // STARTTLS is required without authentication.
1094        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        // SERVFAIL leaves the status unknown: defer, do not downgrade.
1107        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        // A resolver error is also a temporary failure.
1116        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    /// Builds a DNSSEC-validating hickory resolver that queries `server` over
1127    /// both UDP and TCP on port 53. TCP matters because DNSKEY/RRSIG responses
1128    /// are large and validation needs them.
1129    #[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    /// Confirms that a DNSSEC-validating hickory resolver, via our adapter,
1157    /// reports signed records as secure and resolves unsigned records as
1158    /// insecure (rather than failing). Validation requires a DNSSEC-capable
1159    /// upstream reachable over TCP, since DNSKEY/RRSIG responses are large.
1160    #[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    /// A securely denied answer (NODATA/NXDOMAIN in a signed zone) must be
1189    /// reported as secure so that, for example, a securely proven "no MX" can
1190    /// engage DANE for the implicit MX. An insecure (unsigned) denial must
1191    /// remain insecure. Exercises the authority-section proof handling on the
1192    /// hickory backend.
1193    #[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        // Signed zone with no TLSA at the apex: a securely proven NODATA.
1199        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        // Unsigned zone NODATA: must not be reported as secure.
1211        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    // Requires DNSSEC-validated TLSA records, so it only holds when the default
1224    // resolver validates, i.e. the unbound backend.
1225    #[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}