trust_dns_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// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://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)
251                    .or_insert_with(Vec::default)
252                    .push((record, ttl));
253
254                map
255            },
256        );
257
258        // now insert by record type and name
259        let mut lookup = None;
260        for (query, records_and_ttl) in records {
261            let is_query = original_query == query;
262            let inserted = self.insert(query, records_and_ttl, now);
263
264            if is_query {
265                lookup = Some(inserted)
266            }
267        }
268
269        lookup
270    }
271
272    /// Generally for inserting a set of records that have already been cached, but with a different Query.
273    pub(crate) fn duplicate(&self, query: Query, lookup: Lookup, ttl: u32, now: Instant) -> Lookup {
274        let ttl = Duration::from_secs(u64::from(ttl));
275        let valid_until = now + ttl;
276
277        self.cache.lock().insert(
278            query,
279            LruValue {
280                lookup: Ok(lookup.clone()),
281                valid_until,
282            },
283        );
284
285        lookup
286    }
287
288    /// This converts the ResolveError to set the inner negative_ttl value to be the
289    ///  current expiration ttl.
290    fn nx_error_with_ttl(error: &mut ResolveError, new_ttl: Duration) {
291        if let ResolveError {
292            kind:
293                ResolveErrorKind::NoRecordsFound {
294                    ref mut negative_ttl,
295                    ..
296                },
297            ..
298        } = error
299        {
300            *negative_ttl = Some(u32::try_from(new_ttl.as_secs()).unwrap_or(MAX_TTL));
301        }
302    }
303
304    pub(crate) fn negative(
305        &self,
306        query: Query,
307        mut error: ResolveError,
308        now: Instant,
309    ) -> ResolveError {
310        // TODO: if we are getting a negative response, should we instead fallback to cache?
311        //   this would cache indefinitely, probably not correct
312        if let ResolveError {
313            kind:
314                ResolveErrorKind::NoRecordsFound {
315                    negative_ttl: Some(ttl),
316                    ..
317                },
318            ..
319        } = error
320        {
321            let ttl_duration = Duration::from_secs(u64::from(ttl))
322                // Clamp the TTL so that it's between the cache's configured
323                // minimum and maximum TTLs for negative responses.
324                .clamp(self.negative_min_ttl, self.negative_max_ttl);
325            let valid_until = now + ttl_duration;
326
327            {
328                let error = error.clone();
329
330                self.cache.lock().insert(
331                    query,
332                    LruValue {
333                        lookup: Err(error),
334                        valid_until,
335                    },
336                );
337            }
338
339            Self::nx_error_with_ttl(&mut error, ttl_duration);
340        }
341
342        error
343    }
344
345    /// Based on the query, see if there are any records available
346    pub fn get(&self, query: &Query, now: Instant) -> Option<Result<Lookup, ResolveError>> {
347        let mut out_of_date = false;
348        let mut cache = self.cache.lock();
349        let lookup = cache.get_mut(query).and_then(|value| {
350            if value.is_current(now) {
351                out_of_date = false;
352                let mut result = value.with_updated_ttl(now).lookup;
353                if let Err(ref mut err) = result {
354                    Self::nx_error_with_ttl(err, value.ttl(now));
355                }
356                Some(result)
357            } else {
358                out_of_date = true;
359                None
360            }
361        });
362
363        // in this case, we can preemptively remove out of data elements
364        // this assumes time is always moving forward, this would only not be true in contrived situations where now
365        //  is not current time, like tests...
366        if out_of_date {
367            cache.remove(query);
368        }
369
370        lookup
371    }
372}
373
374// see also the lookup_tests.rs in integration-tests crate
375#[cfg(test)]
376mod tests {
377    use std::str::FromStr;
378    use std::time::*;
379
380    use proto::op::{Query, ResponseCode};
381    use proto::rr::rdata::A;
382    use proto::rr::{Name, RData, RecordType};
383
384    use super::*;
385
386    #[test]
387    fn test_is_current() {
388        let now = Instant::now();
389        let not_the_future = now + Duration::from_secs(4);
390        let future = now + Duration::from_secs(5);
391        let past_the_future = now + Duration::from_secs(6);
392
393        let value = LruValue {
394            lookup: Err(ResolveErrorKind::Message("test error").into()),
395            valid_until: future,
396        };
397
398        assert!(value.is_current(now));
399        assert!(value.is_current(not_the_future));
400        assert!(value.is_current(future));
401        assert!(!value.is_current(past_the_future));
402    }
403
404    #[test]
405    fn test_lookup_uses_positive_min_ttl() {
406        let now = Instant::now();
407
408        let name = Name::from_str("www.example.com.").unwrap();
409        let query = Query::query(name.clone(), RecordType::A);
410        // record should have TTL of 1 second.
411        let ips_ttl = vec![(
412            Record::from_rdata(name.clone(), 1, RData::A(A::new(127, 0, 0, 1))),
413            1,
414        )];
415        let ips = vec![RData::A(A::new(127, 0, 0, 1))];
416
417        // configure the cache with a minimum TTL of 2 seconds.
418        let ttls = TtlConfig {
419            positive_min_ttl: Some(Duration::from_secs(2)),
420            ..TtlConfig::default()
421        };
422        let lru = DnsLru::new(1, ttls);
423
424        let rc_ips = lru.insert(query.clone(), ips_ttl, now);
425        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
426        // the returned lookup should use the cache's min TTL, since the
427        // query's TTL was below the minimum.
428        assert_eq!(rc_ips.valid_until(), now + Duration::from_secs(2));
429
430        // record should have TTL of 3 seconds.
431        let ips_ttl = vec![(
432            Record::from_rdata(name, 3, RData::A(A::new(127, 0, 0, 1))),
433            3,
434        )];
435
436        let rc_ips = lru.insert(query, ips_ttl, now);
437        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
438        // the returned lookup should use the record's TTL, since it's
439        // greater than the cache's minimum.
440        assert_eq!(rc_ips.valid_until(), now + Duration::from_secs(3));
441    }
442
443    #[test]
444    fn test_error_uses_negative_min_ttl() {
445        let now = Instant::now();
446
447        let name = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
448
449        // configure the cache with a maximum TTL of 2 seconds.
450        let ttls = TtlConfig {
451            negative_min_ttl: Some(Duration::from_secs(2)),
452            ..TtlConfig::default()
453        };
454        let lru = DnsLru::new(1, ttls);
455
456        // neg response should have TTL of 1 seconds.
457        let err = ResolveErrorKind::NoRecordsFound {
458            query: Box::new(name.clone()),
459            soa: None,
460            negative_ttl: Some(1),
461            response_code: ResponseCode::NoError,
462            trusted: false,
463        };
464        let nx_error = lru.negative(name.clone(), err.into(), now);
465        match nx_error.kind() {
466            &ResolveErrorKind::NoRecordsFound { negative_ttl, .. } => {
467                let valid_until = negative_ttl.expect("resolve error should have a deadline");
468                // the error's `valid_until` field should have been limited to 2 seconds.
469                assert_eq!(valid_until, 2);
470            }
471            other => panic!("expected ResolveErrorKind::NoRecordsFound, got {:?}", other),
472        }
473
474        // neg response should have TTL of 3 seconds.
475        let err = ResolveErrorKind::NoRecordsFound {
476            query: Box::new(name.clone()),
477            soa: None,
478            negative_ttl: Some(3),
479            response_code: ResponseCode::NoError,
480            trusted: false,
481        };
482        let nx_error = lru.negative(name, err.into(), now);
483        match nx_error.kind() {
484            &ResolveErrorKind::NoRecordsFound { negative_ttl, .. } => {
485                let negative_ttl = negative_ttl.expect("ResolveError should have a deadline");
486                // the error's `valid_until` field should not have been limited, as it was
487                // over the min TTL.
488                assert_eq!(negative_ttl, 3);
489            }
490            other => panic!("expected ResolveErrorKind::NoRecordsFound, got {:?}", other),
491        }
492    }
493
494    #[test]
495    fn test_lookup_uses_positive_max_ttl() {
496        let now = Instant::now();
497
498        let name = Name::from_str("www.example.com.").unwrap();
499        let query = Query::query(name.clone(), RecordType::A);
500        // record should have TTL of 62 seconds.
501        let ips_ttl = vec![(
502            Record::from_rdata(name.clone(), 62, RData::A(A::new(127, 0, 0, 1))),
503            62,
504        )];
505        let ips = vec![RData::A(A::new(127, 0, 0, 1))];
506
507        // configure the cache with a maximum TTL of 60 seconds.
508        let ttls = TtlConfig {
509            positive_max_ttl: Some(Duration::from_secs(60)),
510            ..TtlConfig::default()
511        };
512        let lru = DnsLru::new(1, ttls);
513
514        let rc_ips = lru.insert(query.clone(), ips_ttl, now);
515        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
516        // the returned lookup should use the cache's min TTL, since the
517        // query's TTL was above the maximum.
518        assert_eq!(rc_ips.valid_until(), now + Duration::from_secs(60));
519
520        // record should have TTL of 59 seconds.
521        let ips_ttl = vec![(
522            Record::from_rdata(name, 59, RData::A(A::new(127, 0, 0, 1))),
523            59,
524        )];
525
526        let rc_ips = lru.insert(query, ips_ttl, now);
527        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
528        // the returned lookup should use the record's TTL, since it's
529        // below than the cache's maximum.
530        assert_eq!(rc_ips.valid_until(), now + Duration::from_secs(59));
531    }
532
533    #[test]
534    fn test_error_uses_negative_max_ttl() {
535        let now = Instant::now();
536
537        let name = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
538
539        // configure the cache with a maximum TTL of 60 seconds.
540        let ttls = TtlConfig {
541            negative_max_ttl: Some(Duration::from_secs(60)),
542            ..TtlConfig::default()
543        };
544        let lru = DnsLru::new(1, ttls);
545
546        // neg response should have TTL of 62 seconds.
547        let err = ResolveErrorKind::NoRecordsFound {
548            query: Box::new(name.clone()),
549            soa: None,
550            negative_ttl: Some(62),
551            response_code: ResponseCode::NoError,
552            trusted: false,
553        };
554        let nx_error = lru.negative(name.clone(), err.into(), now);
555        match nx_error.kind() {
556            &ResolveErrorKind::NoRecordsFound { negative_ttl, .. } => {
557                let negative_ttl = negative_ttl.expect("resolve error should have a deadline");
558                // the error's `valid_until` field should have been limited to 60 seconds.
559                assert_eq!(negative_ttl, 60);
560            }
561            other => panic!("expected ResolveErrorKind::NoRecordsFound, got {:?}", other),
562        }
563
564        // neg response should have TTL of 59 seconds.
565        let err = ResolveErrorKind::NoRecordsFound {
566            query: Box::new(name.clone()),
567            soa: None,
568            negative_ttl: Some(59),
569            response_code: ResponseCode::NoError,
570            trusted: false,
571        };
572        let nx_error = lru.negative(name, err.into(), now);
573        match nx_error.kind() {
574            &ResolveErrorKind::NoRecordsFound { negative_ttl, .. } => {
575                let negative_ttl = negative_ttl.expect("resolve error should have a deadline");
576                // the error's `valid_until` field should not have been limited, as it was
577                // under the max TTL.
578                assert_eq!(negative_ttl, 59);
579            }
580            other => panic!("expected ResolveErrorKind::NoRecordsFound, got {:?}", other),
581        }
582    }
583
584    #[test]
585    fn test_insert() {
586        let now = Instant::now();
587
588        let name = Name::from_str("www.example.com.").unwrap();
589        let query = Query::query(name.clone(), RecordType::A);
590        let ips_ttl = vec![(
591            Record::from_rdata(name, 1, RData::A(A::new(127, 0, 0, 1))),
592            1,
593        )];
594        let ips = vec![RData::A(A::new(127, 0, 0, 1))];
595        let lru = DnsLru::new(1, TtlConfig::default());
596
597        let rc_ips = lru.insert(query.clone(), ips_ttl, now);
598        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
599
600        let rc_ips = lru.get(&query, now).unwrap().expect("records should exist");
601        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
602    }
603
604    #[test]
605    fn test_update_ttl() {
606        let now = Instant::now();
607
608        let name = Name::from_str("www.example.com.").unwrap();
609        let query = Query::query(name.clone(), RecordType::A);
610        let ips_ttl = vec![(
611            Record::from_rdata(name, 10, RData::A(A::new(127, 0, 0, 1))),
612            10,
613        )];
614        let ips = vec![RData::A(A::new(127, 0, 0, 1))];
615        let lru = DnsLru::new(1, TtlConfig::default());
616
617        let rc_ips = lru.insert(query.clone(), ips_ttl, now);
618        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
619
620        let ttl = lru
621            .get(&query, now + Duration::from_secs(2))
622            .unwrap()
623            .expect("records should exist")
624            .record_iter()
625            .next()
626            .unwrap()
627            .ttl();
628        assert!(ttl <= 8);
629    }
630
631    #[test]
632    fn test_insert_ttl() {
633        let now = Instant::now();
634        let name = Name::from_str("www.example.com.").unwrap();
635        let query = Query::query(name.clone(), RecordType::A);
636        // TTL should be 1
637        let ips_ttl = vec![
638            (
639                Record::from_rdata(name.clone(), 1, RData::A(A::new(127, 0, 0, 1))),
640                1,
641            ),
642            (
643                Record::from_rdata(name, 2, RData::A(A::new(127, 0, 0, 2))),
644                2,
645            ),
646        ];
647        let ips = vec![
648            RData::A(A::new(127, 0, 0, 1)),
649            RData::A(A::new(127, 0, 0, 2)),
650        ];
651        let lru = DnsLru::new(1, TtlConfig::default());
652
653        lru.insert(query.clone(), ips_ttl, now);
654
655        // still valid
656        let rc_ips = lru
657            .get(&query, now + Duration::from_secs(1))
658            .unwrap()
659            .expect("records should exist");
660        assert_eq!(*rc_ips.iter().next().unwrap(), ips[0]);
661
662        // 2 should be one too far
663        let rc_ips = lru.get(&query, now + Duration::from_secs(2));
664        assert!(rc_ips.is_none());
665    }
666
667    #[test]
668    fn test_insert_positive_min_ttl() {
669        let now = Instant::now();
670        let name = Name::from_str("www.example.com.").unwrap();
671        let query = Query::query(name.clone(), RecordType::A);
672        // TTL should be 1
673        let ips_ttl = vec![
674            (
675                Record::from_rdata(name.clone(), 1, RData::A(A::new(127, 0, 0, 1))),
676                1,
677            ),
678            (
679                Record::from_rdata(name, 2, RData::A(A::new(127, 0, 0, 2))),
680                2,
681            ),
682        ];
683        let ips = vec![
684            RData::A(A::new(127, 0, 0, 1)),
685            RData::A(A::new(127, 0, 0, 2)),
686        ];
687
688        // this cache should override the TTL of 1 seconds with the configured
689        // minimum TTL of 3 seconds.
690        let ttls = TtlConfig {
691            positive_min_ttl: Some(Duration::from_secs(3)),
692            ..TtlConfig::default()
693        };
694        let lru = DnsLru::new(1, ttls);
695        lru.insert(query.clone(), ips_ttl, now);
696
697        // still valid
698        let rc_ips = lru
699            .get(&query, now + Duration::from_secs(1))
700            .unwrap()
701            .expect("records should exist");
702        for (rc_ip, ip) in rc_ips.iter().zip(ips.iter()) {
703            assert_eq!(rc_ip, ip, "after 1 second");
704        }
705
706        let rc_ips = lru
707            .get(&query, now + Duration::from_secs(2))
708            .unwrap()
709            .expect("records should exist");
710        for (rc_ip, ip) in rc_ips.iter().zip(ips.iter()) {
711            assert_eq!(rc_ip, ip, "after 2 seconds");
712        }
713
714        let rc_ips = lru
715            .get(&query, now + Duration::from_secs(3))
716            .unwrap()
717            .expect("records should exist");
718        for (rc_ip, ip) in rc_ips.iter().zip(ips.iter()) {
719            assert_eq!(rc_ip, ip, "after 3 seconds");
720        }
721
722        // after 4 seconds, the records should be invalid.
723        let rc_ips = lru.get(&query, now + Duration::from_secs(4));
724        assert!(rc_ips.is_none());
725    }
726
727    #[test]
728    fn test_insert_positive_max_ttl() {
729        let now = Instant::now();
730        let name = Name::from_str("www.example.com.").unwrap();
731        let query = Query::query(name.clone(), RecordType::A);
732        // TTL should be 500
733        let ips_ttl = vec![
734            (
735                Record::from_rdata(name.clone(), 400, RData::A(A::new(127, 0, 0, 1))),
736                400,
737            ),
738            (
739                Record::from_rdata(name, 500, RData::A(A::new(127, 0, 0, 2))),
740                500,
741            ),
742        ];
743        let ips = vec![
744            RData::A(A::new(127, 0, 0, 1)),
745            RData::A(A::new(127, 0, 0, 2)),
746        ];
747
748        // this cache should override the TTL of 500 seconds with the configured
749        // minimum TTL of 2 seconds.
750        let ttls = TtlConfig {
751            positive_max_ttl: Some(Duration::from_secs(2)),
752            ..TtlConfig::default()
753        };
754        let lru = DnsLru::new(1, ttls);
755        lru.insert(query.clone(), ips_ttl, now);
756
757        // still valid
758        let rc_ips = lru
759            .get(&query, now + Duration::from_secs(1))
760            .unwrap()
761            .expect("records should exist");
762        for (rc_ip, ip) in rc_ips.iter().zip(ips.iter()) {
763            assert_eq!(rc_ip, ip, "after 1 second");
764        }
765
766        let rc_ips = lru
767            .get(&query, now + Duration::from_secs(2))
768            .unwrap()
769            .expect("records should exist");
770        for (rc_ip, ip) in rc_ips.iter().zip(ips.iter()) {
771            assert_eq!(rc_ip, ip, "after 2 seconds");
772        }
773
774        // after 3 seconds, the records should be invalid.
775        let rc_ips = lru.get(&query, now + Duration::from_secs(3));
776        assert!(rc_ips.is_none());
777    }
778}