fix: escape DNS label text per RFC 1035 §5.1 (closes #36) #54

Merged
razvandimescu merged 5 commits from fix/qname-label-escaping into main 2026-04-10 13:53:47 +08:00
Showing only changes of commit 938317f1d4 - Show all commits

View File

@@ -5,6 +5,7 @@ use log::{debug, trace};
use ring::digest; use ring::digest;
use ring::signature; use ring::signature;
use crate::buffer::BytePacketBuffer;
use crate::cache::{DnsCache, DnssecStatus}; use crate::cache::{DnsCache, DnssecStatus};
use crate::packet::DnsPacket; use crate::packet::DnsPacket;
use crate::question::QueryType; use crate::question::QueryType;
@@ -720,22 +721,33 @@ pub fn verify_ds(ds: &DnsRecord, dnskey: &DnsRecord, owner: &str) -> bool {
// -- Canonical wire format -- // -- Canonical wire format --
/// Encode a DNS name in canonical wire form per RFC 4034 §6.2:
/// uncompressed, with all ASCII letters lowercased.
///
/// Delegates label parsing and RFC 1035 §5.1 escape handling to
/// `BytePacketBuffer::write_qname`, then post-processes the emitted bytes
/// to lowercase label bodies (length bytes stay untouched). This keeps
/// the escape logic in exactly one place — see #55.
pub fn name_to_wire(name: &str) -> Vec<u8> { pub fn name_to_wire(name: &str) -> Vec<u8> {
let mut wire = Vec::with_capacity(name.len() + 2); let mut buf = BytePacketBuffer::new();
if name == "." || name.is_empty() { buf.write_qname(name)
wire.push(0); .expect("DNSSEC canonical form: name must be a well-formed DNS name");
return wire; let mut wire = buf.filled().to_vec();
}
for label in name.split('.') { let mut i = 0;
if label.is_empty() { while i < wire.len() {
continue; let label_len = wire[i] as usize;
if label_len == 0 {
break;
} }
wire.push(label.len() as u8); i += 1;
for &b in label.as_bytes() { let end = i + label_len;
wire.push(b.to_ascii_lowercase()); for b in &mut wire[i..end] {
*b = b.to_ascii_lowercase();
} }
i = end;
} }
wire.push(0);
wire wire
} }
@@ -1475,6 +1487,23 @@ mod tests {
); );
} }
#[test]
fn name_to_wire_escaped_dot_in_label_is_not_a_separator() {
// `exa\.mple.com` is two labels: `exa.mple` (8 bytes including the 0x2E) and `com`.
let wire = name_to_wire("exa\\.mple.com");
assert_eq!(
wire,
vec![8, b'e', b'x', b'a', b'.', b'm', b'p', b'l', b'e', 3, b'c', b'o', b'm', 0]
);
}
#[test]
fn name_to_wire_decimal_escape_is_lowercased() {
// `\065` is the byte 0x41 ('A'), which canonical form must lowercase to 'a'.
let wire = name_to_wire("\\065bc.com");
assert_eq!(wire, vec![3, b'a', b'b', b'c', 3, b'c', b'o', b'm', 0]);
}
#[test] #[test]
fn parent_zone_cases() { fn parent_zone_cases() {
assert_eq!(parent_zone("example.com"), "com"); assert_eq!(parent_zone("example.com"), "com");