dns_resolver/
lib.rs

1use anyhow::Context;
2use arc_swap::ArcSwap;
3pub use hickory_resolver::proto::rr::rdata::tlsa::TLSA;
4use hickory_resolver::proto::rr::RecordType;
5use hickory_resolver::proto::ProtoError;
6pub use hickory_resolver::Name;
7use kumo_address::host::HostAddress;
8use kumo_address::host_or_socket::HostOrSocketAddress;
9use kumo_log_types::ResolvedAddress;
10use lruttl::declare_cache;
11use rand::prelude::SliceRandom;
12use serde::Serialize;
13use std::collections::BTreeMap;
14use std::net::{IpAddr, Ipv6Addr};
15use std::sync::atomic::{AtomicUsize, Ordering};
16use std::sync::{Arc, LazyLock};
17use std::time::{Duration, Instant};
18use tokio::sync::Semaphore;
19use tokio::time::timeout;
20
21mod resolver;
22#[cfg(feature = "unbound")]
23pub use resolver::UnboundResolver;
24pub use resolver::{
25    ptr_host, reverse_ip, AggregateResolver, DnsError, HickoryResolver, IpDisplay, Resolver,
26    TestResolver,
27};
28
29// An `ArcSwap` can only hold `Sized` types, so we cannot stuff a `dyn Resolver` directly into it.
30// Instead, the documentation recommends adding a level of indirection, so we wrap the `Resolver`
31// trait object in a `Box`. In the context of DNS requests, the additional pointer chasing should
32// not be a significant performance concern.
33static RESOLVER: LazyLock<ArcSwap<Box<dyn Resolver>>> =
34    LazyLock::new(|| ArcSwap::from_pointee(Box::new(default_resolver())));
35
36declare_cache! {
37static MX_CACHE: LruCacheWithTtl<(Name, Option<u16>), Result<Arc<MailExchanger>, String>>::new("dns_resolver_mx", 64 * 1024);
38}
39declare_cache! {
40static IPV4_CACHE: LruCacheWithTtl<Name, Arc<Vec<IpAddr>>>::new("dns_resolver_ipv4", 1024);
41}
42declare_cache! {
43static IPV6_CACHE: LruCacheWithTtl<Name, Arc<Vec<IpAddr>>>::new("dns_resolver_ipv6", 1024);
44}
45declare_cache! {
46static IP_CACHE: LruCacheWithTtl<Name, Arc<Vec<IpAddr>>>::new("dns_resolver_ip", 1024);
47}
48
49/// Maximum number of concurrent mx resolves permitted
50static MX_MAX_CONCURRENCY: AtomicUsize = AtomicUsize::new(128);
51static MX_CONCURRENCY_SEMA: LazyLock<Semaphore> =
52    LazyLock::new(|| Semaphore::new(MX_MAX_CONCURRENCY.load(Ordering::SeqCst)));
53
54/// 5 seconds in ms
55static MX_TIMEOUT_MS: AtomicUsize = AtomicUsize::new(5000);
56
57/// 5 minutes in ms
58static MX_NEGATIVE_TTL: AtomicUsize = AtomicUsize::new(300 * 1000);
59
60static MX_IN_PROGRESS: LazyLock<prometheus::IntGauge> = LazyLock::new(|| {
61    prometheus::register_int_gauge!(
62        "dns_mx_resolve_in_progress",
63        "number of MailExchanger::resolve calls currently in progress"
64    )
65    .unwrap()
66});
67static MX_SUCCESS: LazyLock<prometheus::IntCounter> = LazyLock::new(|| {
68    prometheus::register_int_counter!(
69        "dns_mx_resolve_status_ok",
70        "total number of successful MailExchanger::resolve calls"
71    )
72    .unwrap()
73});
74static MX_FAIL: LazyLock<prometheus::IntCounter> = LazyLock::new(|| {
75    prometheus::register_int_counter!(
76        "dns_mx_resolve_status_fail",
77        "total number of failed MailExchanger::resolve calls"
78    )
79    .unwrap()
80});
81static MX_CACHED: LazyLock<prometheus::IntCounter> = LazyLock::new(|| {
82    prometheus::register_int_counter!(
83        "dns_mx_resolve_cache_hit",
84        "total number of MailExchanger::resolve calls satisfied by level 1 cache"
85    )
86    .unwrap()
87});
88static MX_QUERIES: LazyLock<prometheus::IntCounter> = LazyLock::new(|| {
89    prometheus::register_int_counter!(
90        "dns_mx_resolve_cache_miss",
91        "total number of MailExchanger::resolve calls that resulted in an \
92        MX DNS request to the next level of cache"
93    )
94    .unwrap()
95});
96
97fn default_resolver() -> impl Resolver {
98    #[cfg(feature = "default-unbound")]
99    return UnboundResolver::new().unwrap();
100    #[cfg(not(feature = "default-unbound"))]
101    return HickoryResolver::new().expect("Parsing /etc/resolv.conf failed");
102}
103
104pub fn set_mx_concurrency_limit(n: usize) {
105    MX_MAX_CONCURRENCY.store(n, Ordering::SeqCst);
106}
107
108pub fn set_mx_timeout(duration: Duration) -> anyhow::Result<()> {
109    let ms = duration
110        .as_millis()
111        .try_into()
112        .context("set_mx_timeout: duration is too large")?;
113    MX_TIMEOUT_MS.store(ms, Ordering::Relaxed);
114    Ok(())
115}
116
117pub fn get_mx_timeout() -> Duration {
118    Duration::from_millis(MX_TIMEOUT_MS.load(Ordering::Relaxed) as u64)
119}
120
121pub fn set_mx_negative_cache_ttl(duration: Duration) -> anyhow::Result<()> {
122    let ms = duration
123        .as_millis()
124        .try_into()
125        .context("set_mx_negative_cache_ttl: duration is too large")?;
126    MX_NEGATIVE_TTL.store(ms, Ordering::Relaxed);
127    Ok(())
128}
129
130pub fn get_mx_negative_ttl() -> Duration {
131    Duration::from_millis(MX_NEGATIVE_TTL.load(Ordering::Relaxed) as u64)
132}
133
134#[derive(Clone, Debug, Serialize)]
135pub struct MailExchanger {
136    pub domain_name: String,
137    pub hosts: Vec<String>,
138    pub site_name: String,
139    pub by_pref: BTreeMap<u16, Vec<String>>,
140    pub is_domain_literal: bool,
141    /// DNSSEC verified
142    pub is_secure: bool,
143    pub is_mx: bool,
144    #[serde(skip)]
145    expires: Option<Instant>,
146}
147
148pub fn fully_qualify(domain_name: &str) -> Result<Name, ProtoError> {
149    let mut name = Name::from_str_relaxed(domain_name)?.to_lowercase();
150
151    // Treat it as fully qualified
152    name.set_fqdn(true);
153
154    Ok(name)
155}
156
157pub fn reconfigure_resolver(resolver: impl Resolver) {
158    RESOLVER.store(Arc::new(Box::new(resolver)));
159}
160
161pub fn get_resolver() -> Arc<Box<dyn Resolver>> {
162    RESOLVER.load_full()
163}
164
165/// Resolves TLSA records for a destination name and port according to
166/// <https://datatracker.ietf.org/doc/html/rfc6698#appendix-B.2>
167pub async fn resolve_dane(hostname: &str, port: u16) -> anyhow::Result<Vec<TLSA>> {
168    let name = fully_qualify(&format!("_{port}._tcp.{hostname}"))?;
169    let answer = RESOLVER.load().resolve(name, RecordType::TLSA).await?;
170    tracing::debug!("resolve_dane {hostname}:{port} TLSA answer is: {answer:?}");
171
172    if answer.bogus {
173        // Bogus records are either tampered with, or due to misconfiguration
174        // of the local resolver
175        anyhow::bail!(
176            "DANE result for {hostname}:{port} unusable because: {}",
177            answer
178                .why_bogus
179                .as_deref()
180                .unwrap_or("DNSSEC validation failed")
181        );
182    }
183
184    let mut result = vec![];
185    // We ignore TLSA records unless they are validated; in other words,
186    // we'll return an empty list (without raising an error) if the resolver
187    // is not configured to verify DNSSEC
188    if answer.secure {
189        for r in &answer.records {
190            if let Some(tlsa) = r.as_tlsa() {
191                result.push(tlsa.clone());
192            }
193        }
194        // DNS results are unordered. For the sake of tests,
195        // sort these records.
196        // Unfortunately, the TLSA type is nor Ord so we
197        // convert to string and order by that, which is a bit
198        // wasteful but the cardinality of TLSA records is
199        // generally low
200        result.sort_by_key(|a| a.to_string());
201    }
202
203    tracing::info!("resolve_dane {hostname}:{port} result is: {result:?}");
204
205    Ok(result)
206}
207
208/// If the provided parameter ends with `:PORT` and `PORT` is a valid u16,
209/// then crack apart and return the LABEL and PORT number portions.
210/// Otherwise, returns None
211pub fn has_colon_port(a: &str) -> Option<(&str, u16)> {
212    let (label, maybe_port) = a.rsplit_once(':')?;
213
214    // v6 addresses can look like `::1` and confuse us. Try not
215    // to be confused here
216    if label.contains(':') {
217        return None;
218    }
219
220    let port = maybe_port.parse::<u16>().ok()?;
221    Some((label, port))
222}
223
224/// Helper to reason about a domain name string.
225/// It can either be name that needs to be resolved, or some kind
226/// of IP literal.
227/// We also allow for an optional port number to be present in
228/// the domain name string.
229pub enum DomainClassification {
230    /// A DNS Name pending resolution, plus an optional port number
231    Domain(Name, Option<u16>),
232    /// A literal IP address (no port), or socket address (with port)
233    Literal(HostOrSocketAddress),
234}
235
236impl DomainClassification {
237    pub fn classify(domain_name: &str) -> anyhow::Result<Self> {
238        let (domain_name, mut opt_port) = match has_colon_port(domain_name) {
239            Some((domain_name, port)) => (domain_name, Some(port)),
240            None => (domain_name, None),
241        };
242
243        if domain_name.starts_with('[') {
244            if !domain_name.ends_with(']') {
245                anyhow::bail!(
246                    "domain_name `{domain_name}` is a malformed literal \
247                     domain with no trailing `]`"
248                );
249            }
250
251            let lowered = domain_name.to_ascii_lowercase();
252            let literal = &lowered[1..lowered.len() - 1];
253
254            let literal = match has_colon_port(literal) {
255                Some((_, _)) if opt_port.is_some() => {
256                    anyhow::bail!("invalid address: `{domain_name}` specifies a port both inside and outside a literal address enclosed in square brackets");
257                }
258                Some((literal, port)) => {
259                    opt_port.replace(port);
260                    literal
261                }
262                None => literal,
263            };
264
265            if let Some(v6_literal) = literal.strip_prefix("ipv6:") {
266                match v6_literal.parse::<Ipv6Addr>() {
267                    Ok(addr) => {
268                        let mut host_addr: HostOrSocketAddress = addr.into();
269                        if let Some(port) = opt_port {
270                            host_addr.set_port(port);
271                        }
272                        return Ok(Self::Literal(host_addr));
273                    }
274                    Err(err) => {
275                        anyhow::bail!("invalid ipv6 address: `{v6_literal}`: {err:#}");
276                    }
277                }
278            }
279
280            // Try to interpret the literal as either an IPv4 or IPv6 address.
281            // Note that RFC5321 doesn't actually permit using an untagged
282            // IPv6 address, so this is non-conforming behavior.
283            match literal.parse::<IpAddr>() {
284                Ok(ip_addr) => {
285                    let mut host_addr: HostOrSocketAddress = ip_addr.into();
286                    if let Some(port) = opt_port {
287                        host_addr.set_port(port);
288                    }
289                    return Ok(Self::Literal(host_addr));
290                }
291                Err(err) => {
292                    anyhow::bail!("invalid address: `{literal}`: {err:#}");
293                }
294            }
295        }
296
297        let name_fq = fully_qualify(domain_name)?;
298        Ok(Self::Domain(name_fq, opt_port))
299    }
300
301    pub fn has_port(&self) -> bool {
302        match self {
303            Self::Domain(_, Some(_)) => true,
304            Self::Domain(_, None) => false,
305            Self::Literal(addr) => addr.port().is_some(),
306        }
307    }
308}
309
310pub async fn resolve_a_or_aaaa(
311    domain_name: &str,
312    resolver: Option<&dyn Resolver>,
313) -> anyhow::Result<Vec<ResolvedAddress>> {
314    if domain_name.starts_with('[') {
315        // It's a literal address, no DNS lookup necessary
316
317        if !domain_name.ends_with(']') {
318            anyhow::bail!(
319                "domain_name `{domain_name}` is a malformed literal \
320                     domain with no trailing `]`"
321            );
322        }
323
324        let lowered = domain_name.to_ascii_lowercase();
325        let literal = &lowered[1..lowered.len() - 1];
326
327        if let Some(v6_literal) = literal.strip_prefix("ipv6:") {
328            match v6_literal.parse::<Ipv6Addr>() {
329                Ok(addr) => {
330                    return Ok(vec![ResolvedAddress {
331                        name: domain_name.to_string(),
332                        addr: std::net::IpAddr::V6(addr).into(),
333                    }]);
334                }
335                Err(err) => {
336                    anyhow::bail!("invalid ipv6 address: `{v6_literal}`: {err:#}");
337                }
338            }
339        }
340
341        // Try to interpret the literal as either an IPv4 or IPv6 address.
342        // Note that RFC5321 doesn't actually permit using an untagged
343        // IPv6 address, so this is non-conforming behavior.
344        match literal.parse::<HostAddress>() {
345            Ok(addr) => {
346                return Ok(vec![ResolvedAddress {
347                    name: domain_name.to_string(),
348                    addr: addr.into(),
349                }]);
350            }
351            Err(err) => {
352                anyhow::bail!("invalid address: `{literal}`: {err:#}");
353            }
354        }
355    } else {
356        // Maybe its a unix domain socket path
357        if let Ok(addr) = domain_name.parse::<HostAddress>() {
358            return Ok(vec![ResolvedAddress {
359                name: domain_name.to_string(),
360                addr: addr.into(),
361            }]);
362        }
363    }
364
365    match ip_lookup(domain_name, resolver).await {
366        Ok((addrs, _expires)) => {
367            let addrs = addrs
368                .iter()
369                .map(|&addr| ResolvedAddress {
370                    name: domain_name.to_string(),
371                    addr: addr.into(),
372                })
373                .collect();
374            Ok(addrs)
375        }
376        Err(err) => anyhow::bail!("{err:#}"),
377    }
378}
379
380impl MailExchanger {
381    pub async fn resolve(domain_name: &str) -> anyhow::Result<Arc<Self>> {
382        MX_IN_PROGRESS.inc();
383        let result = Self::resolve_impl(domain_name).await;
384        MX_IN_PROGRESS.dec();
385        if result.is_ok() {
386            MX_SUCCESS.inc();
387        } else {
388            MX_FAIL.inc();
389        }
390        result
391    }
392
393    async fn resolve_impl(domain_name: &str) -> anyhow::Result<Arc<Self>> {
394        let (name_fq, opt_port) = match DomainClassification::classify(domain_name)? {
395            DomainClassification::Literal(addr) => {
396                let mut by_pref = BTreeMap::new();
397                by_pref.insert(1, vec![addr.to_string()]);
398                return Ok(Arc::new(Self {
399                    domain_name: domain_name.to_string(),
400                    hosts: vec![addr.to_string()],
401                    site_name: addr.to_string(),
402                    by_pref,
403                    is_domain_literal: true,
404                    is_secure: false,
405                    is_mx: false,
406                    expires: None,
407                }));
408            }
409            DomainClassification::Domain(name_fq, opt_port) => (name_fq, opt_port),
410        };
411
412        let lookup_result = MX_CACHE
413            .get_or_try_insert(
414                &(name_fq.clone(), opt_port),
415                |mx_result| {
416                    if let Ok(mx) = mx_result {
417                        if let Some(exp) = mx.expires {
418                            return exp
419                                .checked_duration_since(std::time::Instant::now())
420                                .unwrap_or_else(|| Duration::from_secs(10));
421                        }
422                    }
423                    get_mx_negative_ttl()
424                },
425                async {
426                    MX_QUERIES.inc();
427                    let start = Instant::now();
428                    let (mut by_pref, expires) = match lookup_mx_record(&name_fq).await {
429                        Ok((by_pref, expires)) => (by_pref, expires),
430                        Err(err) => {
431                            let error = format!(
432                                "MX lookup for {domain_name} failed after {elapsed:?}: {err:#}",
433                                elapsed = start.elapsed()
434                            );
435                            return Ok::<Result<Arc<MailExchanger>, String>, anyhow::Error>(Err(
436                                error,
437                            ));
438                        }
439                    };
440
441                    let mut hosts = vec![];
442                    for pref in &mut by_pref {
443                        for host in &mut pref.hosts {
444                            if let Some(port) = opt_port {
445                                *host = format!("{host}:{port}");
446                            };
447                            hosts.push(host.to_string());
448                        }
449                    }
450
451                    let is_secure = by_pref.iter().all(|p| p.is_secure);
452                    let is_mx = by_pref.iter().all(|p| p.is_mx);
453
454                    let by_pref = by_pref
455                        .into_iter()
456                        .map(|pref| (pref.pref, pref.hosts))
457                        .collect();
458
459                    let site_name = factor_names(&hosts);
460                    let mx = Self {
461                        hosts,
462                        domain_name: name_fq.to_ascii(),
463                        site_name,
464                        by_pref,
465                        is_domain_literal: false,
466                        is_secure,
467                        is_mx,
468                        expires: Some(expires),
469                    };
470
471                    Ok(Ok(Arc::new(mx)))
472                },
473            )
474            .await
475            .map_err(|err| anyhow::anyhow!("{err}"))?;
476
477        if !lookup_result.is_fresh {
478            MX_CACHED.inc();
479        }
480
481        lookup_result.item.map_err(|err| anyhow::anyhow!("{err}"))
482    }
483
484    pub fn has_expired(&self) -> bool {
485        match self.expires {
486            Some(deadline) => deadline <= Instant::now(),
487            None => false,
488        }
489    }
490
491    /// Returns the list of resolve MX hosts in *reverse* preference
492    /// order; the first one to try is the last element.
493    /// smtp_dispatcher.rs relies on this ordering, as it will pop
494    /// off candidates until it has exhausted its connection plan.
495    pub async fn resolve_addresses(&self) -> ResolvedMxAddresses {
496        let mut result = vec![];
497
498        for hosts in self.by_pref.values().rev() {
499            let mut by_pref = vec![];
500
501            for mx_host in hosts {
502                // '.' is a null mx; skip trying to resolve it
503                if mx_host == "." {
504                    return ResolvedMxAddresses::NullMx;
505                }
506
507                // Handle the literal address case
508                let (mx_host, opt_port) = match has_colon_port(mx_host) {
509                    Some((domain_name, port)) => (domain_name, Some(port)),
510                    None => (mx_host.as_str(), None),
511                };
512                if let Ok(addr) = mx_host.parse::<IpAddr>() {
513                    let mut addr: HostOrSocketAddress = addr.into();
514                    if let Some(port) = opt_port {
515                        addr.set_port(port);
516                    }
517                    by_pref.push(ResolvedAddress {
518                        name: mx_host.to_string(),
519                        addr: addr.into(),
520                    });
521                    continue;
522                }
523
524                match ip_lookup(mx_host, None).await {
525                    Err(err) => {
526                        tracing::error!("failed to resolve {mx_host}: {err:#}");
527                        continue;
528                    }
529                    Ok((addresses, _expires)) => {
530                        for addr in addresses.iter() {
531                            let mut addr: HostOrSocketAddress = (*addr).into();
532                            if let Some(port) = opt_port {
533                                addr.set_port(port);
534                            }
535                            by_pref.push(ResolvedAddress {
536                                name: mx_host.to_string(),
537                                addr,
538                            });
539                        }
540                    }
541                }
542            }
543
544            // Randomize the list of addresses within this preference
545            // level. This probablistically "load balances" outgoing
546            // traffic across MX hosts with equal preference value.
547            let mut rng = rand::thread_rng();
548            by_pref.shuffle(&mut rng);
549            result.append(&mut by_pref);
550        }
551        ResolvedMxAddresses::Addresses(result)
552    }
553}
554
555#[derive(Debug, Clone, Serialize)]
556pub enum ResolvedMxAddresses {
557    NullMx,
558    Addresses(Vec<ResolvedAddress>),
559}
560
561struct ByPreference {
562    hosts: Vec<String>,
563    pref: u16,
564    is_secure: bool,
565    is_mx: bool,
566}
567
568async fn lookup_mx_record(domain_name: &Name) -> anyhow::Result<(Vec<ByPreference>, Instant)> {
569    let mx_lookup = timeout(get_mx_timeout(), async {
570        let _permit = MX_CONCURRENCY_SEMA.acquire().await;
571        RESOLVER
572            .load()
573            .resolve(domain_name.clone(), RecordType::MX)
574            .await
575    })
576    .await??;
577    let mx_records = mx_lookup.records;
578
579    if mx_records.is_empty() {
580        if mx_lookup.nxdomain {
581            anyhow::bail!("NXDOMAIN");
582        }
583
584        return Ok((
585            vec![ByPreference {
586                hosts: vec![domain_name.to_lowercase().to_ascii()],
587                pref: 1,
588                is_secure: false,
589                is_mx: false,
590            }],
591            mx_lookup.expires,
592        ));
593    }
594
595    let mut records: Vec<ByPreference> = Vec::with_capacity(mx_records.len());
596
597    for mx_record in mx_records {
598        if let Some(mx) = mx_record.as_mx() {
599            let pref = mx.preference();
600            let host = mx.exchange().to_lowercase().to_string();
601
602            if let Some(record) = records.iter_mut().find(|r| r.pref == pref) {
603                record.hosts.push(host);
604            } else {
605                records.push(ByPreference {
606                    hosts: vec![host],
607                    pref,
608                    is_secure: mx_lookup.secure,
609                    is_mx: true,
610                });
611            }
612        }
613    }
614
615    // Sort by preference
616    records.sort_unstable_by(|a, b| a.pref.cmp(&b.pref));
617
618    // Sort the hosts at each preference level to produce the
619    // overall ordered list of hosts for this site
620    for mx in &mut records {
621        mx.hosts.sort();
622    }
623
624    Ok((records, mx_lookup.expires))
625}
626
627pub async fn ip_lookup(
628    key: &str,
629    resolver: Option<&dyn Resolver>,
630) -> anyhow::Result<(Arc<Vec<IpAddr>>, Instant)> {
631    let key_fq = fully_qualify(key)?;
632
633    if resolver.is_none() {
634        if let Some(lookup) = IP_CACHE.lookup(&key_fq) {
635            return Ok((lookup.item, lookup.expiration.into()));
636        }
637    }
638
639    let (v4, v6) = tokio::join!(ipv4_lookup(key, resolver), ipv6_lookup(key, resolver));
640
641    let mut results = vec![];
642    let mut errors = vec![];
643    let mut expires = None;
644
645    match v4 {
646        Ok((addrs, exp)) => {
647            expires.replace(exp);
648            for a in addrs.iter() {
649                results.push(*a);
650            }
651        }
652        Err(err) => errors.push(err),
653    }
654
655    match v6 {
656        Ok((addrs, exp)) => {
657            let exp = match expires.take() {
658                Some(existing) => exp.min(existing),
659                None => exp,
660            };
661            expires.replace(exp);
662
663            for a in addrs.iter() {
664                results.push(*a);
665            }
666        }
667        Err(err) => errors.push(err),
668    }
669
670    if results.is_empty() && !errors.is_empty() {
671        return Err(errors.remove(0));
672    }
673
674    let addr = Arc::new(results);
675    let exp = expires.take().unwrap_or_else(Instant::now);
676
677    if resolver.is_none() {
678        IP_CACHE.insert(key_fq, addr.clone(), exp.into()).await;
679    }
680    Ok((addr, exp))
681}
682
683pub async fn ipv4_lookup(
684    key: &str,
685    resolver: Option<&dyn Resolver>,
686) -> anyhow::Result<(Arc<Vec<IpAddr>>, Instant)> {
687    let key_fq = fully_qualify(key)?;
688    if resolver.is_none() {
689        if let Some(lookup) = IPV4_CACHE.lookup(&key_fq) {
690            return Ok((lookup.item, lookup.expiration.into()));
691        }
692    }
693
694    let answer = match resolver {
695        Some(r) => r.resolve(key_fq.clone(), RecordType::A).await?,
696        None => {
697            RESOLVER
698                .load()
699                .resolve(key_fq.clone(), RecordType::A)
700                .await?
701        }
702    };
703    let ips = answer.as_addr();
704
705    let ips = Arc::new(ips);
706    let expires = answer.expires;
707    if resolver.is_none() {
708        IPV4_CACHE.insert(key_fq, ips.clone(), expires.into()).await;
709    }
710    Ok((ips, expires))
711}
712
713pub async fn ipv6_lookup(
714    key: &str,
715    resolver: Option<&dyn Resolver>,
716) -> anyhow::Result<(Arc<Vec<IpAddr>>, Instant)> {
717    let key_fq = fully_qualify(key)?;
718    if resolver.is_none() {
719        if let Some(lookup) = IPV6_CACHE.lookup(&key_fq) {
720            return Ok((lookup.item, lookup.expiration.into()));
721        }
722    }
723
724    let answer = match resolver {
725        Some(r) => r.resolve(key_fq.clone(), RecordType::AAAA).await?,
726        None => {
727            RESOLVER
728                .load()
729                .resolve(key_fq.clone(), RecordType::AAAA)
730                .await?
731        }
732    };
733    let ips = answer.as_addr();
734
735    let ips = Arc::new(ips);
736    let expires = answer.expires;
737    if resolver.is_none() {
738        IPV6_CACHE.insert(key_fq, ips.clone(), expires.into()).await;
739    }
740    Ok((ips, expires))
741}
742
743/// Given a list of host names, produce a pseudo-regex style alternation list
744/// of the different elements of the hostnames.
745/// The goal is to produce a more compact representation of the name list
746/// with the common components factored out.
747fn factor_names<S: AsRef<str>>(name_strings: &[S]) -> String {
748    let mut max_element_count = 0;
749
750    let mut names = vec![];
751
752    for name in name_strings {
753        let (name, opt_port) = match has_colon_port(name.as_ref()) {
754            Some((name, port)) => (name, Some(port)),
755            None => (name.as_ref(), None),
756        };
757        if let Ok(name) = fully_qualify(name) {
758            names.push((name.to_lowercase(), opt_port));
759        }
760    }
761
762    let mut elements: Vec<Vec<&str>> = vec![];
763
764    let mut split_names = vec![];
765    for (name, opt_port) in names {
766        let mut fields: Vec<_> = name
767            .iter()
768            .map(|s| String::from_utf8_lossy(s).to_string())
769            .collect();
770        if let Some(port) = opt_port {
771            fields.last_mut().map(|s| {
772                s.push_str(&format!(":{port}"));
773            });
774        }
775        fields.reverse();
776        max_element_count = max_element_count.max(fields.len());
777        split_names.push(fields);
778    }
779
780    fn add_element<'a>(elements: &mut Vec<Vec<&'a str>>, field: &'a str, i: usize) {
781        match elements.get_mut(i) {
782            Some(ele) => {
783                if !ele.contains(&field) {
784                    ele.push(field);
785                }
786            }
787            None => {
788                elements.push(vec![field]);
789            }
790        }
791    }
792
793    for fields in &split_names {
794        for (i, field) in fields.iter().enumerate() {
795            add_element(&mut elements, field, i);
796        }
797        for i in fields.len()..max_element_count {
798            add_element(&mut elements, "?", i);
799        }
800    }
801
802    let mut result = vec![];
803    for mut ele in elements {
804        let has_q = ele.contains(&"?");
805        ele.retain(|&e| e != "?");
806        let mut item_text = if ele.len() == 1 {
807            ele[0].to_string()
808        } else {
809            format!("({})", ele.join("|"))
810        };
811        if has_q {
812            item_text.push('?');
813        }
814        result.push(item_text);
815    }
816    result.reverse();
817
818    result.join(".")
819}
820
821#[cfg(test)]
822mod test {
823    use super::*;
824
825    #[tokio::test]
826    async fn literal_resolve() {
827        let v4_loopback = MailExchanger::resolve("[127.0.0.1]").await.unwrap();
828        k9::snapshot!(
829            &v4_loopback,
830            r#"
831MailExchanger {
832    domain_name: "[127.0.0.1]",
833    hosts: [
834        "127.0.0.1",
835    ],
836    site_name: "127.0.0.1",
837    by_pref: {
838        1: [
839            "127.0.0.1",
840        ],
841    },
842    is_domain_literal: true,
843    is_secure: false,
844    is_mx: false,
845    expires: None,
846}
847"#
848        );
849        k9::snapshot!(
850            v4_loopback.resolve_addresses().await,
851            r#"
852Addresses(
853    [
854        ResolvedAddress {
855            name: "127.0.0.1",
856            addr: 127.0.0.1,
857        },
858    ],
859)
860"#
861        );
862
863        let v6_loopback_non_conforming = MailExchanger::resolve("[::1]").await.unwrap();
864        k9::snapshot!(
865            &v6_loopback_non_conforming,
866            r#"
867MailExchanger {
868    domain_name: "[::1]",
869    hosts: [
870        "::1",
871    ],
872    site_name: "::1",
873    by_pref: {
874        1: [
875            "::1",
876        ],
877    },
878    is_domain_literal: true,
879    is_secure: false,
880    is_mx: false,
881    expires: None,
882}
883"#
884        );
885        k9::snapshot!(
886            v6_loopback_non_conforming.resolve_addresses().await,
887            r#"
888Addresses(
889    [
890        ResolvedAddress {
891            name: "::1",
892            addr: ::1,
893        },
894    ],
895)
896"#
897        );
898
899        let v6_loopback = MailExchanger::resolve("[IPv6:::1]").await.unwrap();
900        k9::snapshot!(
901            &v6_loopback,
902            r#"
903MailExchanger {
904    domain_name: "[IPv6:::1]",
905    hosts: [
906        "::1",
907    ],
908    site_name: "::1",
909    by_pref: {
910        1: [
911            "::1",
912        ],
913    },
914    is_domain_literal: true,
915    is_secure: false,
916    is_mx: false,
917    expires: None,
918}
919"#
920        );
921        k9::snapshot!(
922            v6_loopback.resolve_addresses().await,
923            r#"
924Addresses(
925    [
926        ResolvedAddress {
927            name: "::1",
928            addr: ::1,
929        },
930    ],
931)
932"#
933        );
934    }
935
936    #[test]
937    fn name_factoring() {
938        assert_eq!(
939            factor_names(&[
940                "mta5.am0.yahoodns.net",
941                "mta6.am0.yahoodns.net",
942                "mta7.am0.yahoodns.net"
943            ]),
944            "(mta5|mta6|mta7).am0.yahoodns.net".to_string()
945        );
946
947        // Verify that the case is normalized to lowercase
948        assert_eq!(
949            factor_names(&[
950                "mta5.AM0.yahoodns.net",
951                "mta6.am0.yAHOodns.net",
952                "mta7.am0.yahoodns.net"
953            ]),
954            "(mta5|mta6|mta7).am0.yahoodns.net".to_string()
955        );
956
957        // When the names have mismatched lengths, do we produce
958        // something reasonable?
959        assert_eq!(
960            factor_names(&[
961                "gmail-smtp-in.l.google.com",
962                "alt1.gmail-smtp-in.l.google.com",
963                "alt2.gmail-smtp-in.l.google.com",
964                "alt3.gmail-smtp-in.l.google.com",
965                "alt4.gmail-smtp-in.l.google.com",
966            ]),
967            "(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com".to_string()
968        );
969
970        assert_eq!(
971            factor_names(&[
972                "mta5.am0.yahoodns.net:123",
973                "mta6.am0.yahoodns.net:123",
974                "mta7.am0.yahoodns.net:123"
975            ]),
976            "(mta5|mta6|mta7).am0.yahoodns.net:123".to_string()
977        );
978        assert_eq!(
979            factor_names(&[
980                "mta5.am0.yahoodns.net:123",
981                "mta6.am0.yahoodns.net:456",
982                "mta7.am0.yahoodns.net:123"
983            ]),
984            "(mta5|mta6|mta7).am0.yahoodns.(net:123|net:456)".to_string()
985        );
986    }
987
988    /// Verify that the order is preserved and that we treat these two
989    /// examples of differently ordered sets of the same names as two
990    /// separate site name strings
991    #[test]
992    fn mx_order_name_factor() {
993        assert_eq!(
994            factor_names(&[
995                "example-com.mail.protection.outlook.com.",
996                "mx-biz.mail.am0.yahoodns.net.",
997                "mx-biz.mail.am0.yahoodns.net.",
998            ]),
999            "(example-com|mx-biz).mail.(protection|am0).(outlook|yahoodns).(com|net)".to_string()
1000        );
1001        assert_eq!(
1002            factor_names(&[
1003                "mx-biz.mail.am0.yahoodns.net.",
1004                "mx-biz.mail.am0.yahoodns.net.",
1005                "example-com.mail.protection.outlook.com.",
1006            ]),
1007            "(mx-biz|example-com).mail.(am0|protection).(yahoodns|outlook).(net|com)".to_string()
1008        );
1009    }
1010
1011    #[cfg(feature = "live-dns-tests")]
1012    #[tokio::test]
1013    async fn lookup_gmail_mx() {
1014        let mut gmail = (*MailExchanger::resolve("gmail.com").await.unwrap()).clone();
1015        gmail.expires.take();
1016        k9::snapshot!(
1017            &gmail,
1018            r#"
1019MailExchanger {
1020    domain_name: "gmail.com.",
1021    hosts: [
1022        "gmail-smtp-in.l.google.com.",
1023        "alt1.gmail-smtp-in.l.google.com.",
1024        "alt2.gmail-smtp-in.l.google.com.",
1025        "alt3.gmail-smtp-in.l.google.com.",
1026        "alt4.gmail-smtp-in.l.google.com.",
1027    ],
1028    site_name: "(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com",
1029    by_pref: {
1030        5: [
1031            "gmail-smtp-in.l.google.com.",
1032        ],
1033        10: [
1034            "alt1.gmail-smtp-in.l.google.com.",
1035        ],
1036        20: [
1037            "alt2.gmail-smtp-in.l.google.com.",
1038        ],
1039        30: [
1040            "alt3.gmail-smtp-in.l.google.com.",
1041        ],
1042        40: [
1043            "alt4.gmail-smtp-in.l.google.com.",
1044        ],
1045    },
1046    is_domain_literal: false,
1047    is_secure: false,
1048    is_mx: true,
1049    expires: None,
1050}
1051"#
1052        );
1053
1054        // This is a bad thing to have in a snapshot test really,
1055        // but the whole set of live-dns-tests are already inherently
1056        // unstable and flakey anyway.
1057        // The main thing we expect to see here is that the list of
1058        // names starts with alt4 and goes backwards through the priority
1059        // order such that the last element is gmail-smtp.
1060        // We expect the addresses within a given preference level to
1061        // be randomized, because that is what resolve_addresses does.
1062        k9::snapshot!(
1063            gmail.resolve_addresses().await,
1064            r#"
1065Addresses(
1066    [
1067        ResolvedAddress {
1068            name: "alt4.gmail-smtp-in.l.google.com.",
1069            addr: 2607:f8b0:4023:401::1b,
1070        },
1071        ResolvedAddress {
1072            name: "alt4.gmail-smtp-in.l.google.com.",
1073            addr: 173.194.77.27,
1074        },
1075        ResolvedAddress {
1076            name: "alt3.gmail-smtp-in.l.google.com.",
1077            addr: 2607:f8b0:4023:1::1a,
1078        },
1079        ResolvedAddress {
1080            name: "alt3.gmail-smtp-in.l.google.com.",
1081            addr: 172.253.113.26,
1082        },
1083        ResolvedAddress {
1084            name: "alt2.gmail-smtp-in.l.google.com.",
1085            addr: 2607:f8b0:4001:c1d::1b,
1086        },
1087        ResolvedAddress {
1088            name: "alt2.gmail-smtp-in.l.google.com.",
1089            addr: 74.125.126.27,
1090        },
1091        ResolvedAddress {
1092            name: "alt1.gmail-smtp-in.l.google.com.",
1093            addr: 2607:f8b0:4003:c04::1b,
1094        },
1095        ResolvedAddress {
1096            name: "alt1.gmail-smtp-in.l.google.com.",
1097            addr: 108.177.104.27,
1098        },
1099        ResolvedAddress {
1100            name: "gmail-smtp-in.l.google.com.",
1101            addr: 2607:f8b0:4023:c06::1b,
1102        },
1103        ResolvedAddress {
1104            name: "gmail-smtp-in.l.google.com.",
1105            addr: 142.251.2.26,
1106        },
1107    ],
1108)
1109"#
1110        );
1111    }
1112
1113    #[cfg(feature = "live-dns-tests")]
1114    #[tokio::test]
1115    async fn lookup_punycode_no_mx_only_a() {
1116        let mx = MailExchanger::resolve("xn--bb-eka.at").await.unwrap();
1117        assert_eq!(mx.domain_name, "xn--bb-eka.at.");
1118        assert_eq!(mx.hosts[0], "xn--bb-eka.at.");
1119    }
1120
1121    #[cfg(feature = "live-dns-tests")]
1122    #[tokio::test]
1123    async fn lookup_bogus_aasland() {
1124        let err = MailExchanger::resolve("not-mairs.aasland.com")
1125            .await
1126            .unwrap_err();
1127        k9::snapshot!(err, "MX lookup for not-mairs.aasland.com failed: NXDOMAIN");
1128    }
1129
1130    #[cfg(feature = "live-dns-tests")]
1131    #[tokio::test]
1132    async fn lookup_example_com() {
1133        // Has a NULL MX record
1134        let mx = MailExchanger::resolve("example.com").await.unwrap();
1135        k9::snapshot!(
1136            mx,
1137            r#"
1138MailExchanger {
1139    domain_name: "example.com.",
1140    hosts: [
1141        ".",
1142    ],
1143    site_name: "",
1144    by_pref: {
1145        0: [
1146            ".",
1147        ],
1148    },
1149    is_domain_literal: false,
1150    is_secure: true,
1151    is_mx: true,
1152}
1153"#
1154        );
1155    }
1156
1157    #[cfg(feature = "live-dns-tests")]
1158    #[tokio::test]
1159    async fn lookup_have_dane() {
1160        let mx = MailExchanger::resolve("do.havedane.net").await.unwrap();
1161        k9::snapshot!(
1162            mx,
1163            r#"
1164MailExchanger {
1165    domain_name: "do.havedane.net.",
1166    hosts: [
1167        "do.havedane.net.",
1168    ],
1169    site_name: "do.havedane.net",
1170    by_pref: {
1171        10: [
1172            "do.havedane.net.",
1173        ],
1174    },
1175    is_domain_literal: false,
1176    is_secure: true,
1177    is_mx: true,
1178}
1179"#
1180        );
1181    }
1182
1183    #[cfg(feature = "live-dns-tests")]
1184    #[tokio::test]
1185    async fn tlsa_have_dane() {
1186        let tlsa = resolve_dane("do.havedane.net", 25).await.unwrap();
1187        k9::snapshot!(
1188            tlsa,
1189            "
1190[
1191    TLSA {
1192        cert_usage: TrustAnchor,
1193        selector: Spki,
1194        matching: Sha256,
1195        cert_data: [
1196            39,
1197            182,
1198            148,
1199            181,
1200            29,
1201            31,
1202            239,
1203            136,
1204            133,
1205            55,
1206            42,
1207            207,
1208            179,
1209            145,
1210            147,
1211            117,
1212            151,
1213            34,
1214            183,
1215            54,
1216            176,
1217            66,
1218            104,
1219            100,
1220            220,
1221            28,
1222            121,
1223            208,
1224            101,
1225            31,
1226            239,
1227            115,
1228        ],
1229    },
1230    TLSA {
1231        cert_usage: DomainIssued,
1232        selector: Spki,
1233        matching: Sha256,
1234        cert_data: [
1235            85,
1236            58,
1237            207,
1238            136,
1239            249,
1240            238,
1241            24,
1242            204,
1243            170,
1244            230,
1245            53,
1246            202,
1247            84,
1248            15,
1249            50,
1250            203,
1251            132,
1252            172,
1253            167,
1254            124,
1255            71,
1256            145,
1257            102,
1258            130,
1259            188,
1260            181,
1261            66,
1262            213,
1263            29,
1264            170,
1265            135,
1266            31,
1267        ],
1268    },
1269]
1270"
1271        );
1272    }
1273
1274    #[cfg(feature = "live-dns-tests")]
1275    #[tokio::test]
1276    async fn mx_lookup_www_example_com() {
1277        // Has no MX, should fall back to A lookup
1278        let mx = MailExchanger::resolve("www.example.com").await.unwrap();
1279        k9::snapshot!(
1280            mx,
1281            r#"
1282MailExchanger {
1283    domain_name: "www.example.com.",
1284    hosts: [
1285        "www.example.com.",
1286    ],
1287    site_name: "www.example.com",
1288    by_pref: {
1289        1: [
1290            "www.example.com.",
1291        ],
1292    },
1293    is_domain_literal: false,
1294    is_secure: false,
1295    is_mx: false,
1296}
1297"#
1298        );
1299    }
1300
1301    #[cfg(feature = "live-dns-tests")]
1302    #[tokio::test]
1303    async fn txt_lookup_gmail() {
1304        let name = Name::from_str_relaxed("_mta-sts.gmail.com").unwrap();
1305        let answer = get_resolver().resolve(name, RecordType::TXT).await.unwrap();
1306        k9::snapshot!(
1307            answer.as_txt(),
1308            r#"
1309[
1310    "v=STSv1; id=20190429T010101;",
1311]
1312"#
1313        );
1314    }
1315}