fix: escape DNS label text per RFC 1035 §5.1 (closes #36) #54
Reference in New Issue
Block a user
Delete Branch "fix/qname-label-escaping"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #36.
Closes #55.
Bug
read_qnamepushed raw label bytes directly into the output string, producing ambiguous text for labels containing dots, backslashes, or non-printable bytes. fanf2 spotted this on HN:Code review of the first fix cut also surfaced that
src/dnssec.rs::name_to_wirewas a parallel implementation of the oldwrite_qnamesplitting loop — it predated and duplicated the buffer.rs logic, and was similarly broken for escaped labels. Both sides of the DNSSEC canonical form pipeline were silently wrong in the same way; makingread_qnamecorrect exposed the divergence, so it's fixed here in the same PR (#55).Fix
Both sides of the text representation now follow RFC 1035 §5.1, and DNSSEC canonical form delegates to the same code path.
read_qname— rendering wire bytes to text.within a label →\.\→\\0x21..=0x7E→\DDD(3-digit decimal)write_qname— parsing text back to wire\.produces a literal0x2Einside the current label (not a separator)\\produces a literal0x5C\DDDproduces the byte with that decimal value (0..=255).still separates labels; empty labels (leading/trailing/consecutive dots) still skipped\, short\DD, and\DDD > 255Implementation is a streaming byte-level pass: reserve a length byte, write the label body directly into the buffer, backpatch the length via
set(). Zero intermediate allocations on the common path. Byte-level scanning is safe for UTF-8 input because continuation bytes (0x80..=0xBF) never collide with ASCII.(0x2E) or\(0x5C).read_qname's\DDDrendering also inlines the decimal digits to avoid a per-byteformat!allocation on non-printable input.dnssec::name_to_wire— DNSSEC canonical formReimplemented on top of
BytePacketBuffer::write_qname: scratch buffer →write_qname→ copy filled bytes → post-walk to lowercase label bodies (length bytes untouched). Lowercasing has to happen post-escape-resolution because a decimal escape like\065yields0x41('A') which must become'a'in canonical form per RFC 4034 §6.2.Call sites in
build_signed_data,record_to_canonical_wire,record_rdata_canonical, andnsec3_hashare unchanged —name_to_wire's public signature stays the same, infallibleVec<u8>return.Impact
Low in practice — real-world domains don't contain dots in labels — but this is a correctness bug in the wire format parser and the DNSSEC canonical form builder, which is the core of the project. Correctness bugs take priority over practical impact.
Performance: streaming implementation means zero allocations on the common
write_qnamepath, matchingmain. The first iteration of this PR went via aVec<Vec<u8>>helper; code review caught thatbench_buffer_serializecallswrite_qname~6 times per response, so that approach would have added ~24 extra allocations per response. The streaming rewrite (commit 2) fixes that.Tests
18 new unit tests across
src/buffer.rs(16) andsrc/dnssec.rs(2).buffer.rshad no tests before this PR.Buffer — read side
read_plain_domainread_label_with_literal_dot_is_escaped— fanf2's exact exampleread_label_with_backslash_is_escapedread_label_with_nonprintable_byte_uses_decimal_escaperead_label_with_space_uses_decimal_escapeBuffer — write side
write_plain_domainwrite_escaped_dot_does_not_split_labelwrite_escaped_backslashwrite_decimal_escape_yields_raw_bytewrite_rejects_out_of_range_decimal_escape(\999)write_rejects_trailing_backslashwrite_rejects_short_decimal_escape(\1)Buffer — round-trip
roundtrip_preserves_dot_in_labelroundtrip_preserves_backslash_in_labelroundtrip_preserves_nonprintable_byteroot_name_empty_and_dot_both_produce_single_zeroDNSSEC canonical form
name_to_wire_escaped_dot_in_label_is_not_a_separatorname_to_wire_decimal_escape_is_lowercased— verifies post-escape-resolution lowercasingExisting
name_to_wire_root,name_to_wire_domain, andds_verificationtests continue to pass unchanged.Total test count: 142 → 160.
Test plan
cargo test --lib— 160 passed, 0 failedcargo clippy -- -D warnings— cleancargo fmt --check— cleanCommits
fix: escape DNS label text per RFC 1035 §5.1— initial fix inbuffer.rsrefactor: stream label writes directly into buffer— addresses review feedback on allocation overhead inwrite_qnamefix: route dnssec::name_to_wire through write_qname for escape handling— addresses #55 (surfaced by the review)Credit
fanf2 on HN.
🤖 Generated with Claude Code