resolver transport: udp/tcp servers, doh/dot clients, pool failover with health
This commit is contained in:
@@ -0,0 +1,497 @@
|
||||
//! Shared vocabulary for every upstream client: endpoint URLs, the three
|
||||
//! disjoint failure groups, the `Client` interface, and response validation.
|
||||
//!
|
||||
//! Everything here except the `Client` vtable is pure. `validateResponse` takes
|
||||
//! two byte slices and returns; `Endpoint.parse` takes text. No socket, no
|
||||
//! clock, no allocator. The transport implementations (DoH, DoT) and the pool
|
||||
//! own all of the `std.Io` work.
|
||||
//!
|
||||
//! The failure classification is the reason this file exists. Health and
|
||||
//! backoff must count only what the peer did wrong: a local `OutOfMemory` says
|
||||
//! nothing about the upstream, and `error.Canceled` says nothing at all. The
|
||||
//! three error sets below are disjoint by construction and `group` switches
|
||||
//! over them exhaustively, so a new failure mode cannot silently land in the
|
||||
//! wrong bucket.
|
||||
|
||||
const std = @import("std");
|
||||
const packet = @import("../dns/packet.zig");
|
||||
const name = @import("../dns/name.zig");
|
||||
|
||||
/// RFC 1035 §4.2.2: the TCP length prefix is 16-bit, so no DNS message can be
|
||||
/// larger than this on any transport nxdns speaks.
|
||||
pub const max_message_len = 65535;
|
||||
pub const doh_default_port = 443;
|
||||
pub const dot_default_port = 853; // RFC 7858 §3.1
|
||||
pub const doh_default_path = "/dns-query"; // RFC 8484 §4.1 well-known template
|
||||
|
||||
pub const Scheme = enum { doh, dot };
|
||||
|
||||
/// Borrowed view over the configured URL text; the caller owns the string.
|
||||
pub const Endpoint = struct {
|
||||
scheme: Scheme,
|
||||
/// The original text, for logs and the health API.
|
||||
url: []const u8,
|
||||
/// No brackets, no port. Used for SNI and certificate verification.
|
||||
host: []const u8,
|
||||
port: u16,
|
||||
/// DoH only; always starts with '/'; `doh_default_path` when absent. A DoT
|
||||
/// endpoint has no request path, so it carries "/" and nothing reads it.
|
||||
path: []const u8,
|
||||
|
||||
pub const ParseError = error{ UnsupportedScheme, MissingHost, BadPort, BadUrl };
|
||||
|
||||
const doh_prefix = "https://";
|
||||
const dot_prefix = "tls://";
|
||||
|
||||
/// `https://…` => .doh, `tls://…` => .dot (PLAN §9). Accepts `[v6]:port`.
|
||||
///
|
||||
/// Hand-written rather than a `std.Uri` round trip: `std.Uri` hands back
|
||||
/// percent-encoded components that would need re-decoding into a caller
|
||||
/// buffer, which is a lot of machinery for one household-scale config
|
||||
/// value that is an IP literal or a hostname.
|
||||
pub fn parse(url: []const u8) ParseError!Endpoint {
|
||||
const scheme: Scheme, const rest = if (std.mem.startsWith(u8, url, doh_prefix))
|
||||
.{ .doh, url[doh_prefix.len..] }
|
||||
else if (std.mem.startsWith(u8, url, dot_prefix))
|
||||
.{ .dot, url[dot_prefix.len..] }
|
||||
else
|
||||
return error.UnsupportedScheme;
|
||||
|
||||
const authority, const path = split: {
|
||||
const slash = std.mem.findScalar(u8, rest, '/') orelse break :split .{ rest, "" };
|
||||
break :split .{ rest[0..slash], rest[slash..] };
|
||||
};
|
||||
|
||||
try rejectDelimiters(authority, "@?#");
|
||||
try rejectDelimiters(path, "?#");
|
||||
|
||||
const host, const port_text = try splitAuthority(authority);
|
||||
if (host.len == 0) return error.MissingHost;
|
||||
|
||||
const port: u16 = if (port_text) |text| blk: {
|
||||
if (text.len == 0) return error.BadPort;
|
||||
break :blk std.fmt.parseInt(u16, text, 10) catch return error.BadPort;
|
||||
} else switch (scheme) {
|
||||
.doh => doh_default_port,
|
||||
.dot => dot_default_port,
|
||||
};
|
||||
|
||||
return switch (scheme) {
|
||||
.doh => .{
|
||||
.scheme = .doh,
|
||||
.url = url,
|
||||
.host = host,
|
||||
.port = port,
|
||||
.path = if (path.len == 0) doh_default_path else path,
|
||||
},
|
||||
.dot => blk: {
|
||||
// RFC 7858 frames DNS directly on the TLS stream; a path would
|
||||
// be config the transport cannot honour, so it is rejected
|
||||
// rather than ignored.
|
||||
if (path.len != 0 and !std.mem.eql(u8, path, "/")) return error.BadUrl;
|
||||
break :blk .{
|
||||
.scheme = .dot,
|
||||
.url = url,
|
||||
.host = host,
|
||||
.port = port,
|
||||
.path = "/",
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// `@`, `?` and `#` open a userinfo, query or fragment component. This
|
||||
/// parser implements none of them, so keeping one as literal host or path
|
||||
/// text would let `host` disagree with the authority an RFC 3986 parser
|
||||
/// reads out of the same URL — the name verified against the certificate
|
||||
/// would not be the name dialed. A household config has no use for them,
|
||||
/// so they are a config error rather than something to strip.
|
||||
fn rejectDelimiters(text: []const u8, comptime delimiters: []const u8) ParseError!void {
|
||||
inline for (delimiters) |delimiter| {
|
||||
if (std.mem.findScalar(u8, text, delimiter) != null) return error.BadUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the host without brackets and the port text when one is present.
|
||||
fn splitAuthority(authority: []const u8) ParseError!struct { []const u8, ?[]const u8 } {
|
||||
if (authority.len != 0 and authority[0] == '[') {
|
||||
const close = std.mem.findScalar(u8, authority, ']') orelse return error.BadUrl;
|
||||
const host = authority[1..close];
|
||||
const tail = authority[close + 1 ..];
|
||||
if (tail.len == 0) return .{ host, null };
|
||||
if (tail[0] != ':') return error.BadUrl;
|
||||
return .{ host, tail[1..] };
|
||||
}
|
||||
const colon = std.mem.findScalar(u8, authority, ':') orelse return .{ authority, null };
|
||||
return .{ authority[0..colon], authority[colon + 1 ..] };
|
||||
}
|
||||
};
|
||||
|
||||
/// The upstream misbehaved, timed out, or was unreachable. Only these count
|
||||
/// against health.
|
||||
pub const PeerFault = error{
|
||||
ConnectFailed,
|
||||
TlsFailed,
|
||||
SendFailed,
|
||||
ReceiveFailed,
|
||||
Timeout,
|
||||
/// Unparseable, not a response, or QDCOUNT != 1.
|
||||
BadResponse,
|
||||
/// ID or question does not match the query.
|
||||
ResponseMismatch,
|
||||
/// Does not fit the caller's buffer.
|
||||
ResponseTooLarge,
|
||||
/// DoH: status other than 200.
|
||||
HttpStatus,
|
||||
/// DoH: content-type other than application/dns-message.
|
||||
HttpContentType,
|
||||
};
|
||||
|
||||
/// This process ran out of something. Never the upstream's fault, so never
|
||||
/// recorded against an endpoint's health.
|
||||
pub const LocalResource = error{
|
||||
OutOfMemory,
|
||||
SystemResources,
|
||||
ProcessFdQuotaExceeded,
|
||||
SystemFdQuotaExceeded,
|
||||
/// A caller-supplied buffer cannot hold even a query.
|
||||
BufferTooSmall,
|
||||
Unexpected,
|
||||
};
|
||||
|
||||
pub const Cancellation = error{Canceled};
|
||||
|
||||
pub const ExchangeError = PeerFault || LocalResource || Cancellation;
|
||||
|
||||
pub const Group = enum { peer_fault, local_resource, cancellation };
|
||||
|
||||
/// Exhaustive switch over `ExchangeError` — no `else` arm. A new error member
|
||||
/// must break the build here, so no failure can silently land in the wrong
|
||||
/// group.
|
||||
pub fn group(err: ExchangeError) Group {
|
||||
return switch (err) {
|
||||
error.ConnectFailed,
|
||||
error.TlsFailed,
|
||||
error.SendFailed,
|
||||
error.ReceiveFailed,
|
||||
error.Timeout,
|
||||
error.BadResponse,
|
||||
error.ResponseMismatch,
|
||||
error.ResponseTooLarge,
|
||||
error.HttpStatus,
|
||||
error.HttpContentType,
|
||||
=> .peer_fault,
|
||||
|
||||
error.OutOfMemory,
|
||||
error.SystemResources,
|
||||
error.ProcessFdQuotaExceeded,
|
||||
error.SystemFdQuotaExceeded,
|
||||
error.BufferTooSmall,
|
||||
error.Unexpected,
|
||||
=> .local_resource,
|
||||
|
||||
error.Canceled => .cancellation,
|
||||
};
|
||||
}
|
||||
|
||||
/// Folds a foreign stdlib error into `ExchangeError` when its name matches a
|
||||
/// `LocalResource` or `Cancellation` member; `null` means the caller should
|
||||
/// classify the error as a peer fault from the phase it occurred in.
|
||||
///
|
||||
/// This is the only place a foreign error set is folded in. Everywhere else
|
||||
/// the call site names the peer fault it means, because the call site is what
|
||||
/// knows whether it was connecting, sending or receiving.
|
||||
pub fn mapLocal(err: anyerror) ?ExchangeError {
|
||||
return switch (err) {
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
error.SystemResources => error.SystemResources,
|
||||
error.ProcessFdQuotaExceeded => error.ProcessFdQuotaExceeded,
|
||||
error.SystemFdQuotaExceeded => error.SystemFdQuotaExceeded,
|
||||
error.BufferTooSmall => error.BufferTooSmall,
|
||||
error.Unexpected => error.Unexpected,
|
||||
error.Canceled => error.Canceled,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// A thing that sends one DNS message and returns one validated DNS message.
|
||||
/// Implemented by DohClient, DotClient, Pool, and test fakes.
|
||||
pub const Client = struct {
|
||||
ptr: *anyopaque,
|
||||
exchangeFn: *const fn (
|
||||
ptr: *anyopaque,
|
||||
io: std.Io,
|
||||
query: []const u8,
|
||||
response_buf: []u8,
|
||||
) ExchangeError![]u8,
|
||||
|
||||
/// Returns a prefix of `response_buf`. The returned message has already
|
||||
/// passed `validateResponse` against `query`.
|
||||
pub fn exchange(
|
||||
self: Client,
|
||||
io: std.Io,
|
||||
query: []const u8,
|
||||
response_buf: []u8,
|
||||
) ExchangeError![]u8 {
|
||||
return self.exchangeFn(self.ptr, io, query, response_buf);
|
||||
}
|
||||
};
|
||||
|
||||
pub const ValidateError = error{ BadResponse, ResponseMismatch };
|
||||
|
||||
/// RFC 9619: exactly one question on both sides. RFC 4343: names compare
|
||||
/// case-insensitively, so a case-mangling (0x20) upstream still matches. Does
|
||||
/// not inspect the answer section — content policy is not this layer's
|
||||
/// business.
|
||||
pub fn validateResponse(query: []const u8, response: []const u8) ValidateError!void {
|
||||
const resp = packet.parse(response) catch return error.BadResponse;
|
||||
if (!resp.header.flags.qr) return error.BadResponse;
|
||||
if (resp.header.qdcount != 1) return error.BadResponse;
|
||||
|
||||
// A malformed query here is a bug in this process, not in the upstream.
|
||||
// It still may not crash, so it reports the same structural error.
|
||||
const req = packet.parse(query) catch return error.BadResponse;
|
||||
if (req.header.qdcount != 1) return error.BadResponse;
|
||||
|
||||
if (resp.header.id != req.header.id) return error.ResponseMismatch;
|
||||
|
||||
const rq = packet.firstQuestion(resp) orelse return error.BadResponse;
|
||||
const qq = packet.firstQuestion(req) orelse return error.BadResponse;
|
||||
if (rq.qtype != qq.qtype) return error.ResponseMismatch;
|
||||
if (rq.qclass != qq.qclass) return error.ResponseMismatch;
|
||||
if (!name.eqlIgnoreCase(rq.name, qq.name)) return error.ResponseMismatch;
|
||||
}
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
test "parse a DoH url with an explicit path" {
|
||||
const e = try Endpoint.parse("https://cloudflare-dns.com/dns-query");
|
||||
try testing.expectEqual(Scheme.doh, e.scheme);
|
||||
try testing.expectEqualStrings("cloudflare-dns.com", e.host);
|
||||
try testing.expectEqual(@as(u16, 443), e.port);
|
||||
try testing.expectEqualStrings("/dns-query", e.path);
|
||||
try testing.expectEqualStrings("https://cloudflare-dns.com/dns-query", e.url);
|
||||
}
|
||||
|
||||
test "parse preserves a non-default DoH path" {
|
||||
const e = try Endpoint.parse("https://dns.example/x");
|
||||
try testing.expectEqualStrings("dns.example", e.host);
|
||||
try testing.expectEqualStrings("/x", e.path);
|
||||
}
|
||||
|
||||
test "parse defaults the DoH path" {
|
||||
const e = try Endpoint.parse("https://dns.example");
|
||||
try testing.expectEqualStrings("dns.example", e.host);
|
||||
try testing.expectEqual(@as(u16, 443), e.port);
|
||||
try testing.expectEqualStrings(doh_default_path, e.path);
|
||||
}
|
||||
|
||||
test "parse a DoT url with an explicit port" {
|
||||
const e = try Endpoint.parse("tls://dns.google:853");
|
||||
try testing.expectEqual(Scheme.dot, e.scheme);
|
||||
try testing.expectEqualStrings("dns.google", e.host);
|
||||
try testing.expectEqual(@as(u16, 853), e.port);
|
||||
}
|
||||
|
||||
test "parse defaults the DoT port" {
|
||||
const e = try Endpoint.parse("tls://dns.google");
|
||||
try testing.expectEqual(Scheme.dot, e.scheme);
|
||||
try testing.expectEqualStrings("dns.google", e.host);
|
||||
try testing.expectEqual(@as(u16, dot_default_port), e.port);
|
||||
}
|
||||
|
||||
test "parse strips IPv6 brackets and keeps the port" {
|
||||
const e = try Endpoint.parse("https://[2606:4700:4700::1111]:8443/dns-query");
|
||||
try testing.expectEqualStrings("2606:4700:4700::1111", e.host);
|
||||
try testing.expectEqual(@as(u16, 8443), e.port);
|
||||
try testing.expectEqualStrings("/dns-query", e.path);
|
||||
}
|
||||
|
||||
test "parse rejects an unsupported scheme" {
|
||||
try testing.expectError(error.UnsupportedScheme, Endpoint.parse("udp://1.1.1.1:53"));
|
||||
try testing.expectError(error.UnsupportedScheme, Endpoint.parse("http://dns.example/"));
|
||||
try testing.expectError(error.UnsupportedScheme, Endpoint.parse("1.1.1.1"));
|
||||
}
|
||||
|
||||
test "parse rejects an empty host" {
|
||||
try testing.expectError(error.MissingHost, Endpoint.parse("https://"));
|
||||
try testing.expectError(error.MissingHost, Endpoint.parse("https://:443/dns-query"));
|
||||
try testing.expectError(error.MissingHost, Endpoint.parse("tls://"));
|
||||
}
|
||||
|
||||
test "parse rejects a bad port" {
|
||||
try testing.expectError(error.BadPort, Endpoint.parse("https://h:99999/"));
|
||||
try testing.expectError(error.BadPort, Endpoint.parse("https://h:/"));
|
||||
try testing.expectError(error.BadPort, Endpoint.parse("https://h:abc/"));
|
||||
try testing.expectError(error.BadPort, Endpoint.parse("tls://[::1]:70000"));
|
||||
}
|
||||
|
||||
test "parse rejects a malformed url" {
|
||||
try testing.expectError(error.BadUrl, Endpoint.parse("https://[::1"));
|
||||
try testing.expectError(error.BadUrl, Endpoint.parse("https://[::1]x"));
|
||||
// A DoT endpoint has no request path.
|
||||
try testing.expectError(error.BadUrl, Endpoint.parse("tls://dns.google/dns-query"));
|
||||
// A bare trailing slash is not a path, so it is accepted.
|
||||
try testing.expectEqual(Scheme.dot, (try Endpoint.parse("tls://dns.google/")).scheme);
|
||||
}
|
||||
|
||||
test "parse rejects userinfo" {
|
||||
try testing.expectError(error.BadUrl, Endpoint.parse("https://user@host/"));
|
||||
try testing.expectError(error.BadUrl, Endpoint.parse("https://user:pass@host/dns-query"));
|
||||
try testing.expectError(error.BadUrl, Endpoint.parse("tls://user@dns.google:853"));
|
||||
}
|
||||
|
||||
test "parse rejects a query string" {
|
||||
try testing.expectError(error.BadUrl, Endpoint.parse("https://host?x"));
|
||||
try testing.expectError(error.BadUrl, Endpoint.parse("https://host/dns-query?x=1"));
|
||||
try testing.expectError(error.BadUrl, Endpoint.parse("tls://dns.google/?x"));
|
||||
}
|
||||
|
||||
test "parse rejects a fragment" {
|
||||
try testing.expectError(error.BadUrl, Endpoint.parse("https://host#f"));
|
||||
try testing.expectError(error.BadUrl, Endpoint.parse("https://host/dns-query#f"));
|
||||
try testing.expectError(error.BadUrl, Endpoint.parse("tls://dns.google#f"));
|
||||
}
|
||||
|
||||
test "the three error groups are disjoint" {
|
||||
const sets = .{ PeerFault, LocalResource, Cancellation };
|
||||
inline for (sets, 0..) |a, i| {
|
||||
inline for (sets, 0..) |b, j| {
|
||||
if (i >= j) continue;
|
||||
inline for (@typeInfo(a).error_set.?) |member_a| {
|
||||
inline for (@typeInfo(b).error_set.?) |member_b| {
|
||||
try testing.expect(!std.mem.eql(u8, member_a.name, member_b.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every member of the union belongs to exactly one group, which `group`
|
||||
// proves by being an exhaustive switch. Assert the counts line up so a
|
||||
// member added to two sets at once cannot pass unnoticed.
|
||||
const total = @typeInfo(PeerFault).error_set.?.len +
|
||||
@typeInfo(LocalResource).error_set.?.len +
|
||||
@typeInfo(Cancellation).error_set.?.len;
|
||||
try testing.expectEqual(total, @typeInfo(ExchangeError).error_set.?.len);
|
||||
}
|
||||
|
||||
test "group classifies each member" {
|
||||
try testing.expectEqual(Group.peer_fault, group(error.Timeout));
|
||||
try testing.expectEqual(Group.peer_fault, group(error.HttpContentType));
|
||||
try testing.expectEqual(Group.local_resource, group(error.OutOfMemory));
|
||||
try testing.expectEqual(Group.local_resource, group(error.BufferTooSmall));
|
||||
try testing.expectEqual(Group.cancellation, group(error.Canceled));
|
||||
}
|
||||
|
||||
test "mapLocal folds only local and cancellation errors" {
|
||||
try testing.expectEqual(@as(?ExchangeError, error.OutOfMemory), mapLocal(error.OutOfMemory));
|
||||
try testing.expectEqual(@as(?ExchangeError, error.Canceled), mapLocal(error.Canceled));
|
||||
try testing.expectEqual(@as(?ExchangeError, error.Unexpected), mapLocal(error.Unexpected));
|
||||
try testing.expectEqual(@as(?ExchangeError, null), mapLocal(error.ConnectionRefused));
|
||||
try testing.expectEqual(@as(?ExchangeError, null), mapLocal(error.TlsInitializationFailed));
|
||||
}
|
||||
|
||||
test "a fake client satisfies the Client interface" {
|
||||
const Fake = struct {
|
||||
calls: usize = 0,
|
||||
|
||||
fn exchangeFn(
|
||||
ptr: *anyopaque,
|
||||
io: std.Io,
|
||||
query: []const u8,
|
||||
response_buf: []u8,
|
||||
) ExchangeError![]u8 {
|
||||
_ = io;
|
||||
const self: *@This() = @ptrCast(@alignCast(ptr));
|
||||
self.calls += 1;
|
||||
if (query.len > response_buf.len) return error.ResponseTooLarge;
|
||||
@memcpy(response_buf[0..query.len], query);
|
||||
return response_buf[0..query.len];
|
||||
}
|
||||
|
||||
fn client(self: *@This()) Client {
|
||||
return .{ .ptr = self, .exchangeFn = exchangeFn };
|
||||
}
|
||||
};
|
||||
|
||||
var fake: Fake = .{};
|
||||
var buf: [16]u8 = undefined;
|
||||
const echoed = try fake.client().exchange(undefined, "hello", &buf);
|
||||
try testing.expectEqualStrings("hello", echoed);
|
||||
try testing.expectEqual(@as(usize, 1), fake.calls);
|
||||
}
|
||||
|
||||
/// A query for example.com A: 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";
|
||||
|
||||
/// 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";
|
||||
|
||||
test "validateResponse accepts a matching pair" {
|
||||
try validateResponse(query_bytes, response_bytes);
|
||||
}
|
||||
|
||||
test "validateResponse accepts a mixed-case question name" {
|
||||
const mangled =
|
||||
"\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";
|
||||
try validateResponse(query_bytes, mangled);
|
||||
}
|
||||
|
||||
test "validateResponse rejects a wrong id" {
|
||||
var bytes: [response_bytes.len]u8 = response_bytes.*;
|
||||
packet.setId(&bytes, 0x4321);
|
||||
try testing.expectError(error.ResponseMismatch, validateResponse(query_bytes, &bytes));
|
||||
}
|
||||
|
||||
test "validateResponse rejects a different qtype" {
|
||||
const aaaa =
|
||||
"\x12\x34\x81\x80\x00\x01\x00\x00\x00\x00\x00\x00" ++
|
||||
"\x07example\x03com\x00\x00\x1c\x00\x01";
|
||||
try testing.expectError(error.ResponseMismatch, validateResponse(query_bytes, aaaa));
|
||||
}
|
||||
|
||||
test "validateResponse rejects a different question name" {
|
||||
const other =
|
||||
"\x12\x34\x81\x80\x00\x01\x00\x00\x00\x00\x00\x00" ++
|
||||
"\x07example\x03org\x00\x00\x01\x00\x01";
|
||||
try testing.expectError(error.ResponseMismatch, validateResponse(query_bytes, other));
|
||||
}
|
||||
|
||||
test "validateResponse rejects a response with QR clear" {
|
||||
try testing.expectError(error.BadResponse, validateResponse(query_bytes, query_bytes));
|
||||
}
|
||||
|
||||
test "validateResponse rejects a response with QDCOUNT 0" {
|
||||
const no_question = "\x12\x34\x81\x80\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
try testing.expectError(error.BadResponse, validateResponse(query_bytes, no_question));
|
||||
}
|
||||
|
||||
test "validateResponse rejects a response with QDCOUNT 2" {
|
||||
const two =
|
||||
"\x12\x34\x81\x80\x00\x02\x00\x00\x00\x00\x00\x00" ++
|
||||
"\x07example\x03com\x00\x00\x01\x00\x01" ++
|
||||
"\x07example\x03com\x00\x00\x01\x00\x01";
|
||||
try testing.expectError(error.BadResponse, validateResponse(query_bytes, two));
|
||||
}
|
||||
|
||||
test "validateResponse rejects truncated garbage" {
|
||||
try testing.expectError(error.BadResponse, validateResponse(query_bytes, "\x12\x34\x81"));
|
||||
try testing.expectError(error.BadResponse, validateResponse(query_bytes, ""));
|
||||
try testing.expectError(
|
||||
error.BadResponse,
|
||||
validateResponse(query_bytes, response_bytes[0 .. response_bytes.len - 3]),
|
||||
);
|
||||
}
|
||||
|
||||
test "validateResponse rejects a query that does not carry exactly one question" {
|
||||
const no_question = "\x12\x34\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
try testing.expectError(error.BadResponse, validateResponse(no_question, response_bytes));
|
||||
try testing.expectError(error.BadResponse, validateResponse("\x12\x34", response_bytes));
|
||||
}
|
||||
Reference in New Issue
Block a user