milestone 18: collapse duplicated infrastructure into shared listener core, crud list helper, resource shells, transport race, name and line helpers, ui modules
This commit is contained in:
@@ -135,13 +135,13 @@ pub const ForwardClient = struct {
|
||||
|
||||
const socket = local.bind(io, .{ .mode = .dgram }) catch |err| {
|
||||
log.debug("forward resolver: udp bind failed: {s}", .{@errorName(err)});
|
||||
return mapPhase(err, error.ConnectFailed);
|
||||
return transport.mapPhase(err, error.ConnectFailed);
|
||||
};
|
||||
defer closeSocket(io, &socket);
|
||||
defer transport.closeBlocked(io, &socket);
|
||||
|
||||
socket.send(io, &dest, query) catch |err| {
|
||||
log.debug("forward resolver: udp send failed: {s}", .{@errorName(err)});
|
||||
return mapPhase(err, error.SendFailed);
|
||||
return transport.mapPhase(err, error.SendFailed);
|
||||
};
|
||||
|
||||
// A deadline, not a duration: a discarded foreign datagram restarts the
|
||||
@@ -152,7 +152,7 @@ pub const ForwardClient = struct {
|
||||
const msg = socket.receiveTimeout(io, response_buf, deadline) catch |err| switch (err) {
|
||||
error.Timeout => return error.Timeout,
|
||||
error.ConcurrencyUnavailable => return error.SystemResources,
|
||||
else => return mapPhase(err, error.ReceiveFailed),
|
||||
else => return transport.mapPhase(err, error.ReceiveFailed),
|
||||
};
|
||||
|
||||
// Off-path spoofing is the reason the source address is checked at
|
||||
@@ -183,35 +183,16 @@ pub const ForwardClient = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// No stream read or write in 0.16.0 takes a timeout, so the budget is a
|
||||
/// second task and the loser is canceled. `ConnectOptions.timeout` is never
|
||||
/// set: the Threaded backend panics on it (Threaded.zig:12076).
|
||||
/// The read budget bounds the whole TCP exchange through
|
||||
/// `transport.raceWithin`. `ConnectOptions.timeout` is never set: the
|
||||
/// Threaded backend panics on it (Threaded.zig:12076).
|
||||
fn exchangeTcp(
|
||||
self: *ForwardClient,
|
||||
io: std.Io,
|
||||
query: []const u8,
|
||||
response_buf: []u8,
|
||||
) transport.ExchangeError![]u8 {
|
||||
var outcomes: [2]Outcome = undefined;
|
||||
var race: std.Io.Select(Outcome) = .init(io, &outcomes);
|
||||
defer race.cancelDiscard();
|
||||
|
||||
race.concurrent(.exchange, tcpOnce, .{ self, io, query, response_buf }) catch |err| switch (err) {
|
||||
error.ConcurrencyUnavailable => return error.SystemResources,
|
||||
};
|
||||
race.concurrent(.expiry, expire, .{ io, self.read_timeout }) catch |err| switch (err) {
|
||||
error.ConcurrencyUnavailable => return error.SystemResources,
|
||||
};
|
||||
|
||||
switch (try race.await()) {
|
||||
.exchange => |result| return result,
|
||||
.expiry => |result| {
|
||||
// A canceled sleep means this whole task is being torn down,
|
||||
// not that the resolver is slow.
|
||||
try result;
|
||||
return error.Timeout;
|
||||
},
|
||||
}
|
||||
return transport.raceWithin(io, self.read_timeout, tcpOnce, .{ self, io, query, response_buf });
|
||||
}
|
||||
|
||||
fn tcpOnce(
|
||||
@@ -224,9 +205,9 @@ pub const ForwardClient = struct {
|
||||
|
||||
const stream = dest.connect(io, .{ .mode = .stream }) catch |err| {
|
||||
log.debug("forward resolver: tcp connect failed: {s}", .{@errorName(err)});
|
||||
return mapPhase(err, error.ConnectFailed);
|
||||
return transport.mapPhase(err, error.ConnectFailed);
|
||||
};
|
||||
defer closeStream(io, &stream);
|
||||
defer transport.closeBlocked(io, &stream);
|
||||
|
||||
const split = self.frame_buf.len / 2;
|
||||
var stream_writer = stream.writer(io, self.frame_buf[0..split]);
|
||||
@@ -257,15 +238,6 @@ pub const ForwardClient = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const Outcome = union(enum) {
|
||||
exchange: transport.ExchangeError![]u8,
|
||||
expiry: std.Io.Cancelable!void,
|
||||
};
|
||||
|
||||
fn expire(io: std.Io, duration: std.Io.Clock.Duration) std.Io.Cancelable!void {
|
||||
return duration.sleep(io);
|
||||
}
|
||||
|
||||
/// The local address a datagram to `dest` is sent from: same family, port
|
||||
/// chosen by the kernel.
|
||||
fn wildcardFor(dest: net.IpAddress) net.IpAddress {
|
||||
@@ -275,25 +247,6 @@ fn wildcardFor(dest: net.IpAddress) net.IpAddress {
|
||||
};
|
||||
}
|
||||
|
||||
/// The TCP budget cancels the exchange task. The next cancelable `Io` call in
|
||||
/// the `defer` chain would then return `error.Canceled` and skip the close,
|
||||
/// leaking the descriptor, so both closes run with cancellation blocked.
|
||||
fn closeStream(io: std.Io, stream: *const net.Stream) void {
|
||||
const prev = io.swapCancelProtection(.blocked);
|
||||
defer _ = io.swapCancelProtection(prev);
|
||||
stream.close(io);
|
||||
}
|
||||
|
||||
fn closeSocket(io: std.Io, socket: *const net.Socket) void {
|
||||
const prev = io.swapCancelProtection(.blocked);
|
||||
defer _ = io.swapCancelProtection(prev);
|
||||
socket.close(io);
|
||||
}
|
||||
|
||||
fn mapPhase(err: anyerror, phase: transport.PeerFault) transport.ExchangeError {
|
||||
return transport.mapLocal(err) orelse phase;
|
||||
}
|
||||
|
||||
/// `Io.Writer` collapses everything to `error.WriteFailed` and stashes the
|
||||
/// cause. Unwrapping it is what keeps `error.Canceled` and the local resource
|
||||
/// errors out of the peer fault group.
|
||||
@@ -302,7 +255,7 @@ fn sendFailure(stream_writer: *const net.Stream.Writer, err: anyerror) transport
|
||||
stream_writer.err.?
|
||||
else
|
||||
err;
|
||||
return mapPhase(cause, error.SendFailed);
|
||||
return transport.mapPhase(cause, error.SendFailed);
|
||||
}
|
||||
|
||||
fn receiveFailure(stream_reader: *const net.Stream.Reader, err: anyerror) transport.ExchangeError {
|
||||
@@ -310,7 +263,7 @@ fn receiveFailure(stream_reader: *const net.Stream.Reader, err: anyerror) transp
|
||||
stream_reader.err.?
|
||||
else
|
||||
err;
|
||||
return mapPhase(cause, error.ReceiveFailed);
|
||||
return transport.mapPhase(cause, error.ReceiveFailed);
|
||||
}
|
||||
|
||||
const testing = std.testing;
|
||||
@@ -408,19 +361,19 @@ test "mapPhase keeps local resource and cancellation errors out of the peer faul
|
||||
for (local) |err| {
|
||||
try testing.expectEqual(
|
||||
transport.Group.local_resource,
|
||||
transport.group(mapPhase(err, error.ReceiveFailed)),
|
||||
transport.group(transport.mapPhase(err, error.ReceiveFailed)),
|
||||
);
|
||||
}
|
||||
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.Canceled,
|
||||
mapPhase(error.Canceled, error.ConnectFailed),
|
||||
transport.mapPhase(error.Canceled, error.ConnectFailed),
|
||||
);
|
||||
|
||||
// A refused connection is the resolver's side, so it stays a peer fault.
|
||||
try testing.expectEqual(
|
||||
transport.ExchangeError.ConnectFailed,
|
||||
mapPhase(error.ConnectionRefused, error.ConnectFailed),
|
||||
transport.mapPhase(error.ConnectionRefused, error.ConnectFailed),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,10 @@ pub const Zones = struct {
|
||||
|
||||
var buf: [types.max_name_len]u8 = undefined;
|
||||
for (rows) |row| {
|
||||
const zone = normalizeName(row.zone, &buf) catch return error.BadZone;
|
||||
// The root is rejected with everything else `normalizeText` refuses:
|
||||
// a zone that forwards everything would bypass the upstream pool
|
||||
// entirely, which is not what conditional forwarding means.
|
||||
const zone = name.normalizeText(row.zone, &buf) catch return error.BadZone;
|
||||
const resolver = validate.parseResolver(row.resolver) catch return error.BadResolver;
|
||||
try spans.append(gpa, .{
|
||||
.offset = names.items.len,
|
||||
@@ -119,27 +122,6 @@ fn suffixMatches(zone: []const u8, domain: []const u8) bool {
|
||||
return domain[start - 1] == '.' and std.mem.eql(u8, domain[start..], zone);
|
||||
}
|
||||
|
||||
const NameError = error{BadName};
|
||||
|
||||
/// Lowercases over ASCII, strips one trailing dot, and checks the result is a
|
||||
/// name `dns.name.fromText` accepts. A byte ≥ 0x80 is rejected because query
|
||||
/// names arrive ASCII-lowercased, so a high byte could never match. The root
|
||||
/// zone is rejected too: a zone that forwards everything would bypass the
|
||||
/// upstream pool entirely, which is not what conditional forwarding means.
|
||||
fn normalizeName(text: []const u8, buf: *[types.max_name_len]u8) NameError![]const u8 {
|
||||
var rest = text;
|
||||
if (rest.len > 0 and rest[rest.len - 1] == '.') rest = rest[0 .. rest.len - 1];
|
||||
if (rest.len == 0 or rest.len > types.max_name_len) return error.BadName;
|
||||
|
||||
for (rest, 0..) |byte, i| {
|
||||
if (byte >= 0x80) return error.BadName;
|
||||
buf[i] = std.ascii.toLower(byte);
|
||||
}
|
||||
const normalized = buf[0..rest.len];
|
||||
_ = name.fromText(normalized) catch return error.BadName;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+4
-23
@@ -52,7 +52,9 @@ pub const Records = struct {
|
||||
|
||||
var buf: [types.max_name_len]u8 = undefined;
|
||||
for (rows) |row| {
|
||||
const owner = normalizeName(row.name, &buf) catch return error.BadRecordName;
|
||||
// A record whose owner can never be matched — a high byte, or the
|
||||
// root — is a configuration error, not a record that never answers.
|
||||
const owner = name.normalizeText(row.name, &buf) catch return error.BadRecordName;
|
||||
const value = try parseValue(row.rtype, row.value);
|
||||
try spans.append(gpa, .{
|
||||
.offset = owners.items.len,
|
||||
@@ -187,27 +189,6 @@ fn rankRun(records: []const Record, wanted: u2) []const Record {
|
||||
return records[start..end];
|
||||
}
|
||||
|
||||
const NameError = error{BadName};
|
||||
|
||||
/// Lowercases over ASCII, strips one trailing dot, and checks the result is a
|
||||
/// name `dns.name.fromText` accepts. A byte ≥ 0x80 is rejected: query names
|
||||
/// arrive ASCII-lowercased, so a high byte here could never be matched and a
|
||||
/// record that can never answer is a configuration error worth reporting. The
|
||||
/// root name is rejected for the same reason — nothing can match it.
|
||||
fn normalizeName(text: []const u8, buf: *[types.max_name_len]u8) NameError![]const u8 {
|
||||
var rest = text;
|
||||
if (rest.len > 0 and rest[rest.len - 1] == '.') rest = rest[0 .. rest.len - 1];
|
||||
if (rest.len == 0 or rest.len > types.max_name_len) return error.BadName;
|
||||
|
||||
for (rest, 0..) |byte, i| {
|
||||
if (byte >= 0x80) return error.BadName;
|
||||
buf[i] = std.ascii.toLower(byte);
|
||||
}
|
||||
const normalized = buf[0..rest.len];
|
||||
_ = name.fromText(normalized) catch return error.BadName;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
fn parseValue(rtype: model.RecordType, text: []const u8) error{BadRecordValue}!Value {
|
||||
switch (rtype) {
|
||||
.a => {
|
||||
@@ -226,7 +207,7 @@ fn parseValue(rtype: model.RecordType, text: []const u8) error{BadRecordValue}!V
|
||||
},
|
||||
.cname => {
|
||||
var buf: [types.max_name_len]u8 = undefined;
|
||||
const target = normalizeName(text, &buf) catch return error.BadRecordValue;
|
||||
const target = name.normalizeText(text, &buf) catch return error.BadRecordValue;
|
||||
return .{ .cname = name.fromText(target) catch return error.BadRecordValue };
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user