dns core: wire-format parse/encode, iterators, response builder

This commit is contained in:
2026-08-01 01:06:11 +02:00
parent bf02f83adc
commit 7429b96cde
9 changed files with 2525 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
//! Public root of the pure DNS wire-format module. The fuzz-target compile
//! imports this as a named module; file-level tests stay reachable through
//! `src/tests.zig`, which imports each file directly (`zig test` collects
//! tests only from the root module).
pub const types = @import("types.zig");
pub const header = @import("header.zig");
pub const name = @import("name.zig");
pub const question = @import("question.zig");
pub const record = @import("record.zig");
pub const edns = @import("edns.zig");
pub const packet = @import("packet.zig");
+429
View File
@@ -0,0 +1,429 @@
//! EDNS(0) OPT pseudo-record (RFC 6891) and the Client Subnet option
//! (RFC 7871). Pure: no allocation, no `std.Io` beyond writing encoded bytes
//! to a caller's writer.
//!
//! OPT reuses two record fields for other purposes: CLASS carries the
//! requestor's UDP payload size and TTL carries the extended RCODE, the EDNS
//! version and the DO bit. `record.Record` therefore keeps both as plain
//! integers, and this module reinterprets them.
const std = @import("std");
const types = @import("types.zig");
const record = @import("record.zig");
const Writer = std.Io.Writer;
/// The EDNS Client Subnet option code (RFC 7871 §6).
pub const ecs_option_code: u16 = 8;
pub const OptRecord = struct {
udp_payload_size: u16,
extended_rcode: u8,
version: u8,
do_bit: bool,
/// The option list, as a span into the packet the OPT record came from.
options: record.RdataSpan,
};
pub const ParseError = error{
/// The record is not a well-formed OPT: wrong type, or an owner name other
/// than the root, which RFC 6891 §6.1.2 requires.
NotOpt,
/// An option header or body runs past the end of the option list.
BadOption,
};
/// Reinterprets an already-parsed record as an OPT record and checks that its
/// option list is structurally sound, so a returned `OptRecord` always
/// iterates without error.
pub fn parseOpt(packet: []const u8, rec: record.Record) ParseError!OptRecord {
if (rec.rtype != .opt) return error.NotOpt;
if (!rec.name.isRoot()) return error.NotOpt;
const opt: OptRecord = .{
.udp_payload_size = rec.class,
.extended_rcode = @truncate(rec.ttl >> 24),
.version = @truncate(rec.ttl >> 16),
.do_bit = rec.ttl & 0x8000 != 0,
.options = rec.rdata,
};
var it = options(packet, opt);
while (try it.next()) |_| {}
return opt;
}
pub const Option = struct {
code: u16,
data: []const u8,
};
pub const OptionIterator = struct {
bytes: []const u8,
pos: usize,
pub fn next(self: *OptionIterator) error{BadOption}!?Option {
if (self.pos == self.bytes.len) return null;
if (self.pos + 4 > self.bytes.len) return error.BadOption;
const code = std.mem.readInt(u16, self.bytes[self.pos..][0..2], .big);
const len: usize = std.mem.readInt(u16, self.bytes[self.pos + 2 ..][0..2], .big);
const data_start = self.pos + 4;
if (data_start + len > self.bytes.len) return error.BadOption;
self.pos = data_start + len;
return .{ .code = code, .data = self.bytes[data_start..][0..len] };
}
};
pub fn options(packet: []const u8, opt: OptRecord) OptionIterator {
return .{ .bytes = opt.options.slice(packet), .pos = 0 };
}
/// Returns the first option carrying `code`, or null. An `OptRecord` from
/// `parseOpt` cannot fail here, but the error stays visible so that a
/// hand-built `OptRecord` cannot smuggle a malformed list past this.
pub fn findOption(packet: []const u8, opt: OptRecord, code: u16) error{BadOption}!?Option {
var it = options(packet, opt);
while (try it.next()) |o| {
if (o.code == code) return o;
}
return null;
}
/// Address families in the EDNS Client Subnet option, from the IANA Address
/// Family Numbers registry.
pub const ecs_family_ipv4: u16 = 1;
pub const ecs_family_ipv6: u16 = 2;
pub const Ecs = struct {
family: u16,
source_prefix: u8,
scope_prefix: u8,
/// The truncated address, `ceil(source_prefix / 8)` bytes, as a slice into
/// the option data.
address: []const u8,
};
pub const EcsError = error{BadEcs};
/// RFC 7871 §6: FAMILY, SOURCE PREFIX-LENGTH, SCOPE PREFIX-LENGTH, then only
/// as many address bytes as the source prefix covers.
pub fn parseEcs(data: []const u8) EcsError!Ecs {
if (data.len < 4) return error.BadEcs;
const family = std.mem.readInt(u16, data[0..2], .big);
const source_prefix = data[2];
const scope_prefix = data[3];
const max_prefix: u16 = switch (family) {
ecs_family_ipv4 => 32,
ecs_family_ipv6 => 128,
// An unknown family has no known address width, so only the encoded
// length can be checked.
else => 255,
};
if (source_prefix > max_prefix) return error.BadEcs;
if (scope_prefix > max_prefix) return error.BadEcs;
// RFC 7871 §6 truncates the address to the source prefix and pads the last
// octet with zero bits, so a prefix of 0 carries no address bytes at all.
const address_len = (@as(usize, source_prefix) + 7) / 8;
if (data.len - 4 != address_len) return error.BadEcs;
const significant_bits: u3 = @intCast(source_prefix % 8);
if (significant_bits != 0) {
const padding_mask = @as(u8, 0xff) >> significant_bits;
if (data[3 + address_len] & padding_mask != 0) return error.BadEcs;
}
return .{
.family = family,
.source_prefix = source_prefix,
.scope_prefix = scope_prefix,
.address = data[4..],
};
}
/// Writes the OPT record: root owner name, type OPT, the payload size in
/// CLASS, the flags in TTL, then the option list verbatim.
pub fn encodeOpt(opt: OptRecord, options_bytes: []const u8, w: *Writer) (Writer.Error || error{OptionsTooLong})!void {
if (options_bytes.len > std.math.maxInt(u16)) return error.OptionsTooLong;
try w.writeByte(0);
try w.writeInt(u16, @intFromEnum(types.Type.opt), .big);
try w.writeInt(u16, opt.udp_payload_size, .big);
try w.writeInt(u32, ttlFrom(opt), .big);
try w.writeInt(u16, @intCast(options_bytes.len), .big);
try w.writeAll(options_bytes);
}
fn ttlFrom(opt: OptRecord) u32 {
return (@as(u32, opt.extended_rcode) << 24) |
(@as(u32, opt.version) << 16) |
(@as(u32, @intFromBool(opt.do_bit)) << 15);
}
/// The full 12-bit RCODE (RFC 6891 §6.1.3): the OPT record supplies the upper
/// eight bits, the header the lower four. Without an OPT record the value is
/// just the header's four bits.
pub fn extendedRcode(header_rcode: types.Rcode, opt: ?OptRecord) u12 {
const low: u12 = @intFromEnum(header_rcode);
const o = opt orelse return low;
return (@as(u12, o.extended_rcode) << 4) | low;
}
const testing = std.testing;
/// An OPT record with a 4096-byte payload size, DO set, and no options.
const opt_do = "\x00\x00\x29\x10\x00\x00\x00\x80\x00\x00\x00";
test "parseOpt reads payload size, version and the DO bit" {
const r = try record.parse(opt_do, 0);
const opt = try parseOpt(opt_do, r.record);
try testing.expectEqual(@as(u16, 4096), opt.udp_payload_size);
try testing.expectEqual(@as(u8, 0), opt.extended_rcode);
try testing.expectEqual(@as(u8, 0), opt.version);
try testing.expectEqual(true, opt.do_bit);
try testing.expectEqual(@as(usize, 0), opt.options.len);
}
test "parseOpt reads a cleared DO bit and an extended rcode" {
// TTL 0x01_00_0000: extended rcode 1, version 0, DO clear.
const packet = "\x00\x00\x29\x02\x00\x01\x00\x00\x00\x00\x00";
const r = try record.parse(packet, 0);
const opt = try parseOpt(packet, r.record);
try testing.expectEqual(@as(u16, 512), opt.udp_payload_size);
try testing.expectEqual(@as(u8, 1), opt.extended_rcode);
try testing.expectEqual(false, opt.do_bit);
}
test "parseOpt keeps an unknown EDNS version" {
const packet = "\x00\x00\x29\x10\x00\x00\x01\x00\x00\x00\x00";
const r = try record.parse(packet, 0);
const opt = try parseOpt(packet, r.record);
try testing.expectEqual(@as(u8, 1), opt.version);
}
test "parseOpt rejects the wrong type and a non-root owner name" {
const not_opt = "\x00\x00\x01\x00\x01\x00\x00\x00\x0a\x00\x04\x01\x02\x03\x04";
const r = try record.parse(not_opt, 0);
try testing.expectError(error.NotOpt, parseOpt(not_opt, r.record));
const named = "\x03com\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00";
const rn = try record.parse(named, 0);
try testing.expectEqual(types.Type.opt, rn.record.rtype);
try testing.expectError(error.NotOpt, parseOpt(named, rn.record));
}
test "option iterator yields every option" {
// Two options: ECS with 7 bytes, code 12 (padding) with 2 bytes.
const packet = "\x00\x00\x29\x10\x00\x00\x00\x80\x00\x00\x11" ++
"\x00\x08\x00\x07\x00\x01\x18\x00\xc0\x00\x02" ++
"\x00\x0c\x00\x02\x00\x00";
const r = try record.parse(packet, 0);
const opt = try parseOpt(packet, r.record);
try testing.expectEqual(@as(usize, 17), opt.options.len);
var it = options(packet, opt);
const first = (try it.next()).?;
try testing.expectEqual(ecs_option_code, first.code);
try testing.expectEqualSlices(u8, "\x00\x01\x18\x00\xc0\x00\x02", first.data);
const second = (try it.next()).?;
try testing.expectEqual(@as(u16, 12), second.code);
try testing.expectEqualSlices(u8, "\x00\x00", second.data);
try testing.expectEqual(@as(?Option, null), try it.next());
const found = (try findOption(packet, opt, ecs_option_code)).?;
try testing.expectEqualSlices(u8, first.data, found.data);
try testing.expectEqual(@as(?Option, null), try findOption(packet, opt, 99));
}
test "option iterator accepts a zero-length option body" {
const packet = "\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x04\x00\x0c\x00\x00";
const r = try record.parse(packet, 0);
const opt = try parseOpt(packet, r.record);
var it = options(packet, opt);
const only = (try it.next()).?;
try testing.expectEqual(@as(u16, 12), only.code);
try testing.expectEqual(@as(usize, 0), only.data.len);
try testing.expectEqual(@as(?Option, null), try it.next());
}
test "parseOpt rejects a malformed option list" {
// Option length claims 8 bytes but only 4 follow.
const overrun = "\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x08" ++
"\x00\x08\x00\x08\x00\x01\x18\x00";
const ro = try record.parse(overrun, 0);
try testing.expectError(error.BadOption, parseOpt(overrun, ro.record));
// A trailing partial option header (three bytes, not four).
const partial = "\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x03\x00\x08\x00";
const rp = try record.parse(partial, 0);
try testing.expectError(error.BadOption, parseOpt(partial, rp.record));
}
test "parseEcs reads an IPv4 subnet" {
const ecs = try parseEcs("\x00\x01\x18\x00\xc0\x00\x02");
try testing.expectEqual(ecs_family_ipv4, ecs.family);
try testing.expectEqual(@as(u8, 24), ecs.source_prefix);
try testing.expectEqual(@as(u8, 0), ecs.scope_prefix);
try testing.expectEqualSlices(u8, "\xc0\x00\x02", ecs.address);
}
test "parseEcs reads an IPv6 subnet" {
const ecs = try parseEcs("\x00\x02\x38\x38\x20\x01\x0d\xb8\x00\x00\x00");
try testing.expectEqual(ecs_family_ipv6, ecs.family);
try testing.expectEqual(@as(u8, 56), ecs.source_prefix);
try testing.expectEqual(@as(u8, 56), ecs.scope_prefix);
try testing.expectEqual(@as(usize, 7), ecs.address.len);
}
test "parseEcs reads a zero-length prefix" {
const ecs = try parseEcs("\x00\x01\x00\x00");
try testing.expectEqual(@as(u8, 0), ecs.source_prefix);
try testing.expectEqual(@as(usize, 0), ecs.address.len);
// A prefix of 0 covers no address byte, so any address byte is a length
// mismatch.
try testing.expectError(error.BadEcs, parseEcs("\x00\x01\x00\x00\x00"));
try testing.expectError(error.BadEcs, parseEcs("\x00\x01\x00\x00\xc0\x00\x02\x00"));
}
test "parseEcs rejects nonzero padding bits past the source prefix" {
// IPv4 /25: the low seven bits of the fourth address byte must be zero.
try testing.expectError(error.BadEcs, parseEcs("\x00\x01\x19\x00\xc0\x00\x02\x01"));
try testing.expectError(error.BadEcs, parseEcs("\x00\x01\x19\x00\xc0\x00\x02\xff"));
const zero_padded = try parseEcs("\x00\x01\x19\x00\xc0\x00\x02\x80");
try testing.expectEqual(@as(u8, 25), zero_padded.source_prefix);
try testing.expectEqualSlices(u8, "\xc0\x00\x02\x80", zero_padded.address);
// IPv4 /20: the low four bits of the third address byte must be zero.
try testing.expectError(error.BadEcs, parseEcs("\x00\x01\x14\x00\xc0\x00\x0f"));
try testing.expectEqualSlices(u8, "\xc0\x00\x00", (try parseEcs("\x00\x01\x14\x00\xc0\x00\x00")).address);
// IPv6 /57: the low seven bits of the eighth address byte must be zero.
try testing.expectError(error.BadEcs, parseEcs("\x00\x02\x39\x00\x20\x01\x0d\xb8\x00\x00\x00\x7f"));
try testing.expectEqualSlices(
u8,
"\x20\x01\x0d\xb8\x00\x00\x00\x80",
(try parseEcs("\x00\x02\x39\x00\x20\x01\x0d\xb8\x00\x00\x00\x80")).address,
);
// An unknown family uses the same encoding, so the rule holds there too.
try testing.expectError(error.BadEcs, parseEcs("\x12\x34\x03\x00\xff"));
try testing.expectEqualSlices(u8, "\xe0", (try parseEcs("\x12\x34\x03\x00\xe0")).address);
}
test "parseEcs rejects malformed options" {
// Shorter than the fixed fields.
try testing.expectError(error.BadEcs, parseEcs(""));
try testing.expectError(error.BadEcs, parseEcs("\x00\x01\x18"));
// Prefix wider than the family allows.
try testing.expectError(error.BadEcs, parseEcs("\x00\x01\x21" ++ "\x00\x00\x00\x00\x00"));
try testing.expectError(error.BadEcs, parseEcs("\x00\x02\x81" ++ "\x00" ** 17));
// Scope wider than the family allows.
try testing.expectError(error.BadEcs, parseEcs("\x00\x01\x18\x21\xc0\x00\x02"));
// Address shorter than the prefix needs.
try testing.expectError(error.BadEcs, parseEcs("\x00\x01\x18\x00\xc0\x00"));
// Address longer than the prefix needs.
try testing.expectError(error.BadEcs, parseEcs("\x00\x01\x18\x00\xc0\x00\x02\x00"));
}
test "parseEcs accepts an unknown family with a consistent length" {
const ecs = try parseEcs("\x12\x34\x08\x00\xff");
try testing.expectEqual(@as(u16, 0x1234), ecs.family);
try testing.expectEqualSlices(u8, "\xff", ecs.address);
try testing.expectError(error.BadEcs, parseEcs("\x12\x34\x08\x00\xff\xff"));
}
test "encodeOpt round-trips through record parse and parseOpt" {
const original: OptRecord = .{
.udp_payload_size = 1232,
.extended_rcode = 0x10,
.version = 0,
.do_bit = true,
.options = .{ .offset = 0, .len = 0 },
};
const options_bytes = "\x00\x08\x00\x07\x00\x01\x18\x00\xc0\x00\x02";
var buf: [64]u8 = undefined;
var w = Writer.fixed(&buf);
try encodeOpt(original, options_bytes, &w);
const bytes = w.buffered();
const r = try record.parse(bytes, 0);
try testing.expectEqual(@as(usize, bytes.len), r.end);
const opt = try parseOpt(bytes, r.record);
try testing.expectEqual(original.udp_payload_size, opt.udp_payload_size);
try testing.expectEqual(original.extended_rcode, opt.extended_rcode);
try testing.expectEqual(original.version, opt.version);
try testing.expectEqual(original.do_bit, opt.do_bit);
try testing.expectEqualSlices(u8, options_bytes, opt.options.slice(bytes));
const ecs = try parseEcs((try findOption(bytes, opt, ecs_option_code)).?.data);
try testing.expectEqual(@as(u8, 24), ecs.source_prefix);
}
test "encodeOpt round-trips both DO states" {
for ([_]bool{ false, true }) |do_bit| {
const opt: OptRecord = .{
.udp_payload_size = 4096,
.extended_rcode = 0,
.version = 0,
.do_bit = do_bit,
.options = .{ .offset = 0, .len = 0 },
};
var buf: [32]u8 = undefined;
var w = Writer.fixed(&buf);
try encodeOpt(opt, "", &w);
const bytes = w.buffered();
try testing.expectEqualSlices(u8, opt_do[0..3], bytes[0..3]);
const r = try record.parse(bytes, 0);
try testing.expectEqual(do_bit, (try parseOpt(bytes, r.record)).do_bit);
}
}
test "encodeOpt leaves the Z bits clear" {
const opt: OptRecord = .{
.udp_payload_size = 512,
.extended_rcode = 0,
.version = 0,
.do_bit = true,
.options = .{ .offset = 0, .len = 0 },
};
try testing.expectEqual(@as(u32, 0x0000_8000), ttlFrom(opt));
}
test "encodeOpt reports a short buffer" {
const opt: OptRecord = .{
.udp_payload_size = 512,
.extended_rcode = 0,
.version = 0,
.do_bit = false,
.options = .{ .offset = 0, .len = 0 },
};
var buf: [8]u8 = undefined;
var w = Writer.fixed(&buf);
try testing.expectError(error.WriteFailed, encodeOpt(opt, "", &w));
}
test "extendedRcode composes the twelve bits" {
try testing.expectEqual(@as(u12, 3), extendedRcode(.nx_domain, null));
const opt: OptRecord = .{
.udp_payload_size = 4096,
.extended_rcode = 1,
.version = 0,
.do_bit = false,
.options = .{ .offset = 0, .len = 0 },
};
// Extended rcode 1 over header rcode 0 is BADVERS (16).
try testing.expectEqual(@as(u12, 16), extendedRcode(.no_error, opt));
var not_auth = opt;
not_auth.extended_rcode = 0;
try testing.expectEqual(@as(u12, 9), extendedRcode(.not_auth, not_auth));
var high = opt;
high.extended_rcode = 0xff;
try testing.expectEqual(@as(u12, 0xfff), extendedRcode(@enumFromInt(15), high));
}
+244
View File
@@ -0,0 +1,244 @@
//! The 12-byte DNS message header (RFC 1035 §4.1.1). Pure: no allocation,
//! no `std.Io`.
//!
//! Encode convention for the whole `dns/` module: fixed-size items encode into
//! a caller-provided buffer; variable-size items encode through a
//! `*std.Io.Writer`. The header is fixed-size, so it takes a `*[12]u8`.
const std = @import("std");
const types = @import("types.zig");
/// The second 16-bit word of the header, laid out from the least significant
/// bit up. `z` carries the three reserved bits (RFC 1035 §4.1.1; bits later
/// claimed as AD and CD by RFC 4035) so that an unknown bit pattern survives a
/// parse/encode round-trip unchanged.
pub const Flags = packed struct(u16) {
rcode: types.Rcode,
z: u3,
ra: bool,
rd: bool,
tc: bool,
aa: bool,
opcode: types.Opcode,
qr: bool,
pub fn fromInt(value: u16) Flags {
return @bitCast(value);
}
pub fn toInt(self: Flags) u16 {
return @bitCast(self);
}
};
pub const ParseError = error{Truncated};
pub const Header = struct {
id: u16,
flags: Flags,
qdcount: u16,
ancount: u16,
nscount: u16,
arcount: u16,
};
/// Reads the first `types.header_len` bytes. Trailing bytes are ignored.
pub fn parse(bytes: []const u8) ParseError!Header {
if (bytes.len < types.header_len) return error.Truncated;
const b = bytes[0..types.header_len];
return .{
.id = std.mem.readInt(u16, b[0..2], .big),
.flags = Flags.fromInt(std.mem.readInt(u16, b[2..4], .big)),
.qdcount = std.mem.readInt(u16, b[4..6], .big),
.ancount = std.mem.readInt(u16, b[6..8], .big),
.nscount = std.mem.readInt(u16, b[8..10], .big),
.arcount = std.mem.readInt(u16, b[10..12], .big),
};
}
pub fn encode(h: Header, out: *[types.header_len]u8) void {
std.mem.writeInt(u16, out[0..2], h.id, .big);
std.mem.writeInt(u16, out[2..4], h.flags.toInt(), .big);
std.mem.writeInt(u16, out[4..6], h.qdcount, .big);
std.mem.writeInt(u16, out[6..8], h.ancount, .big);
std.mem.writeInt(u16, out[8..10], h.nscount, .big);
std.mem.writeInt(u16, out[10..12], h.arcount, .big);
}
const testing = std.testing;
test "flags bit positions follow RFC 1035" {
try testing.expectEqual(@as(u16, 0x8000), (Flags{
.rcode = .no_error,
.z = 0,
.ra = false,
.rd = false,
.tc = false,
.aa = false,
.opcode = .query,
.qr = true,
}).toInt());
try testing.expectEqual(@as(u16, 0x2800), (Flags{
.rcode = .no_error,
.z = 0,
.ra = false,
.rd = false,
.tc = false,
.aa = false,
.opcode = .update,
.qr = false,
}).toInt());
try testing.expectEqual(@as(u16, 0x0400), (Flags{
.rcode = .no_error,
.z = 0,
.ra = false,
.rd = false,
.tc = false,
.aa = true,
.opcode = .query,
.qr = false,
}).toInt());
try testing.expectEqual(@as(u16, 0x0200), (Flags{
.rcode = .no_error,
.z = 0,
.ra = false,
.rd = false,
.tc = true,
.aa = false,
.opcode = .query,
.qr = false,
}).toInt());
try testing.expectEqual(@as(u16, 0x0100), (Flags{
.rcode = .no_error,
.z = 0,
.ra = false,
.rd = true,
.tc = false,
.aa = false,
.opcode = .query,
.qr = false,
}).toInt());
try testing.expectEqual(@as(u16, 0x0080), (Flags{
.rcode = .no_error,
.z = 0,
.ra = true,
.rd = false,
.tc = false,
.aa = false,
.opcode = .query,
.qr = false,
}).toInt());
try testing.expectEqual(@as(u16, 0x0070), (Flags{
.rcode = .no_error,
.z = 7,
.ra = false,
.rd = false,
.tc = false,
.aa = false,
.opcode = .query,
.qr = false,
}).toInt());
try testing.expectEqual(@as(u16, 0x0003), (Flags{
.rcode = .nx_domain,
.z = 0,
.ra = false,
.rd = false,
.tc = false,
.aa = false,
.opcode = .query,
.qr = false,
}).toInt());
}
test "every flags word round-trips, reserved bits included" {
var value: u32 = 0;
while (value <= std.math.maxInt(u16)) : (value += 1) {
const word: u16 = @intCast(value);
try testing.expectEqual(word, Flags.fromInt(word).toInt());
}
}
test "header parse decodes a standard query" {
const bytes = [_]u8{
0xab, 0xcd, // id
0x01, 0x20, // flags: RD set, z = 2
0x00, 0x01, // qdcount
0x00, 0x00, // ancount
0x00, 0x00, // nscount
0x00, 0x01, // arcount
};
const h = try parse(&bytes);
try testing.expectEqual(@as(u16, 0xabcd), h.id);
try testing.expectEqual(false, h.flags.qr);
try testing.expectEqual(types.Opcode.query, h.flags.opcode);
try testing.expectEqual(true, h.flags.rd);
try testing.expectEqual(false, h.flags.ra);
try testing.expectEqual(@as(u3, 2), h.flags.z);
try testing.expectEqual(types.Rcode.no_error, h.flags.rcode);
try testing.expectEqual(@as(u16, 1), h.qdcount);
try testing.expectEqual(@as(u16, 0), h.ancount);
try testing.expectEqual(@as(u16, 0), h.nscount);
try testing.expectEqual(@as(u16, 1), h.arcount);
}
test "header round-trips through encode, unknown opcode and rcode included" {
const original: Header = .{
.id = 0x1234,
.flags = .{
.rcode = @enumFromInt(15),
.z = 5,
.ra = true,
.rd = true,
.tc = true,
.aa = true,
.opcode = @enumFromInt(3),
.qr = true,
},
.qdcount = 1,
.ancount = 2,
.nscount = 3,
.arcount = 4,
};
var buf: [types.header_len]u8 = undefined;
encode(original, &buf);
const parsed = try parse(&buf);
try testing.expectEqual(original, parsed);
}
test "parse ignores bytes past the header" {
const bytes = [_]u8{0xff} ** (types.header_len + 8);
const h = try parse(&bytes);
try testing.expectEqual(@as(u16, 0xffff), h.arcount);
}
test "truncated input" {
var i: usize = 0;
while (i < types.header_len) : (i += 1) {
const bytes = [_]u8{0} ** types.header_len;
try testing.expectError(error.Truncated, parse(bytes[0..i]));
}
}
test "encode writes big-endian counts" {
const h: Header = .{
.id = 0x0102,
.flags = Flags.fromInt(0x8180),
.qdcount = 0x0304,
.ancount = 0x0506,
.nscount = 0x0708,
.arcount = 0x090a,
};
var buf: [types.header_len]u8 = undefined;
encode(h, &buf);
try testing.expectEqualSlices(u8, &.{
0x01, 0x02, 0x81, 0x80, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
}, &buf);
}
+394
View File
@@ -0,0 +1,394 @@
//! Domain names in wire form (RFC 1035 §3.1, §4.1.4). Pure: no allocation,
//! no `std.Io` beyond writing encoded bytes to a caller's writer.
//!
//! A `Name` always holds the decoded, uncompressed form: a sequence of
//! length-prefixed labels ended by a zero byte, which `len` counts.
const std = @import("std");
const types = @import("types.zig");
const Writer = std.Io.Writer;
pub const Name = struct {
bytes: [types.max_name_len]u8,
len: u8,
/// The uncompressed wire bytes, terminating zero included.
pub fn wire(self: *const Name) []const u8 {
return self.bytes[0..self.len];
}
pub fn isRoot(self: Name) bool {
return self.len == 1 and self.bytes[0] == 0;
}
/// Labels excluding the root. The root name has zero labels.
pub fn labelCount(self: Name) usize {
var count: usize = 0;
var i: usize = 0;
while (i < self.len and self.bytes[i] != 0) : (i += 1 + self.bytes[i]) count += 1;
return count;
}
};
pub const ParseError = error{
Truncated,
LabelTooLong,
NameTooLong,
BadPointer,
TooManyJumps,
};
pub const Parsed = struct {
name: Name,
/// Offset just past the name as it appears at `offset`: past the first
/// compression pointer, or past the terminating zero when there is none.
end: usize,
};
/// Decodes the name at `offset`, following compression pointers within
/// `packet`. Every pointer must target an offset strictly lower than the
/// pointer's own offset, which makes a chain strictly decreasing and therefore
/// finite; `types.max_compression_jumps` bounds the work regardless.
pub fn parse(packet: []const u8, offset: usize) ParseError!Parsed {
var name: Name = .{ .bytes = undefined, .len = 0 };
var pos = offset;
var end: ?usize = null;
var jumps: usize = 0;
while (true) {
if (pos >= packet.len) return error.Truncated;
const control = packet[pos];
switch (control & 0xc0) {
0x00 => {
if (control == 0) {
std.debug.assert(name.len < types.max_name_len);
name.bytes[name.len] = 0;
name.len += 1;
return .{ .name = name, .end = end orelse pos + 1 };
}
const label_len: usize = control;
if (pos + 1 + label_len > packet.len) return error.Truncated;
// The terminating zero still has to fit.
if (name.len + 1 + label_len + 1 > types.max_name_len) return error.NameTooLong;
name.bytes[name.len] = control;
@memcpy(name.bytes[name.len + 1 ..][0..label_len], packet[pos + 1 ..][0..label_len]);
name.len += @intCast(1 + label_len);
pos += 1 + label_len;
},
0xc0 => {
if (pos + 1 >= packet.len) return error.Truncated;
const target = (@as(usize, control & 0x3f) << 8) | packet[pos + 1];
if (end == null) end = pos + 2;
if (target >= pos) return error.BadPointer;
jumps += 1;
if (jumps > types.max_compression_jumps) return error.TooManyJumps;
pos = target;
},
// 0x40 and 0x80 are reserved label types (RFC 6891 §3 retired the
// only assignment). A label of 64 bytes or more encodes into this
// range, which is the case worth naming.
else => return error.LabelTooLong,
}
}
}
/// Writes the name uncompressed. Compression on encode is out of scope: at
/// household query volumes the saved bytes do not pay for the offset
/// bookkeeping, and uncompressed output is always valid (RFC 1035 §4.1.4).
pub fn encode(name: Name, w: *Writer) Writer.Error!void {
try w.writeAll(name.wire());
}
pub const FromTextError = error{
EmptyLabel,
LabelTooLong,
NameTooLong,
};
/// Parses presentation form. A single trailing dot is optional; "" and "."
/// both denote the root. Bytes inside labels pass through opaquely, so no
/// escape sequences and no punycode.
pub fn fromText(text: []const u8) FromTextError!Name {
var name: Name = .{ .bytes = undefined, .len = 0 };
var rest = text;
if (rest.len > 0 and rest[rest.len - 1] == '.') rest = rest[0 .. rest.len - 1];
if (rest.len == 0) {
name.bytes[0] = 0;
name.len = 1;
return name;
}
var it = std.mem.splitScalar(u8, rest, '.');
while (it.next()) |label| {
if (label.len == 0) return error.EmptyLabel;
if (label.len > types.max_label_len) return error.LabelTooLong;
if (name.len + 1 + label.len + 1 > types.max_name_len) return error.NameTooLong;
name.bytes[name.len] = @intCast(label.len);
@memcpy(name.bytes[name.len + 1 ..][0..label.len], label);
name.len += @intCast(1 + label.len);
}
name.bytes[name.len] = 0;
name.len += 1;
return name;
}
/// Writes presentation form: labels joined by dots, no trailing dot. The root
/// name writes as ".".
pub fn formatText(name: Name, w: *Writer) Writer.Error!void {
if (name.isRoot()) {
try w.writeByte('.');
return;
}
var i: usize = 0;
var first = true;
while (i < name.len and name.bytes[i] != 0) {
const label_len = name.bytes[i];
if (!first) try w.writeByte('.');
try w.writeAll(name.bytes[i + 1 ..][0..label_len]);
first = false;
i += 1 + @as(usize, label_len);
}
}
/// Names compare case-insensitively over ASCII only (RFC 1035 §2.3.3); bytes
/// outside A-Z and a-z compare exactly.
pub fn eqlIgnoreCase(a: Name, b: Name) bool {
if (a.len != b.len) return false;
var i: usize = 0;
while (i < a.len and a.bytes[i] != 0) {
const label_len = a.bytes[i];
if (b.bytes[i] != label_len) return false;
const end = i + 1 + @as(usize, label_len);
if (!std.ascii.eqlIgnoreCase(a.bytes[i + 1 .. end], b.bytes[i + 1 .. end])) return false;
i = end;
}
return i < b.len and b.bytes[i] == 0;
}
const testing = std.testing;
fn expectText(expected: []const u8, name: Name) !void {
var buf: [512]u8 = undefined;
var w = Writer.fixed(&buf);
try formatText(name, &w);
try testing.expectEqualStrings(expected, w.buffered());
}
fn expectWire(expected: []const u8, name: Name) !void {
var buf: [512]u8 = undefined;
var w = Writer.fixed(&buf);
try encode(name, &w);
try testing.expectEqualStrings(expected, w.buffered());
}
test "fromText builds wire form" {
const n = try fromText("example.com.");
try expectWire("\x07example\x03com\x00", n);
try testing.expectEqual(@as(u8, 13), n.len);
try testing.expectEqual(@as(usize, 2), n.labelCount());
try testing.expect(!n.isRoot());
}
test "fromText accepts a missing trailing dot" {
const with_dot = try fromText("example.com.");
const without_dot = try fromText("example.com");
try testing.expectEqualSlices(u8, with_dot.wire(), without_dot.wire());
}
test "root name" {
for ([_][]const u8{ "", "." }) |text| {
const n = try fromText(text);
try testing.expect(n.isRoot());
try testing.expectEqual(@as(usize, 0), n.labelCount());
try expectWire("\x00", n);
try expectText(".", n);
}
}
test "text round-trips" {
for ([_][]const u8{ ".", "com", "example.com", "a.b.c.d.e.f" }) |text| {
try expectText(text, try fromText(text));
}
}
test "fromText rejects empty labels" {
try testing.expectError(error.EmptyLabel, fromText("example..com"));
try testing.expectError(error.EmptyLabel, fromText(".example.com"));
try testing.expectError(error.EmptyLabel, fromText(".."));
}
test "fromText rejects an oversize label" {
const ok = "a" ** types.max_label_len;
_ = try fromText(ok ++ ".com");
const too_long = "a" ** (types.max_label_len + 1);
try testing.expectError(error.LabelTooLong, fromText(too_long ++ ".com"));
}
test "fromText rejects an oversize name" {
// Four labels of 63 bytes encode to 4 * 64 + 1 = 257 bytes.
const label = "a" ** types.max_label_len;
try testing.expectError(
error.NameTooLong,
fromText(label ++ "." ++ label ++ "." ++ label ++ "." ++ label),
);
// Three of those (3 * 64 = 192) plus a 61-byte label (62) plus the
// terminating zero reach exactly 255.
const last = "b" ** 61;
const max = try fromText(label ++ "." ++ label ++ "." ++ label ++ "." ++ last);
try testing.expectEqual(@as(u8, types.max_name_len), max.len);
const one_over = "b" ** 62;
try testing.expectError(
error.NameTooLong,
fromText(label ++ "." ++ label ++ "." ++ label ++ "." ++ one_over),
);
}
test "parse decodes an uncompressed name" {
const packet = "\x07example\x03com\x00";
const r = try parse(packet, 0);
try expectText("example.com", r.name);
try testing.expectEqual(@as(usize, 13), r.end);
}
test "parse follows a compression pointer chain" {
// 0: "com" root at the top, then "example" pointing at it, then "www"
// pointing at "example.com".
const packet =
"\x03com\x00" ++ // offset 0, ends at 5
"\x07example\xc0\x00" ++ // offset 5, ends at 15
"\x03www\xc0\x05"; // offset 15, ends at 21
const com = try parse(packet, 0);
try expectText("com", com.name);
try testing.expectEqual(@as(usize, 5), com.end);
const example = try parse(packet, 5);
try expectText("example.com", example.name);
try testing.expectEqual(@as(usize, 15), example.end);
const www = try parse(packet, 15);
try expectText("www.example.com", www.name);
try testing.expectEqual(@as(usize, 21), www.end);
try testing.expectEqualSlices(u8, (try fromText("www.example.com")).wire(), www.name.wire());
}
test "parse rejects a pointer to itself" {
const packet = "\xc0\x00";
try testing.expectError(error.BadPointer, parse(packet, 0));
}
test "parse rejects a two-pointer loop" {
// The pointer at offset 2 targets 0, which points forward to 2.
const packet = "\xc0\x02\xc0\x00";
try testing.expectError(error.BadPointer, parse(packet, 0));
try testing.expectError(error.BadPointer, parse(packet, 2));
}
test "parse rejects a forward pointer" {
const packet = "\xc0\x04\x00\x00\x03com\x00";
try testing.expectError(error.BadPointer, parse(packet, 0));
}
test "parse rejects a pointer to its own offset" {
const packet = "\x00\x00\xc0\x02";
try testing.expectError(error.BadPointer, parse(packet, 2));
}
test "parse caps the jump count" {
// A descending chain of two-byte pointers: each entry at offset 2*i points
// to 2*(i-1). Entry 0 is the root label, so a chain of n entries costs n-1
// jumps from the last one.
const chain_len = types.max_compression_jumps + 2;
var packet: [chain_len * 2]u8 = undefined;
packet[0] = 0;
packet[1] = 0;
var i: usize = 1;
while (i < chain_len) : (i += 1) {
packet[i * 2] = 0xc0;
packet[i * 2 + 1] = @intCast((i - 1) * 2);
}
const at_limit = try parse(&packet, (types.max_compression_jumps) * 2);
try testing.expect(at_limit.name.isRoot());
try testing.expectError(
error.TooManyJumps,
parse(&packet, (types.max_compression_jumps + 1) * 2),
);
}
test "parse rejects a reserved label type" {
try testing.expectError(error.LabelTooLong, parse("\x40abc\x00", 0));
try testing.expectError(error.LabelTooLong, parse("\x80abc\x00", 0));
}
test "parse rejects truncation" {
// No terminating zero at all.
try testing.expectError(error.Truncated, parse("\x03com", 0));
// Label claims more bytes than the packet holds.
try testing.expectError(error.Truncated, parse("\x07exa", 0));
// Pointer's second byte is missing.
try testing.expectError(error.Truncated, parse("\x00\xc0", 1));
// Offset past the end.
try testing.expectError(error.Truncated, parse("\x00", 1));
try testing.expectError(error.Truncated, parse("", 0));
}
test "parse rejects a name longer than the limit" {
// Five 63-byte labels through pointers would exceed 255 bytes.
const label = "\x3f" ++ "a" ** types.max_label_len;
const packet = label ++ label ++ label ++ label ++ label ++ "\x00";
try testing.expectError(error.NameTooLong, parse(packet, 0));
}
test "parse round-trips the maximum length name" {
const label = "\x3f" ++ "a" ** types.max_label_len;
const last = "\x3d" ++ "b" ** 61;
const packet = label ++ label ++ label ++ last ++ "\x00";
const r = try parse(packet, 0);
try testing.expectEqual(@as(u8, types.max_name_len), r.name.len);
try testing.expectEqual(@as(usize, packet.len), r.end);
try expectWire(packet, r.name);
}
test "parse then encode round-trips through a pointer" {
const packet = "\x03com\x00\x07example\xc0\x00";
const r = try parse(packet, 5);
try expectWire("\x07example\x03com\x00", r.name);
}
test "eqlIgnoreCase folds ASCII only" {
const lower = try fromText("example.com");
const upper = try fromText("EXAMPLE.COM");
const mixed = try fromText("ExAmPlE.cOm");
try testing.expect(eqlIgnoreCase(lower, upper));
try testing.expect(eqlIgnoreCase(lower, mixed));
try testing.expect(eqlIgnoreCase(lower, lower));
const other = try fromText("example.net");
try testing.expect(!eqlIgnoreCase(lower, other));
const shorter = try fromText("com");
try testing.expect(!eqlIgnoreCase(lower, shorter));
const root = try fromText(".");
try testing.expect(eqlIgnoreCase(root, try fromText("")));
try testing.expect(!eqlIgnoreCase(root, lower));
// Same length, different label split: "aa.b" versus "a.ab".
try testing.expect(!eqlIgnoreCase(try fromText("aa.b"), try fromText("a.ab")));
// Byte 0xc0 is not an ASCII letter and must compare exactly.
const high_a = try fromText("\xc0");
const high_b = try fromText("\xe0");
try testing.expect(!eqlIgnoreCase(high_a, high_b));
}
test "labelCount" {
try testing.expectEqual(@as(usize, 0), (try fromText(".")).labelCount());
try testing.expectEqual(@as(usize, 1), (try fromText("com")).labelCount());
try testing.expectEqual(@as(usize, 3), (try fromText("www.example.com")).labelCount());
}
+868
View File
@@ -0,0 +1,868 @@
//! Whole DNS messages (RFC 1035 §4.1): structural validation, section
//! iteration, in-place mutation of a raw buffer, and a builder for synthesized
//! replies. Pure: no allocation, no `std.Io` beyond writing encoded bytes to a
//! caller's buffer.
//!
//! `parse` separates two failure classes because the server answers them
//! differently (PLAN §6.1):
//!
//! - `error.Truncated` — fewer than the 12 header bytes arrived. There is no
//! ID and no question to echo, so no reply is possible: the caller drops
//! the datagram silently.
//! - `WalkError` — the header is intact but a section is malformed. The
//! caller can echo the ID and answer FORMERR.
//!
//! `name.ParseError.Truncated` therefore cannot pass through unchanged: a name
//! that runs off the end of a packet is a malformed section, not a short
//! header, and mapping it onto `error.Truncated` would turn a FORMERR into a
//! silent drop. `mapNameError` performs that translation once.
const std = @import("std");
const types = @import("types.zig");
const header = @import("header.zig");
const name = @import("name.zig");
const question = @import("question.zig");
const record = @import("record.zig");
const edns = @import("edns.zig");
const Writer = std.Io.Writer;
/// A malformed section in a packet whose header is intact.
pub const WalkError = error{
/// A name is undecodable: a bad compression pointer, an over-long label or
/// name, or too many pointer jumps.
BadName,
/// A question or record runs past the end of the packet. A count field
/// larger than the records actually present lands here too, because the
/// walk then reads past the last record.
SectionOverrun,
/// Bytes follow the last counted record. RFC 1035 §4.1 gives a message no
/// padding outside its four sections, and EDNS padding (RFC 7830) lives
/// inside the OPT record's option list, so nothing legitimate lands here.
TrailingBytes,
/// More than one OPT record. RFC 6891 §6.1.1 allows at most one per
/// message and requires FORMERR for the rest.
MultipleOptRecords,
};
pub const ParseError = error{Truncated} || WalkError;
fn mapNameError(err: name.ParseError) WalkError {
return switch (err) {
error.Truncated => error.SectionOverrun,
error.LabelTooLong, error.NameTooLong, error.BadPointer, error.TooManyJumps => error.BadName,
};
}
/// A validated view of a message: the bytes plus the decoded header. Sections
/// are not stored — iterators re-walk the bytes on demand, which keeps the
/// struct small enough to pass by value and free of pointers into itself.
pub const Packet = struct {
bytes: []const u8,
header: header.Header,
};
/// Decodes the header and walks every section once, bounds-checking each name
/// and record. The walk must land exactly on the end of the buffer and must
/// meet at most one OPT record, so a `Packet` from here has no bytes outside
/// its sections and at most one OPT.
pub fn parse(bytes: []const u8) ParseError!Packet {
const h = header.parse(bytes) catch |err| switch (err) {
error.Truncated => return error.Truncated,
};
var pos: usize = types.header_len;
var q: u16 = 0;
while (q < h.qdcount) : (q += 1) {
const parsed = question.parse(bytes, pos) catch |err| return mapNameError(err);
pos = parsed.end;
}
const record_count = recordCount(h);
var opt_seen = false;
var r: u32 = 0;
while (r < record_count) : (r += 1) {
const parsed = record.parse(bytes, pos) catch |err| return mapNameError(err);
pos = parsed.end;
if (parsed.record.rtype == .opt) {
if (opt_seen) return error.MultipleOptRecords;
opt_seen = true;
}
}
if (pos != bytes.len) return error.TrailingBytes;
return .{ .bytes = bytes, .header = h };
}
fn recordCount(h: header.Header) u32 {
return @as(u32, h.ancount) + @as(u32, h.nscount) + @as(u32, h.arcount);
}
pub const QuestionIterator = struct {
packet: []const u8,
pos: usize,
remaining: u16,
pub fn next(self: *QuestionIterator) WalkError!?question.Question {
if (self.remaining == 0) return null;
const parsed = question.parse(self.packet, self.pos) catch |err| return mapNameError(err);
self.pos = parsed.end;
self.remaining -= 1;
return parsed.question;
}
};
/// Records of one section. The section's first byte is only reachable by
/// walking everything before it, so the first `next` call does that walk; the
/// iterator is otherwise inert.
pub const RecordIterator = struct {
packet: []const u8,
skip_questions: u16,
skip_records: u32,
remaining: u16,
pos: usize,
positioned: bool,
pub fn next(self: *RecordIterator) WalkError!?record.Record {
if (!self.positioned) try self.position();
if (self.remaining == 0) return null;
const parsed = record.parse(self.packet, self.pos) catch |err| return mapNameError(err);
self.pos = parsed.end;
self.remaining -= 1;
return parsed.record;
}
fn position(self: *RecordIterator) WalkError!void {
var pos: usize = types.header_len;
var q: u16 = 0;
while (q < self.skip_questions) : (q += 1) {
const parsed = question.parse(self.packet, pos) catch |err| return mapNameError(err);
pos = parsed.end;
}
var r: u32 = 0;
while (r < self.skip_records) : (r += 1) {
const parsed = record.parse(self.packet, pos) catch |err| return mapNameError(err);
pos = parsed.end;
}
self.pos = pos;
self.positioned = true;
}
};
pub fn questions(p: Packet) QuestionIterator {
return .{ .packet = p.bytes, .pos = types.header_len, .remaining = p.header.qdcount };
}
pub fn answers(p: Packet) RecordIterator {
return recordIterator(p, 0, p.header.ancount);
}
pub fn authorities(p: Packet) RecordIterator {
return recordIterator(p, p.header.ancount, p.header.nscount);
}
pub fn additionals(p: Packet) RecordIterator {
return recordIterator(p, recordCount(p.header) - p.header.arcount, p.header.arcount);
}
fn recordIterator(p: Packet, skip_records: u32, remaining: u16) RecordIterator {
return .{
.packet = p.bytes,
.skip_questions = p.header.qdcount,
.skip_records = skip_records,
.remaining = remaining,
.pos = types.header_len,
.positioned = false,
};
}
/// The first question, or null when there is none. A `Packet` from `parse` has
/// a decodable question section, so the parse below only fails for a `Packet`
/// assembled by hand around unvalidated bytes.
pub fn firstQuestion(p: Packet) ?question.Question {
var it = questions(p);
return it.next() catch null;
}
/// The OPT record from the additional section, or null when there is none. A
/// `Packet` from `parse` carries at most one, so the search below finds either
/// nothing or that one record. The signature also accepts a `Packet` assembled
/// by hand around unvalidated bytes; for those the last OPT wins, which is what
/// a sequential walk of the section yields.
pub fn findOptRecord(p: Packet) ?record.Record {
var it = additionals(p);
var found: ?record.Record = null;
while (it.next() catch return found) |rec| {
if (rec.rtype == .opt) found = rec;
}
return found;
}
/// Overwrites the message ID in place. Used on the cache hit path, where a
/// stored response answers a new query.
pub fn setId(bytes: []u8, id: u16) void {
std.debug.assert(bytes.len >= types.header_len);
std.mem.writeInt(u16, bytes[0..2], id, .big);
}
/// TTL sits four bytes into the fixed fields and RDLENGTH's two bytes follow
/// it, so the TTL starts six bytes before the RDATA.
const ttl_bytes_before_rdata = 6;
/// Ages every record in place by `elapsed_seconds`, saturating at zero, and
/// returns the smallest resulting TTL — or null when the message carries no
/// record whose TTL means anything. The caller decides what a small or absent
/// TTL means; this function only does the arithmetic.
///
/// OPT records are skipped: RFC 6891 §6.1.3 reuses their TTL field for the
/// extended RCODE, the EDNS version and the DO bit, so subtracting from it
/// would corrupt the flags.
///
/// A TTL with its top bit set is a wire-level oddity that RFC 2181 §8 says to
/// treat as zero. That is a caching decision, so it stays with the caller and
/// this function ages such a value like any other.
///
/// A full structural validation runs before any byte changes, so malformed
/// input leaves the buffer exactly as it arrived. Aging and validating in one
/// pass would age the records before a malformed one and then report an error.
///
/// One case still ends mid-way: an owner name may be a compression pointer into
/// an earlier record's TTL field, and aging that TTL can make the name
/// undecodable. The walk below therefore keeps checking each record instead of
/// trusting the validation above. On any error the buffer holds a partly aged
/// message that no longer parses, so a caller that gets an error must discard
/// the buffer rather than send it.
pub fn decrementTtls(bytes: []u8, elapsed_seconds: u32) ParseError!?u32 {
const p = try parse(bytes);
var pos: usize = types.header_len;
var q: u16 = 0;
while (q < p.header.qdcount) : (q += 1) {
const parsed = question.parse(bytes, pos) catch |err| return mapNameError(err);
pos = parsed.end;
}
var minimum: ?u32 = null;
const record_count = recordCount(p.header);
var r: u32 = 0;
while (r < record_count) : (r += 1) {
const parsed = record.parse(bytes, pos) catch |err| return mapNameError(err);
pos = parsed.end;
if (parsed.record.rtype == .opt) continue;
const aged = parsed.record.ttl -| elapsed_seconds;
const ttl_offset = parsed.record.rdata.offset - ttl_bytes_before_rdata;
std.mem.writeInt(u32, bytes[ttl_offset..][0..4], aged, .big);
minimum = if (minimum) |m| @min(m, aged) else aged;
}
return minimum;
}
/// Encodes a reply into a caller buffer. Mechanism only: which RCODE to set
/// and which answers to add is the caller's policy.
///
/// Sections must be filled in wire order, so every `addAnswer` call has to
/// precede `addOptEcho` — the OPT record belongs to the additional section, and
/// an answer written after it would land in the wrong section. `addOptEcho`
/// also runs at most once, because RFC 6891 §6.1.1 allows one OPT record per
/// message. Both rules are programmer errors, so both are assertions.
pub const ResponseBuilder = struct {
writer: Writer,
header: header.Header,
opt_added: bool,
pub const Error = Writer.Error || error{RdataTooLong};
/// Copies the request's ID, opcode and RD bit, marks the message a
/// response, and advertises recursion. The question is echoed when given,
/// as RFC 1035 §4.1.2 expects of a reply.
pub fn init(buf: []u8, request: header.Header, q: ?question.Question) Error!ResponseBuilder {
// A DNS message carries a two-byte length prefix over TCP
// (RFC 1035 §4.2.2), so 65535 bytes is the protocol maximum. Holding
// the buffer to it keeps the section counters from overflowing: the
// smallest possible record is 11 bytes.
std.debug.assert(buf.len <= std.math.maxInt(u16));
var self: ResponseBuilder = .{
.writer = Writer.fixed(buf),
.header = .{
.id = request.id,
.flags = .{
.rcode = .no_error,
.z = 0,
.ra = true,
.rd = request.flags.rd,
.tc = false,
.aa = false,
.opcode = request.flags.opcode,
.qr = true,
},
.qdcount = 0,
.ancount = 0,
.nscount = 0,
.arcount = 0,
},
.opt_added = false,
};
var placeholder: [types.header_len]u8 = undefined;
header.encode(self.header, &placeholder);
try self.writer.writeAll(&placeholder);
if (q) |echoed| {
try question.encode(echoed, &self.writer);
self.header.qdcount = 1;
}
return self;
}
pub fn setRcode(self: *ResponseBuilder, rcode: types.Rcode) void {
self.header.flags.rcode = rcode;
}
pub fn setAuthoritative(self: *ResponseBuilder, aa: bool) void {
self.header.flags.aa = aa;
}
/// `rdata` is written verbatim, so it must hold no compression pointers.
pub fn addAnswer(
self: *ResponseBuilder,
owner: name.Name,
rtype: types.Type,
class: types.Class,
ttl: u32,
rdata: []const u8,
) Error!void {
std.debug.assert(!self.opt_added);
const rec: record.Record = .{
.name = owner,
.rtype = rtype,
.class = @intFromEnum(class),
.ttl = ttl,
.rdata = .{ .offset = 0, .len = 0 },
};
try record.encode(rec, rdata, &self.writer);
self.header.ancount += 1;
}
/// Answers an EDNS query with an EDNS reply: the requestor's payload size
/// comes back unchanged and the DO bit passes through. No options are
/// echoed — nxdns implements none of them.
pub fn addOptEcho(self: *ResponseBuilder, request_opt: edns.OptRecord, do_bit: bool) Error!void {
std.debug.assert(!self.opt_added);
const opt: edns.OptRecord = .{
.udp_payload_size = request_opt.udp_payload_size,
.extended_rcode = 0,
.version = 0,
.do_bit = do_bit,
.options = .{ .offset = 0, .len = 0 },
};
edns.encodeOpt(opt, &.{}, &self.writer) catch |err| switch (err) {
error.OptionsTooLong => unreachable, // the option list is empty
error.WriteFailed => return error.WriteFailed,
};
self.header.arcount += 1;
self.opt_added = true;
}
/// Patches the counts into the reserved header bytes and returns the
/// finished message, a prefix of the caller's buffer.
pub fn finish(self: *ResponseBuilder) []u8 {
const bytes = self.writer.buffered();
header.encode(self.header, bytes[0..types.header_len]);
return bytes;
}
};
const testing = std.testing;
/// A query for example.com A with an EDNS(0) OPT record advertising 4096
/// bytes: id 0x1234, RD set, one question, one additional.
const query_bytes =
"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01" ++ // header
"\x07example\x03com\x00\x00\x01\x00\x01" ++ // question at 12, ends at 29
"\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00"; // OPT at 29, ends at 40
/// The matching response: a CNAME to www.example.com and its A record, both
/// with compressed owner names, plus the echoed OPT record.
/// 12 question, 29 CNAME (rdata at 41, ttl at 35),
/// 47 A (rdata at 59, ttl at 53), 63 OPT, 74 end.
const response_bytes =
"\x12\x34\x81\x80\x00\x01\x00\x02\x00\x00\x00\x01" ++ // header
"\x07example\x03com\x00\x00\x01\x00\x01" ++ // question at 12
"\xc0\x0c\x00\x05\x00\x01\x00\x00\x01\x2c\x00\x06\x03www\xc0\x0c" ++ // CNAME, ttl 300
"\xc0\x29\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04\x5d\xb8\xd8\x22" ++ // A, ttl 60
"\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00"; // OPT at 63
test "fixtures have the documented layout" {
try testing.expectEqual(@as(usize, 40), query_bytes.len);
try testing.expectEqual(@as(usize, 74), response_bytes.len);
}
test "parse a query" {
const p = try parse(query_bytes);
try testing.expectEqual(@as(u16, 0x1234), p.header.id);
try testing.expectEqual(false, p.header.flags.qr);
try testing.expectEqual(true, p.header.flags.rd);
try testing.expectEqual(@as(u16, 1), p.header.qdcount);
try testing.expectEqual(@as(u16, 1), p.header.arcount);
const q = firstQuestion(p).?;
try testing.expectEqualSlices(u8, (try name.fromText("example.com")).wire(), q.name.wire());
try testing.expectEqual(types.Type.a, q.qtype);
try testing.expectEqual(types.Class.in, q.qclass);
}
test "parse a response and walk every section" {
const p = try parse(response_bytes);
try testing.expectEqual(true, p.header.flags.qr);
try testing.expectEqual(@as(u16, 2), p.header.ancount);
var qit = questions(p);
const q = (try qit.next()).?;
try testing.expectEqual(types.Type.a, q.qtype);
try testing.expectEqual(@as(?question.Question, null), try qit.next());
var ait = answers(p);
const cname = (try ait.next()).?;
try testing.expectEqual(types.Type.cname, cname.rtype);
try testing.expectEqual(@as(u32, 300), cname.ttl);
try testing.expectEqualSlices(
u8,
(try name.fromText("example.com")).wire(),
cname.name.wire(),
);
try testing.expectEqualSlices(
u8,
(try name.fromText("www.example.com")).wire(),
(try record.rdataCname(response_bytes, cname)).wire(),
);
const a = (try ait.next()).?;
try testing.expectEqual(types.Type.a, a.rtype);
try testing.expectEqual(@as(u32, 60), a.ttl);
try testing.expectEqualSlices(
u8,
(try name.fromText("www.example.com")).wire(),
a.name.wire(),
);
try testing.expectEqual([4]u8{ 93, 184, 216, 34 }, try record.rdataA(response_bytes, a));
try testing.expectEqual(@as(?record.Record, null), try ait.next());
var nit = authorities(p);
try testing.expectEqual(@as(?record.Record, null), try nit.next());
var dit = additionals(p);
const opt_rec = (try dit.next()).?;
try testing.expectEqual(types.Type.opt, opt_rec.rtype);
try testing.expectEqual(@as(?record.Record, null), try dit.next());
}
test "iterators are independent and re-walk on demand" {
const p = try parse(response_bytes);
var first = answers(p);
var second = answers(p);
const a1 = (try first.next()).?;
const a2 = (try second.next()).?;
try testing.expectEqual(a1.rdata.offset, a2.rdata.offset);
_ = try first.next();
const a2_second = (try second.next()).?;
try testing.expectEqual(types.Type.a, a2_second.rtype);
}
test "authorities and additionals skip the sections before them" {
// One question, one answer, one authority, one additional, all with root
// owner names and 4-byte A rdata.
const rec = "\x00\x00\x01\x00\x01\x00\x00\x00\x0a\x00\x04";
const bytes = "\x00\x01\x81\x80\x00\x01\x00\x01\x00\x01\x00\x01" ++
"\x00\x00\x01\x00\x01" ++ // question: root A IN
rec ++ "\x01\x01\x01\x01" ++
rec ++ "\x02\x02\x02\x02" ++
rec ++ "\x03\x03\x03\x03";
const p = try parse(bytes);
var ait = answers(p);
try testing.expectEqual([4]u8{ 1, 1, 1, 1 }, try record.rdataA(bytes, (try ait.next()).?));
try testing.expectEqual(@as(?record.Record, null), try ait.next());
var nit = authorities(p);
try testing.expectEqual([4]u8{ 2, 2, 2, 2 }, try record.rdataA(bytes, (try nit.next()).?));
try testing.expectEqual(@as(?record.Record, null), try nit.next());
var dit = additionals(p);
try testing.expectEqual([4]u8{ 3, 3, 3, 3 }, try record.rdataA(bytes, (try dit.next()).?));
try testing.expectEqual(@as(?record.Record, null), try dit.next());
}
test "parse reports a short header as truncated" {
var i: usize = 0;
while (i < types.header_len) : (i += 1) {
try testing.expectError(error.Truncated, parse(query_bytes[0..i]));
}
// Exactly a header with no sections is well-formed.
const empty = try parse("\x00\x01\x81\x83" ++ "\x00" ** 8);
try testing.expectEqual(@as(u16, 0), empty.header.qdcount);
try testing.expectEqual(@as(?question.Question, null), firstQuestion(empty));
}
test "parse reports a truncated section as an overrun, not as truncation" {
// The header promises a question that is not there.
try testing.expectError(
error.SectionOverrun,
parse("\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00"),
);
// Every prefix that keeps the header but cuts a section.
var i: usize = types.header_len;
while (i < query_bytes.len) : (i += 1) {
try testing.expectError(error.SectionOverrun, parse(query_bytes[0..i]));
}
}
test "parse rejects a count larger than the records present" {
// ancount claims two answers, one follows.
const bytes = "\x00\x01\x81\x80\x00\x00\x00\x02\x00\x00\x00\x00" ++
"\x00\x00\x01\x00\x01\x00\x00\x00\x0a\x00\x04\x01\x02\x03\x04";
try testing.expectError(error.SectionOverrun, parse(bytes));
// qdcount claims two questions, one follows.
const two_questions = "\x00\x01\x01\x00\x00\x02\x00\x00\x00\x00\x00\x00" ++
"\x00\x00\x01\x00\x01";
try testing.expectError(error.SectionOverrun, parse(two_questions));
}
test "parse rejects an rdlength that overruns the packet" {
const bytes = "\x00\x01\x81\x80\x00\x00\x00\x01\x00\x00\x00\x00" ++
"\x00\x00\x01\x00\x01\x00\x00\x00\x0a\x00\x40\x01\x02\x03\x04";
try testing.expectError(error.SectionOverrun, parse(bytes));
}
test "parse rejects a bad name in the question section" {
// A pointer to itself: no chain can terminate.
const loop = "\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
"\xc0\x0c\x00\x01\x00\x01";
try testing.expectError(error.BadName, parse(loop));
// A forward pointer.
const forward = "\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
"\xc0\x12\x00\x01\x00\x01\x03com\x00";
try testing.expectError(error.BadName, parse(forward));
// A reserved label type, which is also what a 64-byte label looks like.
const reserved = "\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
"\x40abc\x00\x00\x01\x00\x01";
try testing.expectError(error.BadName, parse(reserved));
}
test "parse rejects a bad name in a record" {
const bytes = "\x00\x01\x81\x80\x00\x00\x00\x01\x00\x00\x00\x00" ++
"\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x0a\x00\x04\x01\x02\x03\x04";
try testing.expectError(error.BadName, parse(bytes));
}
test "parse rejects trailing bytes after the last record" {
try testing.expectError(error.TrailingBytes, parse(query_bytes ++ "\xff\xff\xff"));
// A single trailing byte counts too.
try testing.expectError(error.TrailingBytes, parse(query_bytes ++ "\x00"));
// A header-only message with a byte after it.
try testing.expectError(error.TrailingBytes, parse("\x00\x01\x81\x83" ++ "\x00" ** 9));
}
test "parse rejects a second OPT record" {
// Two OPT records in the additional section, payload sizes 512 and 1232.
const two_opts = "\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02" ++
"\x00\x00\x29\x02\x00\x00\x00\x00\x00\x00\x00" ++
"\x00\x00\x29\x04\xd0\x00\x00\x00\x00\x00\x00";
try testing.expectError(error.MultipleOptRecords, parse(two_opts));
// One OPT in the answer section and one in the additional section: still
// two OPT records in one message.
const split = "\x00\x01\x01\x00\x00\x00\x00\x01\x00\x00\x00\x01" ++
"\x00\x00\x29\x02\x00\x00\x00\x00\x00\x00\x00" ++
"\x00\x00\x29\x04\xd0\x00\x00\x00\x00\x00\x00";
try testing.expectError(error.MultipleOptRecords, parse(split));
// One OPT beside a non-OPT additional record stays acceptable.
const single = "\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02" ++
"\x00\x00\x01\x00\x01\x00\x00\x00\x0a\x00\x04\x01\x02\x03\x04" ++
"\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00";
try testing.expectEqual(@as(u16, 2), (try parse(single)).header.arcount);
}
test "findOptRecord finds the OPT record and reads it" {
const p = try parse(query_bytes);
const rec = findOptRecord(p).?;
const opt = try edns.parseOpt(query_bytes, rec);
try testing.expectEqual(@as(u16, 4096), opt.udp_payload_size);
try testing.expectEqual(false, opt.do_bit);
const no_opt = try parse("\x00\x01\x01\x00" ++ "\x00" ** 8);
try testing.expectEqual(@as(?record.Record, null), findOptRecord(no_opt));
}
test "findOptRecord skips a non-OPT additional record" {
const bytes = "\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02" ++
"\x00\x00\x01\x00\x01\x00\x00\x00\x0a\x00\x04\x01\x02\x03\x04" ++
"\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00";
const p = try parse(bytes);
const opt = try edns.parseOpt(bytes, findOptRecord(p).?);
try testing.expectEqual(@as(u16, 4096), opt.udp_payload_size);
}
test "setId overwrites the id and nothing else" {
var bytes: [query_bytes.len]u8 = query_bytes.*;
setId(&bytes, 0xbeef);
try testing.expectEqual(@as(u16, 0xbeef), (try parse(&bytes)).header.id);
try testing.expectEqualSlices(u8, query_bytes[2..], bytes[2..]);
}
test "decrementTtls ages every record and returns the minimum" {
var bytes: [response_bytes.len]u8 = response_bytes.*;
const minimum = (try decrementTtls(&bytes, 10)).?;
try testing.expectEqual(@as(u32, 50), minimum);
const p = try parse(&bytes);
var it = answers(p);
try testing.expectEqual(@as(u32, 290), (try it.next()).?.ttl);
try testing.expectEqual(@as(u32, 50), (try it.next()).?.ttl);
}
test "decrementTtls saturates at zero" {
var bytes: [response_bytes.len]u8 = response_bytes.*;
try testing.expectEqual(@as(?u32, 0), try decrementTtls(&bytes, 1_000_000));
const p = try parse(&bytes);
var it = answers(p);
try testing.expectEqual(@as(u32, 0), (try it.next()).?.ttl);
try testing.expectEqual(@as(u32, 0), (try it.next()).?.ttl);
}
test "decrementTtls leaves the OPT flags alone" {
// OPT with DO set and a 4096-byte payload size, so its TTL word is
// 0x0000_8000 — a plain subtraction would clear the DO bit.
const bytes_const = "\x12\x34\x81\x80\x00\x00\x00\x01\x00\x00\x00\x01" ++
"\x00\x00\x01\x00\x01\x00\x00\x00\x64\x00\x04\x01\x02\x03\x04" ++
"\x00\x00\x29\x10\x00\x00\x00\x80\x00\x00\x00";
var bytes: [bytes_const.len]u8 = bytes_const.*;
try testing.expectEqual(@as(?u32, 40), try decrementTtls(&bytes, 60));
const p = try parse(&bytes);
const opt = try edns.parseOpt(&bytes, findOptRecord(p).?);
try testing.expectEqual(true, opt.do_bit);
try testing.expectEqual(@as(u16, 4096), opt.udp_payload_size);
try testing.expectEqual(@as(u8, 0), opt.extended_rcode);
}
test "decrementTtls ages authority and additional records too" {
const rec = "\x00\x00\x01\x00\x01";
const bytes_const = "\x00\x01\x81\x80\x00\x00\x00\x01\x00\x01\x00\x01" ++
rec ++ "\x00\x00\x00\x64\x00\x04\x01\x01\x01\x01" ++ // ttl 100
rec ++ "\x00\x00\x00\x1e\x00\x04\x02\x02\x02\x02" ++ // ttl 30
rec ++ "\x00\x00\x00\x50\x00\x04\x03\x03\x03\x03"; // ttl 80
var bytes: [bytes_const.len]u8 = bytes_const.*;
try testing.expectEqual(@as(?u32, 10), try decrementTtls(&bytes, 20));
const p = try parse(&bytes);
var ait = answers(p);
try testing.expectEqual(@as(u32, 80), (try ait.next()).?.ttl);
var nit = authorities(p);
try testing.expectEqual(@as(u32, 10), (try nit.next()).?.ttl);
var dit = additionals(p);
try testing.expectEqual(@as(u32, 60), (try dit.next()).?.ttl);
}
test "decrementTtls reports no minimum when nothing carries a ttl" {
var query: [query_bytes.len]u8 = query_bytes.*;
// The only record is the OPT, which is skipped.
try testing.expectEqual(@as(?u32, null), try decrementTtls(&query, 5));
try testing.expectEqualSlices(u8, query_bytes, &query);
}
test "decrementTtls propagates structural errors" {
var short = [_]u8{0} ** 8;
try testing.expectError(error.Truncated, decrementTtls(&short, 1));
var overrun = "\x00\x01\x81\x80\x00\x01\x00\x00\x00\x00\x00\x00".*;
try testing.expectError(error.SectionOverrun, decrementTtls(&overrun, 1));
var bad_name = "\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\xc0\x0c\x00\x01\x00\x01".*;
try testing.expectError(error.BadName, decrementTtls(&bad_name, 1));
}
test "decrementTtls leaves the buffer untouched when a later record is malformed" {
// Two answers: a well-formed A record with TTL 100, then a record whose
// owner name is a forward pointer.
const bytes_const = "\x00\x01\x81\x80\x00\x00\x00\x02\x00\x00\x00\x00" ++
"\x00\x00\x01\x00\x01\x00\x00\x00\x64\x00\x04\x01\x02\x03\x04" ++
"\xc0\x40\x00\x01\x00\x01\x00\x00\x00\x64\x00\x00";
var bytes: [bytes_const.len]u8 = bytes_const.*;
try testing.expectError(error.BadName, decrementTtls(&bytes, 60));
try testing.expectEqualSlices(u8, bytes_const, &bytes);
}
test "decrementTtls reports the packet that aging itself breaks" {
// The first record's TTL word is 0x02_68_69_00, which also reads as the
// name "hi." at offset 17. The second record's owner name points there, so
// the packet parses — until aging rewrites those four bytes.
const bytes_const = "\x00\x01\x81\x80\x00\x00\x00\x02\x00\x00\x00\x00" ++
"\x00\x00\x01\x00\x01\x02\x68\x69\x00\x00\x04\x01\x02\x03\x04" ++
"\xc0\x11\x00\x01\x00\x01\x00\x00\x00\x64\x00\x04\x05\x06\x07\x08";
var bytes: [bytes_const.len]u8 = bytes_const.*;
const p = try parse(&bytes);
var it = answers(p);
_ = (try it.next()).?;
try testing.expectEqualSlices(u8, (try name.fromText("hi")).wire(), (try it.next()).?.name.wire());
// Aging the first TTL turns the second owner name into a label that runs
// off the end. The error is reported, not a panic, and the buffer is then
// a partly aged message the caller must discard.
try testing.expectError(error.SectionOverrun, decrementTtls(&bytes, 1000));
try testing.expectError(error.SectionOverrun, parse(&bytes));
}
test "ResponseBuilder builds a reply that re-parses" {
const request = try parse(query_bytes);
const q = firstQuestion(request).?;
const request_opt = try edns.parseOpt(query_bytes, findOptRecord(request).?);
var buf: [512]u8 = undefined;
var b = try ResponseBuilder.init(&buf, request.header, q);
try b.addAnswer(q.name, .a, .in, 60, "\x0a\x00\x00\x01");
try b.addOptEcho(request_opt, false);
const bytes = b.finish();
const p = try parse(bytes);
try testing.expectEqual(request.header.id, p.header.id);
try testing.expectEqual(true, p.header.flags.qr);
try testing.expectEqual(true, p.header.flags.rd);
try testing.expectEqual(true, p.header.flags.ra);
try testing.expectEqual(types.Rcode.no_error, p.header.flags.rcode);
try testing.expectEqual(@as(u16, 1), p.header.qdcount);
try testing.expectEqual(@as(u16, 1), p.header.ancount);
try testing.expectEqual(@as(u16, 0), p.header.nscount);
try testing.expectEqual(@as(u16, 1), p.header.arcount);
const echoed = firstQuestion(p).?;
try testing.expectEqualSlices(u8, q.name.wire(), echoed.name.wire());
try testing.expectEqual(q.qtype, echoed.qtype);
var it = answers(p);
const a = (try it.next()).?;
try testing.expectEqual(types.Type.a, a.rtype);
try testing.expectEqual(@as(u32, 60), a.ttl);
try testing.expectEqual([4]u8{ 10, 0, 0, 1 }, try record.rdataA(bytes, a));
const opt = try edns.parseOpt(bytes, findOptRecord(p).?);
try testing.expectEqual(@as(u16, 4096), opt.udp_payload_size);
try testing.expectEqual(false, opt.do_bit);
}
test "a built reply decrements its ttls" {
const request = try parse(query_bytes);
const q = firstQuestion(request).?;
var buf: [512]u8 = undefined;
var b = try ResponseBuilder.init(&buf, request.header, q);
try b.addAnswer(q.name, .a, .in, 90, "\x0a\x00\x00\x01");
try b.addAnswer(q.name, .a, .in, 30, "\x0a\x00\x00\x02");
const bytes = b.finish();
try testing.expectEqual(@as(?u32, 5), try decrementTtls(bytes, 25));
const p = try parse(bytes);
var it = answers(p);
try testing.expectEqual(@as(u32, 65), (try it.next()).?.ttl);
try testing.expectEqual(@as(u32, 5), (try it.next()).?.ttl);
}
test "ResponseBuilder passes the DO bit through" {
const request = try parse(query_bytes);
const request_opt = try edns.parseOpt(query_bytes, findOptRecord(request).?);
for ([_]bool{ false, true }) |do_bit| {
var buf: [128]u8 = undefined;
var b = try ResponseBuilder.init(&buf, request.header, firstQuestion(request).?);
try b.addOptEcho(request_opt, do_bit);
const bytes = b.finish();
const p = try parse(bytes);
const opt = try edns.parseOpt(bytes, findOptRecord(p).?);
try testing.expectEqual(do_bit, opt.do_bit);
}
}
test "ResponseBuilder sets an rcode and an empty answer section" {
const request = try parse(query_bytes);
var buf: [128]u8 = undefined;
var b = try ResponseBuilder.init(&buf, request.header, firstQuestion(request).?);
b.setRcode(.nx_domain);
b.setAuthoritative(true);
const bytes = b.finish();
const p = try parse(bytes);
try testing.expectEqual(types.Rcode.nx_domain, p.header.flags.rcode);
try testing.expectEqual(true, p.header.flags.aa);
try testing.expectEqual(@as(u16, 0), p.header.ancount);
try testing.expectEqual(@as(usize, types.header_len + 17), bytes.len);
}
test "ResponseBuilder works without a question" {
const request = try parse(query_bytes);
var buf: [64]u8 = undefined;
var b = try ResponseBuilder.init(&buf, request.header, null);
b.setRcode(.form_err);
const bytes = b.finish();
try testing.expectEqual(@as(usize, types.header_len), bytes.len);
const p = try parse(bytes);
try testing.expectEqual(@as(u16, 0), p.header.qdcount);
try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
}
test "ResponseBuilder keeps the opcode and a cleared RD bit" {
const request: header.Header = .{
.id = 0x4321,
.flags = .{
.rcode = .no_error,
.z = 0,
.ra = false,
.rd = false,
.tc = false,
.aa = false,
.opcode = .status,
.qr = false,
},
.qdcount = 0,
.ancount = 0,
.nscount = 0,
.arcount = 0,
};
var buf: [64]u8 = undefined;
var b = try ResponseBuilder.init(&buf, request, null);
const p = try parse(b.finish());
try testing.expectEqual(@as(u16, 0x4321), p.header.id);
try testing.expectEqual(types.Opcode.status, p.header.flags.opcode);
try testing.expectEqual(false, p.header.flags.rd);
try testing.expectEqual(@as(u3, 0), p.header.flags.z);
}
test "ResponseBuilder reports a buffer that is too small" {
const request = try parse(query_bytes);
const q = firstQuestion(request).?;
var tiny: [8]u8 = undefined;
try testing.expectError(error.WriteFailed, ResponseBuilder.init(&tiny, request.header, q));
var no_room_for_question: [16]u8 = undefined;
try testing.expectError(
error.WriteFailed,
ResponseBuilder.init(&no_room_for_question, request.header, q),
);
var no_room_for_answer: [32]u8 = undefined;
var b = try ResponseBuilder.init(&no_room_for_answer, request.header, q);
try testing.expectError(error.WriteFailed, b.addAnswer(q.name, .a, .in, 60, "\x01\x02\x03\x04"));
}
+138
View File
@@ -0,0 +1,138 @@
//! Question section entries (RFC 1035 §4.1.2). Pure: no allocation, no
//! `std.Io` beyond writing encoded bytes to a caller's writer.
const std = @import("std");
const types = @import("types.zig");
const name = @import("name.zig");
const Writer = std.Io.Writer;
pub const Question = struct {
name: name.Name,
qtype: types.Type,
qclass: types.Class,
};
pub const ParseError = name.ParseError;
pub const Parsed = struct {
question: Question,
/// Offset just past the question as it appears at `offset`; follows the
/// same convention as `name.Parsed.end` for a compressed name.
end: usize,
};
pub fn parse(packet: []const u8, offset: usize) ParseError!Parsed {
const parsed_name = try name.parse(packet, offset);
const fixed = parsed_name.end;
if (fixed + 4 > packet.len) return error.Truncated;
return .{
.question = .{
.name = parsed_name.name,
.qtype = @enumFromInt(std.mem.readInt(u16, packet[fixed..][0..2], .big)),
.qclass = @enumFromInt(std.mem.readInt(u16, packet[fixed + 2 ..][0..2], .big)),
},
.end = fixed + 4,
};
}
pub fn encode(q: Question, w: *Writer) Writer.Error!void {
try name.encode(q.name, w);
try w.writeInt(u16, @intFromEnum(q.qtype), .big);
try w.writeInt(u16, @intFromEnum(q.qclass), .big);
}
const testing = std.testing;
test "parse an uncompressed question" {
const packet = "\x07example\x03com\x00\x00\x01\x00\x01";
const r = try parse(packet, 0);
try testing.expectEqualSlices(
u8,
(try name.fromText("example.com")).wire(),
r.question.name.wire(),
);
try testing.expectEqual(types.Type.a, r.question.qtype);
try testing.expectEqual(types.Class.in, r.question.qclass);
try testing.expectEqual(@as(usize, 17), r.end);
}
test "parse a question whose name is compressed" {
// "com" at offset 0, then a question at offset 5 naming "example.com"
// through a pointer.
const packet = "\x03com\x00" ++ "\x07example\xc0\x00\x00\x1c\x00\x01";
const r = try parse(packet, 5);
try testing.expectEqualSlices(
u8,
(try name.fromText("example.com")).wire(),
r.question.name.wire(),
);
try testing.expectEqual(types.Type.aaaa, r.question.qtype);
try testing.expectEqual(types.Class.in, r.question.qclass);
try testing.expectEqual(@as(usize, 19), r.end);
}
test "parse keeps unknown type and class values" {
const packet = "\x00\x12\x34\x56\x78";
const r = try parse(packet, 0);
try testing.expect(r.question.name.isRoot());
try testing.expectEqual(@as(u16, 0x1234), @intFromEnum(r.question.qtype));
try testing.expectEqual(@as(u16, 0x5678), @intFromEnum(r.question.qclass));
try testing.expectEqual(@as(usize, 5), r.end);
}
test "parse rejects a truncated fixed field" {
const full = "\x07example\x03com\x00\x00\x01\x00\x01";
var i: usize = 13;
while (i < full.len) : (i += 1) {
try testing.expectError(error.Truncated, parse(full[0..i], 0));
}
}
test "parse propagates a name error" {
try testing.expectError(error.BadPointer, parse("\xc0\x00\x00\x01\x00\x01", 0));
try testing.expectError(error.Truncated, parse("\x07exa", 0));
try testing.expectError(error.LabelTooLong, parse("\x40abc\x00\x00\x01\x00\x01", 0));
}
test "encode round-trips" {
const original: Question = .{
.name = try name.fromText("www.example.com"),
.qtype = .aaaa,
.qclass = .in,
};
var buf: [512]u8 = undefined;
var w = Writer.fixed(&buf);
try encode(original, &w);
const bytes = w.buffered();
try testing.expectEqual(@as(usize, 21), bytes.len);
const r = try parse(bytes, 0);
try testing.expectEqualSlices(u8, original.name.wire(), r.question.name.wire());
try testing.expectEqual(original.qtype, r.question.qtype);
try testing.expectEqual(original.qclass, r.question.qclass);
try testing.expectEqual(bytes.len, r.end);
}
test "encode writes big-endian fixed fields" {
const q: Question = .{
.name = try name.fromText("."),
.qtype = @enumFromInt(0x1234),
.qclass = @enumFromInt(0x5678),
};
var buf: [16]u8 = undefined;
var w = Writer.fixed(&buf);
try encode(q, &w);
try testing.expectEqualSlices(u8, "\x00\x12\x34\x56\x78", w.buffered());
}
test "encode reports a short buffer" {
const q: Question = .{
.name = try name.fromText("example.com"),
.qtype = .a,
.qclass = .in,
};
var buf: [16]u8 = undefined;
var w = Writer.fixed(&buf);
try testing.expectError(error.WriteFailed, encode(q, &w));
}
+313
View File
@@ -0,0 +1,313 @@
//! Resource records (RFC 1035 §4.1.3). Pure: no allocation, no `std.Io`
//! beyond writing encoded bytes to a caller's writer.
const std = @import("std");
const types = @import("types.zig");
const name = @import("name.zig");
const Writer = std.Io.Writer;
/// Where the RDATA sits inside the packet that produced the record. The bytes
/// stay in the packet: RDATA of NS, CNAME, PTR, MX and SOA can hold names with
/// compression pointers that target any earlier offset in the message, so the
/// span alone does not decode. Every typed accessor therefore takes the whole
/// packet as well as the record.
pub const RdataSpan = struct {
offset: usize,
len: usize,
/// The raw bytes. `packet` must be the packet the record came from.
pub fn slice(self: RdataSpan, packet: []const u8) []const u8 {
return packet[self.offset..][0..self.len];
}
};
pub const Record = struct {
name: name.Name,
rtype: types.Type,
/// Left as a plain `u16`: OPT reuses this field as the requestor's UDP
/// payload size (RFC 6891 §6.1.2), so it is not always a class.
class: u16,
/// Left as a plain `u32` for the same reason: OPT reuses it as flags.
ttl: u32,
rdata: RdataSpan,
};
pub const ParseError = name.ParseError;
pub const Parsed = struct {
record: Record,
/// Offset just past the record. The name follows the `name.Parsed.end`
/// convention, so a compressed owner name costs two bytes here.
end: usize,
};
/// Fixed fields between the owner name and the RDATA: TYPE, CLASS, TTL,
/// RDLENGTH.
const fixed_len = 2 + 2 + 4 + 2;
pub fn parse(packet: []const u8, offset: usize) ParseError!Parsed {
const parsed_name = try name.parse(packet, offset);
const fixed = parsed_name.end;
if (fixed + fixed_len > packet.len) return error.Truncated;
const rdlength: usize = std.mem.readInt(u16, packet[fixed + 8 ..][0..2], .big);
const rdata_offset = fixed + fixed_len;
if (rdata_offset + rdlength > packet.len) return error.Truncated;
return .{
.record = .{
.name = parsed_name.name,
.rtype = @enumFromInt(std.mem.readInt(u16, packet[fixed..][0..2], .big)),
.class = std.mem.readInt(u16, packet[fixed + 2 ..][0..2], .big),
.ttl = std.mem.readInt(u32, packet[fixed + 4 ..][0..4], .big),
.rdata = .{ .offset = rdata_offset, .len = rdlength },
},
.end = rdata_offset + rdlength,
};
}
/// Writes the record with its owner name uncompressed. `rdata_bytes` is copied
/// verbatim, so it must not contain compression pointers: this path serves
/// synthesized records, whose RDATA nxdns builds itself.
pub fn encode(rec: Record, rdata_bytes: []const u8, w: *Writer) (Writer.Error || error{RdataTooLong})!void {
if (rdata_bytes.len > std.math.maxInt(u16)) return error.RdataTooLong;
try name.encode(rec.name, w);
try w.writeInt(u16, @intFromEnum(rec.rtype), .big);
try w.writeInt(u16, rec.class, .big);
try w.writeInt(u32, rec.ttl, .big);
try w.writeInt(u16, @intCast(rdata_bytes.len), .big);
try w.writeAll(rdata_bytes);
}
pub const RdataError = name.ParseError || error{
WrongType,
BadRdata,
};
pub fn rdataA(packet: []const u8, rec: Record) RdataError![4]u8 {
if (rec.rtype != .a) return error.WrongType;
if (rec.rdata.len != 4) return error.BadRdata;
return rec.rdata.slice(packet)[0..4].*;
}
pub fn rdataAaaa(packet: []const u8, rec: Record) RdataError![16]u8 {
if (rec.rtype != .aaaa) return error.WrongType;
if (rec.rdata.len != 16) return error.BadRdata;
return rec.rdata.slice(packet)[0..16].*;
}
/// Decodes the single name in CNAME, NS or PTR RDATA, following compression
/// pointers into the rest of the packet.
pub fn rdataCname(packet: []const u8, rec: Record) RdataError!name.Name {
switch (rec.rtype) {
.cname, .ns, .ptr => {},
else => return error.WrongType,
}
const parsed = try name.parse(packet, rec.rdata.offset);
if (parsed.end != rec.rdata.offset + rec.rdata.len) return error.BadRdata;
return parsed.name;
}
/// The MINIMUM field of SOA RDATA (RFC 1035 §3.3.13), which RFC 2308 §4 makes
/// the ceiling for negative caching. MNAME and RNAME precede the five fixed
/// 32-bit fields and may both be compressed, so they have to be walked.
pub fn rdataSoaMinimumTtl(packet: []const u8, rec: Record) RdataError!u32 {
if (rec.rtype != .soa) return error.WrongType;
const rdata_end = rec.rdata.offset + rec.rdata.len;
const mname = try name.parse(packet, rec.rdata.offset);
if (mname.end > rdata_end) return error.BadRdata;
const rname = try name.parse(packet, mname.end);
if (rname.end > rdata_end) return error.BadRdata;
// SERIAL, REFRESH, RETRY, EXPIRE, MINIMUM.
if (rname.end + 20 != rdata_end) return error.BadRdata;
return std.mem.readInt(u32, packet[rname.end + 16 ..][0..4], .big);
}
const testing = std.testing;
/// An answer for "example.com A 1.2.3.4" preceded by a 12-byte header stand-in
/// and the question name it compresses against.
const a_packet =
"\x00" ** 12 ++ // header stand-in
"\x07example\x03com\x00\x00\x01\x00\x01" ++ // question, offset 12
"\xc0\x0c\x00\x01\x00\x01\x00\x00\x0e\x10\x00\x04\x01\x02\x03\x04";
test "parse an A record with a compressed owner name" {
const r = try parse(a_packet, 29);
try testing.expectEqualSlices(
u8,
(try name.fromText("example.com")).wire(),
r.record.name.wire(),
);
try testing.expectEqual(types.Type.a, r.record.rtype);
try testing.expectEqual(@as(u16, 1), r.record.class);
try testing.expectEqual(@as(u32, 3600), r.record.ttl);
try testing.expectEqual(@as(usize, 4), r.record.rdata.len);
try testing.expectEqual(@as(usize, a_packet.len), r.end);
try testing.expectEqualSlices(u8, "\x01\x02\x03\x04", r.record.rdata.slice(a_packet));
try testing.expectEqual([4]u8{ 1, 2, 3, 4 }, try rdataA(a_packet, r.record));
}
test "parse an AAAA record" {
const packet = "\x00\x00\x1c\x00\x01\x00\x00\x00\x3c\x00\x10" ++
"\x20\x01\x0d\xb8" ++ "\x00" ** 11 ++ "\x01";
const r = try parse(packet, 0);
try testing.expectEqual(types.Type.aaaa, r.record.rtype);
try testing.expectEqual(@as(u32, 60), r.record.ttl);
const addr = try rdataAaaa(packet, r.record);
try testing.expectEqual(@as(u8, 0x20), addr[0]);
try testing.expectEqual(@as(u8, 0x01), addr[15]);
}
test "parse a CNAME whose target is compressed" {
// "example.com" at offset 0; the CNAME RDATA is "www" plus a pointer to it.
const packet = "\x07example\x03com\x00" ++
"\x03www\xc0\x00\x00\x05\x00\x01\x00\x00\x01\x2c\x00\x02\xc0\x00";
const r = try parse(packet, 13);
try testing.expectEqual(types.Type.cname, r.record.rtype);
try testing.expectEqual(@as(usize, 2), r.record.rdata.len);
const target = try rdataCname(packet, r.record);
try testing.expectEqualSlices(u8, (try name.fromText("example.com")).wire(), target.wire());
try testing.expectEqual(@as(usize, packet.len), r.end);
}
test "rdataCname accepts NS and PTR, rejects other types" {
const packet = "\x03com\x00" ++ "\x00\x00\x02\x00\x01\x00\x00\x00\x0a\x00\x02\xc0\x00";
const r = try parse(packet, 5);
try testing.expectEqual(types.Type.ns, r.record.rtype);
try testing.expectEqualSlices(
u8,
(try name.fromText("com")).wire(),
(try rdataCname(packet, r.record)).wire(),
);
var wrong = r.record;
wrong.rtype = .mx;
try testing.expectError(error.WrongType, rdataCname(packet, wrong));
}
test "rdataCname rejects a name that does not fill the rdata" {
// RDLENGTH claims 4 bytes but the name uses 2.
const packet = "\x03com\x00" ++ "\x00\x00\x05\x00\x01\x00\x00\x00\x0a\x00\x04\xc0\x00\x00\x00";
const r = try parse(packet, 5);
try testing.expectError(error.BadRdata, rdataCname(packet, r.record));
}
test "rdataA and rdataAaaa reject the wrong type and the wrong length" {
const short = "\x00\x00\x01\x00\x01\x00\x00\x00\x0a\x00\x03\x01\x02\x03";
const r = try parse(short, 0);
try testing.expectError(error.BadRdata, rdataA(short, r.record));
try testing.expectError(error.WrongType, rdataAaaa(short, r.record));
}
test "SOA minimum ttl extraction with compressed MNAME and RNAME" {
// Offset 0 holds "example.com"; the owner name and both RDATA names are
// pointers to it.
const rdata = "\x03ns1\xc0\x00" ++ // MNAME: ns1.example.com, 6 bytes
"\x0ahostmaster\xc0\x00" ++ // RNAME: hostmaster.example.com, 13 bytes
"\x00\x00\x00\x01" ++ // serial
"\x00\x00\x1c\x20" ++ // refresh
"\x00\x00\x0e\x10" ++ // retry
"\x00\x36\xee\x80" ++ // expire
"\x00\x00\x02\x58"; // minimum = 600
const soa = "\x07example\x03com\x00" ++
"\xc0\x00\x00\x06\x00\x01\x00\x00\x0e\x10" ++
"\x00\x27" ++ // rdlength = 6 + 13 + 20 = 39
rdata;
try testing.expectEqual(@as(usize, 39), rdata.len);
const r = try parse(soa, 13);
try testing.expectEqual(types.Type.soa, r.record.rtype);
try testing.expectEqual(@as(u32, 600), try rdataSoaMinimumTtl(soa, r.record));
try testing.expectEqual(@as(usize, soa.len), r.end);
}
test "SOA minimum ttl rejects a short or long rdata" {
const rdata = "\x00\x00" ++ // root MNAME and RNAME
"\x00\x00\x00\x01\x00\x00\x1c\x20\x00\x00\x0e\x10\x00\x36\xee\x80\x00\x00\x02\x58";
const head = "\x00\x00\x06\x00\x01\x00\x00\x0e\x10";
const good = head ++ "\x00\x16" ++ rdata;
const r = try parse(good, 0);
try testing.expectEqual(@as(u32, 600), try rdataSoaMinimumTtl(good, r.record));
// One fixed field short.
const short = head ++ "\x00\x12" ++ rdata[0..18];
const rs = try parse(short, 0);
try testing.expectError(error.BadRdata, rdataSoaMinimumTtl(short, rs.record));
// One byte of slack past MINIMUM.
const long = head ++ "\x00\x17" ++ rdata ++ "\x00";
const rl = try parse(long, 0);
try testing.expectError(error.BadRdata, rdataSoaMinimumTtl(long, rl.record));
var wrong = r.record;
wrong.rtype = .a;
try testing.expectError(error.WrongType, rdataSoaMinimumTtl(good, wrong));
}
test "parse rejects an rdlength that overruns the packet" {
const packet = "\x00\x00\x01\x00\x01\x00\x00\x0e\x10\x00\x08\x01\x02\x03\x04";
try testing.expectError(error.Truncated, parse(packet, 0));
}
test "parse rejects truncated fixed fields" {
const full = "\x00\x00\x01\x00\x01\x00\x00\x0e\x10\x00\x04\x01\x02\x03\x04";
var i: usize = 1;
while (i < full.len) : (i += 1) {
try testing.expectError(error.Truncated, parse(full[0..i], 0));
}
_ = try parse(full, 0);
}
test "parse propagates a name error" {
try testing.expectError(error.BadPointer, parse("\xc0\x00" ++ "\x00" ** 11, 0));
try testing.expectError(error.LabelTooLong, parse("\x40ab" ++ "\x00" ** 12, 0));
}
test "parse accepts an empty rdata" {
const packet = "\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00";
const r = try parse(packet, 0);
try testing.expectEqual(types.Type.opt, r.record.rtype);
try testing.expectEqual(@as(usize, 0), r.record.rdata.len);
try testing.expectEqual(@as(usize, packet.len), r.end);
try testing.expectEqualSlices(u8, "", r.record.rdata.slice(packet));
}
test "encode round-trips through parse" {
const original: Record = .{
.name = try name.fromText("www.example.com"),
.rtype = .a,
.class = 1,
.ttl = 0x0000_0e10,
.rdata = .{ .offset = 0, .len = 0 },
};
var buf: [512]u8 = undefined;
var w = Writer.fixed(&buf);
try encode(original, "\x0a\x00\x00\x01", &w);
const bytes = w.buffered();
const r = try parse(bytes, 0);
try testing.expectEqualSlices(u8, original.name.wire(), r.record.name.wire());
try testing.expectEqual(original.rtype, r.record.rtype);
try testing.expectEqual(original.class, r.record.class);
try testing.expectEqual(original.ttl, r.record.ttl);
try testing.expectEqual(@as(usize, bytes.len), r.end);
try testing.expectEqual([4]u8{ 10, 0, 0, 1 }, try rdataA(bytes, r.record));
}
test "encode reports a short buffer" {
const rec: Record = .{
.name = try name.fromText("example.com"),
.rtype = .a,
.class = 1,
.ttl = 60,
.rdata = .{ .offset = 0, .len = 0 },
};
var buf: [16]u8 = undefined;
var w = Writer.fixed(&buf);
try testing.expectError(error.WriteFailed, encode(rec, "\x01\x02\x03\x04", &w));
}
+121
View File
@@ -0,0 +1,121 @@
//! DNS wire-format enumerations and protocol limits (RFC 1035 §3.2, §4.1.1).
//! Pure: no allocation, no `std.Io`.
const std = @import("std");
/// RR TYPE / QTYPE. Non-exhaustive: unknown types pass through opaquely.
pub const Type = enum(u16) {
a = 1,
ns = 2,
cname = 5,
soa = 6,
ptr = 12,
mx = 15,
txt = 16,
aaaa = 28,
srv = 33,
opt = 41,
svcb = 64,
https = 65,
any = 255,
_,
};
/// RR CLASS / QCLASS. Non-exhaustive.
pub const Class = enum(u16) {
in = 1,
ch = 3,
hs = 4,
any = 255,
_,
};
/// The 4-bit header RCODE. EDNS(0) extends this to 12 bits; the upper 8 bits
/// live in the OPT record and are handled in edns.zig.
pub const Rcode = enum(u4) {
no_error = 0,
form_err = 1,
serv_fail = 2,
nx_domain = 3,
not_imp = 4,
refused = 5,
yx_domain = 6,
yx_rr_set = 7,
nx_rr_set = 8,
not_auth = 9,
not_zone = 10,
_,
};
/// The 4-bit header OPCODE.
pub const Opcode = enum(u4) {
query = 0,
iquery = 1,
status = 2,
notify = 4,
update = 5,
_,
};
/// Maximum length of a name in uncompressed wire form, terminating zero included.
pub const max_name_len = 255;
/// Maximum length of a single label's data, length byte excluded.
pub const max_label_len = 63;
/// Compression pointers must always target a strictly lower offset, so a chain
/// terminates on its own. This cap bounds the work a single name can cost.
pub const max_compression_jumps = 32;
/// Pre-EDNS UDP payload limit (RFC 1035 §4.2.1).
pub const max_udp_payload = 512;
/// Fixed size of the DNS message header.
pub const header_len = 12;
test "type values match the wire numbers" {
try std.testing.expectEqual(@as(u16, 1), @intFromEnum(Type.a));
try std.testing.expectEqual(@as(u16, 28), @intFromEnum(Type.aaaa));
try std.testing.expectEqual(@as(u16, 41), @intFromEnum(Type.opt));
try std.testing.expectEqual(@as(u16, 64), @intFromEnum(Type.svcb));
try std.testing.expectEqual(@as(u16, 65), @intFromEnum(Type.https));
try std.testing.expectEqual(@as(u16, 255), @intFromEnum(Type.any));
}
test "class values match the wire numbers" {
try std.testing.expectEqual(@as(u16, 1), @intFromEnum(Class.in));
try std.testing.expectEqual(@as(u16, 3), @intFromEnum(Class.ch));
try std.testing.expectEqual(@as(u16, 4), @intFromEnum(Class.hs));
try std.testing.expectEqual(@as(u16, 255), @intFromEnum(Class.any));
}
test "rcode and opcode values match the wire numbers" {
try std.testing.expectEqual(@as(u4, 0), @intFromEnum(Rcode.no_error));
try std.testing.expectEqual(@as(u4, 3), @intFromEnum(Rcode.nx_domain));
try std.testing.expectEqual(@as(u4, 5), @intFromEnum(Rcode.refused));
try std.testing.expectEqual(@as(u4, 0), @intFromEnum(Opcode.query));
try std.testing.expectEqual(@as(u4, 4), @intFromEnum(Opcode.notify));
try std.testing.expectEqual(@as(u4, 5), @intFromEnum(Opcode.update));
}
test "unknown enum values round-trip" {
const unknown_type: Type = @enumFromInt(9999);
try std.testing.expectEqual(@as(u16, 9999), @intFromEnum(unknown_type));
const unknown_class: Class = @enumFromInt(1234);
try std.testing.expectEqual(@as(u16, 1234), @intFromEnum(unknown_class));
const unknown_rcode: Rcode = @enumFromInt(15);
try std.testing.expectEqual(@as(u4, 15), @intFromEnum(unknown_rcode));
const unknown_opcode: Opcode = @enumFromInt(3);
try std.testing.expectEqual(@as(u4, 3), @intFromEnum(unknown_opcode));
}
test "limits" {
try std.testing.expectEqual(255, max_name_len);
try std.testing.expectEqual(63, max_label_len);
try std.testing.expectEqual(32, max_compression_jumps);
try std.testing.expectEqual(512, max_udp_payload);
try std.testing.expectEqual(12, header_len);
}
+7
View File
@@ -3,6 +3,13 @@ const std = @import("std");
comptime {
_ = @import("main.zig");
_ = @import("version.zig");
_ = @import("dns/types.zig");
_ = @import("dns/header.zig");
_ = @import("dns/name.zig");
_ = @import("dns/question.zig");
_ = @import("dns/record.zig");
_ = @import("dns/edns.zig");
_ = @import("dns/packet.zig");
_ = @import("platform/address.zig");
_ = @import("platform/tls_client.zig");
_ = @import("platform/tls_client_integration_test.zig");