//! Network-dependent test for `dot_client.zig`. //! //! Separate file because it needs `@import("build_options")`, which only exists //! when build.zig drives the compilation. It is compiled by every //! `zig build test` run, so it cannot rot, and skips at run time without //! `-Dlive`. (`-Dintegration` stays hermetic; `-Dlive` is the gate for tests //! that leave the machine.) const std = @import("std"); const build_options = @import("build_options"); const tls = std.crypto.tls; const Certificate = std.crypto.Certificate; const dot_client = @import("dot_client.zig"); const transport = @import("transport.zig"); const packet = @import("../dns/packet.zig"); /// Neither `connect` nor a TLS stream read accepts a timeout in 0.16.0, so the /// whole exchange runs as one task raced against a sleep and the loser is /// canceled. const budget: std.Io.Clock.Duration = .{ .raw = .fromSeconds(10), .clock = .awake }; /// An A query for example.com: id 0x1234, RD set, one question. const query_bytes = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++ "\x07example\x03com\x00\x00\x01\x00\x01"; /// This machine's IPv6 egress is dead and upstream name resolution is out of /// scope, so the documented anycast IPv4 literal is used. const upstream_url = "tls://1.1.1.1:853"; /// The name Cloudflare publishes for this endpoint. Without it the handshake is /// `error.CertificateHostMismatch`: `std.crypto.Certificate` matches dNSName /// SANs only, so the IP SAN on the leaf certificate is never consulted. const upstream_tls_name = "one.one.one.one"; const Outcome = union(enum) { exchange: anyerror!usize, expiry: std.Io.Cancelable!void, }; const Params = struct { gpa: std.mem.Allocator, bundle: *Certificate.Bundle, bundle_lock: *std.Io.RwLock, buffers: dot_client.DotClient.Buffers, response_buf: []u8, }; fn runExchange(io: std.Io, params: Params) anyerror!usize { const endpoint: transport.Endpoint = try .parse(upstream_url); var client: dot_client.DotClient = .init( endpoint, upstream_tls_name, params.gpa, params.bundle, params.bundle_lock, params.buffers, ); var selected: ?[]const u8 = null; const reply = try client.client().exchange(io, query_bytes, params.response_buf, &selected); std.debug.assert(std.mem.eql(u8, selected.?, endpoint.url)); return reply.len; } fn expire(io: std.Io, duration: std.Io.Clock.Duration) std.Io.Cancelable!void { return duration.sleep(io); } test "live DoT exchange against 1.1.1.1" { if (!build_options.live) return error.SkipZigTest; const gpa = std.testing.allocator; var threaded: std.Io.Threaded = .init(gpa, .{}); defer threaded.deinit(); const io = threaded.io(); var bundle: Certificate.Bundle = .empty; defer bundle.deinit(gpa); var bundle_lock: std.Io.RwLock = .init; const chunk = tls.Client.min_buffer_len; const scratch = try gpa.alloc(u8, 4 * chunk); defer gpa.free(scratch); var response_buf: [transport.max_message_len]u8 = undefined; var outcomes: [2]Outcome = undefined; var race: std.Io.Select(Outcome) = .init(io, &outcomes); defer race.cancelDiscard(); try race.concurrent(.exchange, runExchange, .{ io, Params{ .gpa = gpa, .bundle = &bundle, .bundle_lock = &bundle_lock, .buffers = .{ .tls_read = scratch[0..chunk], .tls_write = scratch[chunk .. 2 * chunk], .stream_read = scratch[2 * chunk .. 3 * chunk], .stream_write = scratch[3 * chunk ..], }, .response_buf = &response_buf, } }); try race.concurrent(.expiry, expire, .{ io, budget }); const len = switch (try race.await()) { .exchange => |result| result catch |err| { std.debug.print("DoT exchange with {s} failed: {s}\n", .{ upstream_url, @errorName(err), }); return err; }, .expiry => |result| { try result; return error.DotExchangeTimedOut; }, }; // `exchange` already ran `transport.validateResponse`, so the id, question // and QR bit are known good. What is left to check is that the upstream // actually answered the question. const reply = try packet.parse(response_buf[0..len]); try std.testing.expect(reply.header.ancount >= 1); }