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}