Files
numa/src/buffer.rs
Razvan Dimescu 637b374d8b 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>
2026-03-27 18:39:23 +02:00

215 lines
5.3 KiB
Rust

use crate::Result;
const BUF_SIZE: usize = 4096;
pub struct BytePacketBuffer {
pub buf: [u8; BUF_SIZE],
pub pos: usize,
}
impl Default for BytePacketBuffer {
fn default() -> Self {
Self::new()
}
}
impl BytePacketBuffer {
pub fn new() -> BytePacketBuffer {
BytePacketBuffer {
buf: [0; BUF_SIZE],
pos: 0,
}
}
pub fn from_bytes(data: &[u8]) -> Self {
let mut buf = Self::new();
let len = data.len().min(BUF_SIZE);
buf.buf[..len].copy_from_slice(&data[..len]);
buf
}
pub fn pos(&self) -> usize {
self.pos
}
pub fn filled(&self) -> &[u8] {
&self.buf[..self.pos]
}
pub fn step(&mut self, steps: usize) -> Result<()> {
self.pos += steps;
Ok(())
}
pub fn seek(&mut self, pos: usize) -> Result<()> {
self.pos = pos;
Ok(())
}
pub fn read(&mut self) -> Result<u8> {
if self.pos >= BUF_SIZE {
return Err("End of buffer".into());
}
let res = self.buf[self.pos];
self.pos += 1;
Ok(res)
}
pub fn get(&self, pos: usize) -> Result<u8> {
if pos >= BUF_SIZE {
return Err("End of buffer".into());
}
Ok(self.buf[pos])
}
pub fn get_range(&self, start: usize, len: usize) -> Result<&[u8]> {
if start + len > BUF_SIZE {
return Err("End of buffer".into());
}
Ok(&self.buf[start..start + len])
}
pub fn read_u16(&mut self) -> Result<u16> {
let res = ((self.read()? as u16) << 8) | (self.read()? as u16);
Ok(res)
}
pub fn read_u32(&mut self) -> Result<u32> {
let res = ((self.read()? as u32) << 24)
| ((self.read()? as u32) << 16)
| ((self.read()? as u32) << 8)
| (self.read()? as u32);
Ok(res)
}
/// Read a qname, handling label compression (pointer jumps).
/// Converts wire format like [3]www[6]google[3]com[0] into "www.google.com".
pub fn read_qname(&mut self, outstr: &mut String) -> Result<()> {
let mut pos = self.pos();
let mut jumped = false;
let max_jumps = 5;
let mut jumps_performed = 0;
let mut delim = "";
loop {
if jumps_performed > max_jumps {
return Err(format!("Limit of {} jumps exceeded", max_jumps).into());
}
let len = self.get(pos)?;
if (len & 0xC0) == 0xC0 {
if !jumped {
self.seek(pos + 2)?;
}
let b2 = self.get(pos + 1)? as u16;
let offset = (((len as u16) ^ 0xC0) << 8) | b2;
pos = offset as usize;
jumped = true;
jumps_performed += 1;
continue;
} else {
pos += 1;
if len == 0 {
break;
}
outstr.push_str(delim);
let str_buffer = self.get_range(pos, len as usize)?;
for &b in str_buffer {
outstr.push(b.to_ascii_lowercase() as char);
}
delim = ".";
pos += len as usize;
}
}
if !jumped {
self.seek(pos)?;
}
Ok(())
}
pub fn write(&mut self, val: u8) -> Result<()> {
if self.pos >= BUF_SIZE {
return Err("End of buffer".into());
}
self.buf[self.pos] = val;
self.pos += 1;
Ok(())
}
pub fn write_u8(&mut self, val: u8) -> Result<()> {
self.write(val)
}
pub fn write_u16(&mut self, val: u16) -> Result<()> {
self.write((val >> 8) as u8)?;
self.write((val & 0xFF) as u8)?;
Ok(())
}
pub fn write_u32(&mut self, val: u32) -> Result<()> {
self.write(((val >> 24) & 0xFF) as u8)?;
self.write(((val >> 16) & 0xFF) as u8)?;
self.write(((val >> 8) & 0xFF) as u8)?;
self.write((val & 0xFF) as u8)?;
Ok(())
}
pub fn write_qname(&mut self, qname: &str) -> Result<()> {
if qname.is_empty() || qname == "." {
self.write_u8(0)?;
return Ok(());
}
for label in qname.split('.') {
let len = label.len();
if len == 0 {
continue; // skip empty labels from trailing dot
}
if len > 0x3f {
return Err("Single label exceeds 63 characters of length".into());
}
self.write_u8(len as u8)?;
for b in label.as_bytes() {
self.write_u8(*b)?;
}
}
self.write_u8(0)?;
Ok(())
}
pub fn write_bytes(&mut self, data: &[u8]) -> Result<()> {
let end = self.pos + data.len();
if end > BUF_SIZE {
return Err("End of buffer".into());
}
self.buf[self.pos..end].copy_from_slice(data);
self.pos = end;
Ok(())
}
pub fn set(&mut self, pos: usize, val: u8) -> Result<()> {
if pos >= BUF_SIZE {
return Err("End of buffer".into());
}
self.buf[pos] = val;
Ok(())
}
pub fn set_u16(&mut self, pos: usize, val: u16) -> Result<()> {
self.set(pos, (val >> 8) as u8)?;
self.set(pos + 1, (val & 0xFF) as u8)?;
Ok(())
}
}