milestone 18: collapse duplicated infrastructure into shared listener core, crud list helper, resource shells, transport race, name and line helpers, ui modules
CI / test (push) Successful in 1m22s
CI / test-aarch64 (push) Successful in 5m6s
CI / frontend (push) Successful in 45s
CI / cross (push) Successful in 7m53s
CI / docker (push) Failing after 1h10m57s

This commit is contained in:
2026-08-07 18:20:30 +02:00
parent c50c6d285a
commit 6f67940995
82 changed files with 3167 additions and 3114 deletions
+187 -4
View File
@@ -20,6 +20,13 @@ pub const media_type = "application/dns-message";
pub const min_request_buf = 512;
pub const min_transfer_buf = 1024;
/// What `nxdns run` gives every DoH client, and what `nxdns check` probes an
/// upstream with. They live here rather than beside either caller because a
/// probe that used a different buffer than the running server would answer a
/// question nobody asked.
pub const default_request_buf_len = 1024;
pub const default_transfer_buf_len = 4096;
pub const DohClient = struct {
/// Caller-owned; shared across endpoints, pools connections.
http: *std.http.Client,
@@ -110,11 +117,11 @@ pub const DohClient = struct {
defer req.deinit();
req.sendBodyComplete(self.request_buf[0..query.len]) catch |err|
return mapError(err, .send);
return mapError(sendCause(&req, err), .send);
// An empty redirect buffer is legal under `.not_allowed`: a redirect
// is an error before the location is ever read.
var resp = req.receiveHead(&.{}) catch |err| return mapError(err, .receive);
var resp = req.receiveHead(&.{}) catch |err| return mapError(headCause(&req, err), .receive);
if (resp.head.status != .ok) return error.HttpStatus;
// `head.content_type` points into memory that `resp.reader` invalidates,
@@ -129,7 +136,7 @@ pub const DohClient = struct {
var ended = false;
while (len < response_buf.len) {
const n = body.readSliceShort(response_buf[len..]) catch |err|
return mapError(err, .receive);
return mapError(bodyCause(&resp, err), .receive);
len += n;
if (n == 0) {
ended = true;
@@ -140,7 +147,8 @@ pub const DohClient = struct {
// `response_buf` filled exactly. One more read separates a message
// that fits from one that was cut off.
var probe: [1]u8 = undefined;
const n = body.readSliceShort(&probe) catch |err| return mapError(err, .receive);
const n = body.readSliceShort(&probe) catch |err|
return mapError(bodyCause(&resp, err), .receive);
if (n != 0) return error.ResponseTooLarge;
}
@@ -166,6 +174,48 @@ fn mapError(err: anyerror, phase: Phase) transport.ExchangeError {
};
}
const Connection = std.http.Client.Connection;
const Request = std.http.Client.Request;
const Response = std.http.Client.Response;
/// `std.Io.Writer` collapses every send failure to `error.WriteFailed` and
/// stashes the cause on the connection's socket writer. Unwrapping it is what
/// keeps `error.Canceled` and the local resource errors out of the peer fault
/// group, exactly as `concreteWrite` does for DoT.
fn sendCause(req: *const Request, err: anyerror) anyerror {
if (err != error.WriteFailed) return err;
const connection = req.connection orelse return err;
return connection.stream_writer.err orelse err;
}
/// `receiveHead` collapses a transport failure to `error.ReadFailed` and names
/// `Connection.getReadError` as the accessor for the concrete cause.
fn headCause(req: *const Request, err: anyerror) anyerror {
if (err != error.ReadFailed) return err;
const connection = req.connection orelse return err;
return readCause(connection, err);
}
/// A body read reports two different kinds of failure through the same
/// `error.ReadFailed`. An HTTP framing fault lands on the response, and only a
/// read that never reached the framing leaves the connection's cause, so the
/// response is consulted first.
fn bodyCause(resp: *const Response, err: anyerror) anyerror {
if (err != error.ReadFailed) return err;
if (resp.bodyErr()) |cause| return cause;
const connection = resp.request.connection orelse return err;
return readCause(connection, err);
}
/// `Connection.getReadError` reads the socket reader's stashed cause with `.?`.
/// On a plain connection that is its only source, so calling it with nothing
/// stashed would panic rather than return null; the guard keeps this unwrap
/// total on the path this client can reach without TLS.
fn readCause(connection: *const Connection, err: anyerror) anyerror {
if (connection.protocol == .plain and connection.stream_reader.err == null) return err;
return connection.getReadError() orelse err;
}
/// RFC 8484 §6: the response media type is `application/dns-message`. The
/// header may carry parameters (`; charset=…`) and the type is case-insensitive
/// per RFC 9110 §8.3.1.
@@ -269,3 +319,136 @@ test "mapError maps remaining errors by phase" {
try testing.expectEqual(error.ReceiveFailed, mapError(error.ReadFailed, .receive));
try testing.expectEqual(error.ReceiveFailed, mapError(error.HttpHeadersInvalid, .receive));
}
/// Only the fields the unwrap helpers read are set. The rest of a `Connection`
/// is two buffered streams, a host name and a pool node, none of which the
/// helpers touch.
///
/// `.plain` on purpose: `Connection.getReadError` reaches a TLS connection's
/// stashed cause through `@fieldParentPtr`, which on a stub would read memory
/// that was never a `Tls`. What the test is about — that the accessor is
/// consulted at all — is the same on both protocols.
fn stubConnection(
read_err: ?std.Io.net.Stream.Reader.Error,
write_err: ?std.Io.net.Stream.Writer.Error,
) Connection {
var connection: Connection = undefined;
connection.protocol = .plain;
connection.stream_reader.err = read_err;
connection.stream_writer.err = write_err;
return connection;
}
fn stubRequest(connection: *Connection, body_err: ?std.http.Reader.BodyError) Request {
var req: Request = undefined;
req.connection = connection;
req.reader.body_err = body_err;
return req;
}
test "the send unwrap keeps a cancelled write out of the peer fault group" {
var connection = stubConnection(null, error.Canceled);
var req = stubRequest(&connection, null);
const mapped = mapError(sendCause(&req, error.WriteFailed), .send);
try testing.expectEqual(transport.ExchangeError.Canceled, mapped);
try testing.expectEqual(transport.Group.cancellation, transport.group(mapped));
}
test "the send unwrap keeps a local resource write failure out of the peer fault group" {
var connection = stubConnection(null, error.SystemResources);
var req = stubRequest(&connection, null);
const mapped = mapError(sendCause(&req, error.WriteFailed), .send);
try testing.expectEqual(transport.ExchangeError.SystemResources, mapped);
try testing.expectEqual(transport.Group.local_resource, transport.group(mapped));
}
test "the send unwrap reports a peer side cause as a send fault" {
var connection = stubConnection(null, error.ConnectionResetByPeer);
var req = stubRequest(&connection, null);
try testing.expectEqual(
transport.ExchangeError.SendFailed,
mapError(sendCause(&req, error.WriteFailed), .send),
);
}
test "the head unwrap keeps a local resource read failure out of the peer fault group" {
var connection = stubConnection(error.SystemResources, null);
var req = stubRequest(&connection, null);
const mapped = mapError(headCause(&req, error.ReadFailed), .receive);
try testing.expectEqual(transport.ExchangeError.SystemResources, mapped);
try testing.expectEqual(transport.Group.local_resource, transport.group(mapped));
var canceled = stubConnection(error.Canceled, null);
var canceled_req = stubRequest(&canceled, null);
try testing.expectEqual(
transport.ExchangeError.Canceled,
mapError(headCause(&canceled_req, error.ReadFailed), .receive),
);
}
test "the head unwrap reports a peer side cause as a receive fault" {
var connection = stubConnection(error.ConnectionResetByPeer, null);
var req = stubRequest(&connection, null);
try testing.expectEqual(
transport.ExchangeError.ReceiveFailed,
mapError(headCause(&req, error.ReadFailed), .receive),
);
}
test "the body unwrap keeps a cancelled read out of the peer fault group" {
var connection = stubConnection(error.Canceled, null);
var req = stubRequest(&connection, null);
const resp: Response = .{ .request = &req, .head = undefined };
const mapped = mapError(bodyCause(&resp, error.ReadFailed), .receive);
try testing.expectEqual(transport.ExchangeError.Canceled, mapped);
try testing.expectEqual(transport.Group.cancellation, transport.group(mapped));
}
test "the body unwrap prefers an http framing fault over the connection" {
// A truncated chunk is the peer's doing and the connection carries no
// cause at all, so reading the connection first would report the wrong
// thing on the one path where both could be set.
var connection = stubConnection(null, null);
var req = stubRequest(&connection, error.HttpChunkTruncated);
const resp: Response = .{ .request = &req, .head = undefined };
try testing.expectEqual(error.HttpChunkTruncated, bodyCause(&resp, error.ReadFailed));
try testing.expectEqual(
transport.ExchangeError.ReceiveFailed,
mapError(bodyCause(&resp, error.ReadFailed), .receive),
);
}
test "the unwraps report the collapsed error when no cause was stored" {
var connection = stubConnection(null, null);
var req = stubRequest(&connection, null);
const resp: Response = .{ .request = &req, .head = undefined };
try testing.expectEqual(error.WriteFailed, sendCause(&req, error.WriteFailed));
try testing.expectEqual(error.ReadFailed, headCause(&req, error.ReadFailed));
try testing.expectEqual(error.ReadFailed, bodyCause(&resp, error.ReadFailed));
try testing.expectEqual(
transport.ExchangeError.SendFailed,
mapError(sendCause(&req, error.WriteFailed), .send),
);
try testing.expectEqual(
transport.ExchangeError.ReceiveFailed,
mapError(bodyCause(&resp, error.ReadFailed), .receive),
);
}
test "the unwraps pass a non-collapsed error through untouched" {
// A stashed cause belongs to `error.ReadFailed` / `error.WriteFailed`. Any
// other error already names itself, so the stash must not be read over it.
var connection = stubConnection(error.Canceled, error.Canceled);
var req = stubRequest(&connection, error.HttpChunkInvalid);
const resp: Response = .{ .request = &req, .head = undefined };
try testing.expectEqual(error.EndOfStream, sendCause(&req, error.EndOfStream));
try testing.expectEqual(error.HttpHeadersInvalid, headCause(&req, error.HttpHeadersInvalid));
try testing.expectEqual(error.EndOfStream, bodyCause(&resp, error.EndOfStream));
try testing.expectEqual(
transport.ExchangeError.ReceiveFailed,
mapError(headCause(&req, error.HttpHeadersInvalid), .receive),
);
}