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.
|
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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## The crypto
|
## The crypto
|
||||||
|
|
||||||
|
|||||||
@@ -766,7 +766,7 @@ function applyLogFilter() {
|
|||||||
<td>${e.query_type}</td>
|
<td>${e.query_type}</td>
|
||||||
<td class="domain-cell" title="${e.domain}">${e.domain}${allowBtn}</td>
|
<td class="domain-cell" title="${e.domain}">${e.domain}${allowBtn}</td>
|
||||||
<td><span class="path-tag ${e.path}">${e.path}</span></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>
|
<td>${e.latency_ms.toFixed(1)}ms</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|||||||
@@ -480,6 +480,11 @@ async fn network_watch_loop(ctx: Arc<numa::ctx::ServerCtx>) {
|
|||||||
ctx.lan_peers.lock().unwrap().clear();
|
ctx.lan_peers.lock().unwrap().clear();
|
||||||
info!("flushed LAN peers after network change");
|
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);
|
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]) {
|
pub async fn prime_tld_cache(cache: &RwLock<DnsCache>, root_hints: &[SocketAddr], tlds: &[String]) {
|
||||||
if root_hints.is_empty() || tlds.is_empty() {
|
if root_hints.is_empty() || tlds.is_empty() {
|
||||||
return;
|
return;
|
||||||
@@ -315,7 +338,16 @@ fn find_closest_ns(
|
|||||||
let zone = &qname[pos..];
|
let zone = &qname[pos..];
|
||||||
if let Some(cached) = guard.lookup(zone, QueryType::NS) {
|
if let Some(cached) = guard.lookup(zone, QueryType::NS) {
|
||||||
let mut addrs = Vec::new();
|
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 {
|
if let DnsRecord::NS { host, .. } = ns_rec {
|
||||||
for qt in [QueryType::A, QueryType::AAAA] {
|
for qt in [QueryType::A, QueryType::AAAA] {
|
||||||
if let Some(resp) = guard.lookup(host, qt) {
|
if let Some(resp) = guard.lookup(host, qt) {
|
||||||
@@ -719,6 +751,48 @@ mod tests {
|
|||||||
assert_eq!(addrs, hints);
|
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]
|
#[test]
|
||||||
fn minimize_query_from_root() {
|
fn minimize_query_from_root() {
|
||||||
// At root, only reveal TLD
|
// At root, only reveal TLD
|
||||||
|
|||||||
Reference in New Issue
Block a user