diff --git a/example-encrypted-dns.toml b/example-encrypted-dns.toml index 6fb46fa..4ef1c3d 100644 --- a/example-encrypted-dns.toml +++ b/example-encrypted-dns.toml @@ -83,6 +83,13 @@ cache_ttl_max = 86400 cache_ttl_error = 600 +## DNS cache: to avoid bursts of traffic when an RRSET expires, +## introduce jitter to the decreasing TTL sent to clients when the +## actual TTL goes below that value. + +client_ttl_jitter = 60 + + ## Run as a background process daemonize = false diff --git a/src/cache.rs b/src/cache.rs index 62ec5a6..9f10b46 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -30,6 +30,10 @@ impl CachedResponse { pub fn has_expired(&self) -> bool { Instant::recent() > self.expiry } + + pub fn ttl(&self) -> u32 { + (self.expiry - Instant::recent()).as_secs() as _ + } } #[derive(Clone, Derivative)] @@ -37,9 +41,9 @@ impl CachedResponse { pub struct Cache { #[derivative(Debug = "ignore")] cache: Arc>>, - ttl_min: u32, - ttl_max: u32, - ttl_error: u32, + pub ttl_min: u32, + pub ttl_max: u32, + pub ttl_error: u32, } impl Cache { diff --git a/src/config.rs b/src/config.rs index ac23033..b88acb8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -82,6 +82,7 @@ pub struct Config { pub pid_file: Option, pub log_file: Option, pub my_ip: Option, + pub client_ttl_jitter: Option, #[cfg(feature = "metrics")] pub metrics: Option, pub anonymized_dns: Option, diff --git a/src/dns.rs b/src/dns.rs index 448cf02..719817b 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -381,6 +381,28 @@ pub fn min_ttl(packet: &[u8], min_ttl: u32, max_ttl: u32, failure_ttl: u32) -> R Ok(found_min_ttl) } +pub fn set_ttl(packet: &mut [u8], ttl: u32) -> Result<(), Error> { + let packet_len = packet.len(); + ensure!(packet_len > DNS_OFFSET_QUESTION, "Short packet"); + ensure!(packet_len <= DNS_MAX_PACKET_SIZE, "Large packet"); + ensure!(qdcount(packet) == 1, "No question"); + let mut offset = skip_name(packet, DNS_OFFSET_QUESTION)?; + assert!(offset > DNS_OFFSET_QUESTION); + ensure!(packet_len - offset > 4, "Short packet"); + offset += 4; + let (ancount, nscount, arcount) = (ancount(packet), nscount(packet), arcount(packet)); + let rrcount = ancount as usize + nscount as usize + arcount as usize; + offset = traverse_rrs_mut(packet, offset, rrcount, |packet, offset| { + let qtype = BigEndian::read_u16(&packet[offset..]); + if qtype != DNS_TYPE_OPT { + BigEndian::write_u32(&mut packet[offset + 4..], ttl) + } + Ok(()) + })?; + ensure!(packet_len == offset, "Garbage after packet"); + Ok(()) +} + fn add_edns_section(packet: &mut Vec, max_payload_size: u16) -> Result<(), Error> { let opt_rr: [u8; 11] = [ 0, diff --git a/src/globals.rs b/src/globals.rs index 14ebf52..e8a3fb0 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -49,6 +49,7 @@ pub struct Globals { pub anonymized_dns_allow_non_reserved_ports: bool, pub anonymized_dns_blacklisted_ips: Vec, pub access_control_tokens: Option>, + pub client_ttl_jitter: u32, pub my_ip: Option>, #[cfg(feature = "metrics")] #[derivative(Debug = "ignore")] diff --git a/src/main.rs b/src/main.rs index 980e396..5322d2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -705,6 +705,7 @@ fn main() -> Result<(), Error> { anonymized_dns_blacklisted_ips, access_control_tokens, my_ip: config.my_ip.map(|ip| ip.as_bytes().to_ascii_lowercase()), + client_ttl_jitter: config.client_ttl_jitter.unwrap_or(60), #[cfg(feature = "metrics")] varz: Varz::default(), }); diff --git a/src/resolver.rs b/src/resolver.rs index b3c9157..b851cd3 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -226,7 +226,13 @@ pub async fn get_cached_response_or_resolve( #[cfg(feature = "metrics")] globals.varz.client_queries_cached.inc(); cached_response.set_tid(original_tid); + let mut ttl = cached_response.ttl(); + if ttl < globals.client_ttl_jitter { + let jitter = rand::thread_rng().gen::() % globals.client_ttl_jitter; + ttl = globals.client_ttl_jitter.saturating_add(jitter); + } let mut response = cached_response.into_response(); + dns::set_ttl(&mut response, ttl)?; dns::recase_qname(&mut response, &packet_qname)?; return Ok(response); }