fix: NS cache lookup from authorities, UDP re-probe, shield alignment

- find_closest_ns checks authorities (not just answers) for NS records,
  fixing TLD priming cache misses that caused redundant root queries
- Periodic UDP re-probe every 5min when disabled — re-enables UDP
  after switching from a restrictive network to an open one
- Dashboard DNSSEC shield uses fixed-width container for alignment
- Blog post: tuck key-tag into trust anchor paragraph

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Razvan Dimescu
2026-03-27 23:58:30 +02:00
parent 2cdf90c382
commit 231adc523d
4 changed files with 82 additions and 5 deletions

View File

@@ -480,6 +480,11 @@ async fn network_watch_loop(ctx: Arc<numa::ctx::ServerCtx>) {
ctx.lan_peers.lock().unwrap().clear();
info!("flushed LAN peers after network change");
}
// Re-probe UDP every 5 minutes when disabled
if tick.is_multiple_of(60) {
numa::recursive::probe_udp(&ctx.root_hints).await;
}
}
}

View File

@@ -35,6 +35,29 @@ pub fn reset_udp_state() {
UDP_FAILURES.store(0, Ordering::Relaxed);
}
/// Probe whether UDP works again. Called periodically from the network watch loop.
pub async fn probe_udp(root_hints: &[SocketAddr]) {
if !UDP_DISABLED.load(Ordering::Relaxed) {
return;
}
let hint = match root_hints.first() {
Some(h) => *h,
None => return,
};
let mut probe = DnsPacket::new();
probe.header.id = next_id();
probe
.questions
.push(DnsQuestion::new(".".to_string(), QueryType::NS));
if forward_udp(&probe, hint, Duration::from_millis(1500))
.await
.is_ok()
{
info!("UDP probe succeeded — re-enabling UDP");
reset_udp_state();
}
}
pub async fn prime_tld_cache(cache: &RwLock<DnsCache>, root_hints: &[SocketAddr], tlds: &[String]) {
if root_hints.is_empty() || tlds.is_empty() {
return;
@@ -315,7 +338,16 @@ fn find_closest_ns(
let zone = &qname[pos..];
if let Some(cached) = guard.lookup(zone, QueryType::NS) {
let mut addrs = Vec::new();
for ns_rec in &cached.answers {
let ns_records = if cached
.answers
.iter()
.any(|r| matches!(r, DnsRecord::NS { .. }))
{
&cached.answers
} else {
&cached.authorities
};
for ns_rec in ns_records {
if let DnsRecord::NS { host, .. } = ns_rec {
for qt in [QueryType::A, QueryType::AAAA] {
if let Some(resp) = guard.lookup(host, qt) {
@@ -719,6 +751,48 @@ mod tests {
assert_eq!(addrs, hints);
}
#[test]
fn find_closest_ns_uses_authority_ns_records() {
// Simulate what TLD priming does: cache a referral response where
// NS records are in authorities (not answers), with glue in resources.
let cache = RwLock::new(DnsCache::new(100, 60, 86400));
let hints = vec![dns_addr(Ipv4Addr::new(198, 41, 0, 4))];
// Build a referral-style response (NS in authorities, glue in resources)
let mut referral = DnsPacket::new();
referral.header.response = true;
referral.authorities.push(DnsRecord::NS {
domain: "com".into(),
host: "ns1.com".into(),
ttl: 3600,
});
referral.resources.push(DnsRecord::A {
domain: "ns1.com".into(),
addr: Ipv4Addr::new(192, 5, 6, 30),
ttl: 3600,
});
// Cache the referral under "com" NS (same as prime_tld_cache does)
{
let mut c = cache.write().unwrap();
c.insert("com", QueryType::NS, &referral);
// Cache glue separately (as prime_tld_cache does)
let mut glue_pkt = DnsPacket::new();
glue_pkt.header.response = true;
glue_pkt.answers.push(DnsRecord::A {
domain: "ns1.com".into(),
addr: Ipv4Addr::new(192, 5, 6, 30),
ttl: 3600,
});
c.insert("ns1.com", QueryType::A, &glue_pkt);
}
// find_closest_ns should find "com" zone from authority NS records
let (zone, addrs) = find_closest_ns("www.example.com", &cache, &hints);
assert_eq!(zone, "com");
assert_eq!(addrs, vec![dns_addr(Ipv4Addr::new(192, 5, 6, 30))]);
}
#[test]
fn minimize_query_from_root() {
// At root, only reveal TLD