feat: background refresh on stale cache hit (RFC 8767 revalidation)
When a cached entry is expired but within the 1-hour stale window, serve it immediately with TTL=1 AND spawn a background re-resolve. The next query gets a fresh entry instead of another stale serve. Without this, stale entries were served repeatedly for up to an hour with no refresh — effectively ignoring TTL.
This commit is contained in:
@@ -132,18 +132,19 @@ impl DnsCache {
|
||||
|
||||
/// Read-only lookup — expired entries are left in place (cleaned up on insert).
|
||||
pub fn lookup(&self, domain: &str, qtype: QueryType) -> Option<DnsPacket> {
|
||||
self.lookup_with_status(domain, qtype).map(|(pkt, _)| pkt)
|
||||
self.lookup_with_status(domain, qtype)
|
||||
.map(|(pkt, _, _)| pkt)
|
||||
}
|
||||
|
||||
pub fn lookup_with_status(
|
||||
&self,
|
||||
domain: &str,
|
||||
qtype: QueryType,
|
||||
) -> Option<(DnsPacket, DnssecStatus)> {
|
||||
let (wire, status, _stale) = self.lookup_wire(domain, qtype, 0)?;
|
||||
) -> Option<(DnsPacket, DnssecStatus, bool)> {
|
||||
let (wire, status, stale) = self.lookup_wire(domain, qtype, 0)?;
|
||||
let mut buf = BytePacketBuffer::from_bytes(&wire);
|
||||
let pkt = DnsPacket::from_buffer(&mut buf).ok()?;
|
||||
Some((pkt, status))
|
||||
Some((pkt, status, stale))
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, domain: &str, qtype: QueryType, packet: &DnsPacket) {
|
||||
|
||||
53
src/ctx.rs
53
src/ctx.rs
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Mutex, RwLock};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
@@ -84,7 +84,7 @@ pub async fn resolve_query(
|
||||
query: DnsPacket,
|
||||
raw_wire: &[u8],
|
||||
src_addr: SocketAddr,
|
||||
ctx: &ServerCtx,
|
||||
ctx: &Arc<ServerCtx>,
|
||||
) -> crate::Result<BytePacketBuffer> {
|
||||
let start = Instant::now();
|
||||
|
||||
@@ -166,7 +166,12 @@ pub async fn resolve_query(
|
||||
(resp, QueryPath::Blocked, DnssecStatus::Indeterminate)
|
||||
} else {
|
||||
let cached = ctx.cache.read().unwrap().lookup_with_status(&qname, qtype);
|
||||
if let Some((cached, cached_dnssec)) = cached {
|
||||
if let Some((cached, cached_dnssec, stale)) = cached {
|
||||
if stale {
|
||||
let ctx = Arc::clone(ctx);
|
||||
let qname = qname.clone();
|
||||
tokio::spawn(async move { warm_stale(&ctx, &qname, qtype).await });
|
||||
}
|
||||
let mut resp = cached;
|
||||
resp.header.id = query.header.id;
|
||||
if cached_dnssec == DnssecStatus::Secure {
|
||||
@@ -375,6 +380,46 @@ fn cache_and_parse(
|
||||
DnsPacket::from_buffer(&mut buf)
|
||||
}
|
||||
|
||||
/// Background refresh for a stale cache entry (RFC 8767 revalidation).
|
||||
async fn warm_stale(ctx: &ServerCtx, qname: &str, qtype: QueryType) {
|
||||
let query = DnsPacket::query(0, qname, qtype);
|
||||
if ctx.upstream_mode == UpstreamMode::Recursive {
|
||||
if let Ok(resp) = crate::recursive::resolve_recursive(
|
||||
qname,
|
||||
qtype,
|
||||
&ctx.cache,
|
||||
&query,
|
||||
&ctx.root_hints,
|
||||
&ctx.srtt,
|
||||
)
|
||||
.await
|
||||
{
|
||||
ctx.cache.write().unwrap().insert(qname, qtype, &resp);
|
||||
}
|
||||
} else {
|
||||
let mut buf = BytePacketBuffer::new();
|
||||
if query.write(&mut buf).is_ok() {
|
||||
let pool = ctx.upstream_pool.lock().unwrap().clone();
|
||||
if let Ok(wire) = forward_with_failover_raw(
|
||||
buf.filled(),
|
||||
&pool,
|
||||
&ctx.srtt,
|
||||
ctx.timeout,
|
||||
ctx.hedge_delay,
|
||||
)
|
||||
.await
|
||||
{
|
||||
ctx.cache.write().unwrap().insert_wire(
|
||||
qname,
|
||||
qtype,
|
||||
&wire,
|
||||
DnssecStatus::Indeterminate,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn forward_and_cache(
|
||||
wire: &[u8],
|
||||
upstream: &Upstream,
|
||||
@@ -390,7 +435,7 @@ pub async fn handle_query(
|
||||
mut buffer: BytePacketBuffer,
|
||||
raw_len: usize,
|
||||
src_addr: SocketAddr,
|
||||
ctx: &ServerCtx,
|
||||
ctx: &Arc<ServerCtx>,
|
||||
) -> crate::Result<()> {
|
||||
let raw_wire = buffer.buf[..raw_len].to_vec();
|
||||
let query = match DnsPacket::from_buffer(&mut buffer) {
|
||||
|
||||
@@ -60,7 +60,11 @@ fn is_doh_host(host: Option<&str>, tld: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_doh(dns_bytes: &[u8], src: SocketAddr, ctx: &ServerCtx) -> Response {
|
||||
async fn resolve_doh(
|
||||
dns_bytes: &[u8],
|
||||
src: SocketAddr,
|
||||
ctx: &std::sync::Arc<ServerCtx>,
|
||||
) -> Response {
|
||||
let mut buffer = BytePacketBuffer::from_bytes(dns_bytes);
|
||||
let query = match DnsPacket::from_buffer(&mut buffer) {
|
||||
Ok(q) => q,
|
||||
|
||||
@@ -153,8 +153,11 @@ async fn accept_loop(listener: TcpListener, acceptor: TlsAcceptor, ctx: Arc<Serv
|
||||
|
||||
/// Handle a single persistent DoT connection (RFC 7858).
|
||||
/// Reads length-prefixed DNS queries until EOF, idle timeout, or error.
|
||||
async fn handle_dot_connection<S>(mut stream: S, remote_addr: SocketAddr, ctx: &ServerCtx)
|
||||
where
|
||||
async fn handle_dot_connection<S>(
|
||||
mut stream: S,
|
||||
remote_addr: SocketAddr,
|
||||
ctx: &std::sync::Arc<ServerCtx>,
|
||||
) where
|
||||
S: AsyncReadExt + AsyncWriteExt + Unpin,
|
||||
{
|
||||
loop {
|
||||
|
||||
Reference in New Issue
Block a user