dns core: wire-format parse/encode, iterators, response builder
This commit is contained in:
@@ -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");
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
@@ -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"));
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -3,6 +3,13 @@ const std = @import("std");
|
|||||||
comptime {
|
comptime {
|
||||||
_ = @import("main.zig");
|
_ = @import("main.zig");
|
||||||
_ = @import("version.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/address.zig");
|
||||||
_ = @import("platform/tls_client.zig");
|
_ = @import("platform/tls_client.zig");
|
||||||
_ = @import("platform/tls_client_integration_test.zig");
|
_ = @import("platform/tls_client_integration_test.zig");
|
||||||
|
|||||||
Reference in New Issue
Block a user