Skip to main content

hickory_resolver/
cache.rs

1//! A cache for DNS responses.
2
3use std::{
4    collections::HashMap,
5    ops::RangeInclusive,
6    sync::Arc,
7    time::{Duration, Instant},
8};
9
10use moka::{Expiry, sync::Cache};
11#[cfg(feature = "serde")]
12use serde::Deserialize;
13
14use crate::{
15    config,
16    net::{DnsError, NetError, NoRecords},
17    proto::{
18        op::{Message, Query},
19        rr::RecordType,
20    },
21};
22#[cfg(feature = "__dnssec")]
23use crate::{net::dnssec::BOGUS_CACHE_TTL, proto::dnssec::DnssecSummary};
24
25/// A cache for DNS responses.
26#[derive(Clone, Debug)]
27pub struct ResponseCache {
28    cache: Cache<Query, Entry>,
29    ttl_config: Arc<TtlConfig>,
30}
31
32impl ResponseCache {
33    /// Construct a new response cache.
34    ///
35    /// # Arguments
36    ///
37    /// * `capacity` - size in number of cached responses
38    /// * `ttl_config` - minimum and maximum TTLs for cached records
39    pub fn new(capacity: u64, ttl_config: TtlConfig) -> Self {
40        Self {
41            cache: Cache::builder()
42                .max_capacity(capacity)
43                .expire_after(EntryExpiry)
44                .build(),
45            ttl_config: Arc::new(ttl_config),
46        }
47    }
48
49    /// Insert a response into the cache.
50    pub fn insert(&self, query: Query, result: Result<Message, NetError>, now: Instant) {
51        self.upsert_clamped_ttl(query, result, now, false)
52    }
53
54    /// Inserts a message into the cache, but may clamp the TTL of the entry to that of the existing
55    /// entry, if any.
56    ///
57    /// If `clamp_valid_until` is true, the cache expiration time of any existing entry will not be
58    /// extended when the entry's contents are replaced. This is used in a mitigation for ghost
59    /// domain attacks, where queries for an apex NS RRset keep a cache entry warm, without ever
60    /// checking the referral in the parent zone. This can be eliminated once authoritative
61    /// responses and referral responses are cached separately.
62    pub(super) fn upsert_clamped_ttl(
63        &self,
64        query: Query,
65        result: Result<Message, NetError>,
66        now: Instant,
67        clamp_valid_until: bool,
68    ) {
69        let (ttl, result) = match result {
70            Ok(mut message) => {
71                let ttl = self.clamp_positive_ttls(query.query_type(), &mut message);
72                (ttl, Ok(message))
73            }
74            Err(NetError::Dns(DnsError::NoRecordsFound(no_records))) => {
75                let (negative_min_ttl, negative_max_ttl) = self
76                    .ttl_config
77                    .negative_response_ttl_bounds(query.query_type())
78                    .into_inner();
79                let ttl = if let Some(ttl) = no_records.negative_ttl {
80                    Duration::from_secs(u64::from(ttl)).clamp(negative_min_ttl, negative_max_ttl)
81                } else {
82                    negative_min_ttl
83                };
84                (
85                    ttl,
86                    Err(NetError::Dns(DnsError::NoRecordsFound(no_records))),
87                )
88            }
89            Err(_) => return,
90        };
91        let valid_until = now + ttl;
92        if clamp_valid_until {
93            self.cache.entry(query).and_upsert_with(|entry_opt| Entry {
94                result: Arc::new(result),
95                original_time: now,
96                valid_until: entry_opt.map_or(valid_until, |entry| {
97                    entry.value().valid_until.min(valid_until)
98                }),
99            });
100        } else {
101            self.cache.insert(
102                query,
103                Entry {
104                    result: Arc::new(result),
105                    original_time: now,
106                    valid_until,
107                },
108            );
109        }
110    }
111
112    /// Try to retrieve a cached response with the given query.
113    pub fn get(&self, query: &Query, now: Instant) -> Option<Result<Message, NetError>> {
114        let entry = self.cache.get(query)?;
115        if !entry.is_current(now) {
116            return None;
117        }
118        Some(entry.updated_ttl(now))
119    }
120
121    /// Clamp all record TTLs to `[positive_min_ttl, positive_max_ttl]` and return
122    /// the cache duration derived from the minimum TTL of records matching
123    /// `query_type` across all sections.
124    ///
125    /// Each record is clamped according to the TTL bounds configured for its own
126    /// record type, so that per-type overrides are respected even for authority
127    /// and additional section records.
128    pub(crate) fn clamp_positive_ttls(
129        &self,
130        query_type: RecordType,
131        message: &mut Message,
132    ) -> Duration {
133        #[cfg(feature = "__dnssec")]
134        let bogus = matches!(
135            DnssecSummary::from_records(message.all_sections()),
136            DnssecSummary::Bogus,
137        );
138
139        for record in message
140            .answers
141            .iter_mut()
142            .chain(message.authorities.iter_mut())
143            .chain(message.additionals.iter_mut())
144        {
145            let (min_secs, max_secs) = self
146                .ttl_config
147                .positive_ttl_bounds_secs(record.record_type());
148            record.ttl = record.ttl.clamp(min_secs, max_secs);
149        }
150
151        let (positive_min_ttl, positive_max_ttl) = self
152            .ttl_config
153            .positive_response_ttl_bounds(query_type)
154            .into_inner();
155
156        let min_ttl = if query_type == RecordType::NS {
157            // Derive cache duration from the minimum TTL of the NS records. Check both the answer
158            // section and the authority section, to handle both apex NS RRsets and referral
159            // responses.
160            message
161                .answers
162                .iter()
163                .chain(message.authorities.iter())
164                .filter(|r| r.record_type() == RecordType::NS)
165                .map(|r| Duration::from_secs(r.ttl.into()))
166                .min()
167        } else {
168            // Derive cache duration from the minimum TTL of records whose type matches the query
169            // (or CNAME records, see RFC2181 sections 5.4.1 and 10.1.1), from the answers section
170            // only. This avoids letting unrelated authority/additional records skew the cache
171            // lifetime.
172            message
173                .answers
174                .iter()
175                .filter(|r| r.record_type() == query_type || r.record_type() == RecordType::CNAME)
176                .map(|r| Duration::from_secs(r.ttl.into()))
177                .min()
178        };
179
180        let ttl = min_ttl
181            .unwrap_or(positive_min_ttl)
182            .clamp(positive_min_ttl, positive_max_ttl);
183
184        // Cap the cache lifetime independently of operator-configured min/max
185        // bounds for bogus DNSSEC responses.
186        // RFC 4035 §4.7 requires implementations to assign a small TTL
187        // since the wire TTL is attacker-influenced
188        #[cfg(feature = "__dnssec")]
189        let ttl = if bogus { ttl.min(BOGUS_CACHE_TTL) } else { ttl };
190
191        ttl
192    }
193
194    pub(crate) fn clear(&self) {
195        self.cache.invalidate_all();
196    }
197
198    pub(crate) fn clear_query(&self, query: &Query) {
199        self.cache.invalidate(query);
200    }
201
202    /// Returns the approximate number of entries in the cache.
203    #[cfg(feature = "metrics")]
204    pub(crate) fn entry_count(&self) -> u64 {
205        #[cfg(test)]
206        {
207            // For tests, ensure pending tasks are processed before getting the count.
208            // This allows unit tests of the respective cache size metrics to be
209            // written without flakyness. In a production context, we're happier
210            // to defer background work and to return an approximate count.
211            self.cache.run_pending_tasks();
212        }
213
214        self.cache.entry_count()
215    }
216}
217
218/// An entry in the response cache.
219///
220/// This contains the response itself (or an error), the time it was received, and the time at which
221/// it expires.
222#[derive(Debug, Clone)]
223struct Entry {
224    result: Arc<Result<Message, NetError>>,
225    original_time: Instant,
226    valid_until: Instant,
227}
228
229impl Entry {
230    /// Return the `Result` stored in this entry, with modified TTLs, subtracting the elapsed time
231    /// since the response was received.
232    fn updated_ttl(&self, now: Instant) -> Result<Message, NetError> {
233        let elapsed = u32::try_from(now.saturating_duration_since(self.original_time).as_secs())
234            .unwrap_or(u32::MAX);
235        match &*self.result {
236            Ok(response) => {
237                let mut response = response.clone();
238                for records in [
239                    &mut response.answers,
240                    &mut response.authorities,
241                    &mut response.additionals,
242                ] {
243                    for record in records {
244                        record.decrement_ttl(elapsed);
245                    }
246                }
247                Ok(response)
248            }
249            Err(e) => {
250                let mut e = e.clone();
251
252                // The NoRecords error may contain up to four fields with TTL values present: negative_ttl, soa, authorities, and ns.
253                // For completeness, we update each field, if present.
254                if let NetError::Dns(DnsError::NoRecordsFound(NoRecords {
255                    negative_ttl,
256                    soa,
257                    authorities,
258                    ns,
259                    ..
260                })) = &mut e
261                {
262                    if let Some(ttl) = negative_ttl {
263                        *ttl = ttl.saturating_sub(elapsed);
264                    }
265
266                    if let Some(soa) = soa {
267                        soa.decrement_ttl(elapsed);
268                    }
269
270                    if let Some(recs) = authorities.take() {
271                        authorities.replace(Arc::from(
272                            recs.iter()
273                                .cloned()
274                                .map(|mut rec| {
275                                    rec.decrement_ttl(elapsed);
276                                    rec
277                                })
278                                .collect::<Vec<_>>(),
279                        ));
280                    }
281
282                    if let Some(ns_recs) = ns.take() {
283                        ns.replace(Arc::from(
284                            ns_recs
285                                .iter()
286                                .cloned()
287                                .map(|mut ns| {
288                                    ns.ns.decrement_ttl(elapsed);
289                                    ns.glue = Arc::from(
290                                        ns.glue
291                                            .iter()
292                                            .cloned()
293                                            .map(|mut glue| {
294                                                glue.decrement_ttl(elapsed);
295                                                glue
296                                            })
297                                            .collect::<Vec<_>>(),
298                                    );
299
300                                    ns
301                                })
302                                .collect::<Vec<_>>(),
303                        ));
304                    }
305                }
306                Err(e)
307            }
308        }
309    }
310
311    /// Returns whether this cache entry is still valid.
312    fn is_current(&self, now: Instant) -> bool {
313        now <= self.valid_until
314    }
315
316    /// Returns the remaining time that this cache entry is valid for.
317    fn ttl(&self, now: Instant) -> Duration {
318        self.valid_until.saturating_duration_since(now)
319    }
320}
321
322struct EntryExpiry;
323
324impl Expiry<Query, Entry> for EntryExpiry {
325    fn expire_after_create(
326        &self,
327        _key: &Query,
328        value: &Entry,
329        created_at: Instant,
330    ) -> Option<Duration> {
331        Some(value.ttl(created_at))
332    }
333
334    fn expire_after_update(
335        &self,
336        _key: &Query,
337        value: &Entry,
338        updated_at: Instant,
339        _duration_until_expiry: Option<Duration>,
340    ) -> Option<Duration> {
341        Some(value.ttl(updated_at))
342    }
343}
344
345/// The time-to-live (TTL) configuration used by the cache.
346///
347/// Minimum and maximum TTLs can be set for both positive responses and negative responses. Separate
348/// limits may be set depending on the query type. If a minimum value is not provided, it will
349/// default to 0 seconds. If a maximum value is not provided, it will default to one day.
350///
351/// Note that TTLs in DNS are represented as a number of seconds stored in a 32-bit unsigned
352/// integer. We use `Duration` here, instead of `u32`, which can express larger values than the DNS
353/// standard. Generally, a `Duration` greater than `u32::MAX_VALUE` shouldn't cause any issue, as
354/// this will never be used in serialization, but note that this would be outside the standard
355/// range.
356#[derive(Clone, Debug, Default, PartialEq, Eq)]
357#[cfg_attr(feature = "serde", derive(Deserialize))]
358#[cfg_attr(
359    feature = "serde",
360    serde(from = "ttl_config_deserialize::TtlConfigMap")
361)]
362pub struct TtlConfig {
363    /// TTL limits applied to all queries.
364    default: TtlBounds,
365
366    /// TTL limits applied to queries with specific query types.
367    by_query_type: HashMap<RecordType, TtlBounds>,
368}
369
370impl TtlConfig {
371    /// Construct the LRU's TTL configuration based on the ResolverOpts configuration.
372    pub fn from_opts(opts: &config::ResolverOpts) -> Self {
373        Self::from(TtlBounds {
374            positive_min_ttl: opts.positive_min_ttl,
375            negative_min_ttl: opts.negative_min_ttl,
376            positive_max_ttl: opts.positive_max_ttl,
377            negative_max_ttl: opts.negative_max_ttl,
378        })
379    }
380
381    /// Override the minimum and maximum TTL values for a specific query type.
382    ///
383    /// If a minimum value is not provided, it will default to 0 seconds. If a maximum value is not
384    /// provided, it will default to one day.
385    pub fn with_query_type_ttl_bounds(
386        &mut self,
387        query_type: RecordType,
388        bounds: TtlBounds,
389    ) -> &mut Self {
390        self.by_query_type.insert(query_type, bounds);
391        self
392    }
393
394    /// Returns the positive-response TTL bounds as `(min_secs, max_secs)` clamped to `u32`.
395    ///
396    /// This is a convenience wrapper around [`positive_response_ttl_bounds`](Self::positive_response_ttl_bounds)
397    /// for use when clamping individual record TTLs.
398    fn positive_ttl_bounds_secs(&self, record_type: RecordType) -> (u32, u32) {
399        let (min, max) = self.positive_response_ttl_bounds(record_type).into_inner();
400        (
401            u32::try_from(min.as_secs()).unwrap_or(MAX_TTL),
402            u32::try_from(max.as_secs()).unwrap_or(MAX_TTL),
403        )
404    }
405
406    /// Retrieves the minimum and maximum TTL values for positive responses.
407    pub fn positive_response_ttl_bounds(&self, query_type: RecordType) -> RangeInclusive<Duration> {
408        let bounds = self.by_query_type.get(&query_type).unwrap_or(&self.default);
409        let min = bounds
410            .positive_min_ttl
411            .unwrap_or_else(|| Duration::from_secs(0));
412        let max = bounds
413            .positive_max_ttl
414            .unwrap_or_else(|| Duration::from_secs(u64::from(MAX_TTL)));
415        min..=max
416    }
417
418    /// Retrieves the minimum and maximum TTL values for negative responses.
419    pub fn negative_response_ttl_bounds(&self, query_type: RecordType) -> RangeInclusive<Duration> {
420        let bounds = self.by_query_type.get(&query_type).unwrap_or(&self.default);
421        let min = bounds
422            .negative_min_ttl
423            .unwrap_or_else(|| Duration::from_secs(0));
424        let max = bounds
425            .negative_max_ttl
426            .unwrap_or_else(|| Duration::from_secs(u64::from(MAX_TTL)));
427        min..=max
428    }
429}
430
431impl From<TtlBounds> for TtlConfig {
432    fn from(default: TtlBounds) -> Self {
433        Self {
434            default,
435            by_query_type: HashMap::default(),
436        }
437    }
438}
439
440/// Minimum and maximum TTL values for positive and negative responses.
441#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
442#[cfg_attr(feature = "serde", derive(Deserialize))]
443#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
444pub struct TtlBounds {
445    /// An optional minimum TTL value for positive responses.
446    ///
447    /// Positive responses with TTLs under `positive_min_ttl` will use
448    /// `positive_min_ttl` instead.
449    #[cfg_attr(
450        feature = "serde",
451        serde(default, deserialize_with = "config::duration_opt::deserialize")
452    )]
453    positive_min_ttl: Option<Duration>,
454
455    /// An optional minimum TTL value for negative (`NXDOMAIN`) responses.
456    ///
457    /// `NXDOMAIN` responses with TTLs under `negative_min_ttl` will use
458    /// `negative_min_ttl` instead.
459    #[cfg_attr(
460        feature = "serde",
461        serde(default, deserialize_with = "config::duration_opt::deserialize")
462    )]
463    negative_min_ttl: Option<Duration>,
464
465    /// An optional maximum TTL value for positive responses.
466    ///
467    /// Positive responses with TTLs over `positive_max_ttl` will use
468    /// `positive_max_ttl` instead.
469    #[cfg_attr(
470        feature = "serde",
471        serde(default, deserialize_with = "config::duration_opt::deserialize")
472    )]
473    positive_max_ttl: Option<Duration>,
474
475    /// An optional maximum TTL value for negative (`NXDOMAIN`) responses.
476    ///
477    /// `NXDOMAIN` responses with TTLs over `negative_max_ttl` will use
478    /// `negative_max_ttl` instead.
479    #[cfg_attr(
480        feature = "serde",
481        serde(default, deserialize_with = "config::duration_opt::deserialize")
482    )]
483    negative_max_ttl: Option<Duration>,
484}
485
486#[cfg(feature = "serde")]
487mod ttl_config_deserialize {
488    use std::collections::HashMap;
489
490    use serde::Deserialize;
491
492    use super::{TtlBounds, TtlConfig};
493    use crate::proto::rr::RecordType;
494
495    #[derive(Deserialize)]
496    pub(super) struct TtlConfigMap(HashMap<TtlConfigField, TtlBounds>);
497
498    impl From<TtlConfigMap> for TtlConfig {
499        fn from(value: TtlConfigMap) -> Self {
500            let mut default = TtlBounds::default();
501            let mut by_query_type = HashMap::new();
502            for (field, bounds) in value.0.into_iter() {
503                match field {
504                    TtlConfigField::RecordType(record_type) => {
505                        by_query_type.insert(record_type, bounds);
506                    }
507                    TtlConfigField::Default => default = bounds,
508                }
509            }
510            Self {
511                default,
512                by_query_type,
513            }
514        }
515    }
516
517    #[derive(PartialEq, Eq, Hash, Deserialize)]
518    enum TtlConfigField {
519        #[serde(rename = "default")]
520        Default,
521        #[serde(untagged)]
522        RecordType(RecordType),
523    }
524}
525
526/// Maximum TTL. This is set to one day (in seconds).
527///
528/// [RFC 2181, section 8](https://tools.ietf.org/html/rfc2181#section-8) says
529/// that the maximum TTL value is 2147483647, but implementations may place an
530/// upper bound on received TTLs.
531pub const MAX_TTL: u32 = 86400_u32;
532
533#[cfg(test)]
534mod tests {
535    use std::{
536        str::FromStr,
537        time::{Duration, Instant},
538    };
539
540    use hickory_proto::rr::rdata::CNAME;
541    #[cfg(feature = "serde")]
542    use serde::Deserialize;
543
544    use super::*;
545    use crate::{
546        net::{ForwardNSData, NetError},
547        proto::{
548            op::{DnsResponse, Message, OpCode, Query, ResponseCode},
549            rr::{
550                Name, RData, Record, RecordType,
551                rdata::{A, AAAA, NS, SOA, TXT},
552            },
553        },
554    };
555    #[cfg(feature = "__dnssec")]
556    use hickory_proto::dnssec::Proof;
557    use test_support::subscribe;
558
559    #[test]
560    fn test_is_current() {
561        let now = Instant::now();
562        let not_the_future = now + Duration::from_secs(4);
563        let future = now + Duration::from_secs(5);
564        let past_the_future = now + Duration::from_secs(6);
565
566        let entry = Entry {
567            result: Err(NetError::Message("test error")).into(),
568            original_time: now,
569            valid_until: future,
570        };
571
572        assert!(entry.is_current(now));
573        assert!(entry.is_current(not_the_future));
574        assert!(entry.is_current(future));
575        assert!(!entry.is_current(past_the_future));
576    }
577
578    #[test]
579    fn test_positive_min_ttl() {
580        let now = Instant::now();
581
582        let name = Name::from_str("www.example.com.").unwrap();
583        let query = Query::query(name.clone(), RecordType::A);
584        // Record should have TTL of 1 second.
585        let mut message = Message::response(0, OpCode::Query);
586        message.add_answer(Record::from_rdata(
587            name.clone(),
588            1,
589            RData::A(A::new(127, 0, 0, 1)),
590        ));
591
592        // Configure the cache with a minimum TTL of 2 seconds.
593        let ttls = TtlConfig::from(TtlBounds {
594            positive_min_ttl: Some(Duration::from_secs(2)),
595            ..TtlBounds::default()
596        });
597        let cache = ResponseCache::new(1, ttls);
598
599        cache.insert(query.clone(), Ok(message), now);
600        let valid_until = cache.cache.get(&query).unwrap().valid_until;
601        // The returned lookup should use the cache's minimum TTL, since the
602        // query's TTL was below the minimum.
603        assert_eq!(valid_until, now + Duration::from_secs(2));
604
605        // Record should have TTL of 3 seconds.
606        let mut message = Message::response(0, OpCode::Query);
607        message.add_answer(Record::from_rdata(
608            name.clone(),
609            3,
610            RData::A(A::new(127, 0, 0, 1)),
611        ));
612
613        cache.insert(query.clone(), Ok(message), now);
614        let valid_until = cache.cache.get(&query).unwrap().valid_until;
615        // The returned lookup should use the record's TTL, since it's
616        // greater than the cache's minimum.
617        assert_eq!(valid_until, now + Duration::from_secs(3));
618    }
619
620    #[test]
621    fn test_positive_min_ttl_clamps_record_ttls() {
622        // Regression test: records with TTLs below positive_min_ttl must have their
623        // TTLs raised in the cached message. Otherwise `updated_ttl()` subtracts
624        // elapsed time from the original (low) TTL, which saturates to 0 long before
625        // the cache entry expires.
626        let now = Instant::now();
627
628        let name = Name::from_str("www.example.com.").unwrap();
629        let query = Query::query(name.clone(), RecordType::A);
630
631        // Upstream record has TTL=60, but positive_min_ttl is 3600.
632        let mut message = Message::response(0, OpCode::Query);
633        message.add_answer(Record::from_rdata(
634            name.clone(),
635            60,
636            RData::A(A::new(93, 184, 216, 34)),
637        ));
638
639        let ttls = TtlConfig::from(TtlBounds {
640            positive_min_ttl: Some(Duration::from_secs(3600)),
641            ..TtlBounds::default()
642        });
643        let cache = ResponseCache::new(1, ttls);
644
645        cache.insert(query.clone(), Ok(message), now);
646
647        // The cache stores the record with the clamped TTL (3600). At t=0 that is
648        // what clients receive.
649        let result = cache.get(&query, now).unwrap().unwrap();
650        assert_eq!(result.answers.first().unwrap().ttl, 3600);
651
652        // At t=61 the returned TTL counts down from the cached 3600, not the
653        // upstream 60.
654        let result = cache
655            .get(&query, now + Duration::from_secs(61))
656            .unwrap()
657            .unwrap();
658        assert_eq!(result.answers.first().unwrap().ttl, 3539);
659
660        // At t=3599: still valid, TTL=1.
661        let result = cache
662            .get(&query, now + Duration::from_secs(3599))
663            .unwrap()
664            .unwrap();
665        assert_eq!(result.answers.first().unwrap().ttl, 1);
666
667        // At t=3601: cache miss — a new upstream lookup will be issued.
668        assert!(cache.get(&query, now + Duration::from_secs(3601)).is_none());
669    }
670
671    #[test]
672    fn test_positive_max_ttl_clamps_record_ttls() {
673        // Regression test: records with TTLs above positive_max_ttl must have their
674        // TTLs lowered in the cached message. Otherwise clients see the original high
675        // TTL while the cache entry expires at positive_max_ttl, causing the TTL to
676        // appear to "reset" after every max_ttl interval.
677        let now = Instant::now();
678
679        let name = Name::from_str("www.example.com.").unwrap();
680        let query = Query::query(name.clone(), RecordType::A);
681
682        // Upstream record has TTL=3600, but positive_max_ttl is 120.
683        let mut message = Message::response(0, OpCode::Query);
684        message.add_answer(Record::from_rdata(
685            name.clone(),
686            3600,
687            RData::A(A::new(93, 184, 216, 34)),
688        ));
689
690        let ttls = TtlConfig::from(TtlBounds {
691            positive_max_ttl: Some(Duration::from_secs(120)),
692            ..TtlBounds::default()
693        });
694        let cache = ResponseCache::new(1, ttls);
695
696        cache.insert(query.clone(), Ok(message), now);
697
698        // The cache stores the record with the clamped TTL (120), not the
699        // upstream 3600.
700        let result = cache.get(&query, now).unwrap().unwrap();
701        assert_eq!(result.answers.first().unwrap().ttl, 120);
702
703        // At t=60 the returned TTL counts down from the cached 120.
704        let result = cache
705            .get(&query, now + Duration::from_secs(60))
706            .unwrap()
707            .unwrap();
708        assert_eq!(result.answers.first().unwrap().ttl, 60);
709
710        // At t=121: cache miss.
711        assert!(cache.get(&query, now + Duration::from_secs(121)).is_none());
712    }
713
714    #[cfg(feature = "__dnssec")]
715    #[test]
716    fn test_bogus_record_clamps_cache_ttl() {
717        // RFC 4035 §4.7:
718        //   Since RRsets that fail to validate do not have trustworthy TTLs,
719        //   the implementation MUST assign a TTL.  This TTL SHOULD be small,
720        //   in order to mitigate the effect of caching the results of an
721        //   attack.
722        //
723        // We test against both the default TtlConfig and a config with a long
724        // positive_min_ttl: the latter exercises the case where an operator's
725        // floor for legitimate responses must not re-inflate the lifetime of
726        // an attacker-influenced Bogus verdict.
727        let configs = [
728            TtlConfig::default(),
729            TtlConfig::from(TtlBounds {
730                positive_min_ttl: Some(Duration::from_secs(3600)),
731                ..TtlBounds::default()
732            }),
733        ];
734
735        for ttls in configs {
736            let now = Instant::now();
737            let name = Name::from_str("bogus.example.com.").unwrap();
738            let query = Query::query(name.clone(), RecordType::A);
739
740            let mut message = Message::response(0, OpCode::Query);
741            let mut record = Record::from_rdata(name.clone(), 3600, RData::A(A::new(10, 0, 0, 1)));
742            record.proof = Proof::Bogus;
743            message.add_answer(record);
744
745            let cache = ResponseCache::new(1, ttls);
746            cache.insert(query.clone(), Ok(message), now);
747
748            let valid_until = cache.cache.get(&query).unwrap().valid_until;
749            assert!(
750                // RFC 4035 §3.2:
751                //   resolution failures MUST NOT be cached for longer than 5 minutes.
752                valid_until <= now + Duration::from_secs(60 * 5),
753                "Bogus answer cached for {:?}, must be <= 5m",
754                valid_until.duration_since(now),
755            );
756        }
757    }
758
759    #[test]
760    fn test_authority_ttl_does_not_shorten_answer_cache() {
761        // Regression test: authority section records (NS, SOA) with short TTLs must
762        // not reduce the cache lifetime of positive answers when answer records
763        // are present.
764        let now = Instant::now();
765
766        let name = Name::from_str("api.example.com.").unwrap();
767        let query = Query::query(name.clone(), RecordType::AAAA);
768
769        let mut message = Message::response(0, OpCode::Query);
770        // Answer: AAAA record with TTL=120
771        message.add_answer(Record::from_rdata(
772            name.clone(),
773            120,
774            RData::AAAA(AAAA::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1)),
775        ));
776        // Authority: NS record with TTL=30 (much shorter)
777        message.add_authority(Record::from_rdata(
778            Name::from_str("example.com.").unwrap(),
779            30,
780            RData::NS(NS(Name::from_str("ns1.example.com.").unwrap())),
781        ));
782
783        let ttls = TtlConfig::from(TtlBounds {
784            positive_min_ttl: Some(Duration::from_secs(3600)),
785            positive_max_ttl: Some(Duration::from_secs(28800)),
786            ..TtlBounds::default()
787        });
788        let cache = ResponseCache::new(1, ttls);
789
790        cache.insert(query.clone(), Ok(message), now);
791
792        // Cache should be valid for 3600s (answer TTL=120 raised to min_ttl=3600),
793        // NOT 30s from the authority NS record.
794        let valid_until = cache.cache.get(&query).unwrap().valid_until;
795        assert_eq!(valid_until, now + Duration::from_secs(3600));
796
797        // At t=130 (past the original authority TTL of 30): still a cache hit.
798        let result = cache
799            .get(&query, now + Duration::from_secs(130))
800            .unwrap()
801            .unwrap();
802        // AAAA answer TTL counts down from 3600.
803        assert_eq!(result.answers.first().unwrap().ttl, 3470);
804
805        // At t=3601: cache miss.
806        assert!(cache.get(&query, now + Duration::from_secs(3601)).is_none());
807    }
808
809    #[test]
810    fn test_negative_min_ttl() {
811        let now = Instant::now();
812
813        let name = Name::from_str("www.example.com.").unwrap();
814        let query = Query::query(name.clone(), RecordType::A);
815
816        // Configure the cache with a minimum TTL of 2 seconds.
817        let ttls = TtlConfig::from(TtlBounds {
818            negative_min_ttl: Some(Duration::from_secs(2)),
819            ..TtlBounds::default()
820        });
821        let cache = ResponseCache::new(1, ttls);
822
823        // Negative response should have TTL of 1 second.
824        let mut no_records = NoRecords::new(query.clone(), ResponseCode::NoError);
825        no_records.negative_ttl = Some(1);
826        cache.insert(query.clone(), Err(no_records.into()), now);
827        let valid_until = cache.cache.get(&query).unwrap().valid_until;
828        // The error's `valid_until` field should have been limited to 2 seconds.
829        assert_eq!(valid_until, now + Duration::from_secs(2));
830
831        // Negative response should have TTL of 3 seconds.
832        let mut no_records = NoRecords::new(query.clone(), ResponseCode::NoError);
833        no_records.negative_ttl = Some(3);
834        cache.insert(query.clone(), Err(no_records.into()), now);
835        let valid_until = cache.cache.get(&query).unwrap().valid_until;
836        // The error's `valid_until` field should not have been limited, as it was over the minimum
837        // TTL.
838        assert_eq!(valid_until, now + Duration::from_secs(3));
839    }
840
841    #[test]
842    fn test_positive_max_ttl() {
843        let now = Instant::now();
844
845        let name = Name::from_str("www.example.com.").unwrap();
846        let query = Query::query(name.clone(), RecordType::A);
847        // Record should have TTL of 62 seconds.
848        let mut message = Message::response(0, OpCode::Query);
849        message.add_answer(Record::from_rdata(
850            name.clone(),
851            62,
852            RData::A(A::new(127, 0, 0, 1)),
853        ));
854
855        // Configure the cache with a maximum TTL of 60 seconds.
856        let ttls = TtlConfig::from(TtlBounds {
857            positive_max_ttl: Some(Duration::from_secs(60)),
858            ..Default::default()
859        });
860        let cache = ResponseCache::new(1, ttls);
861
862        cache.insert(query.clone(), Ok(message), now);
863        let valid_until = cache.cache.get(&query).unwrap().valid_until;
864        // The returned lookup should use the cache's minimum TTL, since the
865        // query's TTL was above the maximum.
866        assert_eq!(valid_until, now + Duration::from_secs(60));
867
868        // Record should have TTL of 59 seconds.
869        let mut message = Message::response(0, OpCode::Query);
870        message.add_answer(Record::from_rdata(
871            name.clone(),
872            59,
873            RData::A(A::new(127, 0, 0, 1)),
874        ));
875
876        cache.insert(query.clone(), Ok(message), now);
877        let valid_until = cache.cache.get(&query).unwrap().valid_until;
878        // The returned lookup should use the record's TTL, since it's
879        // below than the cache's maximum.
880        assert_eq!(valid_until, now + Duration::from_secs(59));
881    }
882
883    #[test]
884    fn test_negative_max_ttl() {
885        let now = Instant::now();
886
887        let name = Name::from_str("www.example.com.").unwrap();
888        let query = Query::query(name.clone(), RecordType::A);
889
890        // Configure the cache with a maximum TTL of 60 seconds.
891        let ttls = TtlConfig::from(TtlBounds {
892            negative_max_ttl: Some(Duration::from_secs(60)),
893            ..TtlBounds::default()
894        });
895        let cache = ResponseCache::new(1, ttls);
896
897        // Negative response should have TTL of 62 seconds.
898        let mut no_records = NoRecords::new(query.clone(), ResponseCode::NoError);
899        no_records.negative_ttl = Some(62);
900        cache.insert(query.clone(), Err(no_records.into()), now);
901        let valid_until = cache.cache.get(&query).unwrap().valid_until;
902        // The error's `valid_until` field should have been limited to 60 seconds.
903        assert_eq!(valid_until, now + Duration::from_secs(60));
904
905        // Negative response should have TTL of 59 seconds.
906        let mut no_records = NoRecords::new(query.clone(), ResponseCode::NoError);
907        no_records.negative_ttl = Some(59);
908        cache.insert(query.clone(), Err(no_records.into()), now);
909        let valid_until = cache.cache.get(&query).unwrap().valid_until;
910        // The error's `valid_until` field should not have been limited, as it was under the maximum
911        // TTL.
912        assert_eq!(valid_until, now + Duration::from_secs(59));
913    }
914
915    #[test]
916    fn test_insert() {
917        let now = Instant::now();
918
919        let name = Name::from_str("www.example.com.").unwrap();
920        let query = Query::query(name.clone(), RecordType::A);
921        let mut message = Message::response(0, OpCode::Query);
922        message.add_answer(Record::from_rdata(
923            name.clone(),
924            1,
925            RData::A(A::new(127, 0, 0, 1)),
926        ));
927        let cache = ResponseCache::new(1, TtlConfig::default());
928        cache.insert(query.clone(), Ok(message.clone()), now);
929
930        let result = cache.get(&query, now).unwrap();
931        let cache_message = result.unwrap();
932        assert_eq!(cache_message.answers, message.answers);
933    }
934
935    #[test]
936    fn test_insert_negative() {
937        subscribe();
938        let now = Instant::now();
939
940        let query = Query::query(
941            Name::from_str("www.example.com.").unwrap(),
942            RecordType::AAAA,
943        );
944
945        let mut norecs = NoRecords::new(query.clone(), ResponseCode::NXDomain);
946        norecs.negative_ttl = Some(10);
947        let error = NetError::from(norecs);
948        let cache = ResponseCache::new(1, TtlConfig::default());
949
950        cache.insert(query.clone(), Err(error), now);
951
952        let cache_err = cache.get(&query, now).unwrap().unwrap_err();
953        let NetError::Dns(DnsError::NoRecordsFound(_no_records)) = &cache_err else {
954            panic!("expected NoRecordsFound");
955        };
956
957        // Cache should be expired
958        assert!(cache.get(&query, now + Duration::from_secs(11)).is_none());
959    }
960
961    #[test]
962    fn test_update_ttl() {
963        let now = Instant::now();
964
965        let name = Name::from_str("www.example.com.").unwrap();
966        let query = Query::query(name.clone(), RecordType::A);
967        let mut message = Message::response(0, OpCode::Query);
968        message.add_answer(Record::from_rdata(
969            name.clone(),
970            10,
971            RData::A(A::new(127, 0, 0, 1)),
972        ));
973        let cache = ResponseCache::new(1, TtlConfig::default());
974        cache.insert(query.clone(), Ok(message), now);
975
976        let result = cache.get(&query, now + Duration::from_secs(2)).unwrap();
977        let cache_message = result.unwrap();
978        let record = cache_message.answers.first().unwrap();
979        assert_eq!(record.ttl, 8);
980    }
981
982    #[test]
983    fn test_update_ttl_negative() -> Result<(), NetError> {
984        subscribe();
985        let now = Instant::now();
986        let name = Name::from_str("www.example.com.")?;
987        let ns_name = Name::from_str("ns1.example.com")?;
988        let zone_name = name.base_name();
989        let query = Query::query(name.clone(), RecordType::AAAA);
990
991        let mut norecs = NoRecords::new(query.clone(), ResponseCode::NXDomain);
992        norecs.negative_ttl = Some(10);
993        norecs.soa = Some(Box::new(Record::from_rdata(
994            zone_name.clone(),
995            10,
996            SOA::new(name.base_name(), name.clone(), 1, 1, 1, 1, 1),
997        )));
998        norecs.authorities = Some(Arc::new([Record::from_rdata(
999            zone_name.clone(),
1000            10,
1001            RData::NS(NS(ns_name.clone())),
1002        )]));
1003        norecs.ns = Some(Arc::new([ForwardNSData {
1004            ns: Record::from_rdata(zone_name.clone(), 10, RData::NS(NS(ns_name.clone()))),
1005            glue: Arc::new([Record::from_rdata(
1006                ns_name.clone(),
1007                10,
1008                RData::A(A([192, 0, 2, 1].into())),
1009            )]),
1010        }]));
1011
1012        let error = NetError::from(norecs);
1013
1014        let cache = ResponseCache::new(1, TtlConfig::default());
1015        cache.insert(query.clone(), Err(error), now);
1016
1017        let cache_err = cache.get(&query, now).unwrap().unwrap_err();
1018        let NetError::Dns(DnsError::NoRecordsFound(no_records)) = &cache_err else {
1019            panic!("expected NoRecordsFound");
1020        };
1021
1022        let Some(soa) = no_records.soa.clone() else {
1023            panic!("no SOA in NoRecordsFound");
1024        };
1025        assert_eq!(soa.ttl, 10);
1026
1027        let cache_err = cache
1028            .get(&query, now + Duration::from_secs(2))
1029            .unwrap()
1030            .unwrap_err();
1031        let NetError::Dns(DnsError::NoRecordsFound(NoRecords {
1032            negative_ttl: Some(negative_ttl),
1033            soa: Some(soa),
1034            authorities: Some(authorities),
1035            ns: Some(ns),
1036            ..
1037        })) = &cache_err
1038        else {
1039            panic!("expected NoRecordsFound with negative_ttl, soa, authorities, and ns");
1040        };
1041
1042        assert_eq!(*negative_ttl, 8);
1043        assert_eq!(soa.ttl, 8);
1044        assert_eq!(authorities[0].ttl, 8);
1045        assert_eq!(ns[0].ns.ttl, 8);
1046
1047        // Cache should be expired
1048        assert!(cache.get(&query, now + Duration::from_secs(11)).is_none());
1049        Ok(())
1050    }
1051
1052    #[test]
1053    fn test_insert_ttl() {
1054        let now = Instant::now();
1055
1056        let name = Name::from_str("www.example.com.").unwrap();
1057        let query = Query::query(name.clone(), RecordType::A);
1058
1059        // TTL of entry should be 1.
1060        let mut message = Message::response(0, OpCode::Query);
1061        message.add_answer(Record::from_rdata(
1062            name.clone(),
1063            1,
1064            RData::A(A::new(127, 0, 0, 1)),
1065        ));
1066        message.add_answer(Record::from_rdata(name, 2, RData::A(A::new(127, 0, 0, 2))));
1067
1068        let cache = ResponseCache::new(1, TtlConfig::default());
1069        cache.insert(query.clone(), Ok(message), now);
1070
1071        // Entry is still valid.
1072        cache
1073            .get(&query, now + Duration::from_secs(1))
1074            .unwrap()
1075            .unwrap();
1076
1077        // Entry is expired.
1078        let option = cache.get(&query, now + Duration::from_secs(2));
1079        assert!(option.is_none());
1080    }
1081
1082    #[test]
1083    fn test_ttl_different_query_types() {
1084        let now = Instant::now();
1085        let name = Name::from_str("www.example.com.").unwrap();
1086
1087        // Store records with a TTL of 1 second.
1088        let query_a = Query::query(name.clone(), RecordType::A);
1089        let rdata_a = RData::A(A::new(127, 0, 0, 1));
1090        let mut message_a = Message::response(0, OpCode::Query);
1091        message_a.add_answer(Record::from_rdata(name.clone(), 1, rdata_a.clone()));
1092
1093        let query_txt = Query::query(name.clone(), RecordType::TXT);
1094        let rdata_txt = RData::TXT(TXT::new(vec!["data".to_string()]));
1095        let mut message_txt = Message::response(0, OpCode::Query);
1096        message_txt.add_answer(Record::from_rdata(name.clone(), 1, rdata_txt.clone()));
1097
1098        // Set separate positive_min_ttl limits for TXT queries and all others.
1099        let mut ttl_config = TtlConfig::from(TtlBounds {
1100            positive_min_ttl: Some(Duration::from_secs(2)),
1101            ..TtlBounds::default()
1102        });
1103        ttl_config.with_query_type_ttl_bounds(
1104            RecordType::TXT,
1105            TtlBounds {
1106                positive_min_ttl: Some(Duration::from_secs(5)),
1107                ..TtlBounds::default()
1108            },
1109        );
1110        let cache = ResponseCache::new(2, ttl_config);
1111
1112        cache.insert(query_a.clone(), Ok(message_a), now);
1113        // This should use the cache's default minimum TTL, since the record's TTL was below the
1114        // minimum.
1115        assert_eq!(
1116            cache.cache.get(&query_a).unwrap().valid_until,
1117            now + Duration::from_secs(2)
1118        );
1119
1120        cache.insert(query_txt.clone(), Ok(message_txt), now);
1121        // This should use the minimum for TTL records, since the record's TTL was below the
1122        // minimum.
1123        assert_eq!(
1124            cache.cache.get(&query_txt).unwrap().valid_until,
1125            now + Duration::from_secs(5)
1126        );
1127
1128        // store records with a TTL of 7 seconds.
1129        let mut message_a = Message::response(0, OpCode::Query);
1130        message_a.add_answer(Record::from_rdata(name.clone(), 7, rdata_a));
1131
1132        let mut message_txt = Message::response(0, OpCode::Query);
1133        message_txt.add_answer(Record::from_rdata(name.clone(), 7, rdata_txt));
1134
1135        cache.insert(query_a.clone(), Ok(message_a), now);
1136        // This should use the record's TTL, since it's greater than the default minimum TTL.
1137        assert_eq!(
1138            cache.cache.get(&query_a).unwrap().valid_until,
1139            now + Duration::from_secs(7)
1140        );
1141
1142        cache.insert(query_txt.clone(), Ok(message_txt), now);
1143        // This should use the record's TTL, since it's greater than the minimum TTL for TXT records.
1144        assert_eq!(
1145            cache.cache.get(&query_txt).unwrap().valid_until,
1146            now + Duration::from_secs(7)
1147        );
1148    }
1149
1150    #[test]
1151    fn cname_alias_bounds_cache_lifetime() {
1152        let now = Instant::now();
1153        let cname = Name::from_ascii("cname-record.com.").unwrap();
1154        let a = Name::from_ascii("a-record.com.").unwrap();
1155
1156        let query = Query::query(cname.clone(), RecordType::A);
1157
1158        let mut message = Message::response(0, OpCode::Query);
1159        message.add_query(query.clone());
1160
1161        // Short-TTL CNAME alias
1162        message.add_answer(Record::from_rdata(cname, 5, RData::CNAME(CNAME(a.clone()))));
1163
1164        // Terminal A record with 24hr TTL.
1165        message.add_answer(Record::from_rdata(
1166            a,
1167            86_400,
1168            RData::A(A::new(51, 34, 100, 105)),
1169        ));
1170
1171        let cache = ResponseCache::new(1, TtlConfig::default());
1172        cache.insert(query.clone(), Ok::<Message, NetError>(message), now);
1173
1174        // Immediately after insert the response is a cache hit.
1175        assert!(
1176            cache.get(&query, now).is_some(),
1177            "freshly inserted response should be a cache hit"
1178        );
1179
1180        // At t=6s — just past the 5s CNAME TTL — the entry MUST be a cache miss so a
1181        // re-resolution occurs.
1182        assert!(
1183            cache.get(&query, now + Duration::from_secs(6)).is_none(),
1184            "response served past the 5s CNAME TTL"
1185        );
1186    }
1187
1188    // Authority section records (e.g. NS for a delegated zone) must not
1189    // shorten the cache TTL derived from answer records.
1190    #[test]
1191    fn authority_record_cannot_reduce_cache_ttl() {
1192        let now = Instant::now();
1193
1194        let name = Name::from_str("www.example.com.").unwrap();
1195        let query = Query::query(name.clone(), RecordType::A);
1196
1197        let mut message = Message::response(0, OpCode::Query);
1198        // Answer with TTL=300
1199        message.add_answer(Record::from_rdata(
1200            name.clone(),
1201            300,
1202            RData::A(A::new(93, 184, 216, 34)),
1203        ));
1204        // Authority NS with TTL=10 — must not pull cache TTL down to 10
1205        message.add_authority(Record::from_rdata(
1206            Name::from_str("example.com.").unwrap(),
1207            10,
1208            RData::NS(NS(Name::from_str("ns1.example.com.").unwrap())),
1209        ));
1210
1211        let cache = ResponseCache::new(1, TtlConfig::default());
1212        cache.insert(query.clone(), Ok(message), now);
1213
1214        let valid_until = cache.cache.get(&query).unwrap().valid_until;
1215        assert_eq!(valid_until, now + Duration::from_secs(300));
1216
1217        // Still cached well past the authority TTL of 10s
1218        assert!(cache.get(&query, now + Duration::from_secs(60)).is_some());
1219    }
1220
1221    // Additional section records (e.g. glue A/AAAA) must not shorten
1222    // the cache TTL derived from answer records.
1223    #[test]
1224    fn additional_record_cannot_reduce_cache_ttl() {
1225        let now = Instant::now();
1226
1227        let name = Name::from_str("www.example.com.").unwrap();
1228        let query = Query::query(name.clone(), RecordType::A);
1229
1230        let mut message = Message::response(0, OpCode::Query);
1231        // Answer with TTL=300
1232        message.add_answer(Record::from_rdata(
1233            name.clone(),
1234            300,
1235            RData::A(A::new(93, 184, 216, 34)),
1236        ));
1237        // Additional glue record with TTL=5 — must not pull cache TTL down
1238        message.add_additional(Record::from_rdata(
1239            Name::from_str("ns1.example.com.").unwrap(),
1240            5,
1241            RData::A(A::new(198, 51, 100, 1)),
1242        ));
1243
1244        let cache = ResponseCache::new(1, TtlConfig::default());
1245        cache.insert(query.clone(), Ok(message), now);
1246
1247        let valid_until = cache.cache.get(&query).unwrap().valid_until;
1248        assert_eq!(valid_until, now + Duration::from_secs(300));
1249
1250        // Still cached well past the additional record TTL of 5s
1251        assert!(cache.get(&query, now + Duration::from_secs(60)).is_some());
1252    }
1253
1254    // Both authority and additional records present with very low TTLs
1255    // must not affect the answer-derived cache TTL.
1256    #[test]
1257    fn authority_and_additional_combined_cannot_reduce_cache_ttl() {
1258        let now = Instant::now();
1259
1260        let name = Name::from_str("www.example.com.").unwrap();
1261        let query = Query::query(name.clone(), RecordType::A);
1262
1263        let mut message = Message::response(0, OpCode::Query);
1264        // Answer with TTL=600
1265        message.add_answer(Record::from_rdata(
1266            name.clone(),
1267            600,
1268            RData::A(A::new(93, 184, 216, 34)),
1269        ));
1270        // Authority SOA with TTL=1
1271        message.add_authority(Record::from_rdata(
1272            Name::from_str("example.com.").unwrap(),
1273            1,
1274            RData::SOA(SOA::new(
1275                Name::from_str("example.com.").unwrap(),
1276                Name::from_str("admin.example.com.").unwrap(),
1277                2024010100,
1278                3600,
1279                900,
1280                604800,
1281                60,
1282            )),
1283        ));
1284        // Authority NS with TTL=2
1285        message.add_authority(Record::from_rdata(
1286            Name::from_str("example.com.").unwrap(),
1287            2,
1288            RData::NS(NS(Name::from_str("ns1.example.com.").unwrap())),
1289        ));
1290        // Additional glue with TTL=3
1291        message.add_additional(Record::from_rdata(
1292            Name::from_str("ns1.example.com.").unwrap(),
1293            3,
1294            RData::A(A::new(198, 51, 100, 1)),
1295        ));
1296        // Additional glue with TTL=4
1297        message.add_additional(Record::from_rdata(
1298            Name::from_str("ns1.example.com.").unwrap(),
1299            4,
1300            RData::AAAA(AAAA::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 53)),
1301        ));
1302
1303        let cache = ResponseCache::new(1, TtlConfig::default());
1304        cache.insert(query.clone(), Ok(message), now);
1305
1306        // Cache TTL must be 600 from the answer, not 1/2/3/4 from authority/additional
1307        let valid_until = cache.cache.get(&query).unwrap().valid_until;
1308        assert_eq!(valid_until, now + Duration::from_secs(600));
1309    }
1310
1311    #[test]
1312    fn referral_response_cache_ttl() {
1313        let now = Instant::now();
1314
1315        let name = Name::from_str("example.com.").unwrap();
1316        let query = Query::query(name.clone(), RecordType::NS);
1317
1318        let mut message = Message::response(0, OpCode::Query);
1319        message.add_query(query.clone());
1320        // Referral NS record with TTL=300
1321        message.add_authority(Record::from_rdata(
1322            name,
1323            300,
1324            RData::NS(NS(Name::from_str("ns1.example.com.").unwrap())),
1325        ));
1326        message.metadata.authoritative = false;
1327
1328        // Confirm that this response goes down the positive response code path.
1329        // This test may need to be updated or deleted when referral response handling is changed.
1330        let response = DnsResponse::from_message(message.clone()).unwrap();
1331        let result = DnsError::from_response(response);
1332        assert!(
1333            result.is_ok(),
1334            "expected referral response to be returned in Ok: {result:?}"
1335        );
1336
1337        let cache = ResponseCache::new(1, TtlConfig::default());
1338        cache.insert(query.clone(), Ok(message), now);
1339
1340        let valid_until = cache.cache.get(&query).unwrap().valid_until;
1341        assert_eq!(valid_until, now + Duration::from_secs(300));
1342    }
1343
1344    #[cfg(feature = "serde")]
1345    #[test]
1346    fn ttl_config_deserialize_errors() {
1347        // Duplicate of "default"
1348        let input = r#"[default]
1349positive_max_ttl = 3600
1350[default]
1351positive_max_ttl = 3599"#;
1352        let error = toml::from_str::<TtlConfig>(input).unwrap_err();
1353        assert!(
1354            error.message().contains("duplicate key"),
1355            "wrong error message: {error}"
1356        );
1357
1358        // Duplicate of a record type
1359        let input = r#"[default]
1360positive_max_ttl = 86400
1361[OPENPGPKEY]
1362positive_max_ttl = 3600
1363[OPENPGPKEY]
1364negative_min_ttl = 60"#;
1365        let error = toml::from_str::<TtlConfig>(input).unwrap_err();
1366        assert!(
1367            error.message().contains("duplicate key"),
1368            "wrong error message: {error}"
1369        );
1370
1371        // Neither "default" nor a record type
1372        let input = r#"[not_a_record_type]
1373positive_max_ttl = 3600"#;
1374        let error = toml::from_str::<TtlConfig>(input).unwrap_err();
1375        assert!(
1376            error.message().contains("data did not match any variant"),
1377            "wrong error message: {error}"
1378        );
1379
1380        // Array instead of table
1381        #[derive(Debug, Deserialize)]
1382        struct Wrapper {
1383            #[allow(unused)]
1384            cache_policy: TtlConfig,
1385        }
1386        let input = r#"cache_policy = []"#;
1387        let error = toml::from_str::<Wrapper>(input).unwrap_err();
1388        assert!(
1389            error.message().contains("invalid type: sequence"),
1390            "wrong error message: {error}"
1391        );
1392
1393        // String instead of table
1394        let input = r#"cache_policy = "yes""#;
1395        let error = toml::from_str::<Wrapper>(input).unwrap_err();
1396        assert!(
1397            error.message().contains("invalid type: string"),
1398            "wrong error message: {error}"
1399        );
1400    }
1401}