use std::{
collections::HashMap,
ops::RangeInclusive,
sync::Arc,
time::{Duration, Instant},
};
use moka::{Expiry, sync::Cache};
#[cfg(feature = "serde")]
use serde::Deserialize;
use crate::{
config,
net::{DnsError, NetError, NoRecords},
proto::{
op::{Message, Query},
rr::RecordType,
},
};
#[derive(Clone, Debug)]
pub struct ResponseCache {
cache: Cache<Query, Entry>,
ttl_config: Arc<TtlConfig>,
}
impl ResponseCache {
pub fn new(capacity: u64, ttl_config: TtlConfig) -> Self {
Self {
cache: Cache::builder()
.max_capacity(capacity)
.expire_after(EntryExpiry)
.build(),
ttl_config: Arc::new(ttl_config),
}
}
pub fn insert(&self, query: Query, result: Result<Message, NetError>, now: Instant) {
let (ttl, result) = match result {
Ok(mut message) => {
let ttl = self.clamp_positive_ttls(query.query_type(), &mut message);
(ttl, Ok(message))
}
Err(NetError::Dns(DnsError::NoRecordsFound(no_records))) => {
let (negative_min_ttl, negative_max_ttl) = self
.ttl_config
.negative_response_ttl_bounds(query.query_type())
.into_inner();
let ttl = if let Some(ttl) = no_records.negative_ttl {
Duration::from_secs(u64::from(ttl)).clamp(negative_min_ttl, negative_max_ttl)
} else {
negative_min_ttl
};
(
ttl,
Err(NetError::Dns(DnsError::NoRecordsFound(no_records))),
)
}
Err(_) => return,
};
let valid_until = now + ttl;
self.cache.insert(
query,
Entry {
result: Arc::new(result),
original_time: now,
valid_until,
},
);
}
pub fn get(&self, query: &Query, now: Instant) -> Option<Result<Message, NetError>> {
let entry = self.cache.get(query)?;
if !entry.is_current(now) {
return None;
}
Some(entry.updated_ttl(now))
}
pub(crate) fn clamp_positive_ttls(
&self,
query_type: RecordType,
message: &mut Message,
) -> Duration {
for record in message
.answers
.iter_mut()
.chain(message.authorities.iter_mut())
.chain(message.additionals.iter_mut())
{
let (min_secs, max_secs) = self
.ttl_config
.positive_ttl_bounds_secs(record.record_type());
record.ttl = record.ttl.clamp(min_secs, max_secs);
}
let (positive_min_ttl, positive_max_ttl) = self
.ttl_config
.positive_response_ttl_bounds(query_type)
.into_inner();
let min_ttl = message
.all_sections()
.filter(|r| r.record_type() == query_type)
.map(|r| Duration::from_secs(r.ttl.into()))
.min();
min_ttl
.unwrap_or(positive_min_ttl)
.clamp(positive_min_ttl, positive_max_ttl)
}
pub(crate) fn clear(&self) {
self.cache.invalidate_all();
}
pub(crate) fn clear_query(&self, query: &Query) {
self.cache.invalidate(query);
}
#[cfg(feature = "metrics")]
pub(crate) fn entry_count(&self) -> u64 {
#[cfg(test)]
{
self.cache.run_pending_tasks();
}
self.cache.entry_count()
}
}
#[derive(Debug, Clone)]
struct Entry {
result: Arc<Result<Message, NetError>>,
original_time: Instant,
valid_until: Instant,
}
impl Entry {
fn updated_ttl(&self, now: Instant) -> Result<Message, NetError> {
let elapsed = u32::try_from(now.saturating_duration_since(self.original_time).as_secs())
.unwrap_or(u32::MAX);
match &*self.result {
Ok(response) => {
let mut response = response.clone();
for records in [
&mut response.answers,
&mut response.authorities,
&mut response.additionals,
] {
for record in records {
record.decrement_ttl(elapsed);
}
}
Ok(response)
}
Err(e) => {
let mut e = e.clone();
if let NetError::Dns(DnsError::NoRecordsFound(NoRecords {
negative_ttl,
soa,
authorities,
ns,
..
})) = &mut e
{
if let Some(ttl) = negative_ttl {
*ttl = ttl.saturating_sub(elapsed);
}
if let Some(soa) = soa {
soa.decrement_ttl(elapsed);
}
if let Some(recs) = authorities.take() {
authorities.replace(Arc::from(
recs.iter()
.cloned()
.map(|mut rec| {
rec.decrement_ttl(elapsed);
rec
})
.collect::<Vec<_>>(),
));
}
if let Some(ns_recs) = ns.take() {
ns.replace(Arc::from(
ns_recs
.iter()
.cloned()
.map(|mut ns| {
ns.ns.decrement_ttl(elapsed);
ns.glue = Arc::from(
ns.glue
.iter()
.cloned()
.map(|mut glue| {
glue.decrement_ttl(elapsed);
glue
})
.collect::<Vec<_>>(),
);
ns
})
.collect::<Vec<_>>(),
));
}
}
Err(e)
}
}
}
fn is_current(&self, now: Instant) -> bool {
now <= self.valid_until
}
fn ttl(&self, now: Instant) -> Duration {
self.valid_until.saturating_duration_since(now)
}
}
struct EntryExpiry;
impl Expiry<Query, Entry> for EntryExpiry {
fn expire_after_create(
&self,
_key: &Query,
value: &Entry,
created_at: Instant,
) -> Option<Duration> {
Some(value.ttl(created_at))
}
fn expire_after_update(
&self,
_key: &Query,
value: &Entry,
updated_at: Instant,
_duration_until_expiry: Option<Duration>,
) -> Option<Duration> {
Some(value.ttl(updated_at))
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Deserialize))]
#[cfg_attr(
feature = "serde",
serde(from = "ttl_config_deserialize::TtlConfigMap")
)]
pub struct TtlConfig {
default: TtlBounds,
by_query_type: HashMap<RecordType, TtlBounds>,
}
impl TtlConfig {
pub fn from_opts(opts: &config::ResolverOpts) -> Self {
Self::from(TtlBounds {
positive_min_ttl: opts.positive_min_ttl,
negative_min_ttl: opts.negative_min_ttl,
positive_max_ttl: opts.positive_max_ttl,
negative_max_ttl: opts.negative_max_ttl,
})
}
pub fn with_query_type_ttl_bounds(
&mut self,
query_type: RecordType,
bounds: TtlBounds,
) -> &mut Self {
self.by_query_type.insert(query_type, bounds);
self
}
fn positive_ttl_bounds_secs(&self, record_type: RecordType) -> (u32, u32) {
let (min, max) = self.positive_response_ttl_bounds(record_type).into_inner();
(
u32::try_from(min.as_secs()).unwrap_or(MAX_TTL),
u32::try_from(max.as_secs()).unwrap_or(MAX_TTL),
)
}
pub fn positive_response_ttl_bounds(&self, query_type: RecordType) -> RangeInclusive<Duration> {
let bounds = self.by_query_type.get(&query_type).unwrap_or(&self.default);
let min = bounds
.positive_min_ttl
.unwrap_or_else(|| Duration::from_secs(0));
let max = bounds
.positive_max_ttl
.unwrap_or_else(|| Duration::from_secs(u64::from(MAX_TTL)));
min..=max
}
pub fn negative_response_ttl_bounds(&self, query_type: RecordType) -> RangeInclusive<Duration> {
let bounds = self.by_query_type.get(&query_type).unwrap_or(&self.default);
let min = bounds
.negative_min_ttl
.unwrap_or_else(|| Duration::from_secs(0));
let max = bounds
.negative_max_ttl
.unwrap_or_else(|| Duration::from_secs(u64::from(MAX_TTL)));
min..=max
}
}
impl From<TtlBounds> for TtlConfig {
fn from(default: TtlBounds) -> Self {
Self {
default,
by_query_type: HashMap::default(),
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
pub struct TtlBounds {
#[cfg_attr(
feature = "serde",
serde(default, deserialize_with = "config::duration_opt::deserialize")
)]
positive_min_ttl: Option<Duration>,
#[cfg_attr(
feature = "serde",
serde(default, deserialize_with = "config::duration_opt::deserialize")
)]
negative_min_ttl: Option<Duration>,
#[cfg_attr(
feature = "serde",
serde(default, deserialize_with = "config::duration_opt::deserialize")
)]
positive_max_ttl: Option<Duration>,
#[cfg_attr(
feature = "serde",
serde(default, deserialize_with = "config::duration_opt::deserialize")
)]
negative_max_ttl: Option<Duration>,
}
#[cfg(feature = "serde")]
mod ttl_config_deserialize {
use std::collections::HashMap;
use serde::Deserialize;
use super::{TtlBounds, TtlConfig};
use crate::proto::rr::RecordType;
#[derive(Deserialize)]
pub(super) struct TtlConfigMap(HashMap<TtlConfigField, TtlBounds>);
impl From<TtlConfigMap> for TtlConfig {
fn from(value: TtlConfigMap) -> Self {
let mut default = TtlBounds::default();
let mut by_query_type = HashMap::new();
for (field, bounds) in value.0.into_iter() {
match field {
TtlConfigField::RecordType(record_type) => {
by_query_type.insert(record_type, bounds);
}
TtlConfigField::Default => default = bounds,
}
}
Self {
default,
by_query_type,
}
}
}
#[derive(PartialEq, Eq, Hash, Deserialize)]
enum TtlConfigField {
#[serde(rename = "default")]
Default,
#[serde(untagged)]
RecordType(RecordType),
}
}
pub const MAX_TTL: u32 = 86400_u32;
#[cfg(test)]
mod tests {
use std::{
str::FromStr,
time::{Duration, Instant},
};
#[cfg(feature = "serde")]
use serde::Deserialize;
use super::*;
use crate::{
net::{ForwardNSData, NetError},
proto::{
op::{Message, OpCode, Query, ResponseCode},
rr::{
Name, RData, Record, RecordType,
rdata::{A, AAAA, NS, SOA, TXT},
},
},
};
use test_support::subscribe;
#[test]
fn test_is_current() {
let now = Instant::now();
let not_the_future = now + Duration::from_secs(4);
let future = now + Duration::from_secs(5);
let past_the_future = now + Duration::from_secs(6);
let entry = Entry {
result: Err(NetError::Message("test error")).into(),
original_time: now,
valid_until: future,
};
assert!(entry.is_current(now));
assert!(entry.is_current(not_the_future));
assert!(entry.is_current(future));
assert!(!entry.is_current(past_the_future));
}
#[test]
fn test_positive_min_ttl() {
let now = Instant::now();
let name = Name::from_str("www.example.com.").unwrap();
let query = Query::query(name.clone(), RecordType::A);
let mut message = Message::response(0, OpCode::Query);
message.add_answer(Record::from_rdata(
name.clone(),
1,
RData::A(A::new(127, 0, 0, 1)),
));
let ttls = TtlConfig::from(TtlBounds {
positive_min_ttl: Some(Duration::from_secs(2)),
..TtlBounds::default()
});
let cache = ResponseCache::new(1, ttls);
cache.insert(query.clone(), Ok(message), now);
let valid_until = cache.cache.get(&query).unwrap().valid_until;
assert_eq!(valid_until, now + Duration::from_secs(2));
let mut message = Message::response(0, OpCode::Query);
message.add_answer(Record::from_rdata(
name.clone(),
3,
RData::A(A::new(127, 0, 0, 1)),
));
cache.insert(query.clone(), Ok(message), now);
let valid_until = cache.cache.get(&query).unwrap().valid_until;
assert_eq!(valid_until, now + Duration::from_secs(3));
}
#[test]
fn test_positive_min_ttl_clamps_record_ttls() {
let now = Instant::now();
let name = Name::from_str("www.example.com.").unwrap();
let query = Query::query(name.clone(), RecordType::A);
let mut message = Message::response(0, OpCode::Query);
message.add_answer(Record::from_rdata(
name.clone(),
60,
RData::A(A::new(93, 184, 216, 34)),
));
let ttls = TtlConfig::from(TtlBounds {
positive_min_ttl: Some(Duration::from_secs(3600)),
..TtlBounds::default()
});
let cache = ResponseCache::new(1, ttls);
cache.insert(query.clone(), Ok(message), now);
let result = cache.get(&query, now).unwrap().unwrap();
assert_eq!(result.answers.first().unwrap().ttl, 3600);
let result = cache
.get(&query, now + Duration::from_secs(61))
.unwrap()
.unwrap();
assert_eq!(result.answers.first().unwrap().ttl, 3539);
let result = cache
.get(&query, now + Duration::from_secs(3599))
.unwrap()
.unwrap();
assert_eq!(result.answers.first().unwrap().ttl, 1);
assert!(cache.get(&query, now + Duration::from_secs(3601)).is_none());
}
#[test]
fn test_positive_max_ttl_clamps_record_ttls() {
let now = Instant::now();
let name = Name::from_str("www.example.com.").unwrap();
let query = Query::query(name.clone(), RecordType::A);
let mut message = Message::response(0, OpCode::Query);
message.add_answer(Record::from_rdata(
name.clone(),
3600,
RData::A(A::new(93, 184, 216, 34)),
));
let ttls = TtlConfig::from(TtlBounds {
positive_max_ttl: Some(Duration::from_secs(120)),
..TtlBounds::default()
});
let cache = ResponseCache::new(1, ttls);
cache.insert(query.clone(), Ok(message), now);
let result = cache.get(&query, now).unwrap().unwrap();
assert_eq!(result.answers.first().unwrap().ttl, 120);
let result = cache
.get(&query, now + Duration::from_secs(60))
.unwrap()
.unwrap();
assert_eq!(result.answers.first().unwrap().ttl, 60);
assert!(cache.get(&query, now + Duration::from_secs(121)).is_none());
}
#[test]
fn test_authority_ttl_does_not_shorten_answer_cache() {
let now = Instant::now();
let name = Name::from_str("api.example.com.").unwrap();
let query = Query::query(name.clone(), RecordType::AAAA);
let mut message = Message::response(0, OpCode::Query);
message.add_answer(Record::from_rdata(
name.clone(),
120,
RData::AAAA(AAAA::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1)),
));
message.add_authority(Record::from_rdata(
Name::from_str("example.com.").unwrap(),
30,
RData::NS(NS(Name::from_str("ns1.example.com.").unwrap())),
));
let ttls = TtlConfig::from(TtlBounds {
positive_min_ttl: Some(Duration::from_secs(3600)),
positive_max_ttl: Some(Duration::from_secs(28800)),
..TtlBounds::default()
});
let cache = ResponseCache::new(1, ttls);
cache.insert(query.clone(), Ok(message), now);
let valid_until = cache.cache.get(&query).unwrap().valid_until;
assert_eq!(valid_until, now + Duration::from_secs(3600));
let result = cache
.get(&query, now + Duration::from_secs(130))
.unwrap()
.unwrap();
assert_eq!(result.answers.first().unwrap().ttl, 3470);
assert!(cache.get(&query, now + Duration::from_secs(3601)).is_none());
}
#[test]
fn test_negative_min_ttl() {
let now = Instant::now();
let name = Name::from_str("www.example.com.").unwrap();
let query = Query::query(name.clone(), RecordType::A);
let ttls = TtlConfig::from(TtlBounds {
negative_min_ttl: Some(Duration::from_secs(2)),
..TtlBounds::default()
});
let cache = ResponseCache::new(1, ttls);
let mut no_records = NoRecords::new(query.clone(), ResponseCode::NoError);
no_records.negative_ttl = Some(1);
cache.insert(query.clone(), Err(no_records.into()), now);
let valid_until = cache.cache.get(&query).unwrap().valid_until;
assert_eq!(valid_until, now + Duration::from_secs(2));
let mut no_records = NoRecords::new(query.clone(), ResponseCode::NoError);
no_records.negative_ttl = Some(3);
cache.insert(query.clone(), Err(no_records.into()), now);
let valid_until = cache.cache.get(&query).unwrap().valid_until;
assert_eq!(valid_until, now + Duration::from_secs(3));
}
#[test]
fn test_positive_max_ttl() {
let now = Instant::now();
let name = Name::from_str("www.example.com.").unwrap();
let query = Query::query(name.clone(), RecordType::A);
let mut message = Message::response(0, OpCode::Query);
message.add_answer(Record::from_rdata(
name.clone(),
62,
RData::A(A::new(127, 0, 0, 1)),
));
let ttls = TtlConfig::from(TtlBounds {
positive_max_ttl: Some(Duration::from_secs(60)),
..Default::default()
});
let cache = ResponseCache::new(1, ttls);
cache.insert(query.clone(), Ok(message), now);
let valid_until = cache.cache.get(&query).unwrap().valid_until;
assert_eq!(valid_until, now + Duration::from_secs(60));
let mut message = Message::response(0, OpCode::Query);
message.add_answer(Record::from_rdata(
name.clone(),
59,
RData::A(A::new(127, 0, 0, 1)),
));
cache.insert(query.clone(), Ok(message), now);
let valid_until = cache.cache.get(&query).unwrap().valid_until;
assert_eq!(valid_until, now + Duration::from_secs(59));
}
#[test]
fn test_negative_max_ttl() {
let now = Instant::now();
let name = Name::from_str("www.example.com.").unwrap();
let query = Query::query(name.clone(), RecordType::A);
let ttls = TtlConfig::from(TtlBounds {
negative_max_ttl: Some(Duration::from_secs(60)),
..TtlBounds::default()
});
let cache = ResponseCache::new(1, ttls);
let mut no_records = NoRecords::new(query.clone(), ResponseCode::NoError);
no_records.negative_ttl = Some(62);
cache.insert(query.clone(), Err(no_records.into()), now);
let valid_until = cache.cache.get(&query).unwrap().valid_until;
assert_eq!(valid_until, now + Duration::from_secs(60));
let mut no_records = NoRecords::new(query.clone(), ResponseCode::NoError);
no_records.negative_ttl = Some(59);
cache.insert(query.clone(), Err(no_records.into()), now);
let valid_until = cache.cache.get(&query).unwrap().valid_until;
assert_eq!(valid_until, now + Duration::from_secs(59));
}
#[test]
fn test_insert() {
let now = Instant::now();
let name = Name::from_str("www.example.com.").unwrap();
let query = Query::query(name.clone(), RecordType::A);
let mut message = Message::response(0, OpCode::Query);
message.add_answer(Record::from_rdata(
name.clone(),
1,
RData::A(A::new(127, 0, 0, 1)),
));
let cache = ResponseCache::new(1, TtlConfig::default());
cache.insert(query.clone(), Ok(message.clone()), now);
let result = cache.get(&query, now).unwrap();
let cache_message = result.unwrap();
assert_eq!(cache_message.answers, message.answers);
}
#[test]
fn test_insert_negative() {
subscribe();
let now = Instant::now();
let query = Query::query(
Name::from_str("www.example.com.").unwrap(),
RecordType::AAAA,
);
let mut norecs = NoRecords::new(query.clone(), ResponseCode::NXDomain);
norecs.negative_ttl = Some(10);
let error = NetError::from(norecs);
let cache = ResponseCache::new(1, TtlConfig::default());
cache.insert(query.clone(), Err(error), now);
let cache_err = cache.get(&query, now).unwrap().unwrap_err();
let NetError::Dns(DnsError::NoRecordsFound(_no_records)) = &cache_err else {
panic!("expected NoRecordsFound");
};
assert!(cache.get(&query, now + Duration::from_secs(11)).is_none());
}
#[test]
fn test_update_ttl() {
let now = Instant::now();
let name = Name::from_str("www.example.com.").unwrap();
let query = Query::query(name.clone(), RecordType::A);
let mut message = Message::response(0, OpCode::Query);
message.add_answer(Record::from_rdata(
name.clone(),
10,
RData::A(A::new(127, 0, 0, 1)),
));
let cache = ResponseCache::new(1, TtlConfig::default());
cache.insert(query.clone(), Ok(message), now);
let result = cache.get(&query, now + Duration::from_secs(2)).unwrap();
let cache_message = result.unwrap();
let record = cache_message.answers.first().unwrap();
assert_eq!(record.ttl, 8);
}
#[test]
fn test_update_ttl_negative() -> Result<(), NetError> {
subscribe();
let now = Instant::now();
let name = Name::from_str("www.example.com.")?;
let ns_name = Name::from_str("ns1.example.com")?;
let zone_name = name.base_name();
let query = Query::query(name.clone(), RecordType::AAAA);
let mut norecs = NoRecords::new(query.clone(), ResponseCode::NXDomain);
norecs.negative_ttl = Some(10);
norecs.soa = Some(Box::new(Record::from_rdata(
zone_name.clone(),
10,
SOA::new(name.base_name(), name.clone(), 1, 1, 1, 1, 1),
)));
norecs.authorities = Some(Arc::new([Record::from_rdata(
zone_name.clone(),
10,
RData::NS(NS(ns_name.clone())),
)]));
norecs.ns = Some(Arc::new([ForwardNSData {
ns: Record::from_rdata(zone_name.clone(), 10, RData::NS(NS(ns_name.clone()))),
glue: Arc::new([Record::from_rdata(
ns_name.clone(),
10,
RData::A(A([192, 0, 2, 1].into())),
)]),
}]));
let error = NetError::from(norecs);
let cache = ResponseCache::new(1, TtlConfig::default());
cache.insert(query.clone(), Err(error), now);
let cache_err = cache.get(&query, now).unwrap().unwrap_err();
let NetError::Dns(DnsError::NoRecordsFound(no_records)) = &cache_err else {
panic!("expected NoRecordsFound");
};
let Some(soa) = no_records.soa.clone() else {
panic!("no SOA in NoRecordsFound");
};
assert_eq!(soa.ttl, 10);
let cache_err = cache
.get(&query, now + Duration::from_secs(2))
.unwrap()
.unwrap_err();
let NetError::Dns(DnsError::NoRecordsFound(NoRecords {
negative_ttl: Some(negative_ttl),
soa: Some(soa),
authorities: Some(authorities),
ns: Some(ns),
..
})) = &cache_err
else {
panic!("expected NoRecordsFound with negative_ttl, soa, authorities, and ns");
};
assert_eq!(*negative_ttl, 8);
assert_eq!(soa.ttl, 8);
assert_eq!(authorities[0].ttl, 8);
assert_eq!(ns[0].ns.ttl, 8);
assert!(cache.get(&query, now + Duration::from_secs(11)).is_none());
Ok(())
}
#[test]
fn test_insert_ttl() {
let now = Instant::now();
let name = Name::from_str("www.example.com.").unwrap();
let query = Query::query(name.clone(), RecordType::A);
let mut message = Message::response(0, OpCode::Query);
message.add_answer(Record::from_rdata(
name.clone(),
1,
RData::A(A::new(127, 0, 0, 1)),
));
message.add_answer(Record::from_rdata(name, 2, RData::A(A::new(127, 0, 0, 2))));
let cache = ResponseCache::new(1, TtlConfig::default());
cache.insert(query.clone(), Ok(message), now);
cache
.get(&query, now + Duration::from_secs(1))
.unwrap()
.unwrap();
let option = cache.get(&query, now + Duration::from_secs(2));
assert!(option.is_none());
}
#[test]
fn test_ttl_different_query_types() {
let now = Instant::now();
let name = Name::from_str("www.example.com.").unwrap();
let query_a = Query::query(name.clone(), RecordType::A);
let rdata_a = RData::A(A::new(127, 0, 0, 1));
let mut message_a = Message::response(0, OpCode::Query);
message_a.add_answer(Record::from_rdata(name.clone(), 1, rdata_a.clone()));
let query_txt = Query::query(name.clone(), RecordType::TXT);
let rdata_txt = RData::TXT(TXT::new(vec!["data".to_string()]));
let mut message_txt = Message::response(0, OpCode::Query);
message_txt.add_answer(Record::from_rdata(name.clone(), 1, rdata_txt.clone()));
let mut ttl_config = TtlConfig::from(TtlBounds {
positive_min_ttl: Some(Duration::from_secs(2)),
..TtlBounds::default()
});
ttl_config.with_query_type_ttl_bounds(
RecordType::TXT,
TtlBounds {
positive_min_ttl: Some(Duration::from_secs(5)),
..TtlBounds::default()
},
);
let cache = ResponseCache::new(2, ttl_config);
cache.insert(query_a.clone(), Ok(message_a), now);
assert_eq!(
cache.cache.get(&query_a).unwrap().valid_until,
now + Duration::from_secs(2)
);
cache.insert(query_txt.clone(), Ok(message_txt), now);
assert_eq!(
cache.cache.get(&query_txt).unwrap().valid_until,
now + Duration::from_secs(5)
);
let mut message_a = Message::response(0, OpCode::Query);
message_a.add_answer(Record::from_rdata(name.clone(), 7, rdata_a));
let mut message_txt = Message::response(0, OpCode::Query);
message_txt.add_answer(Record::from_rdata(name.clone(), 7, rdata_txt));
cache.insert(query_a.clone(), Ok(message_a), now);
assert_eq!(
cache.cache.get(&query_a).unwrap().valid_until,
now + Duration::from_secs(7)
);
cache.insert(query_txt.clone(), Ok(message_txt), now);
assert_eq!(
cache.cache.get(&query_txt).unwrap().valid_until,
now + Duration::from_secs(7)
);
}
#[cfg(feature = "serde")]
#[test]
fn ttl_config_deserialize_errors() {
let input = r#"[default]
positive_max_ttl = 3600
[default]
positive_max_ttl = 3599"#;
let error = toml::from_str::<TtlConfig>(input).unwrap_err();
assert!(
error.message().contains("duplicate key"),
"wrong error message: {error}"
);
let input = r#"[default]
positive_max_ttl = 86400
[OPENPGPKEY]
positive_max_ttl = 3600
[OPENPGPKEY]
negative_min_ttl = 60"#;
let error = toml::from_str::<TtlConfig>(input).unwrap_err();
assert!(
error.message().contains("duplicate key"),
"wrong error message: {error}"
);
let input = r#"[not_a_record_type]
positive_max_ttl = 3600"#;
let error = toml::from_str::<TtlConfig>(input).unwrap_err();
assert!(
error.message().contains("data did not match any variant"),
"wrong error message: {error}"
);
#[derive(Debug, Deserialize)]
struct Wrapper {
#[allow(unused)]
cache_policy: TtlConfig,
}
let input = r#"cache_policy = []"#;
let error = toml::from_str::<Wrapper>(input).unwrap_err();
assert!(
error.message().contains("invalid type: sequence"),
"wrong error message: {error}"
);
let input = r#"cache_policy = "yes""#;
let error = toml::from_str::<Wrapper>(input).unwrap_err();
assert!(
error.message().contains("invalid type: string"),
"wrong error message: {error}"
);
}
}