milestone 10: doh and dot listeners, cert store with hot reload and cert reload api
This commit is contained in:
+128
-1
@@ -33,6 +33,7 @@ const tls = std.crypto.tls;
|
||||
const api_limiter = @import("web/api_limiter.zig");
|
||||
const auth = @import("web/auth.zig");
|
||||
const bootstrap = @import("config/bootstrap.zig");
|
||||
const cert_store = @import("server/cert_store.zig");
|
||||
const cli = @import("cli.zig");
|
||||
const clients = @import("server/clients.zig");
|
||||
const config_export = @import("config/export.zig");
|
||||
@@ -40,7 +41,9 @@ const db = @import("storage/db.zig");
|
||||
const disk_monitor = @import("storage/disk_monitor.zig");
|
||||
const dns_cache = @import("cache/dns_cache.zig");
|
||||
const doh_client = @import("upstream/doh_client.zig");
|
||||
const doh_server = @import("server/doh_server.zig");
|
||||
const dot_client = @import("upstream/dot_client.zig");
|
||||
const dot_server = @import("server/dot_server.zig");
|
||||
const fetcher = @import("filter/fetcher.zig");
|
||||
const forward_zones = @import("local/forward_zones.zig");
|
||||
const handler = @import("server/handler.zig");
|
||||
@@ -91,6 +94,7 @@ const ConfigError = error{
|
||||
NoUsableUpstreams,
|
||||
BadBindAddress,
|
||||
BadRateLimit,
|
||||
BadCertificate,
|
||||
};
|
||||
|
||||
pub fn run(runner: cli.Runner, args: cli.RunArgs) u8 {
|
||||
@@ -112,7 +116,11 @@ pub fn run(runner: cli.Runner, args: cli.RunArgs) u8 {
|
||||
|
||||
fn isConfigFault(err: anyerror) bool {
|
||||
return switch (err) {
|
||||
error.NoUsableUpstreams, error.BadBindAddress, error.BadRateLimit => true,
|
||||
error.NoUsableUpstreams,
|
||||
error.BadBindAddress,
|
||||
error.BadRateLimit,
|
||||
error.BadCertificate,
|
||||
=> true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
@@ -371,6 +379,41 @@ fn serve(r: cli.Runner, args: cli.RunArgs) !u8 {
|
||||
.tracker = &tracker,
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// DoH/DoT listeners (milestone-10 ruling 11)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// The stores are declared before the listeners on purpose: their deinit
|
||||
// defers run last, and `CertStore.deinit` asserts every connection has
|
||||
// released its generation, which only holds once the listeners are gone.
|
||||
// A certificate that does not load at boot is exit 2 — `nxdns check`
|
||||
// promises that an enabled endpoint has readable certs — while anything
|
||||
// that breaks later is the watcher's to absorb.
|
||||
var doh_certs: ?cert_store.CertStore = null;
|
||||
defer if (doh_certs) |*store| store.deinit(io);
|
||||
if (cfg.doh_server.enabled) {
|
||||
doh_certs = try openCertStore(r, gpa, io, cfg.doh_server, "doh_server", doh_server.alpn_protocols);
|
||||
}
|
||||
|
||||
var dot_certs: ?cert_store.CertStore = null;
|
||||
defer if (dot_certs) |*store| store.deinit(io);
|
||||
if (cfg.dot_server.enabled) {
|
||||
dot_certs = try openCertStore(r, gpa, io, cfg.dot_server, "dot_server", dot_alpn);
|
||||
}
|
||||
|
||||
// Bound here, in this frame, rather than through doh_server's module-level
|
||||
// entry: `/metrics` reads the listeners' counters through `WebState`, and
|
||||
// only a listener that lives in this frame has an address to wire there.
|
||||
// A failed bind warns and stays off (ruling 1, the web precedent): TLS DNS
|
||||
// failing to come up must not stop the plain-DNS side this box exists for.
|
||||
var doh: ?doh_server.DohServer = null;
|
||||
defer if (doh) |*server| server.deinit(gpa, io);
|
||||
if (doh_certs) |*store| doh = bindDoh(gpa, io, cfg.doh_server, &h, store);
|
||||
|
||||
var dot: ?dot_server.DotServer = null;
|
||||
defer if (dot) |*server| server.deinit(io);
|
||||
if (dot_certs) |*store| dot = bindDot(gpa, io, cfg.dot_server, &h, store);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// web interface (ruling 26)
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -401,6 +444,10 @@ fn serve(r: cli.Runner, args: cli.RunArgs) !u8 {
|
||||
.limiter = if (web_limiter) |*l| l else null,
|
||||
.hub = hub,
|
||||
.sink = &sink,
|
||||
.doh_certs = if (doh_certs) |*store| store else null,
|
||||
.dot_certs = if (dot_certs) |*store| store else null,
|
||||
.doh_listener = if (doh) |*server| server else null,
|
||||
.dot_listener = if (dot) |*server| server else null,
|
||||
.config_db = if (web_config_db) |*database| database else null,
|
||||
.querylog_db = if (web_querylog_db) |*database| database else null,
|
||||
.version = version.string,
|
||||
@@ -463,6 +510,10 @@ fn serve(r: cli.Runner, args: cli.RunArgs) !u8 {
|
||||
if (udp4) |*s| try group.concurrent(io, udp_server.UdpServer.serve, .{ s, io });
|
||||
if (tcp6) |*s| try group.concurrent(io, tcp_server.TcpServer.serve, .{ s, io });
|
||||
if (tcp4) |*s| try group.concurrent(io, tcp_server.TcpServer.serve, .{ s, io });
|
||||
if (doh) |*s| try group.concurrent(io, doh_server.DohServer.serve, .{ s, io });
|
||||
if (dot) |*s| try group.concurrent(io, dot_server.DotServer.serve, .{ s, io });
|
||||
if (doh_certs) |*store| try group.concurrent(io, cert_store.CertStore.watch, .{ store, io });
|
||||
if (dot_certs) |*store| try group.concurrent(io, cert_store.CertStore.watch, .{ store, io });
|
||||
|
||||
const gate: ?*disk_monitor.Monitor = &monitor;
|
||||
try group.concurrent(io, logger_mod.Logger.runWriter, .{ &query_logger, io, &querylog_writer_db, gate });
|
||||
@@ -527,6 +578,82 @@ fn serveWebDev(
|
||||
return static.serveFromDisk(web_dev_dir, io, request);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DoH/DoT listeners
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// RFC 7858 has no IANA-registered ALPN id in wide use beyond "dot"
|
||||
/// (milestone-10 ruling 5). Mbed TLS records the pointer, so the list must
|
||||
/// outlive every `ServerContext` built with it; module scope gives it static
|
||||
/// lifetime, the same shape as `doh_server.alpn_protocols`.
|
||||
const dot_alpn: [*:null]const ?[*:0]const u8 = &.{"dot"};
|
||||
|
||||
/// The boot-time certificate load for one enabled endpoint. A failure is a
|
||||
/// configuration fault the operator can fix — the same files `nxdns check`
|
||||
/// verifies — reported in `check`'s style and mapped to exit 2 (ruling 11).
|
||||
/// Out of memory is the one exception: nothing about the configuration is
|
||||
/// wrong, so it keeps its own name and exits 1.
|
||||
fn openCertStore(
|
||||
r: cli.Runner,
|
||||
gpa: Allocator,
|
||||
io: std.Io,
|
||||
endpoint: model.TlsEndpoint,
|
||||
section: []const u8,
|
||||
alpn: ?[*:null]const ?[*:0]const u8,
|
||||
) !cert_store.CertStore {
|
||||
return cert_store.CertStore.init(gpa, io, endpoint.cert_path, endpoint.key_path, alpn) catch |err| {
|
||||
if (err == error.OutOfMemory) return error.OutOfMemory;
|
||||
r.err.print("{s}: '{s}' + '{s}': {s}\n", .{
|
||||
section,
|
||||
endpoint.cert_path,
|
||||
endpoint.key_path,
|
||||
cert_store.humanMessage(err),
|
||||
}) catch {};
|
||||
return error.BadCertificate;
|
||||
};
|
||||
}
|
||||
|
||||
/// Ruling 1: a listener that cannot bind warns and stays off. The bind text
|
||||
/// itself gets the same treatment — `validate` refuses it, but a hand-edited
|
||||
/// database can still carry one, and it is not worth taking DNS down over.
|
||||
fn bindDoh(
|
||||
gpa: Allocator,
|
||||
io: std.Io,
|
||||
endpoint: model.TlsEndpoint,
|
||||
h: *handler.Handler,
|
||||
store: *cert_store.CertStore,
|
||||
) ?doh_server.DohServer {
|
||||
const bind_address = net.IpAddress.parse(endpoint.bind, endpoint.port) catch {
|
||||
log.warn("doh_server.bind '{s}' is not an IP address; DoH is disabled", .{endpoint.bind});
|
||||
return null;
|
||||
};
|
||||
const server = doh_server.DohServer.listen(gpa, io, bind_address, h, store, .{}) catch |err| {
|
||||
log.warn("doh listener cannot listen on {s}:{d}: {t}", .{ endpoint.bind, endpoint.port, err });
|
||||
return null;
|
||||
};
|
||||
log.info("doh listener on {f}", .{server.boundAddress()});
|
||||
return server;
|
||||
}
|
||||
|
||||
fn bindDot(
|
||||
gpa: Allocator,
|
||||
io: std.Io,
|
||||
endpoint: model.TlsEndpoint,
|
||||
h: *handler.Handler,
|
||||
store: *cert_store.CertStore,
|
||||
) ?dot_server.DotServer {
|
||||
const bind_address = net.IpAddress.parse(endpoint.bind, endpoint.port) catch {
|
||||
log.warn("dot_server.bind '{s}' is not an IP address; DoT is disabled", .{endpoint.bind});
|
||||
return null;
|
||||
};
|
||||
const server = dot_server.DotServer.listen(gpa, io, bind_address, h, store, .{}) catch |err| {
|
||||
log.warn("dot listener cannot listen on {s}:{d}: {t}", .{ endpoint.bind, endpoint.port, err });
|
||||
return null;
|
||||
};
|
||||
log.info("dot listener on {f}", .{server.boundAddress()});
|
||||
return server;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// background maintenance
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user