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:
@@ -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
|
||||
|
||||
|
||||
@@ -766,7 +766,7 @@ function applyLogFilter() {
|
||||
<td>${e.query_type}</td>
|
||||
<td class="domain-cell" title="${e.domain}">${e.domain}${allowBtn}</td>
|
||||
<td><span class="path-tag ${e.path}">${e.path}</span></td>
|
||||
<td>${e.dnssec === 'secure' ? '<svg title="DNSSEC verified" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--emerald)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9 12 2 2 4-4"/></svg>' : ''}${e.rescode}</td>
|
||||
<td style="white-space:nowrap;"><span style="display:inline-block;width:15px;text-align:center;">${e.dnssec === 'secure' ? '<svg title="DNSSEC verified" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--emerald)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9 12 2 2 4-4"/></svg>' : ''}</span>${e.rescode}</td>
|
||||
<td>${e.latency_ms.toFixed(1)}ms</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user