a73x

src/client/resolver.zig

Ref:   Size: 4.3 KiB   History

//! DNS lives in an exec'd helper when opening is cancellable. Blocking libc
//! resolution cannot outlive the request: cancellation kills and reaps the
//! helper, without cancelling a thread or using libc after a multithreaded fork.
const std = @import("std");
const quic = @import("quic");
const Wait = @import("open_wait.zig").Wait;
const interrupt = @import("interrupt.zig");
const flag = "--internal-resolve";
const result_len = 21;

/// Dispatch before ordinary CLI parsing. The child has no inherited terminal
/// input and writes one bounded address record, never daemon protocol frames.
pub fn helper(alloc: std.mem.Allocator, args: []const []const u8) !?u8 {
    if (args.len == 0 or !std.mem.eql(u8, args[0], flag)) return null;
    if (args.len != 3 or args[1].len == 0 or args[1].len > 1024) return 2;
    const port = std.fmt.parseInt(u16, args[2], 10) catch return 2;
    const addr = quic.resolveHost(alloc, args[1], port) catch return 1;
    var bytes: [result_len]u8 = @splat(0);
    switch (addr.any.family) {
        std.posix.AF.INET => {
            bytes[0] = 4;
            @memcpy(bytes[1..5], std.mem.asBytes(&addr.in.sa.addr));
        },
        std.posix.AF.INET6 => {
            bytes[0] = 6;
            @memcpy(bytes[1..17], &addr.in6.sa.addr);
            std.mem.writeInt(u32, bytes[17..21], addr.in6.sa.scope_id, .little);
        },
        else => return 1,
    }
    try std.fs.File.stdout().writeAll(&bytes);
    return 0;
}

pub fn resolve(alloc: std.mem.Allocator, host: []const u8, port: u16, wait: *Wait) !std.net.Address {
    try wait.check();
    if (host.len == 0 or host.len > 1024) return error.MalformedAddress;
    if (std.net.Address.parseIp(host, port)) |addr| return addr else |_| {}
    const exe = try std.fs.selfExePathAlloc(alloc);
    defer alloc.free(exe);
    var port_buf: [5]u8 = undefined;
    return resolveChild(alloc, &.{ exe, flag, host, try std.fmt.bufPrint(&port_buf, "{d}", .{port}) }, port, wait);
}
fn resolveChild(alloc: std.mem.Allocator, argv: []const []const u8, port: u16, wait: *Wait) !std.net.Address {
    try wait.check();
    var child = std.process.Child.init(argv, alloc);
    child.stdin_behavior = .Ignore;
    child.stdout_behavior = .Pipe;
    child.stderr_behavior = .Ignore;
    try child.spawn();
    defer @import("link").terminateChild(&child);
    var bytes: [result_len]u8 = undefined;
    var used: usize = 0;
    while (used < bytes.len) {
        var fds = [_]std.posix.pollfd{.{ .fd = child.stdout.?.handle, .events = std.posix.POLL.IN, .revents = 0 }};
        try wait.poll(&fds, 1000);
        if (fds[0].revents == 0) continue;
        const n = try std.posix.read(child.stdout.?.handle, bytes[used..]);
        if (n == 0) return error.UnknownHostName;
        used += n;
    }
    try wait.check();
    return switch (bytes[0]) {
        4 => std.net.Address.initIp4(bytes[1..5].*, port),
        6 => std.net.Address.initIp6(bytes[1..17].*, port, 0, std.mem.readInt(u32, bytes[17..21], .little)),
        else => error.UnknownHostName,
    };
}

test "DNS helper wait is deadline bounded and numeric addresses do not spawn" {
    var wait: Wait = .{ .alloc = std.testing.allocator, .deadline = std.time.milliTimestamp() + 40 };
    try std.testing.expectError(error.Timeout, resolveChild(std.testing.allocator, &.{ "sleep", "30" }, 4433, &wait));
    wait.deadline = std.time.milliTimestamp() + 100;
    const addr = try resolve(std.testing.allocator, "127.0.0.1", 4433, &wait);
    try std.testing.expectEqual(@as(u16, 4433), addr.getPort());
}

test "cancel fd interrupts and reaps a stalled resolver child" {
    const pipe = try std.posix.pipe2(.{ .CLOEXEC = true });
    defer for (pipe) |fd| std.posix.close(fd);
    const Cancel = struct {
        fn fire(fd: std.posix.fd_t) void {
            std.Thread.sleep(20 * std.time.ns_per_ms);
            _ = std.posix.write(fd, &.{interrupt.detach_key}) catch {};
        }
    };
    const thread = try std.Thread.spawn(.{}, Cancel.fire, .{pipe[1]});
    defer thread.join();
    var wait: Wait = .{ .alloc = std.testing.allocator, .abort_fd = pipe[0], .deadline = std.time.milliTimestamp() + 2000 };
    const start = std.time.milliTimestamp();
    try std.testing.expectError(error.UserAbort, resolveChild(std.testing.allocator, &.{ "sleep", "30" }, 4433, &wait));
    try std.testing.expect(std.time.milliTimestamp() - start < 500);
}