resolver transport: udp/tcp servers, doh/dot clients, pool failover with health

This commit is contained in:
2026-08-01 12:36:50 +02:00
parent 346f2dc502
commit 17d0401f8a
15 changed files with 6062 additions and 0 deletions
+825
View File
@@ -0,0 +1,825 @@
//! The serving handler: raw bytes from a listener in, bytes to send back out.
//! It owns no socket, no clock and no allocator — the listener supplies the
//! buffers and the upstream supplies the answer.
//!
//! `handle` returns no error union. Every failure is either a DNS response the
//! client can act on or a counted drop, because a listener has nothing useful
//! to do with an error value: it cannot retry and it must not die. That is the
//! "every failure mode is visible" rule applied to the hot path — the counters
//! are the failure surface.
//!
//! Phase 7 extends this file with filtering, cache, local records and query
//! logging. None of that is here: this handler forwards every query.
const std = @import("std");
const edns = @import("../dns/edns.zig");
const header = @import("../dns/header.zig");
const packet = @import("../dns/packet.zig");
const question = @import("../dns/question.zig");
const types = @import("../dns/types.zig");
const transport = @import("../upstream/transport.zig");
/// Which listener a query arrived on. Only the UDP size limit depends on it.
pub const Transport = enum { udp, tcp };
/// RFC 1035 §4.2.1: 512 bytes is what a client accepts over UDP without EDNS,
/// and RFC 6891 §6.2.3 says a smaller advertised size must not be honoured
/// below that floor.
pub const udp_limit_min: u16 = types.max_udp_payload;
/// The ceiling nxdns advertises and accepts. Above this, fragmentation and
/// reflection amplification cost more than a TCP retry.
pub const udp_limit_max: u16 = 4096;
/// A synthesized reply is a header, at most one question and at most one OPT
/// record: 12 + (255 + 4) + 11 = 282 bytes worst case. Every buffer this file
/// builds into is at least `udp_limit_min`, so the builder cannot run out of
/// room and its `Writer.Error` is unreachable.
const max_synthetic_len = types.header_len + types.max_name_len + 4 + 11;
comptime {
std.debug.assert(max_synthetic_len <= udp_limit_min);
}
pub const Handler = struct {
/// In production this is `pool.client()`.
upstream: transport.Client,
stats: Stats = .{},
pub const Stats = struct {
queries: std.atomic.Value(u64) = .init(0),
dropped_malformed: std.atomic.Value(u64) = .init(0),
formerr: std.atomic.Value(u64) = .init(0),
notimp: std.atomic.Value(u64) = .init(0),
servfail: std.atomic.Value(u64) = .init(0),
truncated: std.atomic.Value(u64) = .init(0),
};
pub const Outcome = union(enum) {
/// A prefix of `response_buf`.
reply: []u8,
/// No response is possible or appropriate.
drop,
};
/// `response_buf.len` must be >= 512 and <= 65535.
pub fn handle(
self: *Handler,
io: std.Io,
which: Transport,
query: []const u8,
response_buf: []u8,
) Outcome {
std.debug.assert(response_buf.len >= udp_limit_min);
std.debug.assert(response_buf.len <= transport.max_message_len);
// Fewer than 12 bytes: there is no ID and no question, so no reply can
// be addressed to this query (PLAN §6.1).
const hdr = header.parse(query) catch {
bump(&self.stats.dropped_malformed);
return .drop;
};
// A response on a listener port is either a misdirected reply or a
// reflection attempt. Answering it would make this server the amplifier.
if (hdr.flags.qr) {
bump(&self.stats.dropped_malformed);
return .drop;
}
const p = packet.parse(query) catch |err| switch (err) {
error.Truncated => {
bump(&self.stats.dropped_malformed);
return .drop;
},
// The header is intact but a section is not. The question is what
// failed to parse, so nothing is echoed and no OPT is trusted.
else => return synthesize(hdr, null, null, .form_err, &self.stats.formerr, response_buf),
};
// RFC 6891 §6.1.1 puts the OPT record in the additional section, and §7
// answers a violation with FORMERR. `packet.parse` already rejects a
// second OPT wherever it sits; the case it accepts and this walk does
// not is a lone OPT in the answer or authority section, which
// `findOptRecord` cannot see.
if (containsOpt(packet.answers(p)) or containsOpt(packet.authorities(p))) {
const echo = if (hdr.qdcount == 1) packet.firstQuestion(p) else null;
return synthesize(hdr, echo, null, .form_err, &self.stats.formerr, response_buf);
}
// RFC 6891 §6.1.1 also gives the OPT record a root owner name. An OPT
// that is present but unusable must not be forwarded as if the query
// carried none: the reply's OPT echo would then disappear with nothing
// to say it ever existed.
const opt: ?edns.OptRecord = if (packet.findOptRecord(p)) |rec|
edns.parseOpt(query, rec) catch {
const echo = if (hdr.qdcount == 1) packet.firstQuestion(p) else null;
return synthesize(hdr, echo, null, .form_err, &self.stats.formerr, response_buf);
}
else
null;
if (hdr.flags.opcode != .query) {
return synthesize(hdr, null, opt, .not_imp, &self.stats.notimp, response_buf);
}
// RFC 9619: exactly one question, in both directions. Any other count
// has no valid interpretation, so there is no question to echo either.
if (hdr.qdcount != 1) {
return synthesize(hdr, null, opt, .form_err, &self.stats.formerr, response_buf);
}
// `parse` already walked the question section, so a packet with
// qdcount 1 always has a first question. The fallback keeps a
// hand-assembled `Packet` from turning into a crash.
const q = packet.firstQuestion(p) orelse
return synthesize(hdr, null, opt, .form_err, &self.stats.formerr, response_buf);
const reply = self.upstream.exchange(io, query, response_buf) catch |err| switch (transport.group(err)) {
// The process is shutting down. There is nothing to say, and the
// client is about to lose the socket anyway; the listener's own
// counters record the abandoned datagram.
.cancellation => return .drop,
.peer_fault, .local_resource => return synthesize(
hdr,
q,
opt,
.serv_fail,
&self.stats.servfail,
response_buf,
),
};
bump(&self.stats.queries);
if (which == .udp and reply.len > udpLimit(p)) {
// The upstream answer occupies `response_buf`, so the replacement
// is built beside it and copied over.
bump(&self.stats.truncated);
var scratch: [udp_limit_min]u8 = undefined;
const message = build(hdr, q, opt, .no_error, true, &scratch);
@memcpy(response_buf[0..message.len], message);
return .{ .reply = response_buf[0..message.len] };
}
return .{ .reply = reply };
}
};
/// Counts one synthesized reply and encodes it. The counter is passed in
/// because the counter *is* the record that this failure mode happened.
fn synthesize(
hdr: header.Header,
q: ?question.Question,
opt: ?edns.OptRecord,
rcode: types.Rcode,
counter: *std.atomic.Value(u64),
response_buf: []u8,
) Handler.Outcome {
bump(counter);
return .{ .reply = build(hdr, q, opt, rcode, false, response_buf) };
}
/// Whether the section holds a record of TYPE=OPT. A section that will not walk
/// counts as holding one: the caller's only answer to either condition is
/// FORMERR, and a `Packet` from `packet.parse` walks in full, so the fallback is
/// reachable only for a `Packet` assembled by hand around unvalidated bytes.
fn containsOpt(section: packet.RecordIterator) bool {
var it = section;
while (it.next() catch return true) |rec| {
if (rec.rtype == .opt) return true;
}
return false;
}
/// The query's advertised UDP payload size, clamped to `[512, 4096]`, or 512
/// when the query carries no usable OPT record.
pub fn udpLimit(query_packet: packet.Packet) u16 {
const rec = packet.findOptRecord(query_packet) orelse return udp_limit_min;
const opt = edns.parseOpt(query_packet.bytes, rec) catch return udp_limit_min;
return std.math.clamp(opt.udp_payload_size, udp_limit_min, udp_limit_max);
}
/// Encodes one synthesized reply. `ResponseBuilder.init` copies the request's
/// ID, opcode and RD bit and sets QR and RA, so every reply built here binds to
/// its request. `buf` is at least `udp_limit_min`, which `max_synthetic_len`
/// proves is enough, so the writer cannot fail.
fn build(
hdr: header.Header,
q: ?question.Question,
opt: ?edns.OptRecord,
rcode: types.Rcode,
tc: bool,
buf: []u8,
) []u8 {
var b = packet.ResponseBuilder.init(buf, hdr, q) catch unreachable;
b.setRcode(rcode);
b.header.flags.tc = tc;
// An EDNS query gets an EDNS reply, and the DO bit passes through
// untouched — nxdns validates no signatures, so it must not claim the
// client asked for none (PLAN §6.1).
if (opt) |o| b.addOptEcho(o, o.do_bit) catch unreachable;
return b.finish();
}
fn bump(counter: *std.atomic.Value(u64)) void {
_ = counter.fetchAdd(1, .monotonic);
}
const testing = std.testing;
/// A query for example.com A: id 0x1234, RD set, one question, no OPT.
const query_bytes =
"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
"\x07example\x03com\x00\x00\x01\x00\x01";
/// The matching response: the question echoed plus one A record.
const response_bytes =
"\x12\x34\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00" ++
"\x07example\x03com\x00\x00\x01\x00\x01" ++
"\xc0\x0c\x00\x01\x00\x01\x00\x00\x01\x2c\x00\x04\x5d\xb8\xd8\x22";
const opt_len = 11;
const query_with_opt_len = query_bytes.len + opt_len;
/// `query_bytes` plus an OPT record advertising `payload_size`.
fn queryWithOpt(buf: *[query_with_opt_len]u8, payload_size: u16, do_bit: bool) []const u8 {
@memcpy(buf[0..query_bytes.len], query_bytes);
std.mem.writeInt(u16, buf[10..12], 1, .big); // arcount
const opt = buf[query_bytes.len..][0..opt_len];
@memset(opt, 0);
opt[2] = @intFromEnum(types.Type.opt);
std.mem.writeInt(u16, opt[3..5], payload_size, .big);
if (do_bit) opt[7] = 0x80; // the DO bit is bit 15 of the TTL word
return buf;
}
/// `query_bytes` plus an OPT record whose owner name is `com.` instead of the
/// root, which RFC 6891 §6.1.1 forbids.
const query_with_named_opt =
"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01" ++
"\x07example\x03com\x00\x00\x01\x00\x01" ++
"\x03com\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00";
/// `query_bytes` plus an OPT record whose RDATA ends in a three-byte option
/// header. The record itself fits the packet; only the option list is broken.
const query_with_bad_option =
"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01" ++
"\x07example\x03com\x00\x00\x01\x00\x01" ++
"\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x03\x00\x08\x00";
/// A bare OPT record: root owner, TYPE 41, 4096-byte payload, empty RDATA.
const opt_record = "\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00";
/// `query_bytes` with the OPT record counted into the answer section, which
/// RFC 6891 §6.1.1 forbids.
const query_with_opt_in_answer =
"\x12\x34\x01\x00\x00\x01\x00\x01\x00\x00\x00\x00" ++
"\x07example\x03com\x00\x00\x01\x00\x01" ++
opt_record;
/// The same OPT record, counted into the authority section instead.
const query_with_opt_in_authority =
"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x01\x00\x00" ++
"\x07example\x03com\x00\x00\x01\x00\x01" ++
opt_record;
/// Two OPT records in the additional section. RFC 6891 §6.1.1 allows one.
const query_with_two_opts =
"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x02" ++
"\x07example\x03com\x00\x00\x01\x00\x01" ++
opt_record ++ opt_record;
const FakeUpstream = struct {
reply: []const u8 = &.{},
err: ?transport.ExchangeError = null,
calls: usize = 0,
fn exchangeFn(
ptr: *anyopaque,
io: std.Io,
query: []const u8,
response_buf: []u8,
) transport.ExchangeError![]u8 {
_ = io;
const self: *FakeUpstream = @ptrCast(@alignCast(ptr));
self.calls += 1;
if (self.err) |e| return e;
if (self.reply.len > response_buf.len) return error.ResponseTooLarge;
@memcpy(response_buf[0..self.reply.len], self.reply);
const bytes = response_buf[0..self.reply.len];
if (bytes.len >= types.header_len) {
packet.setId(bytes, (header.parse(query) catch unreachable).id);
}
return bytes;
}
fn client(self: *FakeUpstream) transport.Client {
return .{ .ptr = self, .exchangeFn = exchangeFn };
}
};
fn expectReply(outcome: Handler.Outcome) ![]u8 {
return switch (outcome) {
.reply => |bytes| bytes,
.drop => error.TestUnexpectedDrop,
};
}
test "a udp query is forwarded and the upstream reply is returned unchanged" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, query_bytes, &buf));
try testing.expectEqualSlices(u8, response_bytes, reply);
try testing.expectEqual(@as(usize, 1), fake.calls);
try testing.expectEqual(@as(u64, 1), h.stats.queries.load(.monotonic));
try testing.expectEqual(@as(u64, 0), h.stats.truncated.load(.monotonic));
// A query that carries no OPT at all is not an EDNS violation.
try testing.expectEqual(@as(u64, 0), h.stats.formerr.load(.monotonic));
}
test "a response arriving on the listener port is dropped" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
var buf: [udp_limit_min]u8 = undefined;
try testing.expectEqual(Handler.Outcome.drop, h.handle(undefined, .udp, response_bytes, &buf));
try testing.expectEqual(@as(u64, 1), h.stats.dropped_malformed.load(.monotonic));
try testing.expectEqual(@as(usize, 0), fake.calls);
}
test "a query shorter than a header is dropped" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
var buf: [udp_limit_min]u8 = undefined;
const short = query_bytes[0..8];
try testing.expectEqual(Handler.Outcome.drop, h.handle(undefined, .udp, short, &buf));
try testing.expectEqual(@as(u64, 1), h.stats.dropped_malformed.load(.monotonic));
try testing.expectEqual(@as(usize, 0), fake.calls);
}
test "a query whose question runs off the end gets FORMERR" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
// The header promises a question; the packet ends after 12 bytes plus a
// partial name, which `packet.parse` reports as a section overrun.
var buf: [udp_limit_min]u8 = undefined;
const overrun = query_bytes[0 .. query_bytes.len - 3];
const outcome = h.handle(undefined, .udp, overrun, &buf);
const reply = try expectReply(outcome);
const p = try packet.parse(reply);
try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
}
test "a query with QDCOUNT 0 gets FORMERR with no question" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
const no_question = "\x12\x34\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00";
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, no_question, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
try testing.expectEqual(@as(u16, 0x1234), p.header.id);
try testing.expectEqual(@as(u16, 0), p.header.qdcount);
try testing.expectEqual(true, p.header.flags.qr);
try testing.expectEqual(true, p.header.flags.ra);
try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
try testing.expectEqual(@as(usize, 0), fake.calls);
}
test "a query with QDCOUNT 2 gets FORMERR" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
const two =
"\x12\x34\x01\x00\x00\x02\x00\x00\x00\x00\x00\x00" ++
"\x07example\x03com\x00\x00\x01\x00\x01" ++
"\x07example\x03com\x00\x00\x1c\x00\x01";
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, two, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
try testing.expectEqual(@as(u16, 0), p.header.qdcount);
try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
try testing.expectEqual(@as(usize, 0), fake.calls);
}
test "a structurally broken question gets FORMERR" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
// A compression pointer to itself: the name never terminates.
const loop = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
"\xc0\x0c\x00\x01\x00\x01";
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, loop, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
try testing.expectEqual(@as(u16, 0x1234), p.header.id);
try testing.expectEqual(@as(u16, 0), p.header.qdcount);
try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
try testing.expectEqual(@as(usize, 0), fake.calls);
}
test "an unimplemented opcode gets NOTIMP" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
var update: [query_bytes.len]u8 = query_bytes.*;
std.mem.writeInt(u16, update[2..4], 0x2900, .big); // opcode update, RD set
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, &update, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(types.Rcode.not_imp, p.header.flags.rcode);
try testing.expectEqual(types.Opcode.update, p.header.flags.opcode);
try testing.expectEqual(@as(u16, 0x1234), p.header.id);
try testing.expectEqual(@as(u16, 0), p.header.qdcount);
try testing.expectEqual(@as(u64, 1), h.stats.notimp.load(.monotonic));
try testing.expectEqual(@as(usize, 0), fake.calls);
}
test "a peer fault becomes SERVFAIL with the question echoed" {
var fake: FakeUpstream = .{ .err = error.Timeout };
var h: Handler = .{ .upstream = fake.client() };
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, query_bytes, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(types.Rcode.serv_fail, p.header.flags.rcode);
try testing.expectEqual(@as(u16, 0x1234), p.header.id);
try testing.expectEqual(true, p.header.flags.rd);
try testing.expectEqual(true, p.header.flags.qr);
try testing.expectEqual(@as(u16, 1), p.header.qdcount);
try testing.expectEqual(@as(u16, 0), p.header.ancount);
const echoed = packet.firstQuestion(p).?;
const asked = packet.firstQuestion(try packet.parse(query_bytes)).?;
try testing.expectEqualSlices(u8, asked.name.wire(), echoed.name.wire());
try testing.expectEqual(types.Type.a, echoed.qtype);
try testing.expectEqual(@as(u64, 1), h.stats.servfail.load(.monotonic));
try testing.expectEqual(@as(u64, 0), h.stats.queries.load(.monotonic));
}
test "a local resource failure becomes SERVFAIL, not a drop" {
var fake: FakeUpstream = .{ .err = error.OutOfMemory };
var h: Handler = .{ .upstream = fake.client() };
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, query_bytes, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(types.Rcode.serv_fail, p.header.flags.rcode);
try testing.expectEqual(@as(u16, 1), p.header.qdcount);
try testing.expectEqual(@as(u64, 1), h.stats.servfail.load(.monotonic));
}
test "a canceled exchange is dropped" {
var fake: FakeUpstream = .{ .err = error.Canceled };
var h: Handler = .{ .upstream = fake.client() };
var buf: [udp_limit_min]u8 = undefined;
try testing.expectEqual(Handler.Outcome.drop, h.handle(undefined, .udp, query_bytes, &buf));
try testing.expectEqual(@as(u64, 0), h.stats.servfail.load(.monotonic));
try testing.expectEqual(@as(u64, 0), h.stats.queries.load(.monotonic));
try testing.expectEqual(@as(u64, 0), h.stats.dropped_malformed.load(.monotonic));
}
test "udpLimit clamps the advertised payload size" {
try testing.expectEqual(@as(u16, 512), udpLimit(try packet.parse(query_bytes)));
const cases = [_]struct { advertised: u16, expected: u16 }{
.{ .advertised = 1232, .expected = 1232 },
.{ .advertised = 200, .expected = 512 },
.{ .advertised = 9000, .expected = 4096 },
.{ .advertised = 512, .expected = 512 },
.{ .advertised = 4096, .expected = 4096 },
};
for (cases) |c| {
var buf: [query_with_opt_len]u8 = undefined;
const q = queryWithOpt(&buf, c.advertised, false);
try testing.expectEqual(c.expected, udpLimit(try packet.parse(q)));
}
}
/// A valid response for `query_bytes` padded past 512 bytes with A records.
fn oversizeResponse(buf: []u8) []u8 {
const request = packet.parse(query_bytes) catch unreachable;
const q = packet.firstQuestion(request).?;
var b = packet.ResponseBuilder.init(buf, request.header, q) catch unreachable;
var i: usize = 0;
while (i < 32) : (i += 1) {
b.addAnswer(q.name, .a, .in, 300, "\x5d\xb8\xd8\x22") catch unreachable;
}
return b.finish();
}
test "an oversize udp reply is replaced by a truncated one" {
var reply_buf: [2048]u8 = undefined;
const oversize = oversizeResponse(&reply_buf);
try testing.expect(oversize.len > udp_limit_min);
var fake: FakeUpstream = .{ .reply = oversize };
var h: Handler = .{ .upstream = fake.client() };
var buf: [4096]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, query_bytes, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(true, p.header.flags.tc);
try testing.expectEqual(types.Rcode.no_error, p.header.flags.rcode);
try testing.expectEqual(@as(u16, 0), p.header.ancount);
try testing.expectEqual(@as(u16, 1), p.header.qdcount);
try testing.expectEqual(@as(u16, 0x1234), p.header.id);
try testing.expectEqual(true, p.header.flags.rd);
const echoed = packet.firstQuestion(p).?;
const asked = packet.firstQuestion(try packet.parse(query_bytes)).?;
try testing.expectEqualSlices(u8, asked.name.wire(), echoed.name.wire());
try testing.expectEqual(@as(u64, 1), h.stats.truncated.load(.monotonic));
try testing.expectEqual(@as(u64, 1), h.stats.queries.load(.monotonic));
}
test "the same oversize reply passes through untouched over tcp" {
var reply_buf: [2048]u8 = undefined;
const oversize = oversizeResponse(&reply_buf);
var fake: FakeUpstream = .{ .reply = oversize };
var h: Handler = .{ .upstream = fake.client() };
var buf: [4096]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .tcp, query_bytes, &buf));
try testing.expectEqualSlices(u8, oversize, reply);
try testing.expectEqual(@as(u64, 0), h.stats.truncated.load(.monotonic));
try testing.expectEqual(@as(u64, 1), h.stats.queries.load(.monotonic));
}
test "a reply within the advertised EDNS limit is not truncated" {
var reply_buf: [2048]u8 = undefined;
const oversize = oversizeResponse(&reply_buf);
var fake: FakeUpstream = .{ .reply = oversize };
var h: Handler = .{ .upstream = fake.client() };
var query_buf: [query_with_opt_len]u8 = undefined;
const query = queryWithOpt(&query_buf, 4096, false);
var buf: [4096]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, query, &buf));
try testing.expectEqual(oversize.len, reply.len);
try testing.expectEqual(@as(u64, 0), h.stats.truncated.load(.monotonic));
}
test "the DO bit passes through into a synthesized reply" {
for ([_]bool{ false, true }) |do_bit| {
var fake: FakeUpstream = .{ .err = error.Timeout };
var h: Handler = .{ .upstream = fake.client() };
var query_buf: [query_with_opt_len]u8 = undefined;
const query = queryWithOpt(&query_buf, 1232, do_bit);
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, query, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(types.Rcode.serv_fail, p.header.flags.rcode);
const opt = try edns.parseOpt(reply, packet.findOptRecord(p).?);
try testing.expectEqual(do_bit, opt.do_bit);
try testing.expectEqual(@as(u16, 1232), opt.udp_payload_size);
}
}
test "the truncated reply echoes the OPT record" {
var reply_buf: [2048]u8 = undefined;
const oversize = oversizeResponse(&reply_buf);
var fake: FakeUpstream = .{ .reply = oversize };
var h: Handler = .{ .upstream = fake.client() };
// 512 is the advertised size, so the oversize answer still does not fit.
var query_buf: [query_with_opt_len]u8 = undefined;
const query = queryWithOpt(&query_buf, 512, true);
var buf: [4096]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, query, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(true, p.header.flags.tc);
const opt = try edns.parseOpt(reply, packet.findOptRecord(p).?);
try testing.expectEqual(true, opt.do_bit);
try testing.expectEqual(@as(u16, 512), opt.udp_payload_size);
try testing.expectEqual(@as(u64, 1), h.stats.truncated.load(.monotonic));
}
test "an OPT record with a non-root owner name gets FORMERR" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, query_with_named_opt, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
try testing.expectEqual(@as(u16, 0x1234), p.header.id);
try testing.expectEqual(true, p.header.flags.qr);
try testing.expectEqual(true, p.header.flags.ra);
// The question parsed, so it is echoed; the OPT did not, so none is.
try testing.expectEqual(@as(u16, 1), p.header.qdcount);
const echoed = packet.firstQuestion(p).?;
const asked = packet.firstQuestion(try packet.parse(query_bytes)).?;
try testing.expectEqualSlices(u8, asked.name.wire(), echoed.name.wire());
try testing.expect(packet.findOptRecord(p) == null);
try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
try testing.expectEqual(@as(usize, 0), fake.calls);
}
test "an OPT record with a malformed option list gets FORMERR" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
// The packet as a whole is well-formed: only `parseOpt` rejects it.
const p_query = try packet.parse(query_with_bad_option);
try testing.expectError(
error.BadOption,
edns.parseOpt(query_with_bad_option, packet.findOptRecord(p_query).?),
);
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, query_with_bad_option, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
try testing.expectEqual(@as(u16, 0x1234), p.header.id);
try testing.expect(packet.findOptRecord(p) == null);
try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
try testing.expectEqual(@as(usize, 0), fake.calls);
}
test "an OPT record whose rdata runs off the end gets FORMERR" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
// RDLENGTH claims three bytes and one follows, which `packet.parse` reports
// as a section overrun before the OPT is ever read.
const truncated = query_with_bad_option[0 .. query_with_bad_option.len - 2];
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, truncated, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
try testing.expectEqual(@as(usize, 0), fake.calls);
}
test "an OPT record in the answer section gets FORMERR" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
// The message walks: only its placement of the OPT is illegal, so the
// handler's own section walk is what has to reject it.
const p_query = try packet.parse(query_with_opt_in_answer);
try testing.expect(packet.findOptRecord(p_query) == null);
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, query_with_opt_in_answer, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
try testing.expectEqual(@as(u16, 0x1234), p.header.id);
// The question parsed, so it is echoed; the OPT is not echoed anywhere.
try testing.expectEqual(@as(u16, 1), p.header.qdcount);
const echoed = packet.firstQuestion(p).?;
const asked = packet.firstQuestion(try packet.parse(query_bytes)).?;
try testing.expectEqualSlices(u8, asked.name.wire(), echoed.name.wire());
try testing.expect(packet.findOptRecord(p) == null);
try testing.expectEqual(@as(u16, 0), p.header.arcount);
try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
try testing.expectEqual(@as(usize, 0), fake.calls);
}
test "an OPT record in the authority section gets FORMERR" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
const p_query = try packet.parse(query_with_opt_in_authority);
try testing.expect(packet.findOptRecord(p_query) == null);
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, query_with_opt_in_authority, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
try testing.expectEqual(@as(u16, 0x1234), p.header.id);
try testing.expectEqual(@as(u16, 1), p.header.qdcount);
try testing.expect(packet.findOptRecord(p) == null);
try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
try testing.expectEqual(@as(usize, 0), fake.calls);
}
test "two OPT records in the additional section get FORMERR" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
// Here the rejection comes from `packet.parse`, one section walk earlier.
try testing.expectError(error.MultipleOptRecords, packet.parse(query_with_two_opts));
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, query_with_two_opts, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
try testing.expectEqual(@as(u16, 0x1234), p.header.id);
try testing.expect(packet.findOptRecord(p) == null);
try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
try testing.expectEqual(@as(usize, 0), fake.calls);
}
test "a query with a valid OPT record is still forwarded" {
var fake: FakeUpstream = .{ .reply = response_bytes };
var h: Handler = .{ .upstream = fake.client() };
var query_buf: [query_with_opt_len]u8 = undefined;
const query = queryWithOpt(&query_buf, 1232, true);
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, query, &buf));
try testing.expectEqualSlices(u8, response_bytes, reply);
try testing.expectEqual(@as(usize, 1), fake.calls);
try testing.expectEqual(@as(u64, 1), h.stats.queries.load(.monotonic));
try testing.expectEqual(@as(u64, 0), h.stats.formerr.load(.monotonic));
// The same query, answered from the SERVFAIL path, still echoes the OPT.
var fail: FakeUpstream = .{ .err = error.Timeout };
var h2: Handler = .{ .upstream = fail.client() };
const synthesized = try expectReply(h2.handle(undefined, .udp, query, &buf));
const p = try packet.parse(synthesized);
const opt = try edns.parseOpt(synthesized, packet.findOptRecord(p).?);
try testing.expectEqual(true, opt.do_bit);
try testing.expectEqual(@as(u16, 1232), opt.udp_payload_size);
try testing.expectEqual(@as(u64, 0), h2.stats.formerr.load(.monotonic));
}
test "every synthesized reply re-parses and binds to its request" {
const Case = struct {
query: []const u8,
err: ?transport.ExchangeError,
rcode: types.Rcode,
};
const broken_question = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
"\xc0\x0c\x00\x01\x00\x01";
const no_question = "\x12\x34\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00";
const update = "\x12\x34\x29\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
"\x07example\x03com\x00\x00\x01\x00\x01";
const cases = [_]Case{
.{ .query = broken_question, .err = null, .rcode = .form_err },
.{ .query = no_question, .err = null, .rcode = .form_err },
.{ .query = query_with_named_opt, .err = null, .rcode = .form_err },
.{ .query = query_with_bad_option, .err = null, .rcode = .form_err },
.{ .query = query_with_opt_in_answer, .err = null, .rcode = .form_err },
.{ .query = query_with_opt_in_authority, .err = null, .rcode = .form_err },
.{ .query = query_with_two_opts, .err = null, .rcode = .form_err },
.{ .query = update, .err = null, .rcode = .not_imp },
.{ .query = query_bytes, .err = error.BadResponse, .rcode = .serv_fail },
.{ .query = query_bytes, .err = error.Unexpected, .rcode = .serv_fail },
};
for (cases) |c| {
var fake: FakeUpstream = .{ .reply = response_bytes, .err = c.err };
var h: Handler = .{ .upstream = fake.client() };
var buf: [udp_limit_min]u8 = undefined;
const reply = try expectReply(h.handle(undefined, .udp, c.query, &buf));
const p = try packet.parse(reply);
try testing.expectEqual(c.rcode, p.header.flags.rcode);
try testing.expectEqual(@as(u16, 0x1234), p.header.id);
try testing.expectEqual(true, p.header.flags.qr);
try testing.expectEqual(true, p.header.flags.ra);
try testing.expectEqual(@as(u16, 0), p.header.ancount);
try testing.expectEqual(@as(u16, 0), p.header.nscount);
}
}