pub const MIN_UDP_PAYLOAD_SIZE: u16 = 512;
pub const DEFAULT_UDP_PAYLOAD_SIZE: u16 = 1232;
#[derive(Debug, Clone, Copy)]
pub struct EdnsRuntime {
pub udp_payload_size: u16,
}
impl Default for EdnsRuntime {
fn default() -> Self {
Self {
udp_payload_size: DEFAULT_UDP_PAYLOAD_SIZE,
}
}
}
impl EdnsRuntime {
pub fn from_config(udp_payload_size: u16) -> Self {
Self {
udp_payload_size: udp_payload_size.max(MIN_UDP_PAYLOAD_SIZE),
}
}
}
static RUNTIME: std::sync::OnceLock<EdnsRuntime> = std::sync::OnceLock::new();
pub fn install_runtime(rt: EdnsRuntime) {
let _ = RUNTIME.set(rt);
}
pub fn runtime() -> EdnsRuntime {
*RUNTIME.get_or_init(EdnsRuntime::default)
}
#[allow(dead_code)]
pub mod opt_code {
pub const NSID: u16 = 3;
pub const CLIENT_SUBNET: u16 = 8;
pub const COOKIE: u16 = 10;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EdnsOption {
pub code: u16,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EdnsOpt {
pub udp_payload_size: u16,
pub extended_rcode: u8,
pub version: u8,
pub dnssec_ok: bool,
pub z: u16,
pub options: Vec<EdnsOption>,
}
impl Default for EdnsOpt {
fn default() -> Self {
Self {
udp_payload_size: 1232,
extended_rcode: 0,
version: 0,
dnssec_ok: false,
z: 0,
options: Vec::new(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum EdnsError {
#[error("option header truncated at offset {0}")]
OptionTruncated(usize),
#[error("option data length {len} exceeds remaining RDATA at offset {offset}")]
OptionOverflow { offset: usize, len: usize },
}
impl EdnsOpt {
pub fn from_rr_fields(class_field: u16, ttl_field: u32, rdata: &[u8]) -> Result<Self, EdnsError> {
let extended_rcode = ((ttl_field >> 24) & 0xFF) as u8;
let version = ((ttl_field >> 16) & 0xFF) as u8;
let dnssec_ok = (ttl_field & 0x0000_8000) != 0;
let z = (ttl_field & 0x0000_7FFF) as u16;
let options = decode_options(rdata)?;
Ok(Self {
udp_payload_size: class_field,
extended_rcode,
version,
dnssec_ok,
z,
options,
})
}
pub fn ttl_field(&self) -> u32 {
let mut v: u32 = 0;
v |= (self.extended_rcode as u32) << 24;
v |= (self.version as u32) << 16;
if self.dnssec_ok {
v |= 0x0000_8000;
}
v |= (self.z & 0x7FFF) as u32;
v
}
pub fn encode_rr(&self, buf: &mut Vec<u8>) {
buf.push(0x00); buf.extend_from_slice(&41u16.to_be_bytes()); buf.extend_from_slice(&self.udp_payload_size.to_be_bytes()); buf.extend_from_slice(&self.ttl_field().to_be_bytes());
let rdlen_pos = buf.len();
buf.extend_from_slice(&[0, 0]); let rdata_start = buf.len();
for opt in &self.options {
buf.extend_from_slice(&opt.code.to_be_bytes());
let len = opt.data.len().min(u16::MAX as usize);
buf.extend_from_slice(&(len as u16).to_be_bytes());
buf.extend_from_slice(&opt.data[..len]);
}
let rdata_len = (buf.len() - rdata_start).min(u16::MAX as usize) as u16;
buf[rdlen_pos..rdlen_pos + 2].copy_from_slice(&rdata_len.to_be_bytes());
}
#[allow(dead_code)]
pub fn is_unsupported_version(&self) -> bool {
self.version != 0
}
}
fn decode_options(rdata: &[u8]) -> Result<Vec<EdnsOption>, EdnsError> {
let mut out = Vec::new();
let mut pos = 0;
while pos < rdata.len() {
if pos + 4 > rdata.len() {
return Err(EdnsError::OptionTruncated(pos));
}
let code = u16::from_be_bytes([rdata[pos], rdata[pos + 1]]);
let len = u16::from_be_bytes([rdata[pos + 2], rdata[pos + 3]]) as usize;
let data_start = pos + 4;
let data_end = data_start + len;
if data_end > rdata.len() {
return Err(EdnsError::OptionOverflow { offset: pos, len });
}
out.push(EdnsOption {
code,
data: rdata[data_start..data_end].to_vec(),
});
pos = data_end;
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runtime_clamps_below_min_to_512() {
let rt = EdnsRuntime::from_config(200);
assert_eq!(rt.udp_payload_size, MIN_UDP_PAYLOAD_SIZE);
}
#[test]
fn runtime_preserves_valid_values() {
for &sz in &[512u16, 1232, 4096, 16384, 65535] {
assert_eq!(EdnsRuntime::from_config(sz).udp_payload_size, sz);
}
}
#[test]
fn runtime_default_matches_flag_day() {
assert_eq!(EdnsRuntime::default().udp_payload_size, DEFAULT_UDP_PAYLOAD_SIZE);
assert_eq!(DEFAULT_UDP_PAYLOAD_SIZE, 1232);
}
#[test]
fn default_advertises_1232() {
assert_eq!(EdnsOpt::default().udp_payload_size, 1232);
}
#[test]
fn ttl_field_packs_extended_rcode_version_do() {
let opt = EdnsOpt {
udp_payload_size: 4096,
extended_rcode: 0x12,
version: 0x34,
dnssec_ok: true,
z: 0,
options: Vec::new(),
};
let ttl = opt.ttl_field();
assert_eq!(ttl >> 24, 0x12, "extended rcode in high byte");
assert_eq!((ttl >> 16) & 0xFF, 0x34, "version in next byte");
assert_eq!(ttl & 0x0000_8000, 0x8000, "DO bit at bit 15 of low half");
}
#[test]
fn from_rr_fields_parses_class_ttl_flags() {
let ttl = 0x0000_8000;
let opt = EdnsOpt::from_rr_fields(4096, ttl, &[]).unwrap();
assert_eq!(opt.udp_payload_size, 4096);
assert_eq!(opt.extended_rcode, 0);
assert_eq!(opt.version, 0);
assert!(opt.dnssec_ok);
assert!(opt.options.is_empty());
}
#[test]
fn unsupported_version_detected() {
let opt = EdnsOpt::from_rr_fields(1232, 0x0001_0000, &[]).unwrap();
assert_eq!(opt.version, 1);
assert!(opt.is_unsupported_version());
}
#[test]
fn options_roundtrip_opaque() {
let original = EdnsOpt {
udp_payload_size: 1232,
extended_rcode: 0,
version: 0,
dnssec_ok: true,
z: 0,
options: vec![
EdnsOption { code: opt_code::COOKIE, data: b"12345678".to_vec() },
EdnsOption { code: opt_code::CLIENT_SUBNET, data: vec![0, 1, 24, 0, 192, 0, 2] },
EdnsOption { code: 0xFFFE, data: vec![0xAA, 0xBB] }, ],
};
let mut rr = Vec::new();
original.encode_rr(&mut rr);
assert_eq!(rr[0], 0x00, "root name");
assert_eq!(u16::from_be_bytes([rr[1], rr[2]]), 41u16);
let class = u16::from_be_bytes([rr[3], rr[4]]);
let ttl = u32::from_be_bytes([rr[5], rr[6], rr[7], rr[8]]);
let rdlen = u16::from_be_bytes([rr[9], rr[10]]) as usize;
let rdata = &rr[11..11 + rdlen];
let decoded = EdnsOpt::from_rr_fields(class, ttl, rdata).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn empty_rdata_roundtrip() {
let original = EdnsOpt::default();
let mut rr = Vec::new();
original.encode_rr(&mut rr);
let class = u16::from_be_bytes([rr[3], rr[4]]);
let ttl = u32::from_be_bytes([rr[5], rr[6], rr[7], rr[8]]);
let rdlen = u16::from_be_bytes([rr[9], rr[10]]) as usize;
assert_eq!(rdlen, 0);
let decoded = EdnsOpt::from_rr_fields(class, ttl, &[]).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn truncated_option_header_errors() {
let err = EdnsOpt::from_rr_fields(1232, 0, &[0x00, 0x0A, 0x00]);
assert!(matches!(err, Err(EdnsError::OptionTruncated(0))));
}
#[test]
fn option_length_overflow_errors() {
let rdata = [0x00, 0x0A, 0x00, 0x64, 0xAA, 0xBB];
let err = EdnsOpt::from_rr_fields(1232, 0, &rdata);
assert!(matches!(err, Err(EdnsError::OptionOverflow { .. })));
}
#[test]
fn z_bits_preserved_on_roundtrip() {
let ttl = 0x0000_4321; let opt = EdnsOpt::from_rr_fields(1232, ttl, &[]).unwrap();
assert_eq!(opt.z, 0x4321);
assert_eq!(opt.ttl_field() & 0x7FFF, 0x4321);
}
#[test]
fn zero_length_option_roundtrip() {
let original = EdnsOpt {
udp_payload_size: 1232,
extended_rcode: 0,
version: 0,
dnssec_ok: false,
z: 0,
options: vec![EdnsOption { code: opt_code::NSID, data: Vec::new() }],
};
let mut rr = Vec::new();
original.encode_rr(&mut rr);
let class = u16::from_be_bytes([rr[3], rr[4]]);
let ttl = u32::from_be_bytes([rr[5], rr[6], rr[7], rr[8]]);
let rdlen = u16::from_be_bytes([rr[9], rr[10]]) as usize;
let rdata = &rr[11..11 + rdlen];
let decoded = EdnsOpt::from_rr_fields(class, ttl, rdata).unwrap();
assert_eq!(decoded, original);
}
}