use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
const DNS_HEADER_LEN: usize = 12;
const DNS_ID_LEN: usize = 2;
const DNS_U16_LEN: usize = 2;
const DNS_U32_LEN: usize = 4;
const DNS_FLAGS_OFFSET: usize = 2;
const DNS_QDCOUNT_OFFSET: usize = 4;
const DNS_ANCOUNT_OFFSET: usize = 6;
const DNS_QUESTION_FIXED_LEN: usize = 4;
const DNS_RR_FIXED_LEN: usize = 10;
const DNS_RR_TYPE_OFFSET: usize = 0;
const DNS_RR_CLASS_OFFSET: usize = 2;
const DNS_RR_TTL_OFFSET: usize = 4;
const DNS_RR_RDLEN_OFFSET: usize = 8;
const DNS_CLASS_IN: u16 = 1;
const DNS_TYPE_A: u16 = 1;
const DNS_TYPE_AAAA: u16 = 28;
const DNS_A_RDATA_LEN: usize = 4;
const DNS_AAAA_RDATA_LEN: usize = 16;
pub const DNS_RCODE_SERVFAIL: u16 = 2;
pub const DNS_RCODE_NXDOMAIN: u16 = 3;
const DNS_RCODE_MASK: u16 = 0x000f;
const DNS_ONE_QUESTION: u16 = 1;
const DNS_FLAG_RESPONSE: u16 = 0x8000;
const DNS_FLAG_RECURSION_DESIRED: u16 = 0x0100;
const DNS_FLAG_RECURSION_AVAILABLE: u16 = 0x0080;
const DNS_POINTER_TAG: u8 = 0xc0;
const DNS_POINTER_MASK: u8 = 0xc0;
const DNS_POINTER_OFFSET_MASK: u8 = 0x3f;
const DNS_MAX_COMPRESSION_JUMPS: usize = 16;
const DNS_MAX_LABEL_LEN: usize = 63;
pub fn normalize_hostname(hostname: &str) -> Option<String> {
let hostname = hostname.trim_end_matches('.').to_ascii_lowercase();
if hostname.is_empty() {
None
} else {
Some(hostname)
}
}
pub fn hostname_allowed(hostname: &str, allowed_hosts: &[String]) -> bool {
let Some(hostname) = normalize_hostname(hostname) else {
return false;
};
allowed_hosts.iter().any(|allowed| {
hostname == *allowed
|| hostname
.strip_suffix(allowed)
.is_some_and(|prefix| prefix.ends_with('.'))
})
}
pub fn question_name(packet: &[u8]) -> Option<String> {
if packet.len() < DNS_HEADER_LEN || read_u16(packet, DNS_QDCOUNT_OFFSET)? != DNS_ONE_QUESTION {
return None;
}
let (name, _) = read_name(packet, DNS_HEADER_LEN)?;
Some(name)
}
pub fn answer_ip_records(packet: &[u8]) -> Vec<(IpAddr, u32)> {
parse_answer_ip_records(packet).unwrap_or_default()
}
fn parse_answer_ip_records(packet: &[u8]) -> Option<Vec<(IpAddr, u32)>> {
if packet.len() < DNS_HEADER_LEN {
return None;
}
let qdcount = read_u16(packet, DNS_QDCOUNT_OFFSET)? as usize;
let ancount = read_u16(packet, DNS_ANCOUNT_OFFSET)? as usize;
let mut offset = DNS_HEADER_LEN;
for _ in 0..qdcount {
let (_, after_name) = read_name(packet, offset)?;
if after_name + DNS_QUESTION_FIXED_LEN > packet.len() {
return None;
}
offset = after_name + DNS_QUESTION_FIXED_LEN;
}
let mut ips = Vec::new();
for _ in 0..ancount {
let (_, after_name) = read_name(packet, offset)?;
if after_name + DNS_RR_FIXED_LEN > packet.len() {
return None;
}
offset = after_name;
let rr_type = read_u16(packet, offset + DNS_RR_TYPE_OFFSET)?;
let class = read_u16(packet, offset + DNS_RR_CLASS_OFFSET)?;
let ttl = read_u32(packet, offset + DNS_RR_TTL_OFFSET)?;
let rdlen = read_u16(packet, offset + DNS_RR_RDLEN_OFFSET)? as usize;
offset += DNS_RR_FIXED_LEN;
if offset + rdlen > packet.len() {
return None;
}
if class == DNS_CLASS_IN && rr_type == DNS_TYPE_A && rdlen == DNS_A_RDATA_LEN {
ips.push((
IpAddr::V4(Ipv4Addr::new(
packet[offset],
packet[offset + 1],
packet[offset + 2],
packet[offset + 3],
)),
ttl,
));
} else if class == DNS_CLASS_IN && rr_type == DNS_TYPE_AAAA && rdlen == DNS_AAAA_RDATA_LEN {
let octets: [u8; DNS_AAAA_RDATA_LEN] = packet[offset..offset + DNS_AAAA_RDATA_LEN]
.try_into()
.ok()?;
ips.push((IpAddr::V6(Ipv6Addr::from(octets)), ttl));
}
offset += rdlen;
}
Some(ips)
}
pub fn error_response(query: &[u8], rcode: u16) -> Vec<u8> {
let id: &[u8] = if query.len() >= DNS_ID_LEN {
&query[..DNS_ID_LEN]
} else {
&[0, 0]
};
let req_flags = if query.len() >= DNS_FLAGS_OFFSET + DNS_U16_LEN {
read_u16(query, DNS_FLAGS_OFFSET).unwrap_or(0)
} else {
0
};
let flags = DNS_FLAG_RESPONSE
| (req_flags & DNS_FLAG_RECURSION_DESIRED)
| DNS_FLAG_RECURSION_AVAILABLE
| (rcode & DNS_RCODE_MASK);
let question_end = question_section_end(query);
let qdcount = if question_end.is_some() { 1u16 } else { 0 };
let mut response = Vec::with_capacity(question_end.unwrap_or(DNS_HEADER_LEN));
response.extend_from_slice(id);
response.extend_from_slice(&flags.to_be_bytes());
response.extend_from_slice(&qdcount.to_be_bytes());
response.extend_from_slice(&0u16.to_be_bytes()); response.extend_from_slice(&0u16.to_be_bytes()); response.extend_from_slice(&0u16.to_be_bytes()); if let Some(end) = question_end {
response.extend_from_slice(&query[DNS_HEADER_LEN..end]);
}
response
}
fn question_section_end(packet: &[u8]) -> Option<usize> {
if packet.len() < DNS_HEADER_LEN || read_u16(packet, DNS_QDCOUNT_OFFSET)? != DNS_ONE_QUESTION {
return None;
}
let (_, after_name) = read_name(packet, DNS_HEADER_LEN)?;
let end = after_name + DNS_QUESTION_FIXED_LEN;
if end > packet.len() {
return None;
}
Some(end)
}
fn read_name(packet: &[u8], offset: usize) -> Option<(String, usize)> {
let mut labels = Vec::new();
let mut pos = offset;
let mut next_offset = offset;
let mut jumped = false;
let mut jumps = 0;
loop {
let len = *packet.get(pos)?;
if len & DNS_POINTER_MASK == DNS_POINTER_TAG {
let lo = *packet.get(pos + 1)?;
let pointer = (((len & DNS_POINTER_OFFSET_MASK) as usize) << 8) | lo as usize;
if pointer >= packet.len() {
return None;
}
if !jumped {
next_offset = pos + DNS_U16_LEN;
}
pos = pointer;
jumped = true;
jumps += 1;
if jumps > DNS_MAX_COMPRESSION_JUMPS {
return None;
}
continue;
}
if len & DNS_POINTER_MASK != 0 {
return None;
}
pos += 1;
if len == 0 {
if !jumped {
next_offset = pos;
}
break;
}
let len = len as usize;
if len > DNS_MAX_LABEL_LEN || pos + len > packet.len() {
return None;
}
let label = std::str::from_utf8(&packet[pos..pos + len]).ok()?;
labels.push(label.to_ascii_lowercase());
pos += len;
if !jumped {
next_offset = pos;
}
}
Some((labels.join("."), next_offset))
}
fn read_u16(buf: &[u8], offset: usize) -> Option<u16> {
let bytes = buf.get(offset..offset + DNS_U16_LEN)?;
Some(u16::from_be_bytes([bytes[0], bytes[1]]))
}
fn read_u32(buf: &[u8], offset: usize) -> Option<u32> {
let bytes = buf.get(offset..offset + DNS_U32_LEN)?;
Some(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
#[cfg(test)]
mod tests {
use super::*;
fn query_for(name: &str) -> Vec<u8> {
let mut q = vec![
0x12, 0x34, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
for label in name.split('.') {
q.push(label.len() as u8);
q.extend_from_slice(label.as_bytes());
}
q.push(0);
q.extend_from_slice(&DNS_TYPE_A.to_be_bytes());
q.extend_from_slice(&DNS_CLASS_IN.to_be_bytes());
q
}
#[test]
fn parses_question_name() {
assert_eq!(
question_name(&query_for("www.example.com")).as_deref(),
Some("www.example.com")
);
}
#[test]
fn hostname_matching_allows_exact_and_subdomain_only() {
let allow = vec!["example.com".to_string()];
assert!(hostname_allowed("example.com", &allow));
assert!(hostname_allowed("www.example.com", &allow));
assert!(hostname_allowed("a.b.example.com", &allow));
assert!(!hostname_allowed("notexample.com", &allow));
assert!(!hostname_allowed("example.com.evil.com", &allow));
}
#[test]
fn empty_allow_list_blocks_all() {
assert!(!hostname_allowed("example.com", &[]));
}
#[test]
fn extracts_a_records_with_ttl() {
let mut r = query_for("example.com");
r[2] = 0x81; r[3] = 0x80; r[6] = 0x00;
r[7] = 0x01; r.extend_from_slice(&[0xc0, 0x0c]);
r.extend_from_slice(&DNS_TYPE_A.to_be_bytes());
r.extend_from_slice(&DNS_CLASS_IN.to_be_bytes());
r.extend_from_slice(&300u32.to_be_bytes());
r.extend_from_slice(&(DNS_A_RDATA_LEN as u16).to_be_bytes());
r.extend_from_slice(&[93, 184, 216, 34]);
let records = answer_ip_records(&r);
assert_eq!(
records,
vec![(IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34)), 300)]
);
}
#[test]
fn extracts_aaaa_records_with_ttl() {
let v6: Ipv6Addr = "2606:2800:21f:cb07:6820:80da:af6b:8b2c".parse().unwrap();
let mut r = query_for("example.com");
r[2] = 0x81;
r[3] = 0x80;
r[6] = 0x00;
r[7] = 0x01; r.extend_from_slice(&[0xc0, 0x0c]);
r.extend_from_slice(&DNS_TYPE_AAAA.to_be_bytes());
r.extend_from_slice(&DNS_CLASS_IN.to_be_bytes());
r.extend_from_slice(&600u32.to_be_bytes());
r.extend_from_slice(&(DNS_AAAA_RDATA_LEN as u16).to_be_bytes());
r.extend_from_slice(&v6.octets());
let records = answer_ip_records(&r);
assert_eq!(records, vec![(IpAddr::V6(v6), 600)]);
}
#[test]
fn extracts_mixed_a_and_aaaa_records() {
let v6: Ipv6Addr = "2606:4700::6810:84e5".parse().unwrap();
let mut r = query_for("example.com");
r[2] = 0x81;
r[3] = 0x80;
r[6] = 0x00;
r[7] = 0x02; r.extend_from_slice(&[0xc0, 0x0c]);
r.extend_from_slice(&DNS_TYPE_A.to_be_bytes());
r.extend_from_slice(&DNS_CLASS_IN.to_be_bytes());
r.extend_from_slice(&300u32.to_be_bytes());
r.extend_from_slice(&(DNS_A_RDATA_LEN as u16).to_be_bytes());
r.extend_from_slice(&[104, 16, 132, 229]);
r.extend_from_slice(&[0xc0, 0x0c]);
r.extend_from_slice(&DNS_TYPE_AAAA.to_be_bytes());
r.extend_from_slice(&DNS_CLASS_IN.to_be_bytes());
r.extend_from_slice(&600u32.to_be_bytes());
r.extend_from_slice(&(DNS_AAAA_RDATA_LEN as u16).to_be_bytes());
r.extend_from_slice(&v6.octets());
let records = answer_ip_records(&r);
assert_eq!(
records,
vec![
(IpAddr::V4(Ipv4Addr::new(104, 16, 132, 229)), 300),
(IpAddr::V6(v6), 600),
]
);
}
#[test]
fn error_response_is_well_formed() {
let q = query_for("blocked.test");
let resp = error_response(&q, DNS_RCODE_NXDOMAIN);
let flags = read_u16(&resp, DNS_FLAGS_OFFSET).unwrap();
assert_eq!(flags & DNS_FLAG_RESPONSE, DNS_FLAG_RESPONSE);
assert_eq!(flags & DNS_RCODE_MASK, DNS_RCODE_NXDOMAIN);
assert_eq!(&resp[..2], &q[..2]); }
#[test]
fn malformed_packets_dont_panic() {
assert_eq!(question_name(&[0, 1, 2]), None);
assert!(answer_ip_records(&[0xff; 4]).is_empty());
}
}