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}