diff --git a/blog/dnssec-from-scratch.md b/blog/dnssec-from-scratch.md
index b95adaa..82c712f 100644
--- a/blog/dnssec-from-scratch.md
+++ b/blog/dnssec-from-scratch.md
@@ -79,9 +79,7 @@ const ROOT_KSK_PUBLIC_KEY: &[u8] = &[
];
```
-When IANA rolls this key (rare — the previous key lasted from 2010 to 2018), every DNSSEC validator on the internet needs updating. For Numa, that means a binary update. Something to watch.
-
-Every DNSKEY has a key tag — a 16-bit checksum over its RDATA (RFC 4034 Appendix B). The first test I wrote: compute the root KSK's key tag and assert it equals 20326. Instant confidence that the RDATA encoding is correct.
+When IANA rolls this key (rare — the previous key lasted from 2010 to 2018), every DNSSEC validator on the internet needs updating. For Numa, that means a binary update. Something to watch. Every DNSKEY also has a key tag — a 16-bit checksum over its RDATA. The first test I wrote: compute the root KSK's key tag and assert it equals 20326. Instant confidence that the encoding is correct.
## The crypto
diff --git a/site/dashboard.html b/site/dashboard.html
index 366db80..6b89b45 100644
--- a/site/dashboard.html
+++ b/site/dashboard.html
@@ -766,7 +766,7 @@ function applyLogFilter() {
${e.query_type}
${e.domain}${allowBtn}
${e.path}
-
${e.dnssec === 'secure' ? '' : ''}${e.rescode}
+
${e.dnssec === 'secure' ? '' : ''}${e.rescode}
${e.latency_ms.toFixed(1)}ms
`;
}).join('');
diff --git a/src/main.rs b/src/main.rs
index 536831f..c8e90e5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -480,6 +480,11 @@ async fn network_watch_loop(ctx: Arc) {
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;
+ }
}
}
diff --git a/src/recursive.rs b/src/recursive.rs
index 3bf4c3b..4a24dda 100644
--- a/src/recursive.rs
+++ b/src/recursive.rs
@@ -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, 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