From 8771a9f082c56a7203949b2abcb3a4bf5a8fd9dd Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 23 Sep 2025 00:12:16 -0700 Subject: [PATCH] std.Io.net: progress towards DNS resolution --- lib/std/Io.zig | 4 +- lib/std/Io/net.zig | 80 +++++++++++++++++++++++++++++---- lib/std/Io/net/HostName.zig | 88 ++++++++++++++++++++++++++++++------- lib/std/Random.zig | 6 +++ 4 files changed, 151 insertions(+), 27 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 2b2f0646f4..2c87c865b1 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -664,10 +664,12 @@ pub const VTable = struct { sleep: *const fn (?*anyopaque, clockid: std.posix.clockid_t, deadline: Deadline) SleepError!void, listen: *const fn (?*anyopaque, address: net.IpAddress, options: net.ListenOptions) net.ListenError!net.Server, + bind: *const fn (?*anyopaque, address: net.IpAddress, options: net.BindOptions) net.BindError!net.Socket, accept: *const fn (?*anyopaque, server: *net.Server) net.Server.AcceptError!net.Server.Connection, + netSend: *const fn (?*anyopaque, address: net.IpAddress, data: []const []const u8) net.SendError!void, netRead: *const fn (?*anyopaque, src: net.Stream, data: [][]u8) net.Stream.Reader.Error!usize, netWrite: *const fn (?*anyopaque, dest: net.Stream, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, - netClose: *const fn (?*anyopaque, stream: net.Stream) void, + netClose: *const fn (?*anyopaque, socket: net.Socket) void, netInterfaceNameResolve: *const fn (?*anyopaque, *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface, netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name, }; diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index bdc9de0c56..c9dc1d619a 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -8,6 +8,8 @@ pub const HostName = @import("net/HostName.zig"); pub const ListenError = std.net.Address.ListenError || Io.Cancelable; +pub const BindError = std.net.Address.BindError || Io.Cancelable; + pub const ListenOptions = struct { /// How many connections the kernel will accept on the application's behalf. /// If more than this many connections pool in the kernel, clients will start @@ -19,6 +21,13 @@ pub const ListenOptions = struct { force_nonblocking: bool = false, }; +pub const BindOptions = struct { + /// The socket is restricted to sending and receiving IPv6 packets only. + /// In this case, an IPv4 and an IPv6 application can bind to a single port + /// at the same time. + ip6_only: bool = false, +}; + pub const IpAddress = union(enum) { ip4: Ip4Address, ip6: Ip6Address, @@ -123,10 +132,21 @@ pub const IpAddress = union(enum) { }; } - /// The returned `Server` has an open `stream`. + /// Waits for a TCP connection. When using this API, `bind` does not need + /// to be called. The returned `Server` has an open `stream`. pub fn listen(address: IpAddress, io: Io, options: ListenOptions) ListenError!Server { return io.vtable.listen(io.userdata, address, options); } + + /// Associates an address with a `Socket` which can be used to receive UDP + /// packets and other kinds of non-streaming messages. See `listen` for a + /// streaming alternative. + /// + /// One bound `Socket` can be used to receive messages from multiple + /// different addresses. + pub fn bind(address: IpAddress, io: Io, options: BindOptions) BindError!Socket { + return io.vtable.bind(io.userdata, address, options); + } }; /// An IPv4 address in binary memory layout. @@ -141,6 +161,13 @@ pub const Ip4Address = struct { }; } + pub fn unspecified(port: u16) Ip4Address { + return .{ + .bytes = .{ 0, 0, 0, 0 }, + .port = port, + }; + } + pub const ParseError = error{ Overflow, InvalidEnd, @@ -217,6 +244,31 @@ pub const Ip6Address = struct { }; } + pub fn unspecified(port: u16) Ip6Address { + return .{ + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + .port = port, + }; + } + + /// Constructs an IPv4-mapped IPv6 address. + pub fn fromIp4(ip4: Ip4Address) Ip6Address { + const b = &ip4.bytes; + return .{ + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, b[0], b[1], b[2], b[3] }, + .port = ip4.port, + }; + } + + /// Given an `IpAddress`, converts it to an `Ip6Address` directly, or via + /// constructing an IPv4-mapped IPv6 address. + pub fn fromAny(addr: IpAddress) Ip6Address { + return switch (addr) { + .ip4 => |ip4| fromIp4(ip4), + .ip6 => |ip6| ip6, + }; + } + /// An IPv6 address but with `Interface` as a name rather than index. pub const Unresolved = struct { /// Big endian @@ -626,11 +678,11 @@ pub const Interface = struct { } }; -/// An open socket connection with a network protocol that guarantees -/// sequencing, delivery, and prevents repetition. Typically TCP or UNIX domain -/// socket. -pub const Stream = struct { +/// An open port with unspecified protocol. +pub const Socket = struct { handle: Handle, + /// Contains the resolved ephemeral port number if requested. + bind_address: IpAddress, /// Underlying platform-defined type which may or may not be /// interchangeable with a file system file descriptor. @@ -639,8 +691,19 @@ pub const Stream = struct { else => std.posix.fd_t, }; + pub fn close(s: Socket, io: Io) void { + return io.vtable.netClose(io.userdata, s); + } +}; + +/// An open socket connection with a network protocol that guarantees +/// sequencing, delivery, and prevents repetition. Typically TCP or UNIX domain +/// socket. +pub const Stream = struct { + socket: Socket, + pub fn close(s: Stream, io: Io) void { - return io.vtable.close(io.userdata, s); + return io.vtable.netClose(io.userdata, s.socket); } pub const Reader = struct { @@ -719,8 +782,7 @@ pub const Stream = struct { }; pub const Server = struct { - listen_address: IpAddress, - stream: Stream, + socket: Socket, pub const Connection = struct { stream: Stream, @@ -728,7 +790,7 @@ pub const Server = struct { }; pub fn deinit(s: *Server, io: Io) void { - s.stream.close(io); + s.socket.close(io); s.* = undefined; } diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index bb86eb7c5e..5e41e12b7d 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -218,6 +218,11 @@ fn lookupDnsSearch(host_name: HostName, io: Io, options: LookupOptions) !LookupR return lookupDns(io, lookup_canon_name, &rc, options); } +const DnsReply = struct { + buf: [512]u8, + len: usize, +}; + fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, options: LookupOptions) !LookupResult { const family_records: [2]struct { af: IpAddress.Family, rr: u8 } = .{ .{ .af = .ip6, .rr = std.posix.RR.A }, @@ -225,21 +230,21 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio }; var query_buffers: [2][280]u8 = undefined; var queries_buffer: [2][]const u8 = undefined; - var answer_buffers: [2][512]u8 = undefined; - var answers_buffer: [2][]u8 = .{ &answer_buffers[0], &answer_buffers[1] }; var nq: usize = 0; for (family_records) |fr| { if (options.family != fr.af) { - const len = writeResolutionQuery(&query_buffers[nq], 0, lookup_canon_name, 1, fr.rr); + const entropy = std.crypto.random.array(u8, 2); + const len = writeResolutionQuery(&query_buffers[nq], 0, lookup_canon_name, 1, fr.rr, entropy); queries_buffer[nq] = query_buffers[nq][0..len]; nq += 1; } } const queries = queries_buffer[0..nq]; - const replies = answers_buffer[0..nq]; - try rc.sendMessage(io, queries, replies); + var replies_buffer: [2]DnsReply = undefined; + var replies: Io.Queue(DnsReply) = .init(&replies_buffer); + try rc.sendMessage(io, queries, &replies); for (replies) |reply| { if (reply.len < 4 or (reply[3] & 15) == 2) return error.TemporaryNameServerFailure; @@ -391,7 +396,7 @@ fn copyCanon(canonical_name_buffer: *[max_len]u8, name: []const u8) HostName { } /// Writes DNS resolution query packet data to `w`; at most 280 bytes. -fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u8) usize { +fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u8, entropy: [2]u8) usize { // This implementation is ported from musl libc. // A more idiomatic "ziggy" implementation would be welcome. var name = dname; @@ -400,7 +405,8 @@ fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u const n = 17 + name.len + @intFromBool(name.len != 0); // Construct query template - ID will be filled later - @memset(q[0..n], 0); + q[0..2].* = entropy; + @memset(q[2..n], 0); q[2] = @as(u8, op) * 8 + 1; q[5] = 1; @memcpy(q[13..][0..name.len], name); @@ -416,8 +422,6 @@ fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u } q[i + 1] = ty; q[i + 3] = class; - - std.crypto.random.bytes(q[0..2]); return n; } @@ -519,12 +523,14 @@ pub fn connectTcp(host_name: HostName, io: Io, port: u16) ConnectTcpError!Stream pub const ResolvConf = struct { attempts: u32, ndots: u32, - timeout: u32, - nameservers_buffer: [3]IpAddress, + timeout: Io.Duration, + nameservers_buffer: [max_nameservers]IpAddress, nameservers_len: usize, search_buffer: [max_len]u8, search_len: usize, + pub const max_nameservers = 3; + /// Returns `error.StreamTooLong` if a line is longer than 512 bytes. fn init(io: Io) !ResolvConf { var rc: ResolvConf = .{ @@ -620,13 +626,61 @@ pub const ResolvConf = struct { rc: *const ResolvConf, io: Io, queries: []const []const u8, - answers: [][]u8, + replies: *Io.Queue(DnsReply), ) !void { - _ = rc; - _ = io; - _ = queries; - _ = answers; - @panic("TODO"); + var ip4_mapped: [ResolvConf.max_nameservers]IpAddress = undefined; + var any_ip6 = false; + for (rc.nameservers(), &ip4_mapped) |*ns, *m| { + m.* = .{ .ip6 = .fromAny(ns.*) }; + any_ip6 = any_ip6 or ns.* == .ip6; + } + + const socket = s: { + if (any_ip6) ip6: { + const ip6_addr: IpAddress = .{ .ip6 = .unspecified(0) }; + const socket = ip6_addr.bind(io, .{ .ip6_only = true }) catch |err| switch (err) { + error.AddressFamilyNotSupported => break :ip6, + }; + break :s socket; + } + any_ip6 = false; + const ip4_addr: IpAddress = .{ .ip4 = .unspecified(0) }; + const socket = try ip4_addr.bind(io, .{}); + break :s socket; + }; + defer socket.close(); + + const mapped_nameservers = if (any_ip6) ip4_mapped[0..rc.nameservers_len] else rc.nameservers(); + + var group: Io.Group = .{}; + defer group.cancel(); + + for (queries) |query| { + for (mapped_nameservers) |*ns| { + group.async(sendOneMessage, .{ io, query, ns }); + } + } + + const deadline: Io.Deadline = .fromDuration(rc.timeout); + + for (0..queries.len) |_| { + const msg = socket.receiveDeadline(deadline) catch |err| switch (err) { + error.Timeout => return error.Timeout, + error.Canceled => return error.Canceled, + else => continue, + }; + _ = msg; + _ = replies; + @panic("TODO check msg for dns reply and put into replies queue"); + } + } + + fn sendOneMessage( + io: Io, + query: []const u8, + ns: *const IpAddress, + ) void { + io.vtable.netSend(io.userdata, ns.*, &.{query}) catch |err| switch (err) {}; } }; diff --git a/lib/std/Random.zig b/lib/std/Random.zig index ae88d8b4fe..b90c8e3958 100644 --- a/lib/std/Random.zig +++ b/lib/std/Random.zig @@ -58,6 +58,12 @@ pub fn bytes(r: Random, buf: []u8) void { r.fillFn(r.ptr, buf); } +pub fn array(r: Random, comptime E: type, comptime N: usize) [N]E { + var result: [N]E = undefined; + bytes(r, &result); + return result; +} + pub fn boolean(r: Random) bool { return r.int(u1) != 0; }