milestone 17: real deadlines, validator holes, upstream editor, trusted proxies, contract samples, badvers
CI / test (push) Successful in 1m22s
CI / test-aarch64 (push) Successful in 4m55s
CI / frontend (push) Successful in 39s
CI / cross (push) Successful in 7m57s
CI / docker (push) Failing after 1h10m42s

This commit is contained in:
2026-08-07 17:55:59 +02:00
parent 9b12dbaaa0
commit c50c6d285a
57 changed files with 2926 additions and 126 deletions
+57 -1
View File
@@ -42,7 +42,14 @@ pub const Config = struct {
};
pub const Upstream = struct {
/// Bounds one attempt against one upstream inside the pool's failover loop.
attempt_timeout_ms: u32 = 2500,
/// The forward-zone client's read deadline, and nothing else. It bounds a
/// different subsystem from the two above (`src/local/forward_client.zig`),
/// so no cross-check relates it to them.
read_timeout_ms: u32 = 3000,
/// The whole-exchange budget: every failover attempt together, not one of
/// them. The pool races the entire loop against it.
total_timeout_ms: u32 = 5000,
};
@@ -91,6 +98,39 @@ pub const Web = struct {
/// abuse the limiter defends against (PLAN §10).
api_localhost_exempt: bool = true,
sse_max_connections_per_ip: u16 = 3,
/// Comma-separated IP literals. A request arriving from one of these peers
/// is identified by the last entry of its `X-Forwarded-For` header instead
/// of by the socket peer, so the API limiter and the per-address SSE cap
/// bind the real client rather than the proxy. Empty means no proxy is
/// trusted and the socket peer is always the client. One string rather than
/// a list because the settings codec stores scalars only.
trusted_proxies: []const u8 = "",
};
/// Walks `web.trusted_proxies`. The validator and the web server both read the
/// setting, so what counts as one element — comma-separated, surrounding spaces
/// and tabs trimmed — is spelled out once here. An empty setting yields nothing;
/// an empty element (a stray comma) is yielded, so the validator can name it
/// rather than silently drop it.
pub fn trustedProxies(text: []const u8) TrustedProxyIterator {
return .{ .rest = text, .done = text.len == 0 };
}
pub const TrustedProxyIterator = struct {
rest: []const u8,
done: bool,
pub fn next(self: *TrustedProxyIterator) ?[]const u8 {
if (self.done) return null;
const end = std.mem.findScalar(u8, self.rest, ',') orelse {
const last = self.rest;
self.done = true;
return std.mem.trim(u8, last, " \t");
};
const element = self.rest[0..end];
self.rest = self.rest[end + 1 ..];
return std.mem.trim(u8, element, " \t");
}
};
pub const TlsEndpoint = struct {
@@ -300,6 +340,10 @@ pub fn readTimeout(u: Upstream) std.Io.Duration {
return .{ .nanoseconds = @as(i96, u.read_timeout_ms) * std.time.ns_per_ms };
}
pub fn attemptTimeout(u: Upstream) std.Io.Duration {
return .{ .nanoseconds = @as(i96, u.attempt_timeout_ms) * std.time.ns_per_ms };
}
pub fn totalTimeout(u: Upstream) std.Io.Duration {
return .{ .nanoseconds = @as(i96, u.total_timeout_ms) * std.time.ns_per_ms };
}
@@ -480,6 +524,7 @@ const expected_keys = [_][]const u8{
"logging.output",
"logging.query_log_buffer_max",
"logging.retention_days",
"upstream.attempt_timeout_ms",
"upstream.read_timeout_ms",
"upstream.total_timeout_ms",
"web.api_localhost_exempt",
@@ -490,6 +535,7 @@ const expected_keys = [_][]const u8{
"web.port",
"web.session_ttl_hours",
"web.sse_max_connections_per_ip",
"web.trusted_proxies",
};
fn lessThanKey(_: void, a: SettingPair, b: SettingPair) bool {
@@ -530,7 +576,7 @@ test "toSettings never emits web.password" {
test "toSettings and fromSettings round-trip a non-default config" {
const gpa = testing.allocator;
const original: Config = .{
.upstream = .{ .read_timeout_ms = 222, .total_timeout_ms = 333 },
.upstream = .{ .attempt_timeout_ms = 111, .read_timeout_ms = 222, .total_timeout_ms = 333 },
.dns = .{
.bind_ipv4 = "127.0.0.1",
.bind_ipv6 = "::1",
@@ -549,6 +595,7 @@ test "toSettings and fromSettings round-trip a non-default config" {
.api_rate_limit_per_min = 29,
.api_localhost_exempt = false,
.sse_max_connections_per_ip = 31,
.trusted_proxies = "10.0.0.9,fd00::9",
},
.doh_server = .{
.enabled = true,
@@ -685,6 +732,10 @@ test "RecordType stores the uppercase DDL spelling" {
}
test "unit conversions" {
try testing.expectEqual(
@as(i96, 2500) * std.time.ns_per_ms,
attemptTimeout(.{}).nanoseconds,
);
try testing.expectEqual(
@as(i96, 3000) * std.time.ns_per_ms,
readTimeout(.{}).nanoseconds,
@@ -703,6 +754,7 @@ test "unit conversions" {
test "unit conversions at the field maximum do not overflow" {
const max_upstream: Upstream = .{
.attempt_timeout_ms = std.math.maxInt(u32),
.read_timeout_ms = std.math.maxInt(u32),
.total_timeout_ms = std.math.maxInt(u32),
};
@@ -710,6 +762,10 @@ test "unit conversions at the field maximum do not overflow" {
@as(i96, std.math.maxInt(u32)) * std.time.ns_per_ms,
readTimeout(max_upstream).nanoseconds,
);
try testing.expectEqual(
@as(i96, std.math.maxInt(u32)) * std.time.ns_per_ms,
attemptTimeout(max_upstream).nanoseconds,
);
try testing.expectEqual(
@as(i64, std.math.maxInt(u16)) * 3600,
sessionTtlSeconds(.{ .session_ttl_hours = std.math.maxInt(u16) }),