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);
}