milestone 17: real deadlines, validator holes, upstream editor, trusted proxies, contract samples, badvers
This commit is contained in:
+122
-1
@@ -133,6 +133,9 @@ pub const Handler = struct {
|
||||
dropped_malformed: std.atomic.Value(u64) = .init(0),
|
||||
formerr: std.atomic.Value(u64) = .init(0),
|
||||
notimp: std.atomic.Value(u64) = .init(0),
|
||||
/// Queries asking for an EDNS version this server does not implement,
|
||||
/// answered with BADVERS (RFC 6891 §6.1.3).
|
||||
badvers: std.atomic.Value(u64) = .init(0),
|
||||
servfail: std.atomic.Value(u64) = .init(0),
|
||||
truncated: std.atomic.Value(u64) = .init(0),
|
||||
refused: std.atomic.Value(u64) = .init(0),
|
||||
@@ -239,6 +242,19 @@ pub const Handler = struct {
|
||||
else
|
||||
null;
|
||||
|
||||
// RFC 6891 §6.1.3: a query naming an EDNS version this server does not
|
||||
// implement is answered with BADVERS, and the reply's OPT reports the
|
||||
// highest version the server does implement. The check precedes the
|
||||
// opcode check because the version governs the whole EDNS exchange,
|
||||
// whatever the query asks for.
|
||||
if (opt) |o| {
|
||||
if (o.version != 0) {
|
||||
bump(&self.stats.badvers);
|
||||
const echo = if (hdr.qdcount == 1) packet.firstQuestion(p) else null;
|
||||
return .{ .reply = buildBadvers(hdr, echo, o, response_buf) };
|
||||
}
|
||||
}
|
||||
|
||||
if (hdr.flags.opcode != .query) {
|
||||
return synthesize(hdr, null, opt, .not_imp, &self.stats.notimp, response_buf);
|
||||
}
|
||||
@@ -887,6 +903,22 @@ fn build(
|
||||
return b.finish();
|
||||
}
|
||||
|
||||
/// Encodes a BADVERS reply. `build` cannot: RCODE 16 does not fit the header's
|
||||
/// four bits, so the code splits across the header and the OPT record and the
|
||||
/// reply carries an OPT even though it echoes none of the query's options.
|
||||
fn buildBadvers(
|
||||
hdr: header.Header,
|
||||
q: ?question.Question,
|
||||
request_opt: edns.OptRecord,
|
||||
buf: []u8,
|
||||
) []u8 {
|
||||
const split = edns.splitRcode(edns.badvers);
|
||||
var b = packet.ResponseBuilder.init(buf, hdr, q) catch unreachable;
|
||||
b.setRcode(split.header);
|
||||
b.addOptWithRcode(request_opt, request_opt.do_bit, split.extended) catch unreachable;
|
||||
return b.finish();
|
||||
}
|
||||
|
||||
fn bump(counter: *std.atomic.Value(u64)) void {
|
||||
_ = counter.fetchAdd(1, .monotonic);
|
||||
}
|
||||
@@ -972,8 +1004,22 @@ const response_bytes =
|
||||
const opt_len = 11;
|
||||
const query_with_opt_len = query_bytes.len + opt_len;
|
||||
|
||||
/// `query_bytes` plus an OPT record advertising `payload_size`.
|
||||
/// The EDNS version byte, counted from the start of the OPT record: past the
|
||||
/// root owner name, TYPE, CLASS and the extended-RCODE byte.
|
||||
const opt_version_offset = 6;
|
||||
|
||||
/// `query_bytes` plus an OPT record advertising `payload_size`, EDNS version 0.
|
||||
fn queryWithOpt(buf: *[query_with_opt_len]u8, payload_size: u16, do_bit: bool) []const u8 {
|
||||
return queryWithOptVersion(buf, payload_size, do_bit, 0);
|
||||
}
|
||||
|
||||
/// `queryWithOpt` for a chosen EDNS version.
|
||||
fn queryWithOptVersion(
|
||||
buf: *[query_with_opt_len]u8,
|
||||
payload_size: u16,
|
||||
do_bit: bool,
|
||||
version: u8,
|
||||
) []const u8 {
|
||||
@memcpy(buf[0..query_bytes.len], query_bytes);
|
||||
std.mem.writeInt(u16, buf[10..12], 1, .big); // arcount
|
||||
|
||||
@@ -981,6 +1027,7 @@ fn queryWithOpt(buf: *[query_with_opt_len]u8, payload_size: u16, do_bit: bool) [
|
||||
@memset(opt, 0);
|
||||
opt[2] = @intFromEnum(types.Type.opt);
|
||||
std.mem.writeInt(u16, opt[3..5], payload_size, .big);
|
||||
opt[opt_version_offset] = version;
|
||||
if (do_bit) opt[7] = 0x80; // the DO bit is bit 15 of the TTL word
|
||||
return buf;
|
||||
}
|
||||
@@ -1414,6 +1461,80 @@ test "the truncated reply echoes the OPT record" {
|
||||
try testing.expectEqual(@as(u64, 1), h.stats.truncated.load(.monotonic));
|
||||
}
|
||||
|
||||
test "an EDNS version this server does not implement gets BADVERS" {
|
||||
var t: TestIo = .init();
|
||||
defer t.deinit();
|
||||
|
||||
var fake: FakeUpstream = .{ .reply = response_bytes };
|
||||
var h = bare(fake.client());
|
||||
|
||||
var query_buf: [query_with_opt_len]u8 = undefined;
|
||||
const query = queryWithOptVersion(&query_buf, 1232, true, 1);
|
||||
|
||||
var buf: [udp_limit_min]u8 = undefined;
|
||||
const reply = try expectReply(udp(&h, t.io(), query, &buf));
|
||||
|
||||
const p = try packet.parse(reply);
|
||||
const opt = try edns.parseOpt(reply, packet.findOptRecord(p).?);
|
||||
// RCODE 16 lives in neither half alone: the header carries 0 and the OPT
|
||||
// record carries 1.
|
||||
try testing.expectEqual(types.Rcode.no_error, p.header.flags.rcode);
|
||||
try testing.expectEqual(@as(u12, 16), edns.extendedRcode(p.header.flags.rcode, opt));
|
||||
try testing.expectEqual(@as(u8, 0), opt.version);
|
||||
try testing.expectEqual(true, opt.do_bit);
|
||||
try testing.expectEqual(@as(u16, 1232), opt.udp_payload_size);
|
||||
try testing.expectEqual(@as(u16, 0x1234), p.header.id);
|
||||
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);
|
||||
try testing.expectEqual(@as(u64, 1), h.stats.badvers.load(.monotonic));
|
||||
// The query never reached the upstream.
|
||||
try testing.expectEqual(@as(u64, 0), h.stats.queries.load(.monotonic));
|
||||
}
|
||||
|
||||
test "an EDNS version 0 query is not answered with BADVERS" {
|
||||
var t: TestIo = .init();
|
||||
defer t.deinit();
|
||||
|
||||
var fake: FakeUpstream = .{ .reply = response_bytes };
|
||||
var h = bare(fake.client());
|
||||
|
||||
var query_buf: [query_with_opt_len]u8 = undefined;
|
||||
const query = queryWithOptVersion(&query_buf, 1232, false, 0);
|
||||
|
||||
var buf: [udp_limit_min]u8 = undefined;
|
||||
const reply = try expectReply(udp(&h, t.io(), query, &buf));
|
||||
|
||||
// The query is forwarded and the upstream's own answer comes back, so the
|
||||
// BADVERS arm never ran.
|
||||
const p = try packet.parse(reply);
|
||||
try testing.expectEqual(types.Rcode.no_error, p.header.flags.rcode);
|
||||
try testing.expectEqual(@as(u16, 1), p.header.ancount);
|
||||
try testing.expectEqual(@as(u64, 0), h.stats.badvers.load(.monotonic));
|
||||
try testing.expectEqual(@as(u64, 1), h.stats.queries.load(.monotonic));
|
||||
}
|
||||
|
||||
test "a malformed OPT record is FORMERR before the version check reads it" {
|
||||
var t: TestIo = .init();
|
||||
defer t.deinit();
|
||||
|
||||
var fake: FakeUpstream = .{ .reply = response_bytes };
|
||||
var h = bare(fake.client());
|
||||
|
||||
// The option list is broken and the EDNS version is 1. `parseOpt` fails
|
||||
// first, so nothing trusts the version byte of an OPT that will not parse.
|
||||
var query_buf: [query_with_bad_option.len]u8 = (query_with_bad_option ++ "").*;
|
||||
query_buf[query_bytes.len + opt_version_offset] = 1;
|
||||
|
||||
var buf: [udp_limit_min]u8 = undefined;
|
||||
const reply = try expectReply(udp(&h, t.io(), &query_buf, &buf));
|
||||
|
||||
const p = try packet.parse(reply);
|
||||
try testing.expectEqual(types.Rcode.form_err, p.header.flags.rcode);
|
||||
try testing.expectEqual(@as(u64, 0), h.stats.badvers.load(.monotonic));
|
||||
try testing.expectEqual(@as(u64, 1), h.stats.formerr.load(.monotonic));
|
||||
}
|
||||
|
||||
test "an OPT record with a non-root owner name gets FORMERR" {
|
||||
var t: TestIo = .init();
|
||||
defer t.deinit();
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
//! the timestamp, so this file holds no clock, no `std.Io` operation and no
|
||||
//! socket. `check` neither allocates nor fails.
|
||||
//!
|
||||
//! Not thread-safe. Phase 7 decides the locking when it wires the limiter into
|
||||
//! the query path.
|
||||
//! Not thread-safe. The handler owns the locking: every call goes through
|
||||
//! `Handler.limiter_mutex` (handler.zig:124), taken uncancelably on the query
|
||||
//! path — `handle` has no error union to carry `error.Canceled` out of — and
|
||||
//! cancelably in the `runMaintenance` sweep that ages the table out
|
||||
//! (app.zig, `maintenanceOnce`), which does.
|
||||
//!
|
||||
//! The window is fixed, not sliding (PLAN §10 reserves the token bucket for the
|
||||
//! API limiter). A fixed window admits at most twice the limit across a window
|
||||
@@ -116,7 +119,7 @@ pub const RateLimiter = struct {
|
||||
|
||||
/// Drops every entry whose window ended more than one full window before
|
||||
/// `now`, that is `now - start_ns > 2 * window_ns`. Returns how many it
|
||||
/// dropped. Phase 7 schedules it.
|
||||
/// dropped. `app.runMaintenance` schedules it.
|
||||
pub fn sweep(self: *RateLimiter, now: std.Io.Timestamp) u32 {
|
||||
const stale_after = 2 * self.window_ns;
|
||||
var stale_count: u32 = 0;
|
||||
|
||||
@@ -77,9 +77,12 @@ const test_cfg: health.Config = .{
|
||||
.max_backoff_ms = 60_000,
|
||||
};
|
||||
|
||||
/// Nothing in this test is slow, so this budget only exists to stop a wedged
|
||||
/// Nothing in this test is slow, so these budgets only exist to stop a wedged
|
||||
/// attempt from hanging the run.
|
||||
const attempt_timeout: std.Io.Clock.Duration = .{ .raw = .fromSeconds(10), .clock = .awake };
|
||||
const pool_timeouts: pool.Timeouts = .{
|
||||
.attempt = .{ .raw = .fromSeconds(10), .clock = .awake },
|
||||
.total = .{ .raw = .fromSeconds(30), .clock = .awake },
|
||||
};
|
||||
|
||||
fn queryWithId(buf: *[query_bytes.len]u8, id: u16) []const u8 {
|
||||
buf.* = query_bytes.*;
|
||||
@@ -247,7 +250,7 @@ test "the whole resolver answers over udp and tcp and fails over to a healthy up
|
||||
testEntry("https://bad.example/dns-query", bad.client(), 10),
|
||||
testEntry("tls://good.example", good.client(), 20),
|
||||
};
|
||||
var upstreams: pool.Pool = .init(&entries, test_cfg, attempt_timeout, 1);
|
||||
var upstreams: pool.Pool = .init(&entries, test_cfg, pool_timeouts, 1);
|
||||
|
||||
var h = bareHandler(upstreams.client());
|
||||
|
||||
|
||||
@@ -58,7 +58,8 @@ pub fn wait(io: std.Io) std.Io.Cancelable!void {
|
||||
}
|
||||
|
||||
/// The programmatic equivalent of the signal: what a test uses to shut the app
|
||||
/// down, and what a Phase 8 restart endpoint would call.
|
||||
/// down. No restart endpoint calls it — none exists, and none is planned; the
|
||||
/// web API echoes `restart_required` and leaves the restart to the operator.
|
||||
pub fn trigger(io: std.Io) void {
|
||||
event.set(io);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user