hickory_resolver/
dns_lru.rs

1// Copyright 2015-2017 Benjamin Fry <benjaminfry@me.com>
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// https://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! An LRU cache designed for work with DNS lookups
9
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::time::{Duration, Instant};
13
14use lru_cache::LruCache;
15use parking_lot::Mutex;
16
17use proto::op::Query;
18use proto::rr::Record;
19
20use crate::config;
21use crate::error::*;
22use crate::lookup::Lookup;
23
24/// Maximum TTL as defined in https://tools.ietf.org/html/rfc2181, 2147483647
25///   Setting this to a value of 1 day, in seconds
26pub(crate) const MAX_TTL: u32 = 86400_u32;
27
28#[derive(Debug)]
29struct LruValue {
30    // In the None case, this represents an NXDomain
31    lookup: Result<Lookup, ResolveError>,
32    valid_until: Instant,
33}
34
35impl LruValue {
36    /// Returns true if this set of ips is still valid
37    fn is_current(&self, now: Instant) -> bool {
38        now <= self.valid_until
39    }
40
41    /// Returns the ttl as a Duration of time remaining.
42    fn ttl(&self, now: Instant) -> Duration {
43        self.valid_until.saturating_duration_since(now)
44    }
45
46    fn with_updated_ttl(&self, now: Instant) -> Self {
47        let lookup = match self.lookup {
48            Ok(ref lookup) => {
49                let records = lookup
50                    .records()
51                    .iter()
52                    .map(|record| {
53                        let mut record = record.clone();
54                        record.set_ttl(self.ttl(now).as_secs() as u32);
55                        record
56                    })
57                    .collect::<Vec<Record>>();
58                Ok(Lookup::new_with_deadline(
59                    lookup.query().clone(),
60                    Arc::from(records),
61                    self.valid_until,
62                ))
63            }
64            Err(ref e) => Err(e.clone()),
65        };
66        Self {
67            lookup,
68            valid_until: self.valid_until,
69        }
70    }
71}
72
73/// And LRU eviction cache specifically for storing DNS records
74#[derive(Clone, Debug)]
75pub struct DnsLru {
76    cache: Arc<Mutex<LruCache<Query, LruValue>>>,
77    /// A minimum TTL value for positive responses.
78    ///
79    /// Positive responses with TTLs under `positive_max_ttl` will use
80    /// `positive_max_ttl` instead.
81    ///
82    /// If this value is not set on the `TtlConfig` used to construct this
83    /// `DnsLru`, it will default to 0.
84    positive_min_ttl: Duration,
85    /// A minimum TTL value for negative (`NXDOMAIN`) responses.
86    ///
87    /// `NXDOMAIN` responses with TTLs under `negative_min_ttl` will use
88    /// `negative_min_ttl` instead.
89    ///
90    /// If this value is not set on the `TtlConfig` used to construct this
91    /// `DnsLru`, it will default to 0.
92    negative_min_ttl: Duration,
93    /// A maximum TTL value for positive responses.
94    ///
95    /// Positive responses with TTLs over `positive_max_ttl` will use
96    /// `positive_max_ttl` instead.
97    ///
98    ///  If this value is not set on the `TtlConfig` used to construct this
99    /// `DnsLru`, it will default to [`MAX_TTL`] seconds.
100    ///
101    /// [`MAX_TTL`]: const.MAX_TTL.html
102    positive_max_ttl: Duration,
103    /// A maximum TTL value for negative (`NXDOMAIN`) responses.
104    ///
105    /// `NXDOMAIN` responses with TTLs over `negative_max_ttl` will use
106    /// `negative_max_ttl` instead.
107    ///
108    ///  If this value is not set on the `TtlConfig` used to construct this
109    /// `DnsLru`, it will default to [`MAX_TTL`] seconds.
110    ///
111    /// [`MAX_TTL`]: const.MAX_TTL.html
112    negative_max_ttl: Duration,
113}
114
115/// The time-to-live, TTL, configuration for use by the cache.
116///
117/// It should be understood that the TTL in DNS is expressed with a u32.
118///   We use Duration here for tracking this which can express larger values
119///   than the DNS standard. Generally a Duration greater than u32::MAX_VALUE
120///   shouldn't cause any issue as this will never be used in serialization,
121///   but understand that this would be outside the standard range.
122#[derive(Copy, Clone, Debug, Default)]
123pub struct TtlConfig {
124    /// An optional minimum TTL value for positive responses.
125    ///
126    /// Positive responses with TTLs under `positive_min_ttl` will use
127    /// `positive_min_ttl` instead.
128    pub(crate) positive_min_ttl: Option<Duration>,
129    /// An optional minimum TTL value for negative (`NXDOMAIN`) responses.
130    ///
131    /// `NXDOMAIN` responses with TTLs under `negative_min_ttl will use
132    /// `negative_min_ttl` instead.
133    pub(crate) negative_min_ttl: Option<Duration>,
134    /// An optional maximum TTL value for positive responses.
135    ///
136    /// Positive responses with TTLs positive `positive_max_ttl` will use
137    /// `positive_max_ttl` instead.
138    pub(crate) positive_max_ttl: Option<Duration>,
139    /// An optional maximum TTL value for negative (`NXDOMAIN`) responses.
140    ///
141    /// `NXDOMAIN` responses with TTLs over `negative_max_ttl` will use
142    /// `negative_max_ttl` instead.
143    pub(crate) negative_max_ttl: Option<Duration>,
144}
145
146impl TtlConfig {
147    /// Construct the LRU based on the ResolverOpts configuration
148    pub fn from_opts(opts: &config::ResolverOpts) -> Self {
149        Self {
150            positive_min_ttl: opts.positive_min_ttl,
151            negative_min_ttl: opts.negative_min_ttl,
152            positive_max_ttl: opts.positive_max_ttl,
153            negative_max_ttl: opts.negative_max_ttl,
154        }
155    }
156}
157
158impl DnsLru {
159    /// Construct a new cache
160    ///
161    /// # Arguments
162    ///
163    /// * `capacity` - size in number of records, this can be the max size of 2048 (record size) * `capacity`
164    /// * `ttl_cfg` - force minimums and maximums for cached records
165    pub fn new(capacity: usize, ttl_cfg: TtlConfig) -> Self {
166        let TtlConfig {
167            positive_min_ttl,
168            negative_min_ttl,
169            positive_max_ttl,
170            negative_max_ttl,
171        } = ttl_cfg;
172        let cache = Arc::new(Mutex::new(LruCache::new(capacity)));
173        Self {
174            cache,
175            positive_min_ttl: positive_min_ttl.unwrap_or_else(|| Duration::from_secs(0)),
176            negative_min_ttl: negative_min_ttl.unwrap_or_else(|| Duration::from_secs(0)),
177            positive_max_ttl: positive_max_ttl
178                .unwrap_or_else(|| Duration::from_secs(u64::from(MAX_TTL))),
179            negative_max_ttl: negative_max_ttl
180                .unwrap_or_else(|| Duration::from_secs(u64::from(MAX_TTL))),
181        }
182    }
183
184    pub(crate) fn clear(&self) {
185        self.cache.lock().clear();
186    }
187
188    pub(crate) fn insert(
189        &self,
190        query: Query,
191        records_and_ttl: Vec<(Record, u32)>,
192        now: Instant,
193    ) -> Lookup {
194        let len = records_and_ttl.len();
195        // collapse the values, we're going to take the Minimum TTL as the correct one
196        let (records, ttl): (Vec<Record>, Duration) = records_and_ttl.into_iter().fold(
197            (Vec::with_capacity(len), self.positive_max_ttl),
198            |(mut records, mut min_ttl), (record, ttl)| {
199                records.push(record);
200                let ttl = Duration::from_secs(u64::from(ttl));
201                min_ttl = min_ttl.min(ttl);
202                (records, min_ttl)
203            },
204        );
205
206        // If the cache was configured with a minimum TTL, and that value is higher
207        // than the minimum TTL in the values, use it instead.
208        let ttl = self.positive_min_ttl.max(ttl);
209        let valid_until = now + ttl;
210
211        // insert into the LRU
212        let lookup = Lookup::new_with_deadline(query.clone(), Arc::from(records), valid_until);
213        self.cache.lock().insert(
214            query,
215            LruValue {
216                lookup: Ok(lookup.clone()),
217                valid_until,
218            },
219        );
220
221        lookup
222    }
223
224    /// inserts a record based on the name and type.
225    ///
226    /// # Arguments
227    ///
228    /// * `original_query` - is used for matching the records that should be returned
229    /// * `records` - the records will be partitioned by type and name for storage in the cache
230    /// * `now` - current time for use in associating TTLs
231    ///
232    /// # Return
233    ///
234    /// This should always return some records, but will be None if there are no records or the original_query matches none
235    pub fn insert_records(
236        &self,
237        original_query: Query,
238        records: impl Iterator<Item = Record>,
239        now: Instant,
240    ) -> Option<Lookup> {
241        // collect all records by name
242        let records = records.fold(
243            HashMap::<Query, Vec<(Record, u32)>>::new(),
244            |mut map, record| {
245                let mut query = Query::query(record.name().clone(), record.record_type());
246                query.set_query_class(record.dns_class());
247
248                let ttl = record.ttl();
249
250                map.entry(query).or_default().push((record, ttl));
251
252                map
253            },
254        );
255
256        // now insert by record type and name
257        let mut lookup = None;
258        for (query, records_and_ttl) in records {
259            let is_query = original_query == query;
260            let inserted = self.insert(query, records_and_ttl, now);
261
262            if is_query {
263                lookup = Some(inserted)
264            }
265        }
266
267        lookup
268    }
269
270    /// Generally for inserting a set of records that have already been cached, but with a different Query.
271    pub(crate) fn duplicate(&self, query: Query, lookup: Lookup, ttl: u32, now: Instant) -> Lookup {
272        let ttl = Duration::from_secs(u64::from(ttl));
273        let valid_until = now + ttl;
274
275        self.cache.lock().insert(
276            query,
277            LruValue {
278                lookup: Ok(lookup.clone()),
279                valid_until,
280            },
281        );
282
283        lookup
284    }
285
286    /// This converts the ResolveError to set the inner negative_ttl value to be the
287    ///  current expiration ttl.
288    fn nx_error_with_ttl(error: &mut ResolveError, new_ttl: Duration) {
289        if let ResolveError {
290            kind:
291                ResolveErrorKind::NoRecordsFound {
292                    ref mut negative_ttl,
293                    ..
294                },
295            ..
296        } = error
297        {
298            *negative_ttl = Some(u32::try_from(new_ttl.as_secs()).unwrap_or(MAX_TTL));
299        }
300    }
301
302    pub(crate) fn negative(
303        &self,
304        query: Query,
305        mut error: ResolveError,
306        now: Instant,
307    ) -> ResolveError {
308        // TODO: if we are getting a negative response, should we instead fallback to cache?
309        //   this would cache indefinitely, probably not correct
310        if let ResolveError {
311            kind:
312                ResolveErrorKind::NoRecordsFound {
313                    negative_ttl: Some(ttl),
314                    ..
315                },
316            ..
317        } = error
318        {
319            let ttl_duration = Duration::from_secs(u64::from(ttl))
320                // Clamp the TTL so that it's between the cache's configured
321                // minimum and maximum TTLs for negative responses.
322                .clamp(self.negative_min_ttl, self.negative_max_ttl);
323            let valid_until = now + ttl_duration;
324
325            {
326                let error = error.clone();
327
328                self.cache.lock().insert(
329                    query,
330                    LruValue {
331                        lookup: Err(error),
332                        valid_until,
333                    },
334                );
335            }
336
337            Self::nx_error_with_ttl(&mut error, ttl_duration);
338        }
339
340        error
341    }
342
343    /// Based on the query, see if there are any records available
344    pub fn get(&self, query: &Query, now: Instant) -> Option<Result<Lookup, ResolveError>> {
345        let mut out_of_date = false;
346        let mut cache = self.cache.lock();
347        let lookup = cache.get_mut(query).and_then(|value| {
348            if value.is_current(now) {
349                out_of_date = false;
350                let mut result = value.with_updated_ttl(now).lookup;
351                if let Err(ref mut err) = result {
352                    Self::nx_error_with_ttl(err, value.ttl(now));
353                }
354                Some(result)
355            } else {
356                out_of_date = true;
357                None
358            }
359        });
360
361        // in this case, we can preemptively remove out of data elements
362        // this assumes time is always moving forward, this would only not be true in contrived situations where now
363        //  is not current time, like tests...
364        if out_of_date {
365            cache.remove(query);
366        }
367
368        lookup
369    }
370}
371
372// see also the lookup_tests.rs in integration-tests crate
373#[cfg(test)]
374mod tests {
375    use std::str::FromStr;
376    use std::time::*;
377
378    use proto::op::{Query, ResponseCode};
379    use proto::rr::rdata::A;
380    use proto::rr::{Name, RData, RecordType};
381
382    use super::*;
383
384    #[test]
385    fn test_is_current() {
386        let now = Instant::now();
387        let not_the_future = now + Duration::from_secs(4);
388        let future = now + Duration::from_secs(5);
389        let past_the_future = now + Duration::from_secs(6);
390
391        let value = LruValue {
392            lookup: Err(ResolveErrorKind::Message("test error").into()),
393            valid_until: future,
394        };
395
396        assert!(value.is_current(now));
397        assert!(value.is_current(not_the_future));
398        assert!(value.is_current(future));
399        assert!(!value.is_current(past_the_future));
400    }
401
402    #[test]
403    fn test_lookup_uses_positive_min_ttl() {
404        let now = Instant::now();
405
406        let name = Name::from_str("www.example.com.").unwrap();
407        let query = Query::query(name.clone(), RecordType::A);
408        // record should have TTL of 1 second.
409        let ips_ttl = vec![(
410            Record::from_rdata(name.clone(), 1, RData::A(A::new(127, 0, 0, 1))),
411            1,
412        )];
413        let ips = [RData::A(A::new(127, 0, 0, 1))];
414
415        // configure the cache with a minimum TTL of 2 seconds.
416        let ttls = TtlConfig {
417            positive_min_ttl: Some(Duration::from_secs(2)),
418            ..TtlConfig::default()
419        };
420        let lru = DnsLru::new(1, ttls);
421
422        let rc_ips = lru.insert(query.clone(), ips_ttl, now);
423        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
424        // the returned lookup should use the cache's min TTL, since the
425        // query's TTL was below the minimum.
426        assert_eq!(rc_ips.valid_until(), now + Duration::from_secs(2));
427
428        // record should have TTL of 3 seconds.
429        let ips_ttl = vec![(
430            Record::from_rdata(name, 3, RData::A(A::new(127, 0, 0, 1))),
431            3,
432        )];
433
434        let rc_ips = lru.insert(query, ips_ttl, now);
435        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
436        // the returned lookup should use the record's TTL, since it's
437        // greater than the cache's minimum.
438        assert_eq!(rc_ips.valid_until(), now + Duration::from_secs(3));
439    }
440
441    #[test]
442    fn test_error_uses_negative_min_ttl() {
443        let now = Instant::now();
444
445        let name = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
446
447        // configure the cache with a maximum TTL of 2 seconds.
448        let ttls = TtlConfig {
449            negative_min_ttl: Some(Duration::from_secs(2)),
450            ..TtlConfig::default()
451        };
452        let lru = DnsLru::new(1, ttls);
453
454        // neg response should have TTL of 1 seconds.
455        let err = ResolveErrorKind::NoRecordsFound {
456            query: Box::new(name.clone()),
457            soa: None,
458            negative_ttl: Some(1),
459            response_code: ResponseCode::NoError,
460            trusted: false,
461        };
462        let nx_error = lru.negative(name.clone(), err.into(), now);
463        match nx_error.kind() {
464            &ResolveErrorKind::NoRecordsFound { negative_ttl, .. } => {
465                let valid_until = negative_ttl.expect("resolve error should have a deadline");
466                // the error's `valid_until` field should have been limited to 2 seconds.
467                assert_eq!(valid_until, 2);
468            }
469            other => panic!("expected ResolveErrorKind::NoRecordsFound, got {:?}", other),
470        }
471
472        // neg response should have TTL of 3 seconds.
473        let err = ResolveErrorKind::NoRecordsFound {
474            query: Box::new(name.clone()),
475            soa: None,
476            negative_ttl: Some(3),
477            response_code: ResponseCode::NoError,
478            trusted: false,
479        };
480        let nx_error = lru.negative(name, err.into(), now);
481        match nx_error.kind() {
482            &ResolveErrorKind::NoRecordsFound { negative_ttl, .. } => {
483                let negative_ttl = negative_ttl.expect("ResolveError should have a deadline");
484                // the error's `valid_until` field should not have been limited, as it was
485                // over the min TTL.
486                assert_eq!(negative_ttl, 3);
487            }
488            other => panic!("expected ResolveErrorKind::NoRecordsFound, got {:?}", other),
489        }
490    }
491
492    #[test]
493    fn test_lookup_uses_positive_max_ttl() {
494        let now = Instant::now();
495
496        let name = Name::from_str("www.example.com.").unwrap();
497        let query = Query::query(name.clone(), RecordType::A);
498        // record should have TTL of 62 seconds.
499        let ips_ttl = vec![(
500            Record::from_rdata(name.clone(), 62, RData::A(A::new(127, 0, 0, 1))),
501            62,
502        )];
503        let ips = [RData::A(A::new(127, 0, 0, 1))];
504
505        // configure the cache with a maximum TTL of 60 seconds.
506        let ttls = TtlConfig {
507            positive_max_ttl: Some(Duration::from_secs(60)),
508            ..TtlConfig::default()
509        };
510        let lru = DnsLru::new(1, ttls);
511
512        let rc_ips = lru.insert(query.clone(), ips_ttl, now);
513        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
514        // the returned lookup should use the cache's min TTL, since the
515        // query's TTL was above the maximum.
516        assert_eq!(rc_ips.valid_until(), now + Duration::from_secs(60));
517
518        // record should have TTL of 59 seconds.
519        let ips_ttl = vec![(
520            Record::from_rdata(name, 59, RData::A(A::new(127, 0, 0, 1))),
521            59,
522        )];
523
524        let rc_ips = lru.insert(query, ips_ttl, now);
525        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
526        // the returned lookup should use the record's TTL, since it's
527        // below than the cache's maximum.
528        assert_eq!(rc_ips.valid_until(), now + Duration::from_secs(59));
529    }
530
531    #[test]
532    fn test_error_uses_negative_max_ttl() {
533        let now = Instant::now();
534
535        let name = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
536
537        // configure the cache with a maximum TTL of 60 seconds.
538        let ttls = TtlConfig {
539            negative_max_ttl: Some(Duration::from_secs(60)),
540            ..TtlConfig::default()
541        };
542        let lru = DnsLru::new(1, ttls);
543
544        // neg response should have TTL of 62 seconds.
545        let err = ResolveErrorKind::NoRecordsFound {
546            query: Box::new(name.clone()),
547            soa: None,
548            negative_ttl: Some(62),
549            response_code: ResponseCode::NoError,
550            trusted: false,
551        };
552        let nx_error = lru.negative(name.clone(), err.into(), now);
553        match nx_error.kind() {
554            &ResolveErrorKind::NoRecordsFound { negative_ttl, .. } => {
555                let negative_ttl = negative_ttl.expect("resolve error should have a deadline");
556                // the error's `valid_until` field should have been limited to 60 seconds.
557                assert_eq!(negative_ttl, 60);
558            }
559            other => panic!("expected ResolveErrorKind::NoRecordsFound, got {:?}", other),
560        }
561
562        // neg response should have TTL of 59 seconds.
563        let err = ResolveErrorKind::NoRecordsFound {
564            query: Box::new(name.clone()),
565            soa: None,
566            negative_ttl: Some(59),
567            response_code: ResponseCode::NoError,
568            trusted: false,
569        };
570        let nx_error = lru.negative(name, err.into(), now);
571        match nx_error.kind() {
572            &ResolveErrorKind::NoRecordsFound { negative_ttl, .. } => {
573                let negative_ttl = negative_ttl.expect("resolve error should have a deadline");
574                // the error's `valid_until` field should not have been limited, as it was
575                // under the max TTL.
576                assert_eq!(negative_ttl, 59);
577            }
578            other => panic!("expected ResolveErrorKind::NoRecordsFound, got {:?}", other),
579        }
580    }
581
582    #[test]
583    fn test_insert() {
584        let now = Instant::now();
585
586        let name = Name::from_str("www.example.com.").unwrap();
587        let query = Query::query(name.clone(), RecordType::A);
588        let ips_ttl = vec![(
589            Record::from_rdata(name, 1, RData::A(A::new(127, 0, 0, 1))),
590            1,
591        )];
592        let ips = [RData::A(A::new(127, 0, 0, 1))];
593        let lru = DnsLru::new(1, TtlConfig::default());
594
595        let rc_ips = lru.insert(query.clone(), ips_ttl, now);
596        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
597
598        let rc_ips = lru.get(&query, now).unwrap().expect("records should exist");
599        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
600    }
601
602    #[test]
603    fn test_update_ttl() {
604        let now = Instant::now();
605
606        let name = Name::from_str("www.example.com.").unwrap();
607        let query = Query::query(name.clone(), RecordType::A);
608        let ips_ttl = vec![(
609            Record::from_rdata(name, 10, RData::A(A::new(127, 0, 0, 1))),
610            10,
611        )];
612        let ips = [RData::A(A::new(127, 0, 0, 1))];
613        let lru = DnsLru::new(1, TtlConfig::default());
614
615        let rc_ips = lru.insert(query.clone(), ips_ttl, now);
616        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
617
618        let ttl = lru
619            .get(&query, now + Duration::from_secs(2))
620            .unwrap()
621            .expect("records should exist")
622            .record_iter()
623            .next()
624            .unwrap()
625            .ttl();
626        assert!(ttl <= 8);
627    }
628
629    #[test]
630    fn test_insert_ttl() {
631        let now = Instant::now();
632        let name = Name::from_str("www.example.com.").unwrap();
633        let query = Query::query(name.clone(), RecordType::A);
634        // TTL should be 1
635        let ips_ttl = vec![
636            (
637                Record::from_rdata(name.clone(), 1, RData::A(A::new(127, 0, 0, 1))),
638                1,
639            ),
640            (
641                Record::from_rdata(name, 2, RData::A(A::new(127, 0, 0, 2))),
642                2,
643            ),
644        ];
645        let ips = vec![
646            RData::A(A::new(127, 0, 0, 1)),
647            RData::A(A::new(127, 0, 0, 2)),
648        ];
649        let lru = DnsLru::new(1, TtlConfig::default());
650
651        lru.insert(query.clone(), ips_ttl, now);
652
653        // still valid
654        let rc_ips = lru
655            .get(&query, now + Duration::from_secs(1))
656            .unwrap()
657            .expect("records should exist");
658        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
659
660        // 2 should be one too far
661        let rc_ips = lru.get(&query, now + Duration::from_secs(2));
662        assert!(rc_ips.is_none());
663    }
664
665    #[test]
666    fn test_insert_positive_min_ttl() {
667        let now = Instant::now();
668        let name = Name::from_str("www.example.com.").unwrap();
669        let query = Query::query(name.clone(), RecordType::A);
670        // TTL should be 1
671        let ips_ttl = vec![
672            (
673                Record::from_rdata(name.clone(), 1, RData::A(A::new(127, 0, 0, 1))),
674                1,
675            ),
676            (
677                Record::from_rdata(name, 2, RData::A(A::new(127, 0, 0, 2))),
678                2,
679            ),
680        ];
681        let ips = vec![
682            RData::A(A::new(127, 0, 0, 1)),
683            RData::A(A::new(127, 0, 0, 2)),
684        ];
685
686        // this cache should override the TTL of 1 seconds with the configured
687        // minimum TTL of 3 seconds.
688        let ttls = TtlConfig {
689            positive_min_ttl: Some(Duration::from_secs(3)),
690            ..TtlConfig::default()
691        };
692        let lru = DnsLru::new(1, ttls);
693        lru.insert(query.clone(), ips_ttl, now);
694
695        // still valid
696        let rc_ips = lru
697            .get(&query, now + Duration::from_secs(1))
698            .unwrap()
699            .expect("records should exist");
700        for (rc_ip, ip) in rc_ips.iter().zip(ips.iter()) {
701            assert_eq!(rc_ip, ip, "after 1 second");
702        }
703
704        let rc_ips = lru
705            .get(&query, now + Duration::from_secs(2))
706            .unwrap()
707            .expect("records should exist");
708        for (rc_ip, ip) in rc_ips.iter().zip(ips.iter()) {
709            assert_eq!(rc_ip, ip, "after 2 seconds");
710        }
711
712        let rc_ips = lru
713            .get(&query, now + Duration::from_secs(3))
714            .unwrap()
715            .expect("records should exist");
716        for (rc_ip, ip) in rc_ips.iter().zip(ips.iter()) {
717            assert_eq!(rc_ip, ip, "after 3 seconds");
718        }
719
720        // after 4 seconds, the records should be invalid.
721        let rc_ips = lru.get(&query, now + Duration::from_secs(4));
722        assert!(rc_ips.is_none());
723    }
724
725    #[test]
726    fn test_insert_positive_max_ttl() {
727        let now = Instant::now();
728        let name = Name::from_str("www.example.com.").unwrap();
729        let query = Query::query(name.clone(), RecordType::A);
730        // TTL should be 500
731        let ips_ttl = vec![
732            (
733                Record::from_rdata(name.clone(), 400, RData::A(A::new(127, 0, 0, 1))),
734                400,
735            ),
736            (
737                Record::from_rdata(name, 500, RData::A(A::new(127, 0, 0, 2))),
738                500,
739            ),
740        ];
741        let ips = vec![
742            RData::A(A::new(127, 0, 0, 1)),
743            RData::A(A::new(127, 0, 0, 2)),
744        ];
745
746        // this cache should override the TTL of 500 seconds with the configured
747        // minimum TTL of 2 seconds.
748        let ttls = TtlConfig {
749            positive_max_ttl: Some(Duration::from_secs(2)),
750            ..TtlConfig::default()
751        };
752        let lru = DnsLru::new(1, ttls);
753        lru.insert(query.clone(), ips_ttl, now);
754
755        // still valid
756        let rc_ips = lru
757            .get(&query, now + Duration::from_secs(1))
758            .unwrap()
759            .expect("records should exist");
760        for (rc_ip, ip) in rc_ips.iter().zip(ips.iter()) {
761            assert_eq!(rc_ip, ip, "after 1 second");
762        }
763
764        let rc_ips = lru
765            .get(&query, now + Duration::from_secs(2))
766            .unwrap()
767            .expect("records should exist");
768        for (rc_ip, ip) in rc_ips.iter().zip(ips.iter()) {
769            assert_eq!(rc_ip, ip, "after 2 seconds");
770        }
771
772        // after 3 seconds, the records should be invalid.
773        let rc_ips = lru.get(&query, now + Duration::from_secs(3));
774        assert!(rc_ips.is_none());
775    }
776}