dot upstreams: per-upstream tls_name for sni and cert verification by dns name
This commit is contained in:
@@ -45,6 +45,12 @@ pub fn resolveAddress(endpoint: transport.Endpoint) ResolveError!net.IpAddress {
|
||||
|
||||
pub const DotClient = struct {
|
||||
endpoint: transport.Endpoint,
|
||||
/// SNI, and the name matched against the leaf certificate. The dial target
|
||||
/// stays `endpoint.host`, so this is what lets an upstream configured as an
|
||||
/// IP literal verify: `std.crypto.Certificate.Parsed.verifyHostName` matches
|
||||
/// dNSName SANs only and never an IP SAN, so `tls://1.1.1.1:853` alone is
|
||||
/// always `error.CertificateHostMismatch`. Borrowed, like `endpoint`.
|
||||
verify_name: []const u8,
|
||||
gpa: std.mem.Allocator,
|
||||
/// Caller-owned, shared across endpoints.
|
||||
bundle: *Certificate.Bundle,
|
||||
@@ -66,8 +72,12 @@ pub const DotClient = struct {
|
||||
|
||||
/// A `.doh` endpoint or an undersized buffer is a wiring bug in this
|
||||
/// process, not a runtime condition, so both are assertions.
|
||||
///
|
||||
/// An empty `tls_name` keeps the endpoint's own host as the verification
|
||||
/// name, which is correct whenever the url already carries a DNS name.
|
||||
pub fn init(
|
||||
endpoint: transport.Endpoint,
|
||||
tls_name: []const u8,
|
||||
gpa: std.mem.Allocator,
|
||||
bundle: *Certificate.Bundle,
|
||||
bundle_lock: *std.Io.RwLock,
|
||||
@@ -80,6 +90,7 @@ pub const DotClient = struct {
|
||||
std.debug.assert(buffers.stream_write.len >= tls.Client.min_buffer_len);
|
||||
return .{
|
||||
.endpoint = endpoint,
|
||||
.verify_name = if (tls_name.len == 0) endpoint.host else tls_name,
|
||||
.gpa = gpa,
|
||||
.bundle = bundle,
|
||||
.bundle_lock = bundle_lock,
|
||||
@@ -149,7 +160,7 @@ pub const DotClient = struct {
|
||||
tls_stream.stream_reader.err = null;
|
||||
tls_stream.stream_writer.err = null;
|
||||
tls_stream.init(io, &stream, self.bundle, self.bundle_lock, self.gpa, .{
|
||||
.host = self.endpoint.host,
|
||||
.host = self.verify_name,
|
||||
.ca = .system,
|
||||
.read_buffer = self.buffers.tls_read,
|
||||
.write_buffer = self.buffers.tls_write,
|
||||
@@ -157,8 +168,9 @@ pub const DotClient = struct {
|
||||
.stream_write_buffer = self.buffers.stream_write,
|
||||
}) catch |err| {
|
||||
const cause = concreteHandshake(&tls_stream, err);
|
||||
log.warn("dot upstream {s}: TLS handshake failed: {s} ({t})", .{
|
||||
log.warn("dot upstream {s}: TLS handshake as \"{s}\" failed: {s} ({t})", .{
|
||||
self.endpoint.url,
|
||||
self.verify_name,
|
||||
@errorName(cause),
|
||||
tls_client.classify(cause),
|
||||
});
|
||||
@@ -170,7 +182,10 @@ pub const DotClient = struct {
|
||||
const prefix = framePrefix(@intCast(query.len));
|
||||
writer.writeAll(&prefix) catch |err| return sendFailure(&tls_stream, err);
|
||||
writer.writeAll(query) catch |err| return sendFailure(&tls_stream, err);
|
||||
writer.flush() catch |err| return sendFailure(&tls_stream, err);
|
||||
// `TlsStream.flush`, not `writer.flush`: the latter leaves the encrypted
|
||||
// record in the socket writer's buffer and the query never leaves this
|
||||
// process.
|
||||
tls_stream.flush() catch |err| return sendFailure(&tls_stream, err);
|
||||
|
||||
const reader = tls_stream.reader();
|
||||
var prefix_bytes: [prefix_len]u8 = undefined;
|
||||
@@ -463,7 +478,7 @@ test "DotClient satisfies the Client interface" {
|
||||
|
||||
// `init` asserts `endpoint.scheme == .dot`; a `.doh` endpoint trips
|
||||
// `std.debug.assert`, which a test cannot catch in-process.
|
||||
var dot: DotClient = .init(try .parse("tls://9.9.9.9:853"), gpa, &bundle, &bundle_lock, .{
|
||||
var dot: DotClient = .init(try .parse("tls://9.9.9.9:853"), "", gpa, &bundle, &bundle_lock, .{
|
||||
.tls_read = buffer[0..chunk],
|
||||
.tls_write = buffer[chunk .. 2 * chunk],
|
||||
.stream_read = buffer[2 * chunk .. 3 * chunk],
|
||||
@@ -476,3 +491,34 @@ test "DotClient satisfies the Client interface" {
|
||||
const iface: transport.Client = dot.client();
|
||||
try testing.expectEqual(@as(*anyopaque, @ptrCast(&dot)), iface.ptr);
|
||||
}
|
||||
|
||||
test "a tls_name replaces the verification name and leaves the dial target alone" {
|
||||
const gpa = testing.allocator;
|
||||
|
||||
const buffer = try gpa.alloc(u8, 4 * tls.Client.min_buffer_len);
|
||||
defer gpa.free(buffer);
|
||||
const chunk = tls.Client.min_buffer_len;
|
||||
|
||||
var bundle: Certificate.Bundle = .empty;
|
||||
defer bundle.deinit(gpa);
|
||||
var bundle_lock: std.Io.RwLock = .init;
|
||||
|
||||
const buffers: DotClient.Buffers = .{
|
||||
.tls_read = buffer[0..chunk],
|
||||
.tls_write = buffer[chunk .. 2 * chunk],
|
||||
.stream_read = buffer[2 * chunk .. 3 * chunk],
|
||||
.stream_write = buffer[3 * chunk ..],
|
||||
};
|
||||
|
||||
const endpoint: transport.Endpoint = try .parse("tls://1.1.1.1:853");
|
||||
const named: DotClient = .init(endpoint, "one.one.one.one", gpa, &bundle, &bundle_lock, buffers);
|
||||
try testing.expectEqualStrings("one.one.one.one", named.verify_name);
|
||||
try testing.expectEqualStrings("1.1.1.1", named.endpoint.host);
|
||||
|
||||
const address = try resolveAddress(named.endpoint);
|
||||
try testing.expectEqualSlices(u8, &.{ 1, 1, 1, 1 }, &address.ip4.bytes);
|
||||
try testing.expectEqual(@as(u16, 853), address.ip4.port);
|
||||
|
||||
const plain: DotClient = .init(endpoint, "", gpa, &bundle, &bundle_lock, buffers);
|
||||
try testing.expectEqualStrings("1.1.1.1", plain.verify_name);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user