Skip to main content

mail_auth/dns/
cache.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//! DNS cache traits and the per-call [`Parameters`] wrapper.
8//!
9//! The crate does not ship a cache implementation. Applications supply one by
10//! implementing [`ResolverCache`] for their cache type and [`DnsCache`] for a
11//! type that groups one cache per record type. The cache is then passed to a
12//! verifier through [`Parameters::with_cache`]. Keys are fully qualified,
13//! lowercased domain names (see [`ToFqdn`](super::ToFqdn)), or IP addresses
14//! for PTR records.
15//!
16//! # Example
17//!
18//! ```rust,no_run
19//! use mail_auth::{
20//!     AuthenticatedMessage, DnsCache, MessageAuthenticator, Mx, Negative, Parameters, RecordSet,
21//!     ResolverCache, TxtRecord,
22//! };
23//! use std::{
24//!     borrow::Borrow,
25//!     collections::HashMap,
26//!     hash::Hash,
27//!     net::{IpAddr, Ipv4Addr, Ipv6Addr},
28//!     sync::Mutex,
29//!     time::Instant,
30//! };
31//!
32//! struct MapCache<K, V>(Mutex<HashMap<K, (V, Instant)>>);
33//!
34//! impl<K: Hash + Eq, V: Clone> ResolverCache<K, V> for MapCache<K, V> {
35//!     fn get<Q>(&self, name: &Q) -> Option<V>
36//!     where
37//!         K: Borrow<Q>,
38//!         Q: Hash + Eq + ?Sized,
39//!     {
40//!         let map = self.0.lock().unwrap();
41//!         let (value, expires) = map.get(name)?;
42//!         (*expires > Instant::now()).then(|| value.clone())
43//!     }
44//!
45//!     fn remove<Q>(&self, name: &Q) -> Option<V>
46//!     where
47//!         K: Borrow<Q>,
48//!         Q: Hash + Eq + ?Sized,
49//!     {
50//!         self.0.lock().unwrap().remove(name).map(|(value, _)| value)
51//!     }
52//!
53//!     fn insert(&self, key: K, value: V, valid_until: Instant) {
54//!         self.0.lock().unwrap().insert(key, (value, valid_until));
55//!     }
56//! }
57//!
58//! type Answer<T> = Result<RecordSet<T>, Negative>;
59//!
60//! struct Caches {
61//!     txt: MapCache<Box<str>, TxtRecord>,
62//!     mx: MapCache<Box<str>, Answer<Mx>>,
63//!     ipv4: MapCache<Box<str>, Answer<Ipv4Addr>>,
64//!     ipv6: MapCache<Box<str>, Answer<Ipv6Addr>>,
65//!     ptr: MapCache<IpAddr, Answer<Box<str>>>,
66//! }
67//!
68//! impl DnsCache for Caches {
69//!     type Txt = MapCache<Box<str>, TxtRecord>;
70//!     type Mx = MapCache<Box<str>, Answer<Mx>>;
71//!     type Ipv4 = MapCache<Box<str>, Answer<Ipv4Addr>>;
72//!     type Ipv6 = MapCache<Box<str>, Answer<Ipv6Addr>>;
73//!     type Ptr = MapCache<IpAddr, Answer<Box<str>>>;
74//!
75//!     fn txt(&self) -> Option<&Self::Txt> { Some(&self.txt) }
76//!     fn mx(&self) -> Option<&Self::Mx> { Some(&self.mx) }
77//!     fn ipv4(&self) -> Option<&Self::Ipv4> { Some(&self.ipv4) }
78//!     fn ipv6(&self) -> Option<&Self::Ipv6> { Some(&self.ipv6) }
79//!     fn ptr(&self) -> Option<&Self::Ptr> { Some(&self.ptr) }
80//! }
81//!
82//! # async fn run(authenticator: &MessageAuthenticator, caches: &Caches, raw: &[u8]) {
83//! let message = AuthenticatedMessage::parse(raw).unwrap();
84//! let results = authenticator
85//!     .verify_dkim(Parameters::new(&message).with_cache(caches))
86//!     .await;
87//! # }
88//! ```
89
90use super::{Mx, Negative, RecordSet, TxtRecord};
91use crate::Instant;
92use std::{
93    borrow::Borrow,
94    hash::Hash,
95    net::{IpAddr, Ipv4Addr, Ipv6Addr},
96};
97
98/// A cache for one DNS record type, keyed by `K` and holding values of type
99/// `V`.
100///
101/// Methods take `&self`, so implementations that mutate must use interior
102/// mutability (a lock or a concurrent map). The lookup helpers call
103/// [`get`](Self::get) before querying and [`insert`](Self::insert) after a
104/// query that returned records or a cacheable negative answer; they do not
105/// check expiry themselves. See the [module documentation](self) for an
106/// example.
107pub trait ResolverCache<K, V>: Sized {
108    /// Returns a copy of the cached value for `name`, or `None` if there is
109    /// none. Implementations should return `None` for expired entries.
110    fn get<Q>(&self, name: &Q) -> Option<V>
111    where
112        K: Borrow<Q>,
113        Q: Hash + Eq + ?Sized;
114    /// Removes the entry for `name` and returns its value. Not called by this
115    /// crate; available to applications that invalidate entries.
116    fn remove<Q>(&self, name: &Q) -> Option<V>
117    where
118        K: Borrow<Q>,
119        Q: Hash + Eq + ?Sized;
120    /// Stores `value` under `key`. `valid_until` is the expiry time derived
121    /// from the TTLs of the DNS answer, or from its negative TTL for a
122    /// negative answer.
123    fn insert(&self, key: K, value: V, valid_until: Instant);
124}
125
126/// A set of DNS caches, one per record type, passed to verifiers through
127/// [`Parameters::with_cache`].
128///
129/// An accessor that returns `None` disables caching for that record type.
130/// See the [module documentation](self) for an example.
131pub trait DnsCache {
132    /// Cache for parsed TXT records (SPF, DKIM, DMARC, ATPS, MTA-STS,
133    /// TLSRPT), keyed by fully qualified domain name.
134    type Txt: ResolverCache<Box<str>, TxtRecord>;
135    /// Cache for MX records and negative MX answers, keyed by fully
136    /// qualified domain name.
137    type Mx: ResolverCache<Box<str>, Result<RecordSet<Mx>, Negative>>;
138    /// Cache for A records and negative A answers, keyed by fully qualified
139    /// domain name.
140    type Ipv4: ResolverCache<Box<str>, Result<RecordSet<Ipv4Addr>, Negative>>;
141    /// Cache for AAAA records and negative AAAA answers, keyed by fully
142    /// qualified domain name.
143    type Ipv6: ResolverCache<Box<str>, Result<RecordSet<Ipv6Addr>, Negative>>;
144    /// Cache for PTR records and negative PTR answers, keyed by IP address.
145    type Ptr: ResolverCache<IpAddr, Result<RecordSet<Box<str>>, Negative>>;
146
147    /// Returns the TXT record cache, or `None` to disable it.
148    fn txt(&self) -> Option<&Self::Txt>;
149    /// Returns the MX record cache, or `None` to disable it.
150    fn mx(&self) -> Option<&Self::Mx>;
151    /// Returns the A record cache, or `None` to disable it.
152    fn ipv4(&self) -> Option<&Self::Ipv4>;
153    /// Returns the AAAA record cache, or `None` to disable it.
154    fn ipv6(&self) -> Option<&Self::Ipv6>;
155    /// Returns the PTR record cache, or `None` to disable it.
156    fn ptr(&self) -> Option<&Self::Ptr>;
157}
158
159/// A cache that stores nothing.
160///
161/// Implements both [`ResolverCache`] (every lookup misses) and [`DnsCache`]
162/// (every accessor returns `None`). It is the default cache type of
163/// [`Parameters`].
164#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
165pub struct NoCache;
166
167impl<K, V> ResolverCache<K, V> for NoCache {
168    fn get<Q>(&self, _: &Q) -> Option<V>
169    where
170        K: Borrow<Q>,
171        Q: Hash + Eq + ?Sized,
172    {
173        None
174    }
175
176    fn remove<Q>(&self, _: &Q) -> Option<V>
177    where
178        K: Borrow<Q>,
179        Q: Hash + Eq + ?Sized,
180    {
181        None
182    }
183
184    fn insert(&self, _: K, _: V, _: Instant) {}
185}
186
187impl DnsCache for NoCache {
188    type Txt = NoCache;
189    type Mx = NoCache;
190    type Ipv4 = NoCache;
191    type Ipv6 = NoCache;
192    type Ptr = NoCache;
193
194    fn txt(&self) -> Option<&Self::Txt> {
195        None
196    }
197
198    fn mx(&self) -> Option<&Self::Mx> {
199        None
200    }
201
202    fn ipv4(&self) -> Option<&Self::Ipv4> {
203        None
204    }
205
206    fn ipv6(&self) -> Option<&Self::Ipv6> {
207        None
208    }
209
210    fn ptr(&self) -> Option<&Self::Ptr> {
211        None
212    }
213}
214
215/// The input of a verifier, optionally paired with a DNS cache.
216///
217/// Every verifier on [`MessageAuthenticator`](crate::MessageAuthenticator)
218/// takes `impl Into<Parameters<..>>`, so the bare input (a message, an IP
219/// address, SPF or DMARC parameters) can be passed directly when no cache is
220/// needed. Wrap it with [`Parameters::new`] and
221/// [`with_cache`](Self::with_cache) to use a cache.
222pub struct Parameters<'x, P, C: DnsCache = NoCache> {
223    /// The verifier input.
224    pub input: P,
225    /// The DNS cache, or `None` to query without caching.
226    pub cache: Option<&'x C>,
227}
228
229impl<P> Parameters<'_, P, NoCache> {
230    /// Wraps `input` without a cache.
231    pub fn new(input: P) -> Self {
232        Parameters { input, cache: None }
233    }
234}
235
236impl<'x, P, C: DnsCache> Parameters<'x, P, C> {
237    /// Sets the DNS cache, replacing any cache set before.
238    pub fn with_cache<NewC: DnsCache>(self, cache: &'x NewC) -> Parameters<'x, P, NewC> {
239        Parameters {
240            input: self.input,
241            cache: Some(cache),
242        }
243    }
244
245    /// Returns parameters with a different input and the same cache. Useful
246    /// for running several verifiers against one cache.
247    pub fn clone_with<NewP>(&self, input: NewP) -> Parameters<'x, NewP, C> {
248        Parameters {
249            input,
250            cache: self.cache,
251        }
252    }
253
254    #[inline(always)]
255    pub(crate) fn txt_cache(&self) -> Option<&'x C::Txt> {
256        self.cache.and_then(DnsCache::txt)
257    }
258
259    #[inline(always)]
260    pub(crate) fn mx_cache(&self) -> Option<&'x C::Mx> {
261        self.cache.and_then(DnsCache::mx)
262    }
263
264    #[inline(always)]
265    pub(crate) fn ipv4_cache(&self) -> Option<&'x C::Ipv4> {
266        self.cache.and_then(DnsCache::ipv4)
267    }
268
269    #[inline(always)]
270    pub(crate) fn ipv6_cache(&self) -> Option<&'x C::Ipv6> {
271        self.cache.and_then(DnsCache::ipv6)
272    }
273
274    #[inline(always)]
275    pub(crate) fn ptr_cache(&self) -> Option<&'x C::Ptr> {
276        self.cache.and_then(DnsCache::ptr)
277    }
278}
279
280#[cfg(test)]
281pub(crate) mod test {
282    use crate::dns::{
283        DnsCache, DnssecStatus, Mx, Negative, Parameters, RecordSet, ResolverCache, ToFqdn,
284        TxtRecord,
285    };
286    use std::{
287        borrow::Borrow,
288        hash::Hash,
289        net::{IpAddr, Ipv4Addr, Ipv6Addr},
290        sync::Arc,
291    };
292
293    pub(crate) struct DummyCache<K, V>(std::sync::Mutex<std::collections::HashMap<K, V>>);
294
295    impl<K: Hash + Eq, V: Clone> DummyCache<K, V> {
296        pub fn new() -> Self {
297            DummyCache(std::sync::Mutex::new(std::collections::HashMap::new()))
298        }
299    }
300
301    impl<K: Hash + Eq, V: Clone> ResolverCache<K, V> for DummyCache<K, V> {
302        fn get<Q>(&self, key: &Q) -> Option<V>
303        where
304            K: Borrow<Q>,
305            Q: Hash + Eq + ?Sized,
306        {
307            self.0.lock().unwrap().get(key).cloned()
308        }
309
310        fn remove<Q>(&self, key: &Q) -> Option<V>
311        where
312            K: Borrow<Q>,
313            Q: Hash + Eq + ?Sized,
314        {
315            self.0.lock().unwrap().remove(key)
316        }
317
318        fn insert(&self, key: K, value: V, _: std::time::Instant) {
319            self.0.lock().unwrap().insert(key, value);
320        }
321    }
322
323    type Answer<T> = Result<RecordSet<T>, Negative>;
324
325    pub(crate) struct DummyCaches {
326        pub txt: DummyCache<Box<str>, TxtRecord>,
327        pub mx: DummyCache<Box<str>, Answer<Mx>>,
328        pub ptr: DummyCache<IpAddr, Answer<Box<str>>>,
329        pub ipv4: DummyCache<Box<str>, Answer<Ipv4Addr>>,
330        pub ipv6: DummyCache<Box<str>, Answer<Ipv6Addr>>,
331    }
332
333    impl DummyCaches {
334        pub fn new() -> Self {
335            Self {
336                txt: DummyCache::new(),
337                mx: DummyCache::new(),
338                ptr: DummyCache::new(),
339                ipv4: DummyCache::new(),
340                ipv6: DummyCache::new(),
341            }
342        }
343
344        pub fn with_txt(
345            self,
346            name: impl ToFqdn,
347            value: impl Into<TxtRecord>,
348            valid_until: std::time::Instant,
349        ) -> Self {
350            self.txt.insert(
351                name.to_fqdn().into_owned().into_boxed_str(),
352                value.into(),
353                valid_until,
354            );
355            self
356        }
357
358        pub fn txt_add(
359            &self,
360            name: impl ToFqdn,
361            value: impl Into<TxtRecord>,
362            valid_until: std::time::Instant,
363        ) {
364            self.txt.insert(
365                name.to_fqdn().into_owned().into_boxed_str(),
366                value.into(),
367                valid_until,
368            );
369        }
370
371        pub fn ipv4_add(
372            &self,
373            name: impl ToFqdn,
374            value: Vec<Ipv4Addr>,
375            valid_until: std::time::Instant,
376        ) {
377            self.ipv4.insert(
378                name.to_fqdn().into_owned().into_boxed_str(),
379                Ok(RecordSet {
380                    records: Arc::from(value.into_boxed_slice()),
381                    dnssec_status: DnssecStatus::Indeterminate,
382                }),
383                valid_until,
384            );
385        }
386
387        pub fn ipv6_add(
388            &self,
389            name: impl ToFqdn,
390            value: Vec<Ipv6Addr>,
391            valid_until: std::time::Instant,
392        ) {
393            self.ipv6.insert(
394                name.to_fqdn().into_owned().into_boxed_str(),
395                Ok(RecordSet {
396                    records: Arc::from(value.into_boxed_slice()),
397                    dnssec_status: DnssecStatus::Indeterminate,
398                }),
399                valid_until,
400            );
401        }
402
403        pub fn ptr_add(&self, name: IpAddr, value: Vec<Box<str>>, valid_until: std::time::Instant) {
404            self.ptr.insert(
405                name,
406                Ok(RecordSet {
407                    records: Arc::from(value.into_boxed_slice()),
408                    dnssec_status: DnssecStatus::Indeterminate,
409                }),
410                valid_until,
411            );
412        }
413
414        pub fn mx_add(&self, name: impl ToFqdn, value: Vec<Mx>, valid_until: std::time::Instant) {
415            self.mx.insert(
416                name.to_fqdn().into_owned().into_boxed_str(),
417                Ok(RecordSet {
418                    records: Arc::from(value.into_boxed_slice()),
419                    dnssec_status: DnssecStatus::Indeterminate,
420                }),
421                valid_until,
422            );
423        }
424
425        pub fn parameters<T>(&self, param: T) -> Parameters<'_, T, DummyCaches> {
426            Parameters::new(param).with_cache(self)
427        }
428    }
429
430    impl DnsCache for DummyCaches {
431        type Txt = DummyCache<Box<str>, TxtRecord>;
432        type Mx = DummyCache<Box<str>, Answer<Mx>>;
433        type Ipv4 = DummyCache<Box<str>, Answer<Ipv4Addr>>;
434        type Ipv6 = DummyCache<Box<str>, Answer<Ipv6Addr>>;
435        type Ptr = DummyCache<IpAddr, Answer<Box<str>>>;
436
437        fn txt(&self) -> Option<&Self::Txt> {
438            Some(&self.txt)
439        }
440
441        fn mx(&self) -> Option<&Self::Mx> {
442            Some(&self.mx)
443        }
444
445        fn ipv4(&self) -> Option<&Self::Ipv4> {
446            Some(&self.ipv4)
447        }
448
449        fn ipv6(&self) -> Option<&Self::Ipv6> {
450            Some(&self.ipv6)
451        }
452
453        fn ptr(&self) -> Option<&Self::Ptr> {
454            Some(&self.ptr)
455        }
456    }
457}