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