milestone 28: query provenance — every logged query is exactly explainable
Gates / frontend (push) Successful in 1m36s
Gates / test (push) Successful in 1m56s
Gates / test-aarch64 (push) Successful in 7m37s
Gates / package (push) Successful in 9m12s
Gates / container (push) Successful in 13s
CI / gates (push) Successful in 19m4s
Gates / frontend (push) Successful in 1m36s
Gates / test (push) Successful in 1m56s
Gates / test-aarch64 (push) Successful in 7m37s
Gates / package (push) Successful in 9m12s
Gates / container (push) Successful in 13s
CI / gates (push) Successful in 19m4s
query rows gain qclass, rcode, group, policy action and reason, the matched rule or list entry with its source, cname and safe-search targets, route kind, forward zone, and the resolver that actually answered — the pool and local markers die. servfails are logged and name the resolver that lost; post-parse protocol refusals become rows. a detail page at /queries/:id renders the ordered explanation, and coverage watermarks distinguish an empty history from a missing one. the schema fingerprint changes: existing query history is recreated with the old file kept aside and the reset filed as a resolved diagnostic. fixes an oversized udp reply being rebuilt as noerror, which handed clients a truncated nxdomain as success.
This commit is contained in:
@@ -33,8 +33,17 @@ const log = std.log.scoped(.forward_client);
|
||||
/// each half large enough to frame a query in one write, not a capacity.
|
||||
pub const min_frame_buf: usize = 1024;
|
||||
|
||||
/// `tcp://[` + the longest IPv6 text form + `]:65535`, the widest spelling
|
||||
/// `identityText` can produce.
|
||||
pub const max_identity_len: usize = "tcp://[".len + 45 + "]:65535".len;
|
||||
|
||||
pub const ForwardClient = struct {
|
||||
resolver: validate.Resolver,
|
||||
/// The resolver as text, owned here so the `transport.Client` out-parameter
|
||||
/// has something stable to borrow: `validate.Resolver` is a parsed address,
|
||||
/// and a caller logging the exchange needs its spelling.
|
||||
identity_buf: [max_identity_len]u8 = undefined,
|
||||
identity_len: usize = 0,
|
||||
/// Caller-owned scratch for the TCP length-prefixed path.
|
||||
frame_buf: []u8,
|
||||
/// On the `.awake` clock at the caller's choosing, so a suspended host does
|
||||
@@ -64,11 +73,20 @@ pub const ForwardClient = struct {
|
||||
read_timeout: std.Io.Clock.Duration,
|
||||
) ForwardClient {
|
||||
std.debug.assert(frame_buf.len >= min_frame_buf);
|
||||
return .{
|
||||
var self: ForwardClient = .{
|
||||
.resolver = resolver,
|
||||
.frame_buf = frame_buf,
|
||||
.read_timeout = read_timeout,
|
||||
};
|
||||
self.identity_len = identityText(resolver, &self.identity_buf).len;
|
||||
return self;
|
||||
}
|
||||
|
||||
/// `udp://192.168.1.1:53`, `tcp://[fd00::1]:53` — the same spelling
|
||||
/// `validate.parseResolver` accepts, so a log row names the configured
|
||||
/// value. Valid for as long as this client is.
|
||||
pub fn identity(self: *const ForwardClient) []const u8 {
|
||||
return self.identity_buf[0..self.identity_len];
|
||||
}
|
||||
|
||||
pub fn client(self: *ForwardClient) transport.Client {
|
||||
@@ -80,8 +98,12 @@ pub const ForwardClient = struct {
|
||||
io: std.Io,
|
||||
query: []const u8,
|
||||
response_buf: []u8,
|
||||
selected: *?[]const u8,
|
||||
) transport.ExchangeError![]u8 {
|
||||
const self: *ForwardClient = @ptrCast(@alignCast(ptr));
|
||||
// Set before the attempt: a failed forward-zone exchange still names
|
||||
// the resolver it was sent to.
|
||||
selected.* = self.identity();
|
||||
return self.exchange(io, query, response_buf);
|
||||
}
|
||||
|
||||
@@ -238,6 +260,24 @@ pub const ForwardClient = struct {
|
||||
}
|
||||
};
|
||||
|
||||
fn identityText(resolver: validate.Resolver, buf: *[max_identity_len]u8) []const u8 {
|
||||
var w: std.Io.Writer = .fixed(buf);
|
||||
w.writeAll(switch (resolver.scheme) {
|
||||
.udp => "udp://",
|
||||
.tcp => "tcp://",
|
||||
}) catch unreachable;
|
||||
|
||||
const bracketed = switch (resolver.addr) {
|
||||
.ip4 => false,
|
||||
.ip6 => true,
|
||||
};
|
||||
if (bracketed) w.writeByte('[') catch unreachable;
|
||||
resolver.addr.format(&w) catch unreachable;
|
||||
if (bracketed) w.writeByte(']') catch unreachable;
|
||||
w.print(":{d}", .{resolver.port}) catch unreachable;
|
||||
return w.buffered();
|
||||
}
|
||||
|
||||
/// The local address a datagram to `dest` is sent from: same family, port
|
||||
/// chosen by the kernel.
|
||||
fn wildcardFor(dest: net.IpAddress) net.IpAddress {
|
||||
@@ -286,6 +326,27 @@ test "ForwardClient satisfies the Client interface" {
|
||||
try testing.expectEqual(@as(u16, 53), fc.resolver.port);
|
||||
}
|
||||
|
||||
test "the client owns its resolver identity in both address families" {
|
||||
var buf = testBuf();
|
||||
const v4: ForwardClient = .init(
|
||||
try validate.parseResolver("udp://192.168.1.1:5300"),
|
||||
&buf,
|
||||
.{ .raw = .fromSeconds(1), .clock = .awake },
|
||||
);
|
||||
try testing.expectEqualStrings("udp://192.168.1.1:5300", v4.identity());
|
||||
|
||||
var buf6 = testBuf();
|
||||
const v6: ForwardClient = .init(
|
||||
try validate.parseResolver("tcp://[fd00::1]:5353"),
|
||||
&buf6,
|
||||
.{ .raw = .fromSeconds(1), .clock = .awake },
|
||||
);
|
||||
try testing.expectEqualStrings("tcp://[fd00::1]:5353", v6.identity());
|
||||
|
||||
// The borrow points into the client, not into `init`'s frame.
|
||||
try testing.expect(@intFromPtr(v6.identity().ptr) >= @intFromPtr(&v6));
|
||||
}
|
||||
|
||||
test "the stats struct starts at zero" {
|
||||
const stats: ForwardClient.Stats = .{};
|
||||
try testing.expectEqual(@as(u64, 0), stats.queries);
|
||||
|
||||
Reference in New Issue
Block a user