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).
|
/// Read-only lookup — expired entries are left in place (cleaned up on insert).
|
||||||
pub fn lookup(&self, domain: &str, qtype: QueryType) -> Option<DnsPacket> {
|
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(
|
pub fn lookup_with_status(
|
||||||
&self,
|
&self,
|
||||||
domain: &str,
|
domain: &str,
|
||||||
qtype: QueryType,
|
qtype: QueryType,
|
||||||
) -> Option<(DnsPacket, DnssecStatus)> {
|
) -> Option<(DnsPacket, DnssecStatus, bool)> {
|
||||||
let (wire, status, _stale) = self.lookup_wire(domain, qtype, 0)?;
|
let (wire, status, stale) = self.lookup_wire(domain, qtype, 0)?;
|
||||||
let mut buf = BytePacketBuffer::from_bytes(&wire);
|
let mut buf = BytePacketBuffer::from_bytes(&wire);
|
||||||
let pkt = DnsPacket::from_buffer(&mut buf).ok()?;
|
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) {
|
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::collections::HashMap;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
use std::time::{Duration, Instant, SystemTime};
|
use std::time::{Duration, Instant, SystemTime};
|
||||||
|
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
@@ -84,7 +84,7 @@ pub async fn resolve_query(
|
|||||||
query: DnsPacket,
|
query: DnsPacket,
|
||||||
raw_wire: &[u8],
|
raw_wire: &[u8],
|
||||||
src_addr: SocketAddr,
|
src_addr: SocketAddr,
|
||||||
ctx: &ServerCtx,
|
ctx: &Arc<ServerCtx>,
|
||||||
) -> crate::Result<BytePacketBuffer> {
|
) -> crate::Result<BytePacketBuffer> {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
@@ -166,7 +166,12 @@ pub async fn resolve_query(
|
|||||||
(resp, QueryPath::Blocked, DnssecStatus::Indeterminate)
|
(resp, QueryPath::Blocked, DnssecStatus::Indeterminate)
|
||||||
} else {
|
} else {
|
||||||
let cached = ctx.cache.read().unwrap().lookup_with_status(&qname, qtype);
|
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;
|
let mut resp = cached;
|
||||||
resp.header.id = query.header.id;
|
resp.header.id = query.header.id;
|
||||||
if cached_dnssec == DnssecStatus::Secure {
|
if cached_dnssec == DnssecStatus::Secure {
|
||||||
@@ -375,6 +380,46 @@ fn cache_and_parse(
|
|||||||
DnsPacket::from_buffer(&mut buf)
|
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(
|
async fn forward_and_cache(
|
||||||
wire: &[u8],
|
wire: &[u8],
|
||||||
upstream: &Upstream,
|
upstream: &Upstream,
|
||||||
@@ -390,7 +435,7 @@ pub async fn handle_query(
|
|||||||
mut buffer: BytePacketBuffer,
|
mut buffer: BytePacketBuffer,
|
||||||
raw_len: usize,
|
raw_len: usize,
|
||||||
src_addr: SocketAddr,
|
src_addr: SocketAddr,
|
||||||
ctx: &ServerCtx,
|
ctx: &Arc<ServerCtx>,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
let raw_wire = buffer.buf[..raw_len].to_vec();
|
let raw_wire = buffer.buf[..raw_len].to_vec();
|
||||||
let query = match DnsPacket::from_buffer(&mut buffer) {
|
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 mut buffer = BytePacketBuffer::from_bytes(dns_bytes);
|
||||||
let query = match DnsPacket::from_buffer(&mut buffer) {
|
let query = match DnsPacket::from_buffer(&mut buffer) {
|
||||||
Ok(q) => q,
|
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).
|
/// Handle a single persistent DoT connection (RFC 7858).
|
||||||
/// Reads length-prefixed DNS queries until EOF, idle timeout, or error.
|
/// 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)
|
async fn handle_dot_connection<S>(
|
||||||
where
|
mut stream: S,
|
||||||
|
remote_addr: SocketAddr,
|
||||||
|
ctx: &std::sync::Arc<ServerCtx>,
|
||||||
|
) where
|
||||||
S: AsyncReadExt + AsyncWriteExt + Unpin,
|
S: AsyncReadExt + AsyncWriteExt + Unpin,
|
||||||
{
|
{
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
Reference in New Issue
Block a user