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.
127 lines
4.3 KiB
Zig
127 lines
4.3 KiB
Zig
//! Network-dependent test for `dot_client.zig`.
|
|
//!
|
|
//! Separate file because it needs `@import("build_options")`, which only exists
|
|
//! when build.zig drives the compilation. It is compiled by every
|
|
//! `zig build test` run, so it cannot rot, and skips at run time without
|
|
//! `-Dlive`. (`-Dintegration` stays hermetic; `-Dlive` is the gate for tests
|
|
//! that leave the machine.)
|
|
|
|
const std = @import("std");
|
|
const build_options = @import("build_options");
|
|
const tls = std.crypto.tls;
|
|
const Certificate = std.crypto.Certificate;
|
|
|
|
const dot_client = @import("dot_client.zig");
|
|
const transport = @import("transport.zig");
|
|
const packet = @import("../dns/packet.zig");
|
|
|
|
/// Neither `connect` nor a TLS stream read accepts a timeout in 0.16.0, so the
|
|
/// whole exchange runs as one task raced against a sleep and the loser is
|
|
/// canceled.
|
|
const budget: std.Io.Clock.Duration = .{ .raw = .fromSeconds(10), .clock = .awake };
|
|
|
|
/// An A query for example.com: id 0x1234, RD set, one question.
|
|
const query_bytes =
|
|
"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
|
|
"\x07example\x03com\x00\x00\x01\x00\x01";
|
|
|
|
/// This machine's IPv6 egress is dead and upstream name resolution is out of
|
|
/// scope, so the documented anycast IPv4 literal is used.
|
|
const upstream_url = "tls://1.1.1.1:853";
|
|
|
|
/// The name Cloudflare publishes for this endpoint. Without it the handshake is
|
|
/// `error.CertificateHostMismatch`: `std.crypto.Certificate` matches dNSName
|
|
/// SANs only, so the IP SAN on the leaf certificate is never consulted.
|
|
const upstream_tls_name = "one.one.one.one";
|
|
|
|
const Outcome = union(enum) {
|
|
exchange: anyerror!usize,
|
|
expiry: std.Io.Cancelable!void,
|
|
};
|
|
|
|
const Params = struct {
|
|
gpa: std.mem.Allocator,
|
|
bundle: *Certificate.Bundle,
|
|
bundle_lock: *std.Io.RwLock,
|
|
buffers: dot_client.DotClient.Buffers,
|
|
response_buf: []u8,
|
|
};
|
|
|
|
fn runExchange(io: std.Io, params: Params) anyerror!usize {
|
|
const endpoint: transport.Endpoint = try .parse(upstream_url);
|
|
var client: dot_client.DotClient = .init(
|
|
endpoint,
|
|
upstream_tls_name,
|
|
params.gpa,
|
|
params.bundle,
|
|
params.bundle_lock,
|
|
params.buffers,
|
|
);
|
|
var selected: ?[]const u8 = null;
|
|
const reply = try client.client().exchange(io, query_bytes, params.response_buf, &selected);
|
|
std.debug.assert(std.mem.eql(u8, selected.?, endpoint.url));
|
|
return reply.len;
|
|
}
|
|
|
|
fn expire(io: std.Io, duration: std.Io.Clock.Duration) std.Io.Cancelable!void {
|
|
return duration.sleep(io);
|
|
}
|
|
|
|
test "live DoT exchange against 1.1.1.1" {
|
|
if (!build_options.live) return error.SkipZigTest;
|
|
|
|
const gpa = std.testing.allocator;
|
|
|
|
var threaded: std.Io.Threaded = .init(gpa, .{});
|
|
defer threaded.deinit();
|
|
const io = threaded.io();
|
|
|
|
var bundle: Certificate.Bundle = .empty;
|
|
defer bundle.deinit(gpa);
|
|
var bundle_lock: std.Io.RwLock = .init;
|
|
|
|
const chunk = tls.Client.min_buffer_len;
|
|
const scratch = try gpa.alloc(u8, 4 * chunk);
|
|
defer gpa.free(scratch);
|
|
|
|
var response_buf: [transport.max_message_len]u8 = undefined;
|
|
|
|
var outcomes: [2]Outcome = undefined;
|
|
var race: std.Io.Select(Outcome) = .init(io, &outcomes);
|
|
defer race.cancelDiscard();
|
|
|
|
try race.concurrent(.exchange, runExchange, .{ io, Params{
|
|
.gpa = gpa,
|
|
.bundle = &bundle,
|
|
.bundle_lock = &bundle_lock,
|
|
.buffers = .{
|
|
.tls_read = scratch[0..chunk],
|
|
.tls_write = scratch[chunk .. 2 * chunk],
|
|
.stream_read = scratch[2 * chunk .. 3 * chunk],
|
|
.stream_write = scratch[3 * chunk ..],
|
|
},
|
|
.response_buf = &response_buf,
|
|
} });
|
|
try race.concurrent(.expiry, expire, .{ io, budget });
|
|
|
|
const len = switch (try race.await()) {
|
|
.exchange => |result| result catch |err| {
|
|
std.debug.print("DoT exchange with {s} failed: {s}\n", .{
|
|
upstream_url,
|
|
@errorName(err),
|
|
});
|
|
return err;
|
|
},
|
|
.expiry => |result| {
|
|
try result;
|
|
return error.DotExchangeTimedOut;
|
|
},
|
|
};
|
|
|
|
// `exchange` already ran `transport.validateResponse`, so the id, question
|
|
// and QR bit are known good. What is left to check is that the upstream
|
|
// actually answered the question.
|
|
const reply = try packet.parse(response_buf[0..len]);
|
|
try std.testing.expect(reply.header.ancount >= 1);
|
|
}
|