Skip to main content

mail_auth/dns/
lookup.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7//! Cached DNS lookup helpers on [`MessageAuthenticator`].
8
9use super::{
10    DnsEntry, DnssecStatus, IpLookupStrategy, Mx, Negative, RecordSet, ResolverCache, ToFqdn,
11    TxtRecord, TxtRecordParser, UnwrapTxtRecord,
12};
13use crate::{Error, MessageAuthenticator};
14use std::{
15    net::{IpAddr, Ipv4Addr, Ipv6Addr},
16    sync::Arc,
17};
18
19impl MessageAuthenticator {
20    /// Queries the TXT records of `key` and returns their contents
21    /// concatenated into one byte string, without parsing or caching.
22    ///
23    /// # Errors
24    ///
25    /// Returns [`Error::Dns`] with [`DnsError::RecordNotFound`](crate::DnsError::RecordNotFound)
26    /// when the name has no records of this type, [`DnsError::Resolver`](crate::DnsError::Resolver)
27    /// when the query fails, or [`Error::Parse`] when the name is not a valid
28    /// DNS name.
29    pub async fn txt_raw_lookup(&self, key: impl ToFqdn) -> crate::Result<Vec<u8>> {
30        let key = key.to_fqdn();
31        Ok(self
32            .query_txt(key.as_ref())
33            .await?
34            .entry
35            .into_iter()
36            .flatten()
37            .collect())
38    }
39
40    /// Queries the TXT records of `key` and returns the first one that parses
41    /// as `T` (for example [`SpfRecord`](crate::spf::SpfRecord),
42    /// [`DmarcRecord`](crate::dmarc::DmarcRecord) or
43    /// [`TlsRptRecord`](crate::mta_sts::TlsRptRecord)).
44    ///
45    /// When `cache` is set, a cached entry is returned without querying, and
46    /// the parse outcome (the record or the parse error) is stored after a
47    /// query. A negative answer that carries an SOA record is stored as
48    /// [`TxtRecord::Error`] until its negative TTL expires.
49    ///
50    /// # Errors
51    ///
52    /// Returns [`Error::Dns`] with [`DnsError::RecordNotFound`](crate::DnsError::RecordNotFound)
53    /// when the name has no records of this type, [`DnsError::Resolver`](crate::DnsError::Resolver)
54    /// when the query fails, or [`Error::Parse`] when the name is not a valid
55    /// DNS name.
56    /// When no record parses as `T`, returns the error of the last parse
57    /// attempt (usually [`DnsError::InvalidRecordType`](crate::DnsError::InvalidRecordType)),
58    /// or `InvalidRecordType` when the answer holds no TXT data.
59    pub async fn txt_lookup<T: TxtRecordParser + Into<TxtRecord> + UnwrapTxtRecord>(
60        &self,
61        key: impl ToFqdn,
62        cache: Option<&impl ResolverCache<Box<str>, TxtRecord>>,
63    ) -> crate::Result<Arc<T>> {
64        let key = key.to_fqdn();
65        if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
66            return T::unwrap_txt(value);
67        }
68
69        let DnsEntry {
70            entry: records,
71            expires,
72        } = match self.resolve_txt(key.as_ref()).await {
73            Ok(entry) => entry,
74            Err(err) => {
75                return Err(self.cache_negative(
76                    err,
77                    cache,
78                    || key.into_owned().into_boxed_str(),
79                    |negative| TxtRecord::Error(negative.into()),
80                ));
81            }
82        };
83
84        let result: TxtRecord = T::parse_records(&records).into();
85
86        if let Some(cache) = cache {
87            cache.insert(key.into_owned().into_boxed_str(), result.clone(), expires);
88        }
89
90        T::unwrap_txt(result)
91    }
92
93    /// Queries the MX records of `key` (RFC 5321 Section 5.1).
94    ///
95    /// Records are grouped by preference into [`Mx`] values sorted by
96    /// ascending preference. When `cache` is set, it is consulted before and
97    /// updated after the query; a negative answer that carries an SOA record
98    /// is stored as `Err(`[`Negative`]`)` until its negative TTL expires.
99    ///
100    /// # Errors
101    ///
102    /// Returns [`Error::Dns`] with [`DnsError::RecordNotFound`](crate::DnsError::RecordNotFound)
103    /// when the name has no records of this type, [`DnsError::Resolver`](crate::DnsError::Resolver)
104    /// when the query fails, or [`Error::Parse`] when the name is not a valid
105    /// DNS name.
106    pub async fn mx_lookup(
107        &self,
108        key: impl ToFqdn,
109        cache: Option<&impl ResolverCache<Box<str>, Result<RecordSet<Mx>, Negative>>>,
110    ) -> crate::Result<RecordSet<Mx>> {
111        let key = key.to_fqdn();
112        if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
113            return value.map_err(Error::from);
114        }
115
116        let DnsEntry {
117            entry: mx_records,
118            expires,
119        } = match self.resolve_mx(key.as_ref()).await {
120            Ok(entry) => entry,
121            Err(err) => {
122                return Err(self.cache_negative(
123                    err,
124                    cache,
125                    || key.into_owned().into_boxed_str(),
126                    Err,
127                ));
128            }
129        };
130
131        let mut records: Vec<(u16, Vec<Box<str>>)> = Vec::with_capacity(mx_records.len());
132        for (preference, exchange) in mx_records {
133            if let Some(record) = records.iter_mut().find(|r| r.0 == preference) {
134                record.1.push(exchange);
135            } else {
136                records.push((preference, vec![exchange]));
137            }
138        }
139
140        records.sort_unstable_by_key(|a| a.0);
141        let records: Arc<[Mx]> = records
142            .into_iter()
143            .map(|(preference, exchanges)| Mx {
144                preference,
145                exchanges: exchanges.into_boxed_slice(),
146            })
147            .collect::<Arc<[Mx]>>();
148        let records = RecordSet {
149            records,
150            dnssec_status: DnssecStatus::Indeterminate,
151        };
152
153        if let Some(cache) = cache {
154            cache.insert(
155                key.into_owned().into_boxed_str(),
156                Ok(records.clone()),
157                expires,
158            );
159        }
160
161        Ok(records)
162    }
163
164    /// Queries the A records of `key`. When `cache` is set, it is consulted
165    /// before and updated after the query; a negative answer that carries an
166    /// SOA record is stored as `Err(`[`Negative`]`)` until its negative TTL
167    /// expires.
168    ///
169    /// # Errors
170    ///
171    /// Returns [`Error::Dns`] with [`DnsError::RecordNotFound`](crate::DnsError::RecordNotFound)
172    /// when the name has no records of this type, [`DnsError::Resolver`](crate::DnsError::Resolver)
173    /// when the query fails, or [`Error::Parse`] when the name is not a valid
174    /// DNS name.
175    pub async fn ipv4_lookup(
176        &self,
177        key: impl ToFqdn,
178        cache: Option<&impl ResolverCache<Box<str>, Result<RecordSet<Ipv4Addr>, Negative>>>,
179    ) -> crate::Result<RecordSet<Ipv4Addr>> {
180        let key = key.to_fqdn();
181        if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
182            return value.map_err(Error::from);
183        }
184
185        let ipv4_lookup = match self.resolve_ipv4(key.as_ref()).await {
186            Ok(entry) => entry,
187            Err(err) => {
188                return Err(self.cache_negative(
189                    err,
190                    cache,
191                    || key.into_owned().into_boxed_str(),
192                    Err,
193                ));
194            }
195        };
196        let records = RecordSet {
197            records: ipv4_lookup.entry,
198            dnssec_status: DnssecStatus::Indeterminate,
199        };
200
201        if let Some(cache) = cache {
202            cache.insert(
203                key.into_owned().into_boxed_str(),
204                Ok(records.clone()),
205                ipv4_lookup.expires,
206            );
207        }
208
209        Ok(records)
210    }
211
212    /// Queries the A records of `key` without caching. `key` is queried as
213    /// given, without conversion to a fully qualified name. Returns the
214    /// addresses with their expiry time.
215    ///
216    /// # Errors
217    ///
218    /// Returns [`Error::Dns`] with [`DnsError::RecordNotFound`](crate::DnsError::RecordNotFound)
219    /// when the name has no records of this type, [`DnsError::Resolver`](crate::DnsError::Resolver)
220    /// when the query fails, or [`Error::Parse`] when the name is not a valid
221    /// DNS name.
222    pub async fn ipv4_lookup_raw(&self, key: &str) -> crate::Result<DnsEntry<Arc<[Ipv4Addr]>>> {
223        self.resolve_ipv4(key).await.map_err(Error::from)
224    }
225
226    /// Queries the AAAA records of `key`. When `cache` is set, it is consulted
227    /// before and updated after the query; a negative answer that carries an
228    /// SOA record is stored as `Err(`[`Negative`]`)` until its negative TTL
229    /// expires.
230    ///
231    /// # Errors
232    ///
233    /// Returns [`Error::Dns`] with [`DnsError::RecordNotFound`](crate::DnsError::RecordNotFound)
234    /// when the name has no records of this type, [`DnsError::Resolver`](crate::DnsError::Resolver)
235    /// when the query fails, or [`Error::Parse`] when the name is not a valid
236    /// DNS name.
237    pub async fn ipv6_lookup(
238        &self,
239        key: impl ToFqdn,
240        cache: Option<&impl ResolverCache<Box<str>, Result<RecordSet<Ipv6Addr>, Negative>>>,
241    ) -> crate::Result<RecordSet<Ipv6Addr>> {
242        let key = key.to_fqdn();
243        if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
244            return value.map_err(Error::from);
245        }
246
247        let ipv6_lookup = match self.resolve_ipv6(key.as_ref()).await {
248            Ok(entry) => entry,
249            Err(err) => {
250                return Err(self.cache_negative(
251                    err,
252                    cache,
253                    || key.into_owned().into_boxed_str(),
254                    Err,
255                ));
256            }
257        };
258        let records = RecordSet {
259            records: ipv6_lookup.entry,
260            dnssec_status: DnssecStatus::Indeterminate,
261        };
262
263        if let Some(cache) = cache {
264            cache.insert(
265                key.into_owned().into_boxed_str(),
266                Ok(records.clone()),
267                ipv6_lookup.expires,
268            );
269        }
270
271        Ok(records)
272    }
273
274    /// Queries the AAAA records of `key` without caching. `key` is queried as
275    /// given, without conversion to a fully qualified name. Returns the
276    /// addresses with their expiry time.
277    ///
278    /// # Errors
279    ///
280    /// Returns [`Error::Dns`] with [`DnsError::RecordNotFound`](crate::DnsError::RecordNotFound)
281    /// when the name has no records of this type, [`DnsError::Resolver`](crate::DnsError::Resolver)
282    /// when the query fails, or [`Error::Parse`] when the name is not a valid
283    /// DNS name.
284    pub async fn ipv6_lookup_raw(&self, key: &str) -> crate::Result<DnsEntry<Arc<[Ipv6Addr]>>> {
285        self.resolve_ipv6(key).await.map_err(Error::from)
286    }
287
288    /// Resolves `key` to at most `max_results` IP addresses, querying A and
289    /// AAAA records in the order given by `strategy`.
290    ///
291    /// With a fallback strategy ([`IpLookupStrategy::Ipv4thenIpv6`] or
292    /// [`IpLookupStrategy::Ipv6thenIpv4`]), any error from the first query
293    /// triggers the second one. The caches are consulted before and updated
294    /// after each query.
295    ///
296    /// # Errors
297    ///
298    /// Returns the error of the last query performed. See
299    /// [`ipv4_lookup`](Self::ipv4_lookup) for the possible errors.
300    pub async fn ip_lookup(
301        &self,
302        key: &str,
303        mut strategy: IpLookupStrategy,
304        max_results: usize,
305        cache_ipv4: Option<&impl ResolverCache<Box<str>, Result<RecordSet<Ipv4Addr>, Negative>>>,
306        cache_ipv6: Option<&impl ResolverCache<Box<str>, Result<RecordSet<Ipv6Addr>, Negative>>>,
307    ) -> crate::Result<Vec<IpAddr>> {
308        loop {
309            match strategy {
310                IpLookupStrategy::Ipv4Only | IpLookupStrategy::Ipv4thenIpv6 => {
311                    match (self.ipv4_lookup(key, cache_ipv4).await, strategy) {
312                        (Ok(result), _) => {
313                            return Ok(result
314                                .records
315                                .iter()
316                                .take(max_results)
317                                .copied()
318                                .map(IpAddr::from)
319                                .collect());
320                        }
321                        (Err(err), IpLookupStrategy::Ipv4Only) => return Err(err),
322                        _ => {
323                            strategy = IpLookupStrategy::Ipv6Only;
324                        }
325                    }
326                }
327                IpLookupStrategy::Ipv6Only | IpLookupStrategy::Ipv6thenIpv4 => {
328                    match (self.ipv6_lookup(key, cache_ipv6).await, strategy) {
329                        (Ok(result), _) => {
330                            return Ok(result
331                                .records
332                                .iter()
333                                .take(max_results)
334                                .copied()
335                                .map(IpAddr::from)
336                                .collect());
337                        }
338                        (Err(err), IpLookupStrategy::Ipv6Only) => return Err(err),
339                        _ => {
340                            strategy = IpLookupStrategy::Ipv4Only;
341                        }
342                    }
343                }
344            }
345        }
346    }
347
348    /// Queries the PTR records of `addr` (reverse DNS). Host names are
349    /// returned lowercased. When `cache` is set, it is consulted before and
350    /// updated after the query; a negative answer that carries an SOA record
351    /// is stored as `Err(`[`Negative`]`)` until its negative TTL expires.
352    ///
353    /// # Errors
354    ///
355    /// Returns [`Error::Dns`] with [`DnsError::RecordNotFound`](crate::DnsError::RecordNotFound)
356    /// when the address has no PTR record, or [`DnsError::Resolver`](crate::DnsError::Resolver)
357    /// when the query fails.
358    pub async fn ptr_lookup(
359        &self,
360        addr: IpAddr,
361        cache: Option<&impl ResolverCache<IpAddr, Result<RecordSet<Box<str>>, Negative>>>,
362    ) -> crate::Result<RecordSet<Box<str>>> {
363        if let Some(value) = cache.as_ref().and_then(|c| c.get(&addr)) {
364            return value.map_err(Error::from);
365        }
366
367        let DnsEntry { entry, expires } = match self.resolve_ptr(addr).await {
368            Ok(entry) => entry,
369            Err(err) => return Err(self.cache_negative(err, cache, || addr, Err)),
370        };
371        let ptr = RecordSet {
372            records: entry,
373            dnssec_status: DnssecStatus::Indeterminate,
374        };
375
376        if let Some(cache) = cache {
377            cache.insert(addr, Ok(ptr.clone()), expires);
378        }
379
380        Ok(ptr)
381    }
382
383    /// Returns `true` if `key` has at least one A or AAAA record, as needed by
384    /// the SPF `exists` mechanism (RFC 7208 Section 5.7).
385    ///
386    /// Returns `true` without querying when a cache holds an A or AAAA answer
387    /// with records, and `false` without querying when the caches hold a
388    /// negative or empty answer for both types. Query results are not added
389    /// to the caches.
390    ///
391    /// # Errors
392    ///
393    /// Returns [`Error::Dns`] with [`DnsError::Resolver`](crate::DnsError::Resolver)
394    /// when a query fails, or [`Error::Parse`] when the name is not a valid
395    /// DNS name. A name without records is not an error.
396    pub async fn exists(
397        &self,
398        key: impl ToFqdn,
399        cache_ipv4: Option<&impl ResolverCache<Box<str>, Result<RecordSet<Ipv4Addr>, Negative>>>,
400        cache_ipv6: Option<&impl ResolverCache<Box<str>, Result<RecordSet<Ipv6Addr>, Negative>>>,
401    ) -> crate::Result<bool> {
402        let key = key.to_fqdn();
403        match cached_exists(key.as_ref(), cache_ipv4, cache_ipv6) {
404            Some(exists) => Ok(exists),
405            None => self.resolve_exists(key.as_ref()).await,
406        }
407    }
408}
409
410fn cached_exists(
411    key: &str,
412    cache_ipv4: Option<&impl ResolverCache<Box<str>, Result<RecordSet<Ipv4Addr>, Negative>>>,
413    cache_ipv6: Option<&impl ResolverCache<Box<str>, Result<RecordSet<Ipv6Addr>, Negative>>>,
414) -> Option<bool> {
415    let ipv4 = cache_ipv4
416        .and_then(|cache| cache.get::<str>(key))
417        .map(|entry| entry.is_ok_and(|set| !set.records.is_empty()));
418    if ipv4 == Some(true) {
419        return Some(true);
420    }
421    let ipv6 = cache_ipv6
422        .and_then(|cache| cache.get::<str>(key))
423        .map(|entry| entry.is_ok_and(|set| !set.records.is_empty()));
424    match (ipv4, ipv6) {
425        (_, Some(true)) => Some(true),
426        (Some(false), Some(false)) => Some(false),
427        _ => None,
428    }
429}