milestone 5: blocklist filtering, local records and conditional forwarding

This commit is contained in:
2026-08-01 16:43:55 +02:00
parent 3baf5d6581
commit 59d94df722
29 changed files with 10257 additions and 81 deletions
+2 -2
View File
@@ -199,11 +199,11 @@ fn tcpQuery(io: std.Io, address: net.IpAddress, id: u16) anyerror!void {
var query_buf: [query_bytes.len]u8 = undefined;
const query = queryWithId(&query_buf, id);
try writer.interface.writeAll(&tcp_server.framePrefix(@intCast(query.len)));
try writer.interface.writeAll(&transport.framePrefix(@intCast(query.len)));
try writer.interface.writeAll(query);
try writer.interface.flush();
const len = tcp_server.parsePrefix((try reader.interface.takeArray(tcp_server.prefix_len)).*);
const len = transport.parsePrefix((try reader.interface.takeArray(transport.prefix_len)).*);
try expectAnswer(try reader.interface.take(len), id);
}
+6 -34
View File
@@ -19,9 +19,6 @@ const transport = @import("../upstream/transport.zig");
const log = std.log.scoped(.tcp_server);
/// RFC 1035 §4.2.2: the message length prefix is two bytes, big-endian.
pub const prefix_len = 2;
/// The stream buffers only stage the framing bytes. A message longer than this
/// is read straight into `Conn.query` and written straight from `Conn.reply`,
/// so making them larger would buy nothing.
@@ -220,7 +217,7 @@ pub const TcpServer = struct {
const budget = self.options.idle_timeout;
while (true) {
var prefix: [prefix_len]u8 = undefined;
var prefix: [transport.prefix_len]u8 = undefined;
var got: usize = 0;
switch (race(io, budget, readPrefix, .{ &reader.interface, &prefix, &got })) {
.ok => {},
@@ -238,14 +235,14 @@ pub const TcpServer = struct {
// A client that closes between messages has finished asking, which
// is the normal end of a connection, not a failure.
if (got == 0) return;
if (got != prefix_len) {
if (got != transport.prefix_len) {
bump(&self.stats.connection_errors);
return;
}
// RFC 1035 §4.2.2 gives no meaning to a zero-length message, and
// the prefix is a u16 so it can never exceed `max_message_len`.
const len = parsePrefix(prefix);
const len = transport.parsePrefix(prefix);
if (len == 0) {
bump(&self.stats.connection_errors);
return;
@@ -267,7 +264,7 @@ pub const TcpServer = struct {
.reply => |b| b,
};
const out = framePrefix(@intCast(bytes.len));
const out = transport.framePrefix(@intCast(bytes.len));
switch (race(io, budget, writeReply, .{ &writer.interface, &out, bytes })) {
.ok => {},
.canceled => return,
@@ -333,17 +330,6 @@ pub const TcpServer = struct {
}
};
/// RFC 1035 §4.2.2: the message length as a 2-byte big-endian integer.
pub fn framePrefix(len: u16) [prefix_len]u8 {
var out: [prefix_len]u8 = undefined;
std.mem.writeInt(u16, &out, len, .big);
return out;
}
pub fn parsePrefix(bytes: [prefix_len]u8) u16 {
return std.mem.readInt(u16, &bytes, .big);
}
/// The capacity rule, without the mutex, so it is testable without a backend.
fn firstFree(conns: []const TcpServer.Conn) ?usize {
for (conns, 0..) |*conn, index| {
@@ -402,7 +388,7 @@ fn expire(io: std.Io, budget: std.Io.Clock.Duration) std.Io.Cancelable!void {
/// `readSliceShort` rather than `readSliceAll`: a zero-length read is a client
/// that closed cleanly between messages, and only a partial prefix is an error.
fn readPrefix(reader: *std.Io.Reader, buf: *[prefix_len]u8, out_len: *usize) anyerror!void {
fn readPrefix(reader: *std.Io.Reader, buf: *[transport.prefix_len]u8, out_len: *usize) anyerror!void {
out_len.* = try reader.readSliceShort(buf);
}
@@ -410,7 +396,7 @@ fn readBody(reader: *std.Io.Reader, buf: []u8) anyerror!void {
return reader.readSliceAll(buf);
}
fn writeReply(writer: *std.Io.Writer, prefix: *const [prefix_len]u8, bytes: []const u8) anyerror!void {
fn writeReply(writer: *std.Io.Writer, prefix: *const [transport.prefix_len]u8, bytes: []const u8) anyerror!void {
try writer.writeAll(prefix);
try writer.writeAll(bytes);
try writer.flush();
@@ -422,20 +408,6 @@ fn bump(counter: *std.atomic.Value(u64)) void {
const testing = std.testing;
test "the length prefix is big-endian and round-trips" {
try testing.expectEqualSlices(u8, &.{ 0x00, 0x00 }, &framePrefix(0));
try testing.expectEqualSlices(u8, &.{ 0x01, 0x00 }, &framePrefix(256));
try testing.expectEqualSlices(u8, &.{ 0xff, 0xff }, &framePrefix(65535));
for ([_]u16{ 0, 1, 12, 512, 4096, 65534, 65535 }) |len| {
try testing.expectEqual(len, parsePrefix(framePrefix(len)));
}
}
test "the prefix ceiling is the message ceiling" {
try testing.expectEqual(@as(u16, transport.max_message_len), parsePrefix(.{ 0xff, 0xff }));
}
fn testConns(count: usize) ![]TcpServer.Conn {
const conns = try testing.allocator.alloc(TcpServer.Conn, count);
for (conns) |*conn| conn.state = .free;
+3 -3
View File
@@ -117,11 +117,11 @@ fn twoQueriesOnOneConnection(io: std.Io, address: net.IpAddress) anyerror!void {
var writer = stream.writer(io, &write_buf);
for (0..2) |_| {
try writer.interface.writeAll(&tcp_server.framePrefix(@intCast(query_bytes.len)));
try writer.interface.writeAll(&transport.framePrefix(@intCast(query_bytes.len)));
try writer.interface.writeAll(query_bytes);
try writer.interface.flush();
const len = tcp_server.parsePrefix((try reader.interface.takeArray(tcp_server.prefix_len)).*);
const len = transport.parsePrefix((try reader.interface.takeArray(transport.prefix_len)).*);
try expectAnswersQuery(try reader.interface.take(len));
}
}
@@ -246,7 +246,7 @@ fn sendZeroLength(io: std.Io, address: net.IpAddress) anyerror!void {
var write_buf: [64]u8 = undefined;
var writer = stream.writer(io, &write_buf);
try writer.interface.writeAll(&tcp_server.framePrefix(0));
try writer.interface.writeAll(&transport.framePrefix(0));
try writer.interface.flush();
var read_buf: [64]u8 = undefined;