dot upstreams: per-upstream tls_name for sni and cert verification by dns name

This commit is contained in:
2026-08-01 14:38:45 +02:00
parent 70bff22d75
commit 3baf5d6581
13 changed files with 405 additions and 17 deletions
+74 -2
View File
@@ -31,6 +31,8 @@ pub const ValidateError = error{
NoUpstreams,
BadUpstreamUrl,
DuplicateUpstreamUrl,
BadTlsName,
TlsNameOnNonTlsUpstream,
MissingDefaultGroup,
DuplicateGroupName,
UnknownGroup,
@@ -400,6 +402,40 @@ fn canonical(scratch: Allocator, value: anytype) error{OutOfMemory}![]u8 {
return scratch.dupe(u8, w.buffered());
}
/// A `tls_name` overrides SNI and certificate verification for a DoT upstream,
/// which is the only transport that needs it: DoH verifies by the url host and
/// the http client would ignore this field, so a `tls_name` there is a config
/// error rather than a setting with no effect.
fn checkTlsName(
diags: *Diagnostics,
server: model.UpstreamServer,
scheme: transport.Scheme,
index: usize,
) error{OutOfMemory}!void {
if (server.tls_name.len == 0) return;
if (scheme != .dot) {
try diags.add(
error.TlsNameOnNonTlsUpstream,
"upstreams[{d}].tls_name",
.{index},
"tls_name is only for a tls:// upstream; '{s}' verifies by its url host",
.{server.url},
);
return;
}
_ = dns_name.fromText(server.tls_name) catch {
try diags.add(
error.BadTlsName,
"upstreams[{d}].tls_name",
.{index},
"'{s}' is not a valid domain name",
.{server.tls_name},
);
};
}
fn checkCollections(cfg: Config, diags: *Diagnostics, scratch: Allocator) error{OutOfMemory}!void {
var group_names: StringSet = .empty;
var has_default = false;
@@ -430,7 +466,12 @@ fn checkCollections(cfg: Config, diags: *Diagnostics, scratch: Allocator) error{
var upstream_urls: StringSet = .empty;
var enabled_upstreams: usize = 0;
for (cfg.upstreams, 0..) |server, i| {
_ = transport.Endpoint.parse(server.url) catch {
// The scheme decides whether `tls_name` is meaningful, so the parse
// result is kept rather than discarded. An unparseable url reports only
// `BadUpstreamUrl`: what its scheme would have been is unknown.
if (transport.Endpoint.parse(server.url)) |endpoint| {
try checkTlsName(diags, server, endpoint.scheme, i);
} else |_| {
try diags.add(
error.BadUpstreamUrl,
"upstreams[{d}].url",
@@ -438,7 +479,7 @@ fn checkCollections(cfg: Config, diags: *Diagnostics, scratch: Allocator) error{
"'{s}' is not an https:// or tls:// endpoint",
.{server.url},
);
};
}
if (try markSeen(&upstream_urls, scratch, server.url)) {
try diags.add(
error.DuplicateUpstreamUrl,
@@ -905,6 +946,37 @@ test "error.DuplicateUpstreamUrl" {
try expectProblem(cfg, error.DuplicateUpstreamUrl, "upstreams[1].url");
}
test "a tls_name on a tls:// upstream validates cleanly" {
var cfg = baseConfig();
cfg.upstreams = &.{.{ .url = "tls://1.1.1.1:853", .tls_name = "one.one.one.one" }};
try expectClean(cfg);
}
test "error.BadTlsName on a malformed name" {
var cfg = baseConfig();
cfg.upstreams = &.{.{ .url = "tls://1.1.1.1:853", .tls_name = "one..one.one" }};
try expectProblem(cfg, error.BadTlsName, "upstreams[0].tls_name");
var too_long = baseConfig();
too_long.upstreams = &.{.{ .url = "tls://1.1.1.1:853", .tls_name = "a" ** 64 ++ ".example" }};
try expectProblem(too_long, error.BadTlsName, "upstreams[0].tls_name");
}
test "error.TlsNameOnNonTlsUpstream on a DoH upstream" {
var cfg = baseConfig();
cfg.upstreams = &.{.{ .url = "https://dns.example/dns-query", .tls_name = "dns.example" }};
try expectProblem(cfg, error.TlsNameOnNonTlsUpstream, "upstreams[0].tls_name");
}
test "an empty tls_name is accepted on every scheme" {
var cfg = baseConfig();
cfg.upstreams = &.{
.{ .url = "https://dns.example/dns-query" },
.{ .url = "tls://9.9.9.9:853" },
};
try expectClean(cfg);
}
test "error.MissingDefaultGroup" {
var cfg = baseConfig();
cfg.groups = &.{.{ .name = "kids" }};