feat: recursive DNS + DNSSEC + TCP fallback (#17)
* feat: recursive resolution + full DNSSEC validation Numa becomes a true DNS resolver — resolves from root nameservers with complete DNSSEC chain-of-trust verification. Recursive resolution: - Iterative RFC 1034 from configurable root hints (13 default) - CNAME chasing (depth 8), referral following (depth 10) - A+AAAA glue extraction, IPv6 nameserver support - TLD priming: NS + DS + DNSKEY for 34 gTLDs + EU ccTLDs - Config: mode = "recursive" in [upstream], root_hints, prime_tlds DNSSEC (all 4 phases): - EDNS0 OPT pseudo-record (DO bit, 1232 payload per DNS Flag Day 2020) - DNSKEY, DS, RRSIG, NSEC, NSEC3 record types with wire read/write - Signature verification via ring: RSA/SHA-256, ECDSA P-256, Ed25519 - Chain-of-trust: zone DNSKEY → parent DS → root KSK (key tag 20326) - DNSKEY RRset self-signature verification (RRSIG(DNSKEY) by KSK) - RRSIG expiration/inception time validation - NSEC: NXDOMAIN gap proofs, NODATA type absence, wildcard denial - NSEC3: SHA-1 iterated hashing, closest encloser proof, hash range - Authority RRSIG verification for denial proofs - Config: [dnssec] enabled/strict (default false, opt-in) - AD bit on Secure, SERVFAIL on Bogus+strict - DnssecStatus cached per entry, ValidationStats logging Performance: - TLD chain pre-warmed on startup (root DNSKEY + TLD DS/DNSKEY) - Referral DS piggybacking from authority sections - DNSKEY prefetch before validation loop - Cold-cache validation: ~1 DNSKEY fetch (down from 5) - Benchmarks: RSA 10.9µs, ECDSA 174ns, DS verify 257ns Also: - write_qname fix for root domain "." (was producing malformed queries) - write_record_header() dedup, write_bytes() bulk writes - DnsRecord::domain() + query_type() accessors - UpstreamMode enum, DEFAULT_EDNS_PAYLOAD const - Real glue TTL (was hardcoded 3600) - DNSSEC restricted to recursive mode only Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: TCP fallback, query minimization, UDP auto-disable Transport resilience for restrictive networks (ISPs blocking UDP:53): - DNS-over-TCP fallback: UDP fail/truncation → automatic TCP retry - UDP auto-disable: after 3 consecutive failures, switch to TCP-first - IPv6 → TCP directly (UDP socket binds 0.0.0.0, can't reach IPv6) - Network change resets UDP detection for re-probing - Root hint rotation in TLD priming Privacy: - RFC 7816 query minimization: root servers see TLD only, not full name Code quality: - Merged find_starting_ns + find_starting_zone → find_closest_ns - Extracted resolve_ns_addrs_from_glue shared helper - Removed overall timeout wrapper (per-hop timeouts sufficient) - forward_tcp for DNS-over-TCP (RFC 1035 §4.2.2) Testing: - Mock TCP-only DNS server for fallback tests (no network needed) - tcp_fallback_resolves_when_udp_blocked - tcp_only_iterative_resolution - tcp_fallback_handles_nxdomain - udp_auto_disable_resets - Integration test suite (4 suites, 51 tests) - Network probe script (tests/network-probe.sh) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: DNSSEC verified badge in dashboard query log - Add dnssec field to QueryLogEntry, track validation status per query - DnssecStatus::as_str() for API serialization - Dashboard shows green checkmark next to DNSSEC-verified responses - Blog post: add "How keys get there" section, transport resilience section, trim code blocks, update What's Next Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use SVG shield for DNSSEC badge, update blog HTML Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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> * fix: TCP single-write, mock server consistency, integration tests - TCP single-write fix: combine length prefix + message to avoid split segments that Microsoft/Azure DNS servers reject - Mock server (spawn_tcp_dns_server) updated to use single-write too - Tests: forward_tcp_wire_format, forward_tcp_single_segment_write - Integration: real-server checks for Microsoft/Office/Azure domains Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: recursive bar in dashboard, special-use domain interception Dashboard: - Add Recursive bar to resolution paths chart (cyan, distinct from Override) - Add RECURSIVE path tag style in query log Special-use domains (RFC 6761/6303/8880/9462): - .localhost → 127.0.0.1 (RFC 6761) - Private reverse PTR (10.x, 192.168.x, 172.16-31.x) → NXDOMAIN - _dns.resolver.arpa (DDR) → NXDOMAIN - ipv4only.arpa (NAT64) → 192.0.0.170/171 - mDNS service discovery for private ranges → NXDOMAIN Eliminates ~900ms SERVFAILs for macOS system queries that were hitting root servers unnecessarily. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: move generated blog HTML to site/blog/posts/, gitignore - Generated HTML now in site/blog/posts/ (gitignored) - CI workflow runs pandoc + make blog before deploy - Updated all internal blog links to /blog/posts/ path - blog/*.md remains the source of truth Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: review feedback — memory ordering, RRSIG time, NS resolution - Ordering::Relaxed → Acquire/Release for UDP_DISABLED/UDP_FAILURES (ARM correctness for cross-thread coordination) - RRSIG time validation: serial number arithmetic (RFC 4034 §3.1.5) + 300s clock skew fudge factor (matches BIND) - resolve_ns_addrs_from_glue collects addresses from ALL NS names, not just the first with glue (improves failover) - is_special_use_domain: eliminate 16 format! allocations per .in-addr.arpa query (parse octet instead) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: API endpoint tests, coverage target - 8 new axum handler tests: health, stats, query-log, overrides CRUD, cache, blocking stats, services CRUD, dashboard HTML - Tests use tower::oneshot — no network, no server startup - test_ctx() builds minimal ServerCtx for isolated testing - `make coverage` target (cargo-tarpaulin), separate from `make all` - 82 total tests (was 74) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
183
benches/dnssec.rs
Normal file
183
benches/dnssec.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use numa::dnssec;
|
||||
use numa::question::QueryType;
|
||||
use numa::record::DnsRecord;
|
||||
|
||||
// Realistic ECDSA P-256 key (64 bytes) and signature (64 bytes)
|
||||
fn make_ecdsa_key() -> Vec<u8> {
|
||||
vec![0xAB; 64]
|
||||
}
|
||||
fn make_ecdsa_sig() -> Vec<u8> {
|
||||
vec![0xCD; 64]
|
||||
}
|
||||
|
||||
// Realistic RSA-2048 key (RFC 3110 format: exp_len=3, exp=65537, mod=256 bytes)
|
||||
fn make_rsa_key() -> Vec<u8> {
|
||||
let mut key = vec![3u8]; // exponent length
|
||||
key.extend(&[0x01, 0x00, 0x01]); // exponent = 65537
|
||||
key.extend(vec![0xFF; 256]); // modulus (256 bytes = 2048 bits)
|
||||
key
|
||||
}
|
||||
|
||||
fn make_ed25519_key() -> Vec<u8> {
|
||||
vec![0xEF; 32]
|
||||
}
|
||||
|
||||
fn make_dnskey(algorithm: u8, public_key: Vec<u8>) -> DnsRecord {
|
||||
DnsRecord::DNSKEY {
|
||||
domain: "example.com".into(),
|
||||
flags: 257,
|
||||
protocol: 3,
|
||||
algorithm,
|
||||
public_key,
|
||||
ttl: 3600,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_rrsig(algorithm: u8, signature: Vec<u8>) -> DnsRecord {
|
||||
DnsRecord::RRSIG {
|
||||
domain: "example.com".into(),
|
||||
type_covered: QueryType::A.to_num(),
|
||||
algorithm,
|
||||
labels: 2,
|
||||
original_ttl: 300,
|
||||
expiration: 2000000000,
|
||||
inception: 1600000000,
|
||||
key_tag: 12345,
|
||||
signer_name: "example.com".into(),
|
||||
signature,
|
||||
ttl: 300,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_rrset() -> Vec<DnsRecord> {
|
||||
vec![
|
||||
DnsRecord::A {
|
||||
domain: "example.com".into(),
|
||||
addr: "93.184.216.34".parse().unwrap(),
|
||||
ttl: 300,
|
||||
},
|
||||
DnsRecord::A {
|
||||
domain: "example.com".into(),
|
||||
addr: "93.184.216.35".parse().unwrap(),
|
||||
ttl: 300,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn bench_key_tag(c: &mut Criterion) {
|
||||
let key = make_rsa_key();
|
||||
c.bench_function("key_tag_rsa2048", |b| {
|
||||
b.iter(|| {
|
||||
dnssec::compute_key_tag(black_box(257), black_box(3), black_box(8), black_box(&key))
|
||||
})
|
||||
});
|
||||
|
||||
let key = make_ecdsa_key();
|
||||
c.bench_function("key_tag_ecdsa_p256", |b| {
|
||||
b.iter(|| {
|
||||
dnssec::compute_key_tag(black_box(257), black_box(3), black_box(13), black_box(&key))
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_name_to_wire(c: &mut Criterion) {
|
||||
c.bench_function("name_to_wire_short", |b| {
|
||||
b.iter(|| dnssec::name_to_wire(black_box("example.com")))
|
||||
});
|
||||
c.bench_function("name_to_wire_long", |b| {
|
||||
b.iter(|| dnssec::name_to_wire(black_box("sub.deep.nested.example.co.uk")))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_build_signed_data(c: &mut Criterion) {
|
||||
let rrsig = make_rrsig(13, make_ecdsa_sig());
|
||||
let rrset = make_rrset();
|
||||
let rrset_refs: Vec<&DnsRecord> = rrset.iter().collect();
|
||||
|
||||
c.bench_function("build_signed_data_2_A_records", |b| {
|
||||
b.iter(|| dnssec::build_signed_data(black_box(&rrsig), black_box(&rrset_refs)))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_verify_signature(c: &mut Criterion) {
|
||||
// These will fail verification (keys/sigs are random), but we measure the
|
||||
// crypto overhead — ring still does the full algorithm before returning error.
|
||||
let data = vec![0u8; 128]; // typical signed data size
|
||||
|
||||
let rsa_key = make_rsa_key();
|
||||
let rsa_sig = vec![0xAA; 256]; // RSA-2048 signature
|
||||
c.bench_function("verify_rsa_sha256_2048", |b| {
|
||||
b.iter(|| {
|
||||
dnssec::verify_signature(
|
||||
black_box(8),
|
||||
black_box(&rsa_key),
|
||||
black_box(&data),
|
||||
black_box(&rsa_sig),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
let ecdsa_key = make_ecdsa_key();
|
||||
let ecdsa_sig = make_ecdsa_sig();
|
||||
c.bench_function("verify_ecdsa_p256", |b| {
|
||||
b.iter(|| {
|
||||
dnssec::verify_signature(
|
||||
black_box(13),
|
||||
black_box(&ecdsa_key),
|
||||
black_box(&data),
|
||||
black_box(&ecdsa_sig),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
let ed_key = make_ed25519_key();
|
||||
let ed_sig = vec![0xBB; 64];
|
||||
c.bench_function("verify_ed25519", |b| {
|
||||
b.iter(|| {
|
||||
dnssec::verify_signature(
|
||||
black_box(15),
|
||||
black_box(&ed_key),
|
||||
black_box(&data),
|
||||
black_box(&ed_sig),
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_ds_verification(c: &mut Criterion) {
|
||||
let dk = make_dnskey(8, make_rsa_key());
|
||||
|
||||
// Compute correct DS digest
|
||||
let owner_wire = dnssec::name_to_wire("example.com");
|
||||
let mut dnskey_rdata = vec![1u8, 1, 3, 8]; // flags=257, proto=3, algo=8
|
||||
dnskey_rdata.extend(&make_rsa_key());
|
||||
let mut input = Vec::new();
|
||||
input.extend(&owner_wire);
|
||||
input.extend(&dnskey_rdata);
|
||||
let digest = ring::digest::digest(&ring::digest::SHA256, &input);
|
||||
|
||||
let ds = DnsRecord::DS {
|
||||
domain: "example.com".into(),
|
||||
key_tag: dnssec::compute_key_tag(257, 3, 8, &make_rsa_key()),
|
||||
algorithm: 8,
|
||||
digest_type: 2,
|
||||
digest: digest.as_ref().to_vec(),
|
||||
ttl: 86400,
|
||||
};
|
||||
|
||||
c.bench_function("verify_ds_sha256", |b| {
|
||||
b.iter(|| dnssec::verify_ds(black_box(&ds), black_box(&dk), black_box("example.com")))
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
dnssec_benches,
|
||||
bench_key_tag,
|
||||
bench_name_to_wire,
|
||||
bench_build_signed_data,
|
||||
bench_verify_signature,
|
||||
bench_ds_verification,
|
||||
);
|
||||
criterion_main!(dnssec_benches);
|
||||
Reference in New Issue
Block a user