-- SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. local testimony = require("testimony") local crypto = require("crypto") local wire = require("dns.wire") local bit = require("bit") local band, bor, lshift, rshift = bit.band, bit.bor, bit.lshift, bit.rshift local fixtures = dofile("tests/dns/fixtures.lua") local expected = dofile("tests/dns/fixtures_expected.lua") local testify = testimony.new("== dns.wire ==") --------------------------------------------------------------------------- -- Helper: build raw bytes from a table of integer byte values --------------------------------------------------------------------------- local function raw(...) local bytes = { ... } local chars = {} for i = 1, #bytes do chars[i] = string.char(bytes[i]) end return table.concat(chars) end --------------------------------------------------------------------------- -- Helper: decode header from a reader (shared by multiple tests) --------------------------------------------------------------------------- local function decode_header(r) local id = r:u16() local flags = r:u16() return { id = id, qr = band(rshift(flags, 15), 0x1), opcode = band(rshift(flags, 11), 0xF), aa = band(rshift(flags, 10), 0x1) == 1, tc = band(rshift(flags, 9), 0x1) == 1, rd = band(rshift(flags, 8), 0x1) == 1, ra = band(rshift(flags, 7), 0x1) == 1, rcode = band(flags, 0xF), qdcount = r:u16(), ancount = r:u16(), nscount = r:u16(), arcount = r:u16(), } end --------------------------------------------------------------------------- -- Helper: encode header with a writer (shared by multiple tests) --------------------------------------------------------------------------- local function encode_header(w, hdr) w:u16(hdr.id) w:u16( bor( lshift(hdr.qr or 0, 15), lshift(hdr.opcode or 0, 11), lshift(hdr.aa and 1 or 0, 10), lshift(hdr.tc and 1 or 0, 9), lshift(hdr.rd and 1 or 0, 8), lshift(hdr.ra and 1 or 0, 7), band(hdr.rcode or 0, 0xF) ) ) w:u16(hdr.qdcount) w:u16(hdr.ancount) w:u16(hdr.nscount) w:u16(hdr.arcount) end -- ========================================================================= -- Category 1: Hand-Crafted Hex Tests -- ========================================================================= -- ── Reader primitives ──────────────────────────────────────────────── testify:that("reader u8 reads single byte", function() local r = wire.reader(raw(0xFF)) testimony.assert_equal(255, r:u8()) testimony.assert_true(r:at_end()) end) testify:that("reader u8 returns nil on empty", function() local r = wire.reader("") local val, err = r:u8() testimony.assert_nil(val) testimony.assert_not_nil(err) end) testify:that("reader u16 reads big-endian", function() local r = wire.reader(raw(0x01, 0x02)) testimony.assert_equal(258, r:u16()) end) testify:that("reader u16 reads zero", function() local r = wire.reader(raw(0x00, 0x00)) testimony.assert_equal(0, r:u16()) end) testify:that("reader u16 reads max value", function() local r = wire.reader(raw(0xFF, 0xFF)) testimony.assert_equal(65535, r:u16()) end) testify:that("reader u32 reads big-endian", function() local r = wire.reader(raw(0x00, 0x01, 0x51, 0x80)) testimony.assert_equal(86400, r:u32()) end) testify:that("reader u32 reads high bit values", function() -- 0xFFFFFFFF = 4294967295 local r = wire.reader(raw(0xFF, 0xFF, 0xFF, 0xFF)) testimony.assert_equal(4294967295, r:u32()) end) testify:that("reader bytes reads exact N bytes", function() local r = wire.reader("hello world") local data = r:bytes(5) testimony.assert_equal("hello", data) testimony.assert_equal(6, r:pos()) end) testify:that("reader bytes returns nil past end", function() local r = wire.reader("hi") local val, err = r:bytes(5) testimony.assert_nil(val) testimony.assert_not_nil(err) end) -- ── Reader position management ─────────────────────────────────────── testify:that("reader pos returns 1-based position", function() local r = wire.reader(raw(0x01, 0x02, 0x03)) testimony.assert_equal(1, r:pos()) r:u8() testimony.assert_equal(2, r:pos()) end) testify:that("reader remaining counts bytes left", function() local r = wire.reader(raw(0x01, 0x02, 0x03)) testimony.assert_equal(3, r:remaining()) r:u8() testimony.assert_equal(2, r:remaining()) r:u16() testimony.assert_equal(0, r:remaining()) end) testify:that("reader seek moves to absolute position", function() local r = wire.reader(raw(0xAA, 0xBB, 0xCC)) r:seek(3) testimony.assert_equal(0xCC, r:u8()) end) testify:that("reader skip advances cursor", function() local r = wire.reader(raw(0xAA, 0xBB, 0xCC)) r:skip(2) testimony.assert_equal(0xCC, r:u8()) end) testify:that("reader at_end detects end of data", function() local r = wire.reader(raw(0x01)) testimony.assert_false(r:at_end()) r:u8() testimony.assert_true(r:at_end()) end) -- ── Reader character_string ────────────────────────────────────────── testify:that("reader character_string reads length-prefixed data", function() -- 5 "hello" local r = wire.reader(raw(0x05) .. "hello") local str = r:character_string() testimony.assert_equal("hello", str) testimony.assert_true(r:at_end()) end) testify:that("reader character_string reads empty string", function() local r = wire.reader(raw(0x00)) local str = r:character_string() testimony.assert_equal("", str) end) -- ── Reader names (hand-crafted) ────────────────────────────────────── testify:that("reader name reads uncompressed name", function() -- 07"example" 03"com" 00 local data = raw(0x07) .. "example" .. raw(0x03) .. "com" .. raw(0x00) local r = wire.reader(data) local name = r:name() testimony.assert_equal("example.com.", name) testimony.assert_true(r:at_end()) end) testify:that("reader name reads root name", function() local r = wire.reader(raw(0x00)) local name = r:name() testimony.assert_equal(".", name) end) testify:that("reader name reads single label", function() local data = raw(0x03) .. "foo" .. raw(0x00) local r = wire.reader(data) testimony.assert_equal("foo.", r:name()) end) testify:that("reader name handles compression pointer", function() -- Simulate a mini DNS message: -- Offset 0-11: 12 bytes of fake header (zeros) -- Offset 12: 07"example" 03"com" 00 (13 bytes, ends at 24) -- Offset 25: 03"www" C0 0C (pointer to offset 12) local header = string.rep(raw(0x00), 12) local name1 = raw(0x07) .. "example" .. raw(0x03) .. "com" .. raw(0x00) local name2 = raw(0x03) .. "www" .. raw(0xC0, 0x0C) local data = header .. name1 .. name2 local r = wire.reader(data) r:seek(26) -- position at name2 local name = r:name() testimony.assert_equal("www.example.com.", name) -- Cursor should be past the pointer: 26 + 1(len) + 3(www) + 2(pointer) = 32 testimony.assert_equal(32, r:pos()) end) testify:that("reader name handles chained pointers", function() -- Offset 0-11: header (12 bytes) -- Offset 12: 03"com" 00 (5 bytes, ends at 16) -- Offset 17: 07"example" C0 0C (pointer to "com." at offset 12, 10 bytes) -- Offset 27: 03"www" C0 11 (pointer to "example.com." at offset 17, 6 bytes) local header = string.rep(raw(0x00), 12) local seg1 = raw(0x03) .. "com" .. raw(0x00) -- offsets 12-16 local seg2 = raw(0x07) .. "example" .. raw(0xC0, 0x0C) -- offsets 17-26 local seg3 = raw(0x03) .. "www" .. raw(0xC0, 0x11) -- offsets 27-32 local data = header .. seg1 .. seg2 .. seg3 local r = wire.reader(data) r:seek(28) -- 1-based position for offset 27 (seg3) local name = r:name() testimony.assert_equal("www.example.com.", name) end) -- ── sub_reader tests ───────────────────────────────────────────────── testify:that("sub_reader bounds reads to N bytes", function() local r = wire.reader(raw(0x01, 0x02, 0x03, 0x04, 0x05)) local sub = r:sub_reader(3) testimony.assert_equal(3, sub:remaining()) testimony.assert_equal(0x01, sub:u8()) testimony.assert_equal(0x02, sub:u8()) testimony.assert_equal(0x03, sub:u8()) testimony.assert_true(sub:at_end()) end) testify:that("sub_reader overread returns nil", function() local r = wire.reader(raw(0x01, 0x02, 0x03, 0x04)) local sub = r:sub_reader(2) local val = sub:u32() testimony.assert_nil(val) end) testify:that("sub_reader advances parent cursor", function() local r = wire.reader(raw(0x01, 0x02, 0x03, 0x04, 0x05)) local _ = r:sub_reader(3) -- Parent cursor should be past the 3 bytes testimony.assert_equal(4, r:pos()) testimony.assert_equal(0x04, r:u8()) end) testify:that("sub_reader resolves compression pointers from parent data", function() -- Full message: header(12) + name "example.com."(13) + QTYPE(2) + QCLASS(2) + answer... -- Offset 12: 07"example" 03"com" 00 -- We'll create a sub_reader at the RDATA position that contains a pointer C0 0C local header = string.rep(raw(0x00), 12) local qname = raw(0x07) .. "example" .. raw(0x03) .. "com" .. raw(0x00) -- After question: TYPE(2) CLASS(2) TTL(4) RDLENGTH(2) = 10 bytes local rr_header = raw(0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x01, 0x2C, 0x00, 0x02) -- RDATA: compression pointer to offset 12 local rdata = raw(0xC0, 0x0C) local rest = raw(0xAA, 0xBB) -- trailing data local data = header .. qname .. raw(0x00, 0x01, 0x00, 0x01) .. rr_header .. rdata .. rest local r = wire.reader(data) -- Seek to RDATA position: 12 + 13 + 4 + 10 = 39 bytes before RDATA -- 1-based position = 40 r:seek(40) local sub = r:sub_reader(2) -- RDLENGTH = 2 -- Read name from sub_reader — should resolve pointer to parent data local name = sub:name() testimony.assert_equal("example.com.", name) testimony.assert_true(sub:at_end()) -- Parent cursor should be past the 2-byte RDATA testimony.assert_equal(42, r:pos()) end) testify:that("sub_reader creation fails with insufficient data", function() local r = wire.reader(raw(0x01, 0x02)) local sub, err = r:sub_reader(5) testimony.assert_nil(sub) testimony.assert_not_nil(err) end) -- ── Writer primitives ──────────────────────────────────────────────── testify:that("writer u8 writes single byte", function() local w = wire.writer() w:u8(0xFF) testimony.assert_equal(raw(0xFF), w:output()) end) testify:that("writer u16 writes big-endian", function() local w = wire.writer() w:u16(0x0102) testimony.assert_equal(raw(0x01, 0x02), w:output()) end) testify:that("writer u32 writes big-endian", function() local w = wire.writer() w:u32(86400) testimony.assert_equal(raw(0x00, 0x01, 0x51, 0x80), w:output()) end) testify:that("writer bytes writes raw string", function() local w = wire.writer() w:bytes("AB") testimony.assert_equal("AB", w:output()) end) testify:that("writer pos returns 0-based offset", function() local w = wire.writer() testimony.assert_equal(0, w:pos()) w:u8(0x01) testimony.assert_equal(1, w:pos()) w:u16(0x0203) testimony.assert_equal(3, w:pos()) end) testify:that("writer len returns current length", function() local w = wire.writer() testimony.assert_equal(0, w:len()) w:u32(1) testimony.assert_equal(4, w:len()) end) testify:that("writer empty output returns empty string", function() local w = wire.writer() testimony.assert_equal("", w:output()) end) -- ── Writer character_string ────────────────────────────────────────── testify:that("writer character_string writes length-prefixed data", function() local w = wire.writer() w:character_string("test") local out = w:output() testimony.assert_equal(5, #out) testimony.assert_equal(4, string.byte(out, 1)) testimony.assert_equal("test", out:sub(2)) end) -- ── Writer patch_u16 ───────────────────────────────────────────────── testify:that("writer patch_u16 overwrites at offset", function() local w = wire.writer() w:u16(0x0000) -- placeholder at offset 0 w:u16(0xBBBB) -- data at offset 2 w:patch_u16(0, 0xAAAA) local out = w:output() testimony.assert_equal(raw(0xAA, 0xAA, 0xBB, 0xBB), out) end) testify:that("writer patch_u16 rejects out-of-bounds offset", function() local w = wire.writer() w:u8(0x01) testimony.assert_error(function() w:patch_u16(5, 0x1234) end) end) -- ── Writer names ───────────────────────────────────────────────────── testify:that("writer name writes uncompressed name", function() local w = wire.writer() w:name("example.com.") local out = w:output() local expected_bytes = raw(0x07) .. "example" .. raw(0x03) .. "com" .. raw(0x00) testimony.assert_equal(expected_bytes, out) end) testify:that("writer name writes root name", function() local w = wire.writer() w:name(".") testimony.assert_equal(raw(0x00), w:output()) end) testify:that("writer name adds trailing dot if missing", function() local w = wire.writer() w:name("example.com") local out = w:output() local expected_bytes = raw(0x07) .. "example" .. raw(0x03) .. "com" .. raw(0x00) testimony.assert_equal(expected_bytes, out) end) testify:that("writer name compresses second occurrence", function() local w = wire.writer() -- Pad 12 bytes (fake header) so pointers are valid for _ = 1, 12 do w:u8(0x00) end w:name("example.com.") -- written at offset 12 local pos_after_first = w:pos() -- should be 12 + 13 = 25 w:name("example.com.") -- should compress to pointer C0 0C local out = w:output() -- Second name should be a 2-byte pointer testimony.assert_equal(pos_after_first + 2, w:len()) -- Check pointer bytes: C0 0C (offset 12) testimony.assert_equal(0xC0, string.byte(out, pos_after_first + 1)) testimony.assert_equal(0x0C, string.byte(out, pos_after_first + 2)) end) testify:that("writer name compresses shared suffix", function() local w = wire.writer() for _ = 1, 12 do w:u8(0x00) end w:name("example.com.") -- offset 12: full write w:name("www.example.com.") -- should write "www" + pointer to "example.com." local out = w:output() -- First name: 12 + 13 = 25 bytes total -- Second name: "www" label (4 bytes) + pointer (2 bytes) = 6 bytes testimony.assert_equal(25 + 6, #out) -- The pointer should be at bytes 29-30 (0-based offset 28-29) -- Pointing to offset 12 (example.com.) testimony.assert_equal(0xC0, string.byte(out, 30)) testimony.assert_equal(0x0C, string.byte(out, 31)) end) -- ── Round-trip tests ───────────────────────────────────────────────── testify:that("u8 round-trips through writer and reader", function() local w = wire.writer() w:u8(0) w:u8(127) w:u8(255) local r = wire.reader(w:output()) testimony.assert_equal(0, r:u8()) testimony.assert_equal(127, r:u8()) testimony.assert_equal(255, r:u8()) end) testify:that("u16 round-trips through writer and reader", function() local w = wire.writer() w:u16(0) w:u16(256) w:u16(65535) local r = wire.reader(w:output()) testimony.assert_equal(0, r:u16()) testimony.assert_equal(256, r:u16()) testimony.assert_equal(65535, r:u16()) end) testify:that("u32 round-trips through writer and reader", function() local w = wire.writer() w:u32(0) w:u32(86400) w:u32(4294967295) local r = wire.reader(w:output()) testimony.assert_equal(0, r:u32()) testimony.assert_equal(86400, r:u32()) testimony.assert_equal(4294967295, r:u32()) end) testify:that("name round-trips through writer and reader", function() local w = wire.writer() w:name("example.com.") local r = wire.reader(w:output()) testimony.assert_equal("example.com.", r:name()) end) testify:that("compressed names round-trip correctly", function() -- Write a mini DNS-like structure with header + two names local w = wire.writer() -- 12-byte fake header for _ = 1, 12 do w:u8(0) end w:name("example.com.") -- offset 12 w:name("www.example.com.") -- should use pointer local r = wire.reader(w:output()) r:seek(13) -- skip header, position at first name testimony.assert_equal("example.com.", r:name()) testimony.assert_equal("www.example.com.", r:name()) end) testify:that("character_string round-trips", function() local w = wire.writer() w:character_string("hello world") local r = wire.reader(w:output()) testimony.assert_equal("hello world", r:character_string()) end) -- ========================================================================= -- Category 2: Adversarial / Security Tests -- ========================================================================= testify:that("reader rejects forward pointer", function() -- Header (12 bytes) + name starting at offset 12 -- The name has a pointer 0xC0 0x20 pointing forward to offset 32 local header = string.rep(raw(0x00), 12) local name = raw(0x03) .. "www" .. raw(0xC0, 0x20) local data = header .. name local r = wire.reader(data) r:seek(13) local val, err = r:name() testimony.assert_nil(val) testimony.assert_match("forward", err) end) testify:that("reader rejects pointer into header", function() -- Pointer 0xC0 0x02 points to offset 2 (inside 12-byte header) local header = string.rep(raw(0x00), 12) local name = raw(0xC0, 0x02) local data = header .. name local r = wire.reader(data) r:seek(13) local val, err = r:name() testimony.assert_nil(val) testimony.assert_match("header", err) end) testify:that("reader rejects pointer loop", function() -- Offset 12: pointer to offset 14 -- Offset 14: pointer to offset 12 local header = string.rep(raw(0x00), 12) local ptr1 = raw(0xC0, 0x0E) -- points to offset 14 local ptr2 = raw(0xC0, 0x0C) -- points to offset 12 local data = header .. ptr1 .. ptr2 local r = wire.reader(data) r:seek(13) local val, err = r:name() testimony.assert_nil(val) -- Should hit either forward pointer or depth limit testimony.assert_not_nil(err) end) testify:that("reader rejects self-pointer", function() -- Offset 12: pointer to offset 12 (self) local header = string.rep(raw(0x00), 12) local ptr = raw(0xC0, 0x0C) local data = header .. ptr local r = wire.reader(data) r:seek(13) local val, err = r:name() testimony.assert_nil(val) testimony.assert_match("forward", err) end) testify:that("reader rejects label > 63 bytes", function() -- Length byte 0x40 = 64 (exceeds 63) local data = raw(0x40) .. string.rep("x", 64) .. raw(0x00) local r = wire.reader(data) local val, err = r:name() testimony.assert_nil(val) testimony.assert_match("63", err) end) testify:that("reader rejects name > 255 wire bytes", function() -- Build a name with labels that total > 255 wire bytes -- 5 labels of 63 bytes each: 5 * (1 + 63) + 1 = 321 bytes (exceeds 255) local parts = {} for _ = 1, 5 do parts[#parts + 1] = raw(0x3F) .. string.rep("a", 63) end parts[#parts + 1] = raw(0x00) local data = table.concat(parts) local r = wire.reader(data) local val, err = r:name() testimony.assert_nil(val) testimony.assert_match("255", err) end) testify:that("reader returns nil on truncated u32", function() local r = wire.reader(raw(0x01, 0x02, 0x03)) local val, err = r:u32() testimony.assert_nil(val) testimony.assert_not_nil(err) end) testify:that("reader returns nil on truncated u16", function() local r = wire.reader(raw(0x01)) local val, err = r:u16() testimony.assert_nil(val) testimony.assert_not_nil(err) end) testify:that("sub_reader rejects read past bounds", function() local r = wire.reader(raw(0x01, 0x02, 0x03, 0x04, 0x05)) local sub = r:sub_reader(2) -- Try to read 4 bytes from a 2-byte sub_reader local val, err = sub:u32() testimony.assert_nil(val) testimony.assert_not_nil(err) end) testify:that("writer rejects label > 63 bytes", function() local w = wire.writer() local long_label = string.rep("a", 64) .. ".com." testimony.assert_error(function() w:name(long_label) end, "63") end) testify:that("writer rejects name > 255 wire bytes", function() local w = wire.writer() -- Build a name with labels totaling > 255 wire bytes -- 5 labels of 50 bytes: 5 * (1 + 50) + 1 = 256 bytes (exceeds 255) local labels = {} for _ = 1, 5 do labels[#labels + 1] = string.rep("a", 50) end local long_name = table.concat(labels, ".") .. "." testimony.assert_error(function() w:name(long_name) end, "255") end) testify:that("writer character_string rejects string > 255 bytes", function() local w = wire.writer() testimony.assert_error(function() w:character_string(string.rep("x", 256)) end, "255") end) -- ========================================================================= -- Category 3: Real Captured Packet Tests (from hand-crafted fixtures) -- ========================================================================= testify:that("header decode from hc_a_simple response", function() local data = crypto.hex_to_bin(fixtures.hc_a_simple_response) local r = wire.reader(data) local hdr = decode_header(r) local exp = expected.hc_a_simple testimony.assert_equal(0xABCD, hdr.id) testimony.assert_equal(exp.flags.qr, hdr.qr) testimony.assert_equal(exp.flags.opcode, hdr.opcode) testimony.assert_equal(exp.flags.aa, hdr.aa) testimony.assert_equal(exp.flags.tc, hdr.tc) testimony.assert_equal(exp.flags.rd, hdr.rd) testimony.assert_equal(exp.flags.ra, hdr.ra) testimony.assert_equal(exp.rcode, hdr.rcode) testimony.assert_equal(exp.counts.qdcount, hdr.qdcount) testimony.assert_equal(exp.counts.ancount, hdr.ancount) testimony.assert_equal(exp.counts.nscount, hdr.nscount) testimony.assert_equal(exp.counts.arcount, hdr.arcount) end) testify:that("question name from hc_a_simple response", function() local data = crypto.hex_to_bin(fixtures.hc_a_simple_response) local r = wire.reader(data) r:skip(12) -- skip header local name = r:name() testimony.assert_equal("example.com.", name) -- Read QTYPE and QCLASS testimony.assert_equal(1, r:u16()) -- A testimony.assert_equal(1, r:u16()) -- IN end) testify:that("compressed answer name from hc_a_simple response", function() local data = crypto.hex_to_bin(fixtures.hc_a_simple_response) local r = wire.reader(data) r:skip(12) -- skip header r:name() -- skip question name r:skip(4) -- skip QTYPE + QCLASS -- Answer RR: name should be pointer to offset 12 → "example.com." local name = r:name() testimony.assert_equal("example.com.", name) -- TYPE, CLASS, TTL, RDLENGTH testimony.assert_equal(1, r:u16()) -- TYPE=A testimony.assert_equal(1, r:u16()) -- CLASS=IN testimony.assert_equal(300, r:u32()) -- TTL testimony.assert_equal(4, r:u16()) -- RDLENGTH end) testify:that("cname_chain fixture resolves all names", function() local data = crypto.hex_to_bin(fixtures.hc_cname_chain_response) local r = wire.reader(data) local hdr = decode_header(r) local exp = expected.hc_cname_chain testimony.assert_equal(exp.counts.ancount, hdr.ancount) -- Question local qname = r:name() testimony.assert_equal(exp.qname, qname) r:skip(4) -- QTYPE + QCLASS -- Answer 1: CNAME local an1_name = r:name() testimony.assert_equal("www.example.com.", an1_name) local an1_type = r:u16() testimony.assert_equal(5, an1_type) -- CNAME r:skip(2) -- CLASS local an1_ttl = r:u32() testimony.assert_equal(600, an1_ttl) local an1_rdlen = r:u16() testimony.assert_equal(2, an1_rdlen) -- CNAME RDATA: pointer to "example.com." local cname_target = r:name() testimony.assert_equal("example.com.", cname_target) -- Answer 2: A record local an2_name = r:name() testimony.assert_equal("example.com.", an2_name) local an2_type = r:u16() testimony.assert_equal(1, an2_type) -- A r:skip(2) -- CLASS local an2_ttl = r:u32() testimony.assert_equal(300, an2_ttl) local an2_rdlen = r:u16() testimony.assert_equal(4, an2_rdlen) end) testify:that("multi_answer fixture reads all answer names", function() local data = crypto.hex_to_bin(fixtures.hc_multi_answer_response) local r = wire.reader(data) local hdr = decode_header(r) testimony.assert_equal(3, hdr.ancount) -- Skip question r:name() r:skip(4) -- Read all 3 answers for i = 1, 3 do local name = r:name() testimony.assert_equal("example.com.", name) local rtype = r:u16() testimony.assert_equal(1, rtype) -- A r:skip(2) -- CLASS local ttl = r:u32() testimony.assert_equal(300, ttl) local rdlen = r:u16() testimony.assert_equal(4, rdlen) r:skip(rdlen) -- skip RDATA end testimony.assert_true(r:at_end()) end) testify:that("nxdomain fixture reads authority section", function() local data = crypto.hex_to_bin(fixtures.hc_nxdomain_response) local r = wire.reader(data) local hdr = decode_header(r) local exp = expected.hc_nxdomain testimony.assert_equal(3, hdr.rcode) -- NXDOMAIN testimony.assert_equal(exp.counts.qdcount, hdr.qdcount) testimony.assert_equal(exp.counts.ancount, hdr.ancount) testimony.assert_equal(exp.counts.nscount, hdr.nscount) -- Question local qname = r:name() testimony.assert_equal(exp.qname, qname) r:skip(4) -- QTYPE + QCLASS -- Authority: SOA record local auth_name = r:name() testimony.assert_equal("example.com.", auth_name) local auth_type = r:u16() testimony.assert_equal(6, auth_type) -- SOA local auth_class = r:u16() testimony.assert_equal(1, auth_class) -- IN local auth_ttl = r:u32() testimony.assert_equal(900, auth_ttl) local rdlen = r:u16() testimony.assert_equal(33, rdlen) -- SOA RDATA via sub_reader local sub = r:sub_reader(rdlen) local mname = sub:name() testimony.assert_equal("ns.example.com.", mname) local rname = sub:name() testimony.assert_equal("admin.example.com.", rname) local serial = sub:u32() testimony.assert_equal(1, serial) local refresh = sub:u32() testimony.assert_equal(7200, refresh) local retry = sub:u32() testimony.assert_equal(1800, retry) local expire = sub:u32() testimony.assert_equal(604800, expire) local minimum = sub:u32() testimony.assert_equal(300, minimum) testimony.assert_true(sub:at_end()) testimony.assert_true(r:at_end()) end) testify:that("header round-trip matches original bytes", function() local data = crypto.hex_to_bin(fixtures.hc_a_simple_response) local r = wire.reader(data) local hdr = decode_header(r) local w = wire.writer() encode_header(w, hdr) local roundtrip = w:output() -- First 12 bytes should be identical testimony.assert_equal(data:sub(1, 12), roundtrip) end) testify:that("name round-trip from real packets produces same names", function() -- Read names from hc_cname_chain, write them, read back local data = crypto.hex_to_bin(fixtures.hc_cname_chain_response) local r = wire.reader(data) r:skip(12) -- header -- Collect all names from the response local names = {} names[#names + 1] = r:name() -- question name r:skip(4) -- QTYPE + QCLASS -- Answer 1: name + skip to RDATA + read CNAME target names[#names + 1] = r:name() r:skip(8) -- TYPE(2) + CLASS(2) + TTL(4) local rdlen1 = r:u16() names[#names + 1] = r:name() -- CNAME target -- Answer 2: name names[#names + 1] = r:name() r:skip(8) local rdlen2 = r:u16() r:skip(rdlen2) -- Write all names into a fresh writer and read back local w = wire.writer() for _ = 1, 12 do w:u8(0) end -- fake header for _, name in ipairs(names) do w:name(name) end local r2 = wire.reader(w:output()) r2:skip(12) for _, original_name in ipairs(names) do local roundtrip_name = r2:name() testimony.assert_equal(original_name, roundtrip_name) end end) testify:conclude()