milestone 10: doh and dot listeners, cert store with hot reload and cert reload api
This commit is contained in:
@@ -21,10 +21,12 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const cert_store = @import("../server/cert_store.zig");
|
||||
const clients = @import("../server/clients.zig");
|
||||
const dns_cache = @import("../cache/dns_cache.zig");
|
||||
const dns_handler = @import("../server/handler.zig");
|
||||
const disk_monitor = @import("../storage/disk_monitor.zig");
|
||||
const dot_server = @import("../server/dot_server.zig");
|
||||
const http_util = @import("http_util.zig");
|
||||
const logging = @import("../platform/logging.zig");
|
||||
const pool_mod = @import("../upstream/pool.zig");
|
||||
@@ -80,6 +82,18 @@ pub const DiskSample = struct {
|
||||
sample_failures: u64,
|
||||
};
|
||||
|
||||
/// The DoH listener counters this exposition exports (milestone-10 ruling 10):
|
||||
/// the four every TLS listener keeps, plus DoH's `bad_requests`. A subset of
|
||||
/// `doh_server.Snapshot` on purpose — the accept-side refusal counters stay
|
||||
/// internal, exactly as they do for the DoT listener and TCP/53.
|
||||
pub const DohListenerSample = struct {
|
||||
connections: u64,
|
||||
tls_handshake_failures: u64,
|
||||
idle_timeouts: u64,
|
||||
connection_errors: u64,
|
||||
bad_requests: u64,
|
||||
};
|
||||
|
||||
/// One upstream, with every string owned by the caller's arena.
|
||||
pub const UpstreamSample = struct {
|
||||
url: []const u8,
|
||||
@@ -103,6 +117,16 @@ pub const Sample = struct {
|
||||
retention: ?retention_mod.Stats = null,
|
||||
blocklist: ?BlocklistSample = null,
|
||||
disk: ?DiskSample = null,
|
||||
/// One entry per enabled TLS endpoint (milestone-10 ruling 10). Rendered
|
||||
/// under an `endpoint` label so both share the two `nxdns_cert_*`
|
||||
/// families. `last_reload_unix` is deliberately not exported: the reload
|
||||
/// endpoint reports cert state on demand.
|
||||
doh_certs: ?cert_store.CertStore.Stats = null,
|
||||
dot_certs: ?cert_store.CertStore.Stats = null,
|
||||
/// The listener families (ruling 10): absent while an endpoint is
|
||||
/// disabled or its bind failed, like every other unwired collaborator.
|
||||
doh_listener: ?DohListenerSample = null,
|
||||
dot_listener: ?dot_server.StatsSnapshot = null,
|
||||
upstreams: []const UpstreamSample = &.{},
|
||||
};
|
||||
|
||||
@@ -169,6 +193,21 @@ pub fn collect(state: *server.WebState, io: std.Io, arena: Allocator) Allocator.
|
||||
.sample_failures = monitor.sample_failures.load(.monotonic),
|
||||
};
|
||||
|
||||
if (state.doh_certs) |store| sample.doh_certs = store.snapshotStats();
|
||||
if (state.dot_certs) |store| sample.dot_certs = store.snapshotStats();
|
||||
|
||||
if (state.doh_listener) |listener| {
|
||||
const snapshot = listener.snapshotStats();
|
||||
sample.doh_listener = .{
|
||||
.connections = snapshot.connections,
|
||||
.tls_handshake_failures = snapshot.tls_handshake_failures,
|
||||
.idle_timeouts = snapshot.idle_timeouts,
|
||||
.connection_errors = snapshot.connection_errors,
|
||||
.bad_requests = snapshot.bad_requests,
|
||||
};
|
||||
}
|
||||
if (state.dot_listener) |listener| sample.dot_listener = listener.snapshotStats();
|
||||
|
||||
if (state.pool) |pool| sample.upstreams = try upstreams(pool, io, arena);
|
||||
|
||||
return sample;
|
||||
@@ -294,9 +333,48 @@ pub fn render(w: *std.Io.Writer, sample: Sample) std.Io.Writer.Error!void {
|
||||
);
|
||||
}
|
||||
|
||||
if (sample.doh_listener) |listener| {
|
||||
try counterGroup(w, "nxdns_doh_server_", "DoH listener counter", listener);
|
||||
}
|
||||
if (sample.dot_listener) |listener| {
|
||||
try counterGroup(w, "nxdns_dot_server_", "DoT listener counter", listener);
|
||||
}
|
||||
|
||||
if (sample.doh_certs != null or sample.dot_certs != null) try renderCerts(w, sample);
|
||||
|
||||
if (sample.upstreams.len != 0) try renderUpstreams(w, sample.upstreams);
|
||||
}
|
||||
|
||||
fn renderCerts(w: *std.Io.Writer, sample: Sample) std.Io.Writer.Error!void {
|
||||
try labeledHead(w, "nxdns_cert_reloads_total", "Certificate reloads that published a new context.", "counter");
|
||||
if (sample.doh_certs) |stats| try endpointValue(w, "nxdns_cert_reloads_total", "doh", stats.reloads);
|
||||
if (sample.dot_certs) |stats| try endpointValue(w, "nxdns_cert_reloads_total", "dot", stats.reloads);
|
||||
|
||||
try labeledHead(
|
||||
w,
|
||||
"nxdns_cert_reload_failures_total",
|
||||
"Certificate reloads that failed; the old certificate keeps serving.",
|
||||
"counter",
|
||||
);
|
||||
if (sample.doh_certs) |stats| {
|
||||
try endpointValue(w, "nxdns_cert_reload_failures_total", "doh", stats.reload_failures);
|
||||
}
|
||||
if (sample.dot_certs) |stats| {
|
||||
try endpointValue(w, "nxdns_cert_reload_failures_total", "dot", stats.reload_failures);
|
||||
}
|
||||
}
|
||||
|
||||
/// The endpoint names are ours ("doh"/"dot"), so unlike a url label there is
|
||||
/// nothing to escape.
|
||||
fn endpointValue(
|
||||
w: *std.Io.Writer,
|
||||
name: []const u8,
|
||||
endpoint: []const u8,
|
||||
value: u64,
|
||||
) std.Io.Writer.Error!void {
|
||||
try w.print("{s}{{endpoint=\"{s}\"}} {d}\n", .{ name, endpoint, value });
|
||||
}
|
||||
|
||||
fn renderUpstreams(w: *std.Io.Writer, list: []const UpstreamSample) std.Io.Writer.Error!void {
|
||||
try labeledHead(w, "nxdns_upstream_up", "1 while an upstream is enabled and healthy.", "gauge");
|
||||
for (list) |entry| try labeledValue(w, "nxdns_upstream_up", entry.url, @intFromBool(entry.available));
|
||||
@@ -528,6 +606,69 @@ test "an unwired collaborator omits its family rather than reporting zeros" {
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, text, 1, "nxdns_disk_"));
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, text, 1, "nxdns_upstream_"));
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, text, 1, "nxdns_blocklist_"));
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, text, 1, "nxdns_cert_"));
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, text, 1, "nxdns_doh_server_"));
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, text, 1, "nxdns_dot_server_"));
|
||||
}
|
||||
|
||||
test "listener counters render only for the wired servers" {
|
||||
const doh_only = try renderToString(testing.allocator, .{
|
||||
.doh_listener = .{
|
||||
.connections = 9,
|
||||
.tls_handshake_failures = 2,
|
||||
.idle_timeouts = 1,
|
||||
.connection_errors = 0,
|
||||
.bad_requests = 4,
|
||||
},
|
||||
});
|
||||
defer testing.allocator.free(doh_only);
|
||||
|
||||
try testing.expect(std.mem.containsAtLeast(u8, doh_only, 1, "nxdns_doh_server_connections_total 9\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, doh_only, 1, "nxdns_doh_server_tls_handshake_failures_total 2\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, doh_only, 1, "nxdns_doh_server_idle_timeouts_total 1\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, doh_only, 1, "nxdns_doh_server_connection_errors_total 0\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, doh_only, 1, "nxdns_doh_server_bad_requests_total 4\n"));
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, doh_only, 1, "nxdns_dot_server_"));
|
||||
|
||||
const dot_only = try renderToString(testing.allocator, .{
|
||||
.dot_listener = .{
|
||||
.connections = 5,
|
||||
.tls_handshake_failures = 0,
|
||||
.idle_timeouts = 3,
|
||||
.connection_errors = 1,
|
||||
},
|
||||
});
|
||||
defer testing.allocator.free(dot_only);
|
||||
|
||||
try testing.expect(std.mem.containsAtLeast(u8, dot_only, 1, "nxdns_dot_server_connections_total 5\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, dot_only, 1, "nxdns_dot_server_idle_timeouts_total 3\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, dot_only, 1, "nxdns_dot_server_connection_errors_total 1\n"));
|
||||
// The DoT listener has no HTTP layer, so no bad_requests family.
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, dot_only, 1, "nxdns_dot_server_bad_requests_total"));
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, dot_only, 1, "nxdns_doh_server_"));
|
||||
}
|
||||
|
||||
test "cert reload counters render per endpoint, only for the wired stores" {
|
||||
const one = try renderToString(testing.allocator, .{
|
||||
.doh_certs = .{ .reloads = 2, .reload_failures = 1, .last_reload_unix = 1_700_000_000 },
|
||||
});
|
||||
defer testing.allocator.free(one);
|
||||
|
||||
try testing.expect(std.mem.containsAtLeast(u8, one, 1, "nxdns_cert_reloads_total{endpoint=\"doh\"} 2\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, one, 1, "nxdns_cert_reload_failures_total{endpoint=\"doh\"} 1\n"));
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, one, 1, "endpoint=\"dot\""));
|
||||
// The wall-clock second stays off the exposition.
|
||||
try testing.expect(!std.mem.containsAtLeast(u8, one, 1, "last_reload"));
|
||||
|
||||
const both = try renderToString(testing.allocator, .{
|
||||
.doh_certs = .{ .reloads = 0, .reload_failures = 0, .last_reload_unix = 0 },
|
||||
.dot_certs = .{ .reloads = 3, .reload_failures = 0, .last_reload_unix = 0 },
|
||||
});
|
||||
defer testing.allocator.free(both);
|
||||
|
||||
try testing.expect(std.mem.containsAtLeast(u8, both, 1, "nxdns_cert_reloads_total{endpoint=\"doh\"} 0\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, both, 1, "nxdns_cert_reloads_total{endpoint=\"dot\"} 3\n"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, both, 1, "nxdns_cert_reload_failures_total{endpoint=\"dot\"} 0\n"));
|
||||
}
|
||||
|
||||
test "a label value escapes the characters the format reserves" {
|
||||
|
||||
Reference in New Issue
Block a user