diff options
| author | Andrew Kelley <andrew@ziglang.org> | 2019-10-30 21:30:16 -0400 |
|---|---|---|
| committer | Andrew Kelley <andrew@ziglang.org> | 2019-10-30 21:30:16 -0400 |
| commit | 61d5a0bf48d034208aea37d72dac5b3531334be7 (patch) | |
| tree | 57e545a972ae44c3bc6bab98396f9cb22203edd1 /lib/std/net.zig | |
| parent | 6a15e8a7a771bcbf2534cceecd77231344aafbf8 (diff) | |
| parent | 7b7ba51642c832c77ec2668491843be3b0114124 (diff) | |
| download | zig-61d5a0bf48d034208aea37d72dac5b3531334be7.tar.gz zig-61d5a0bf48d034208aea37d72dac5b3531334be7.zip | |
Merge branch 'std.net'
Diffstat (limited to 'lib/std/net.zig')
| -rw-r--r-- | lib/std/net.zig | 1437 |
1 files changed, 1261 insertions, 176 deletions
diff --git a/lib/std/net.zig b/lib/std/net.zig index be9d18056c..eca313a84c 100644 --- a/lib/std/net.zig +++ b/lib/std/net.zig @@ -4,244 +4,1329 @@ const assert = std.debug.assert; const net = @This(); const mem = std.mem; const os = std.os; +const fs = std.fs; -pub const TmpWinAddr = struct { - family: u8, - data: [14]u8, -}; +test "" { + _ = @import("net/test.zig"); +} -pub const OsAddress = switch (builtin.os) { - builtin.Os.windows => TmpWinAddr, - else => os.sockaddr, -}; +pub const IpAddress = extern union { + any: os.sockaddr, + in: os.sockaddr_in, + in6: os.sockaddr_in6, -pub const Address = struct { - os_addr: OsAddress, - - pub fn initIp4(ip4: u32, _port: u16) Address { - return Address{ - .os_addr = os.sockaddr{ - .in = os.sockaddr_in{ - .family = os.AF_INET, - .port = mem.nativeToBig(u16, _port), - .addr = ip4, - .zero = [_]u8{0} ** 8, - }, + // TODO this crashed the compiler + //pub const localhost = initIp4(parseIp4("127.0.0.1") catch unreachable, 0); + + pub fn parse(name: []const u8, port: u16) !IpAddress { + if (parseIp4(name, port)) |ip4| return ip4 else |err| switch (err) { + error.Overflow, + error.InvalidEnd, + error.InvalidCharacter, + error.Incomplete, + => {}, + } + + if (parseIp6(name, port)) |ip6| return ip6 else |err| switch (err) { + error.Overflow, + error.InvalidEnd, + error.InvalidCharacter, + error.Incomplete, + => {}, + } + + return error.InvalidIPAddressFormat; + } + + pub fn parseExpectingFamily(name: []const u8, family: os.sa_family_t, port: u16) !IpAddress { + switch (family) { + os.AF_INET => return parseIp4(name, port), + os.AF_INET6 => return parseIp6(name, port), + os.AF_UNSPEC => return parse(name, port), + else => unreachable, + } + } + + pub fn parseIp6(buf: []const u8, port: u16) !IpAddress { + var result = IpAddress{ + .in6 = os.sockaddr_in6{ + .scope_id = undefined, + .port = mem.nativeToBig(u16, port), + .flowinfo = 0, + .addr = undefined, + }, + }; + const ip_slice = result.in6.addr[0..]; + + var x: u16 = 0; + var saw_any_digits = false; + var index: u8 = 0; + var scope_id = false; + for (buf) |c| { + if (scope_id) { + if (c >= '0' and c <= '9') { + const digit = c - '0'; + if (@mulWithOverflow(u32, result.in6.scope_id, 10, &result.in6.scope_id)) { + return error.Overflow; + } + if (@addWithOverflow(u32, result.in6.scope_id, digit, &result.in6.scope_id)) { + return error.Overflow; + } + } else { + return error.InvalidCharacter; + } + } else if (c == ':') { + if (!saw_any_digits) { + return error.InvalidCharacter; + } + if (index == 14) { + return error.InvalidEnd; + } + ip_slice[index] = @truncate(u8, x >> 8); + index += 1; + ip_slice[index] = @truncate(u8, x); + index += 1; + + x = 0; + saw_any_digits = false; + } else if (c == '%') { + if (!saw_any_digits) { + return error.InvalidCharacter; + } + if (index == 14) { + ip_slice[index] = @truncate(u8, x >> 8); + index += 1; + ip_slice[index] = @truncate(u8, x); + index += 1; + } + scope_id = true; + saw_any_digits = false; + } else { + const digit = try std.fmt.charToDigit(c, 16); + if (@mulWithOverflow(u16, x, 16, &x)) { + return error.Overflow; + } + if (@addWithOverflow(u16, x, digit, &x)) { + return error.Overflow; + } + saw_any_digits = true; + } + } + + if (!saw_any_digits) { + return error.Incomplete; + } + + if (scope_id) { + return result; + } + + if (index == 14) { + ip_slice[14] = @truncate(u8, x >> 8); + ip_slice[15] = @truncate(u8, x); + return result; + } + + return error.Incomplete; + } + + pub fn parseIp4(buf: []const u8, port: u16) !IpAddress { + var result = IpAddress{ + .in = os.sockaddr_in{ + .port = mem.nativeToBig(u16, port), + .addr = undefined, }, }; + const out_ptr = @sliceToBytes((*[1]u32)(&result.in.addr)[0..]); + + var x: u8 = 0; + var index: u8 = 0; + var saw_any_digits = false; + for (buf) |c| { + if (c == '.') { + if (!saw_any_digits) { + return error.InvalidCharacter; + } + if (index == 3) { + return error.InvalidEnd; + } + out_ptr[index] = x; + index += 1; + x = 0; + saw_any_digits = false; + } else if (c >= '0' and c <= '9') { + saw_any_digits = true; + x = try std.math.mul(u8, x, 10); + x = try std.math.add(u8, x, c - '0'); + } else { + return error.InvalidCharacter; + } + } + if (index == 3 and saw_any_digits) { + out_ptr[index] = x; + return result; + } + + return error.Incomplete; } - pub fn initIp6(ip6: *const Ip6Addr, _port: u16) Address { - return Address{ - .os_addr = os.sockaddr{ - .in6 = os.sockaddr_in6{ - .family = os.AF_INET6, - .port = mem.nativeToBig(u16, _port), - .flowinfo = 0, - .addr = ip6.addr, - .scope_id = ip6.scope_id, - }, + pub fn initIp4(addr: [4]u8, port: u16) IpAddress { + return IpAddress{ + .in = os.sockaddr_in{ + .port = mem.nativeToBig(u16, port), + .addr = @ptrCast(*align(1) const u32, &addr).*, }, }; } - pub fn port(self: Address) u16 { - return mem.bigToNative(u16, self.os_addr.in.port); + pub fn initIp6(addr: [16]u8, port: u16, flowinfo: u32, scope_id: u32) IpAddress { + return IpAddress{ + .in6 = os.sockaddr_in6{ + .addr = addr, + .port = mem.nativeToBig(u16, port), + .flowinfo = flowinfo, + .scope_id = scope_id, + }, + }; + } + + /// Returns the port in native endian. + pub fn getPort(self: IpAddress) u16 { + const big_endian_port = switch (self.any.family) { + os.AF_INET => self.in.port, + os.AF_INET6 => self.in6.port, + else => unreachable, + }; + return mem.bigToNative(u16, big_endian_port); + } + + /// `port` is native-endian. + pub fn setPort(self: *IpAddress, port: u16) void { + const ptr = switch (self.any.family) { + os.AF_INET => &self.in.port, + os.AF_INET6 => &self.in6.port, + else => unreachable, + }; + ptr.* = mem.nativeToBig(u16, port); } - pub fn initPosix(addr: os.sockaddr) Address { - return Address{ .os_addr = addr }; + /// Asserts that `addr` is an IP address. + /// This function will read past the end of the pointer, with a size depending + /// on the address family. + pub fn initPosix(addr: *align(4) const os.sockaddr) IpAddress { + switch (addr.family) { + os.AF_INET => return IpAddress{ .in = @ptrCast(*const os.sockaddr_in, addr).* }, + os.AF_INET6 => return IpAddress{ .in6 = @ptrCast(*const os.sockaddr_in6, addr).* }, + else => unreachable, + } } - pub fn format(self: *const Address, out_stream: var) !void { - switch (self.os_addr.in.family) { + pub fn format( + self: IpAddress, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + context: var, + comptime Errors: type, + output: fn (@typeOf(context), []const u8) Errors!void, + ) !void { + switch (self.any.family) { os.AF_INET => { - const native_endian_port = mem.bigToNative(u16, self.os_addr.in.port); - const bytes = ([]const u8)((*self.os_addr.in.addr)[0..1]); - try out_stream.print("{}.{}.{}.{}:{}", bytes[0], bytes[1], bytes[2], bytes[3], native_endian_port); + const port = mem.bigToNative(u16, self.in.port); + const bytes = @ptrCast(*const [4]u8, &self.in.addr); + try std.fmt.format( + context, + Errors, + output, + "{}.{}.{}.{}:{}", + bytes[0], + bytes[1], + bytes[2], + bytes[3], + port, + ); }, os.AF_INET6 => { - const native_endian_port = mem.bigToNative(u16, self.os_addr.in6.port); - try out_stream.print("[TODO render ip6 address]:{}", native_endian_port); + const ZeroRun = struct { + index: usize, + count: usize, + }; + const port = mem.bigToNative(u16, self.in6.port); + const big_endian_parts = @ptrCast(*align(1) const [8]u16, &self.in6.addr); + const native_endian_parts = switch (builtin.endian) { + .Big => big_endian_parts.*, + .Little => blk: { + var buf: [8]u16 = undefined; + for (big_endian_parts) |part, i| { + buf[i] = mem.bigToNative(u16, part); + } + break :blk buf; + }, + }; + + var longest_zero_run: ?ZeroRun = null; + var this_zero_run: ?ZeroRun = null; + for (native_endian_parts) |part, i| { + if (part == 0) { + if (this_zero_run) |*zr| { + zr.count += 1; + } else { + this_zero_run = ZeroRun{ + .index = i, + .count = 1, + }; + } + } else if (this_zero_run) |zr| { + if (longest_zero_run) |lzr| { + if (zr.count > lzr.count and zr.count > 1) { + longest_zero_run = zr; + } + } else { + longest_zero_run = zr; + } + } + } + try output(context, "["); + var i: usize = 0; + while (i < native_endian_parts.len) { + if (i != 0) try output(context, ":"); + + if (longest_zero_run) |lzr| { + if (lzr.index == i) { + i += lzr.count; + continue; + } + } + + const part = native_endian_parts[i]; + try std.fmt.format(context, Errors, output, "{x}", part); + i += 1; + } + try std.fmt.format(context, Errors, output, "]:{}", port); }, - else => try out_stream.write("(unrecognized address family)"), + else => unreachable, + } + } + + pub fn eql(a: IpAddress, b: IpAddress) bool { + const a_bytes = @ptrCast([*]const u8, &a.any)[0..a.getOsSockLen()]; + const b_bytes = @ptrCast([*]const u8, &b.any)[0..b.getOsSockLen()]; + return mem.eql(u8, a_bytes, b_bytes); + } + + fn getOsSockLen(self: IpAddress) os.socklen_t { + switch (self.any.family) { + os.AF_INET => return @sizeOf(os.sockaddr_in), + os.AF_INET6 => return @sizeOf(os.sockaddr_in6), + else => unreachable, } } }; -pub fn parseIp4(buf: []const u8) !u32 { - var result: u32 = undefined; - const out_ptr = @sliceToBytes((*[1]u32)(&result)[0..]); +pub fn connectUnixSocket(path: []const u8) !fs.File { + const opt_non_block = if (std.io.mode == .evented) os.SOCK_NONBLOCK else 0; + const sockfd = try os.socket( + os.AF_UNIX, + os.SOCK_STREAM | os.SOCK_CLOEXEC | opt_non_block, + 0, + ); + errdefer os.close(sockfd); - var x: u8 = 0; - var index: u8 = 0; - var saw_any_digits = false; - for (buf) |c| { - if (c == '.') { - if (!saw_any_digits) { - return error.InvalidCharacter; - } - if (index == 3) { - return error.InvalidEnd; - } - out_ptr[index] = x; - index += 1; - x = 0; - saw_any_digits = false; - } else if (c >= '0' and c <= '9') { - saw_any_digits = true; - const digit = c - '0'; - if (@mulWithOverflow(u8, x, 10, &x)) { - return error.Overflow; + var sock_addr = os.sockaddr_un{ + .family = os.AF_UNIX, + .path = undefined, + }; + + if (path.len > sock_addr.path.len) return error.NameTooLong; + mem.copy(u8, &sock_addr.path, path); + + const size = @intCast(u32, @sizeOf(os.sockaddr_un) - sock_addr.path.len + path.len); + try os.connect(sockfd, &sock_addr, size); + + return fs.File.openHandle(sockfd); +} + +pub const AddressList = struct { + arena: std.heap.ArenaAllocator, + addrs: []IpAddress, + canon_name: ?[]u8, + + fn deinit(self: *AddressList) void { + // Here we copy the arena allocator into stack memory, because + // otherwise it would destroy itself while it was still working. + var arena = self.arena; + arena.deinit(); + // self is destroyed + } +}; + +/// All memory allocated with `allocator` will be freed before this function returns. +pub fn tcpConnectToHost(allocator: *mem.Allocator, name: []const u8, port: u16) !fs.File { + const list = getAddressList(allocator, name, port); + defer list.deinit(); + + const addrs = list.addrs.toSliceConst(); + if (addrs.len == 0) return error.UnknownHostName; + + return tcpConnectToAddress(addrs[0], port); +} + +pub fn tcpConnectToAddress(address: IpAddress) !fs.File { + const nonblock = if (std.io.is_async) os.SOCK_NONBLOCK else 0; + const sock_flags = os.SOCK_STREAM | os.SOCK_CLOEXEC | nonblock; + const sockfd = try os.socket(address.any.family, sock_flags, os.IPPROTO_TCP); + errdefer os.close(sockfd); + try os.connect(sockfd, &address.any, address.getOsSockLen()); + + return fs.File{ .handle = sockfd }; +} + +/// Call `AddressList.deinit` on the result. +pub fn getAddressList(allocator: *mem.Allocator, name: []const u8, port: u16) !*AddressList { + const result = blk: { + var arena = std.heap.ArenaAllocator.init(allocator); + errdefer arena.deinit(); + + const result = try arena.allocator.create(AddressList); + result.* = AddressList{ + .arena = arena, + .addrs = undefined, + .canon_name = null, + }; + break :blk result; + }; + const arena = &result.arena.allocator; + errdefer result.arena.deinit(); + + if (builtin.link_libc) { + const c = std.c; + const name_c = try std.cstr.addNullByte(allocator, name); + defer allocator.free(name_c); + + const port_c = try std.fmt.allocPrint(allocator, "{}\x00", port); + defer allocator.free(port_c); + + const hints = os.addrinfo{ + .flags = c.AI_NUMERICSERV, + .family = os.AF_UNSPEC, + .socktype = os.SOCK_STREAM, + .protocol = os.IPPROTO_TCP, + .canonname = null, + .addr = null, + .addrlen = 0, + .next = null, + }; + var res: *os.addrinfo = undefined; + switch (os.system.getaddrinfo(name_c.ptr, port_c.ptr, &hints, &res)) { + 0 => {}, + c.EAI_ADDRFAMILY => return error.HostLacksNetworkAddresses, + c.EAI_AGAIN => return error.TemporaryNameServerFailure, + c.EAI_BADFLAGS => unreachable, // Invalid hints + c.EAI_FAIL => return error.NameServerFailure, + c.EAI_FAMILY => return error.AddressFamilyNotSupported, + c.EAI_MEMORY => return error.OutOfMemory, + c.EAI_NODATA => return error.HostLacksNetworkAddresses, + c.EAI_NONAME => return error.UnknownHostName, + c.EAI_SERVICE => return error.ServiceUnavailable, + c.EAI_SOCKTYPE => unreachable, // Invalid socket type requested in hints + c.EAI_SYSTEM => switch (os.errno(-1)) { + else => |e| return os.unexpectedErrno(e), + }, + else => unreachable, + } + defer os.system.freeaddrinfo(res); + + const addr_count = blk: { + var count: usize = 0; + var it: ?*os.addrinfo = res; + while (it) |info| : (it = info.next) { + if (info.addr != null) { + count += 1; + } } - if (@addWithOverflow(u8, x, digit, &x)) { - return error.Overflow; + break :blk count; + }; + result.addrs = try arena.alloc(IpAddress, addr_count); + + var it: ?*os.addrinfo = res; + var i: usize = 0; + while (it) |info| : (it = info.next) { + const addr = info.addr orelse continue; + result.addrs[i] = IpAddress.initPosix(@alignCast(4, addr)); + + if (info.canonname) |n| { + if (result.canon_name == null) { + result.canon_name = try mem.dupe(arena, u8, mem.toSliceConst(u8, n)); + } } - } else { - return error.InvalidCharacter; + i += 1; } - } - if (index == 3 and saw_any_digits) { - out_ptr[index] = x; + return result; } + if (builtin.os == .linux) { + const flags = std.c.AI_NUMERICSERV; + const family = os.AF_UNSPEC; + var lookup_addrs = std.ArrayList(LookupAddr).init(allocator); + defer lookup_addrs.deinit(); + + var canon = std.Buffer.initNull(arena); + defer canon.deinit(); + + try linuxLookupName(&lookup_addrs, &canon, name, family, flags, port); + + result.addrs = try arena.alloc(IpAddress, lookup_addrs.len); + if (!canon.isNull()) { + result.canon_name = canon.toOwnedSlice(); + } - return error.Incomplete; + for (lookup_addrs.toSliceConst()) |lookup_addr, i| { + result.addrs[i] = lookup_addr.addr; + assert(result.addrs[i].getPort() == port); + } + + return result; + } + @compileError("std.net.getAddresses unimplemented for this OS"); } -pub const Ip6Addr = struct { - scope_id: u32, - addr: [16]u8, +const LookupAddr = struct { + addr: IpAddress, + sortkey: i32 = 0, }; -pub fn parseIp6(buf: []const u8) !Ip6Addr { - var result: Ip6Addr = undefined; - result.scope_id = 0; - const ip_slice = result.addr[0..]; +const DAS_USABLE = 0x40000000; +const DAS_MATCHINGSCOPE = 0x20000000; +const DAS_MATCHINGLABEL = 0x10000000; +const DAS_PREC_SHIFT = 20; +const DAS_SCOPE_SHIFT = 16; +const DAS_PREFIX_SHIFT = 8; +const DAS_ORDER_SHIFT = 0; - var x: u16 = 0; - var saw_any_digits = false; - var index: u8 = 0; - var scope_id = false; - for (buf) |c| { - if (scope_id) { - if (c >= '0' and c <= '9') { - const digit = c - '0'; - if (@mulWithOverflow(u32, result.scope_id, 10, &result.scope_id)) { - return error.Overflow; - } - if (@addWithOverflow(u32, result.scope_id, digit, &result.scope_id)) { - return error.Overflow; - } - } else { - return error.InvalidCharacter; - } - } else if (c == ':') { - if (!saw_any_digits) { - return error.InvalidCharacter; - } - if (index == 14) { - return error.InvalidEnd; - } - ip_slice[index] = @truncate(u8, x >> 8); - index += 1; - ip_slice[index] = @truncate(u8, x); - index += 1; - - x = 0; - saw_any_digits = false; - } else if (c == '%') { - if (!saw_any_digits) { - return error.InvalidCharacter; - } - if (index == 14) { - ip_slice[index] = @truncate(u8, x >> 8); - index += 1; - ip_slice[index] = @truncate(u8, x); - index += 1; +fn linuxLookupName( + addrs: *std.ArrayList(LookupAddr), + canon: *std.Buffer, + opt_name: ?[]const u8, + family: os.sa_family_t, + flags: u32, + port: u16, +) !void { + if (opt_name) |name| { + // reject empty name and check len so it fits into temp bufs + try canon.replaceContents(name); + if (IpAddress.parseExpectingFamily(name, family, port)) |addr| { + try addrs.append(LookupAddr{ .addr = addr }); + } else |name_err| if ((flags & std.c.AI_NUMERICHOST) != 0) { + return name_err; + } else { + try linuxLookupNameFromHosts(addrs, canon, name, family, port); + if (addrs.len == 0) { + try linuxLookupNameFromDnsSearch(addrs, canon, name, family, port); } - scope_id = true; - saw_any_digits = false; + } + } else { + try canon.resize(0); + try linuxLookupNameFromNull(addrs, family, flags, port); + } + if (addrs.len == 0) return error.UnknownHostName; + + // No further processing is needed if there are fewer than 2 + // results or if there are only IPv4 results. + if (addrs.len == 1 or family == os.AF_INET) return; + const all_ip4 = for (addrs.toSliceConst()) |addr| { + if (addr.addr.any.family != os.AF_INET) break false; + } else true; + if (all_ip4) return; + + // The following implements a subset of RFC 3484/6724 destination + // address selection by generating a single 31-bit sort key for + // each address. Rules 3, 4, and 7 are omitted for having + // excessive runtime and code size cost and dubious benefit. + // So far the label/precedence table cannot be customized. + // This implementation is ported from musl libc. + // A more idiomatic "ziggy" implementation would be welcome. + for (addrs.toSlice()) |*addr, i| { + var key: i32 = 0; + var sa6: os.sockaddr_in6 = undefined; + @memset(@ptrCast([*]u8, &sa6), 0, @sizeOf(os.sockaddr_in6)); + var da6 = os.sockaddr_in6{ + .family = os.AF_INET6, + .scope_id = addr.addr.in6.scope_id, + .port = 65535, + .flowinfo = 0, + .addr = [1]u8{0} ** 16, + }; + var sa4: os.sockaddr_in = undefined; + @memset(@ptrCast([*]u8, &sa4), 0, @sizeOf(os.sockaddr_in)); + var da4 = os.sockaddr_in{ + .family = os.AF_INET, + .port = 65535, + .addr = 0, + .zero = [1]u8{0} ** 8, + }; + var sa: *align(4) os.sockaddr = undefined; + var da: *align(4) os.sockaddr = undefined; + var salen: os.socklen_t = undefined; + var dalen: os.socklen_t = undefined; + if (addr.addr.any.family == os.AF_INET6) { + mem.copy(u8, &da6.addr, &addr.addr.in6.addr); + da = @ptrCast(*os.sockaddr, &da6); + dalen = @sizeOf(os.sockaddr_in6); + sa = @ptrCast(*os.sockaddr, &sa6); + salen = @sizeOf(os.sockaddr_in6); } else { - const digit = try std.fmt.charToDigit(c, 16); - if (@mulWithOverflow(u16, x, 16, &x)) { - return error.Overflow; + mem.copy(u8, &sa6.addr, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff"); + mem.copy(u8, &da6.addr, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff"); + // TODO https://github.com/ziglang/zig/issues/863 + mem.writeIntNative(u32, @ptrCast(*[4]u8, da6.addr[12..].ptr), addr.addr.in.addr); + da4.addr = addr.addr.in.addr; + da = @ptrCast(*os.sockaddr, &da4); + dalen = @sizeOf(os.sockaddr_in); + sa = @ptrCast(*os.sockaddr, &sa4); + salen = @sizeOf(os.sockaddr_in); + } + const dpolicy = policyOf(da6.addr); + const dscope: i32 = scopeOf(da6.addr); + const dlabel = dpolicy.label; + const dprec: i32 = dpolicy.prec; + const MAXADDRS = 3; + var prefixlen: i32 = 0; + const sock_flags = os.SOCK_DGRAM | os.SOCK_CLOEXEC; + if (os.socket(addr.addr.any.family, sock_flags, os.IPPROTO_UDP)) |fd| syscalls: { + defer os.close(fd); + os.connect(fd, da, dalen) catch break :syscalls; + key |= DAS_USABLE; + os.getsockname(fd, sa, &salen) catch break :syscalls; + if (addr.addr.any.family == os.AF_INET) { + // TODO sa6.addr[12..16] should return *[4]u8, making this cast unnecessary. + mem.writeIntNative(u32, @ptrCast(*[4]u8, &sa6.addr[12]), sa4.addr); } - if (@addWithOverflow(u16, x, digit, &x)) { - return error.Overflow; + if (dscope == i32(scopeOf(sa6.addr))) key |= DAS_MATCHINGSCOPE; + if (dlabel == labelOf(sa6.addr)) key |= DAS_MATCHINGLABEL; + prefixlen = prefixMatch(sa6.addr, da6.addr); + } else |_| {} + key |= dprec << DAS_PREC_SHIFT; + key |= (15 - dscope) << DAS_SCOPE_SHIFT; + key |= prefixlen << DAS_PREFIX_SHIFT; + key |= (MAXADDRS - @intCast(i32, i)) << DAS_ORDER_SHIFT; + addr.sortkey = key; + } + std.sort.sort(LookupAddr, addrs.toSlice(), addrCmpLessThan); +} + +const Policy = struct { + addr: [16]u8, + len: u8, + mask: u8, + prec: u8, + label: u8, +}; + +const defined_policies = [_]Policy{ + Policy{ + .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", + .len = 15, + .mask = 0xff, + .prec = 50, + .label = 0, + }, + Policy{ + .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00", + .len = 11, + .mask = 0xff, + .prec = 35, + .label = 4, + }, + Policy{ + .addr = "\x20\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + .len = 1, + .mask = 0xff, + .prec = 30, + .label = 2, + }, + Policy{ + .addr = "\x20\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + .len = 3, + .mask = 0xff, + .prec = 5, + .label = 5, + }, + Policy{ + .addr = "\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + .len = 0, + .mask = 0xfe, + .prec = 3, + .label = 13, + }, + // These are deprecated and/or returned to the address + // pool, so despite the RFC, treating them as special + // is probably wrong. + // { "", 11, 0xff, 1, 3 }, + // { "\xfe\xc0", 1, 0xc0, 1, 11 }, + // { "\x3f\xfe", 1, 0xff, 1, 12 }, + // Last rule must match all addresses to stop loop. + Policy{ + .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + .len = 0, + .mask = 0, + .prec = 40, + .label = 1, + }, +}; + +fn policyOf(a: [16]u8) *const Policy { + for (defined_policies) |*policy| { + if (!mem.eql(u8, a[0..policy.len], policy.addr[0..policy.len])) continue; + if ((a[policy.len] & policy.mask) != policy.addr[policy.len]) continue; + return policy; + } + unreachable; +} + +fn scopeOf(a: [16]u8) u8 { + if (IN6_IS_ADDR_MULTICAST(a)) return a[1] & 15; + if (IN6_IS_ADDR_LINKLOCAL(a)) return 2; + if (IN6_IS_ADDR_LOOPBACK(a)) return 2; + if (IN6_IS_ADDR_SITELOCAL(a)) return 5; + return 14; +} + +fn prefixMatch(s: [16]u8, d: [16]u8) u8 { + // TODO: This FIXME inherited from porting from musl libc. + // I don't want this to go into zig std lib 1.0.0. + + // FIXME: The common prefix length should be limited to no greater + // than the nominal length of the prefix portion of the source + // address. However the definition of the source prefix length is + // not clear and thus this limiting is not yet implemented. + var i: u8 = 0; + while (i < 128 and ((s[i / 8] ^ d[i / 8]) & (u8(128) >> @intCast(u3, i % 8))) == 0) : (i += 1) {} + return i; +} + +fn labelOf(a: [16]u8) u8 { + return policyOf(a).label; +} + +fn IN6_IS_ADDR_MULTICAST(a: [16]u8) bool { + return a[0] == 0xff; +} + +fn IN6_IS_ADDR_LINKLOCAL(a: [16]u8) bool { + return a[0] == 0xfe and (a[1] & 0xc0) == 0x80; +} + +fn IN6_IS_ADDR_LOOPBACK(a: [16]u8) bool { + return a[0] == 0 and a[1] == 0 and + a[2] == 0 and + a[12] == 0 and a[13] == 0 and + a[14] == 0 and a[15] == 1; +} + +fn IN6_IS_ADDR_SITELOCAL(a: [16]u8) bool { + return a[0] == 0xfe and (a[1] & 0xc0) == 0xc0; +} + +// Parameters `b` and `a` swapped to make this descending. +fn addrCmpLessThan(b: LookupAddr, a: LookupAddr) bool { + return a.sortkey < b.sortkey; +} + +fn linuxLookupNameFromNull( + addrs: *std.ArrayList(LookupAddr), + family: os.sa_family_t, + flags: u32, + port: u16, +) !void { + if ((flags & std.c.AI_PASSIVE) != 0) { + if (family != os.AF_INET6) { + (try addrs.addOne()).* = LookupAddr{ + .addr = IpAddress.initIp4([1]u8{0} ** 4, port), + }; + } + if (family != os.AF_INET) { + (try addrs.addOne()).* = LookupAddr{ + .addr = IpAddress.initIp6([1]u8{0} ** 16, port, 0, 0), + }; + } + } else { + if (family != os.AF_INET6) { + (try addrs.addOne()).* = LookupAddr{ + .addr = IpAddress.initIp4([4]u8{ 127, 0, 0, 1 }, port), + }; + } + if (family != os.AF_INET) { + (try addrs.addOne()).* = LookupAddr{ + .addr = IpAddress.initIp6(([1]u8{0} ** 15) ++ [1]u8{1}, port, 0, 0), + }; + } + } +} + +fn linuxLookupNameFromHosts( + addrs: *std.ArrayList(LookupAddr), + canon: *std.Buffer, + name: []const u8, + family: os.sa_family_t, + port: u16, +) !void { + const file = fs.File.openReadC(c"/etc/hosts") catch |err| switch (err) { + error.FileNotFound, + error.NotDir, + error.AccessDenied, + => return, + else => |e| return e, + }; + defer file.close(); + + const stream = &std.io.BufferedInStream(fs.File.ReadError).init(&file.inStream().stream).stream; + var line_buf: [512]u8 = undefined; + while (stream.readUntilDelimiterOrEof(&line_buf, '\n') catch |err| switch (err) { + error.StreamTooLong => blk: { + // Skip to the delimiter in the stream, to fix parsing + try stream.skipUntilDelimiterOrEof('\n'); + // Use the truncated line. A truncated comment or hostname will be handled correctly. + break :blk line_buf[0..]; + }, + else => |e| return e, + }) |line| { + const no_comment_line = mem.separate(line, "#").next().?; + + var line_it = mem.tokenize(no_comment_line, " \t"); + const ip_text = line_it.next() orelse continue; + var first_name_text: ?[]const u8 = null; + while (line_it.next()) |name_text| { + if (first_name_text == null) first_name_text = name_text; + if (mem.eql(u8, name_text, name)) { + break; } - saw_any_digits = true; + } else continue; + + const addr = IpAddress.parseExpectingFamily(ip_text, family, port) catch |err| switch (err) { + error.Overflow, + error.InvalidEnd, + error.InvalidCharacter, + error.Incomplete, + error.InvalidIPAddressFormat, + => continue, + }; + try addrs.append(LookupAddr{ .addr = addr }); + + // first name is canonical name + const name_text = first_name_text.?; + if (isValidHostName(name_text)) { + try canon.replaceContents(name_text); } } +} - if (!saw_any_digits) { - return error.Incomplete; +pub fn isValidHostName(hostname: []const u8) bool { + if (hostname.len >= 254) return false; + if (!std.unicode.utf8ValidateSlice(hostname)) return false; + for (hostname) |byte| { + if (byte >= 0x80 or byte == '.' or byte == '-' or std.ascii.isAlNum(byte)) { + continue; + } + return false; } + return true; +} - if (scope_id) { - return result; +fn linuxLookupNameFromDnsSearch( + addrs: *std.ArrayList(LookupAddr), + canon: *std.Buffer, + name: []const u8, + family: os.sa_family_t, + port: u16, +) !void { + var rc: ResolvConf = undefined; + try getResolvConf(addrs.allocator, &rc); + defer rc.deinit(); + + // Count dots, suppress search when >=ndots or name ends in + // a dot, which is an explicit request for global scope. + var dots: usize = 0; + for (name) |byte| { + if (byte == '.') dots += 1; } - if (index == 14) { - ip_slice[14] = @truncate(u8, x >> 8); - ip_slice[15] = @truncate(u8, x); - return result; + const search = if (rc.search.isNull() or dots >= rc.ndots or mem.endsWith(u8, name, ".")) + [_]u8{} + else + rc.search.toSliceConst(); + + var canon_name = name; + + // Strip final dot for canon, fail if multiple trailing dots. + if (mem.endsWith(u8, canon_name, ".")) canon_name.len -= 1; + if (mem.endsWith(u8, canon_name, ".")) return error.UnknownHostName; + + // Name with search domain appended is setup in canon[]. This both + // provides the desired default canonical name (if the requested + // name is not a CNAME record) and serves as a buffer for passing + // the full requested name to name_from_dns. + try canon.resize(canon_name.len); + mem.copy(u8, canon.toSlice(), canon_name); + try canon.appendByte('.'); + + var tok_it = mem.tokenize(search, " \t"); + while (tok_it.next()) |tok| { + canon.shrink(canon_name.len + 1); + try canon.append(tok); + try linuxLookupNameFromDns(addrs, canon, canon.toSliceConst(), family, rc, port); + if (addrs.len != 0) return; } - return error.Incomplete; + canon.shrink(canon_name.len); + return linuxLookupNameFromDns(addrs, canon, name, family, rc, port); } -test "std.net.parseIp4" { - assert((try parseIp4("127.0.0.1")) == mem.bigToNative(u32, 0x7f000001)); +const dpc_ctx = struct { + addrs: *std.ArrayList(LookupAddr), + canon: *std.Buffer, + port: u16, +}; - testParseIp4Fail("256.0.0.1", error.Overflow); - testParseIp4Fail("x.0.0.1", error.InvalidCharacter); - testParseIp4Fail("127.0.0.1.1", error.InvalidEnd); - testParseIp4Fail("127.0.0.", error.Incomplete); - testParseIp4Fail("100..0.1", error.InvalidCharacter); +fn linuxLookupNameFromDns( + addrs: *std.ArrayList(LookupAddr), + canon: *std.Buffer, + name: []const u8, + family: os.sa_family_t, + rc: ResolvConf, + port: u16, +) !void { + var ctx = dpc_ctx{ + .addrs = addrs, + .canon = canon, + .port = port, + }; + const AfRr = struct { + af: os.sa_family_t, + rr: u8, + }; + const afrrs = [_]AfRr{ + AfRr{ .af = os.AF_INET6, .rr = os.RR_A }, + AfRr{ .af = os.AF_INET, .rr = os.RR_AAAA }, + }; + var qbuf: [2][280]u8 = undefined; + var abuf: [2][512]u8 = undefined; + var qp: [2][]const u8 = undefined; + const apbuf = [2][]u8{ &abuf[0], &abuf[1] }; + var nq: usize = 0; + + for (afrrs) |afrr| { + if (family != afrr.af) { + const len = os.res_mkquery(0, name, 1, afrr.rr, [_]u8{}, null, &qbuf[nq]); + qp[nq] = qbuf[nq][0..len]; + nq += 1; + } + } + + var ap = [2][]u8{ apbuf[0][0..0], apbuf[1][0..0] }; + try resMSendRc(qp[0..nq], ap[0..nq], apbuf[0..nq], rc); + + var i: usize = 0; + while (i < nq) : (i += 1) { + dnsParse(ap[i], ctx, dnsParseCallback) catch {}; + } + + if (addrs.len != 0) return; + if (ap[0].len < 4 or (ap[0][3] & 15) == 2) return error.TemporaryNameServerFailure; + if ((ap[0][3] & 15) == 0) return error.UnknownHostName; + if ((ap[0][3] & 15) == 3) return; + return error.NameServerFailure; } -fn testParseIp4Fail(buf: []const u8, expected_err: anyerror) void { - if (parseIp4(buf)) |_| { - @panic("expected error"); - } else |e| { - assert(e == expected_err); +const ResolvConf = struct { + attempts: u32, + ndots: u32, + timeout: u32, + search: std.Buffer, + ns: std.ArrayList(LookupAddr), + + fn deinit(rc: *ResolvConf) void { + rc.ns.deinit(); + rc.search.deinit(); + rc.* = undefined; + } +}; + +/// Ignores lines longer than 512 bytes. +/// TODO: https://github.com/ziglang/zig/issues/2765 and https://github.com/ziglang/zig/issues/2761 +fn getResolvConf(allocator: *mem.Allocator, rc: *ResolvConf) !void { + rc.* = ResolvConf{ + .ns = std.ArrayList(LookupAddr).init(allocator), + .search = std.Buffer.initNull(allocator), + .ndots = 1, + .timeout = 5, + .attempts = 2, + }; + errdefer rc.deinit(); + + const file = fs.File.openReadC(c"/etc/resolv.conf") catch |err| switch (err) { + error.FileNotFound, + error.NotDir, + error.AccessDenied, + => return linuxLookupNameFromNumericUnspec(&rc.ns, "127.0.0.1", 53), + else => |e| return e, + }; + defer file.close(); + + const stream = &std.io.BufferedInStream(fs.File.ReadError).init(&file.inStream().stream).stream; + var line_buf: [512]u8 = undefined; + while (stream.readUntilDelimiterOrEof(&line_buf, '\n') catch |err| switch (err) { + error.StreamTooLong => blk: { + // Skip to the delimiter in the stream, to fix parsing + try stream.skipUntilDelimiterOrEof('\n'); + // Give an empty line to the while loop, which will be skipped. + break :blk line_buf[0..0]; + }, + else => |e| return e, + }) |line| { + const no_comment_line = mem.separate(line, "#").next().?; + var line_it = mem.tokenize(no_comment_line, " \t"); + + const token = line_it.next() orelse continue; + if (mem.eql(u8, token, "options")) { + while (line_it.next()) |sub_tok| { + var colon_it = mem.separate(sub_tok, ":"); + const name = colon_it.next().?; + const value_txt = colon_it.next() orelse continue; + const value = std.fmt.parseInt(u8, value_txt, 10) catch |err| switch (err) { + error.Overflow => 255, + error.InvalidCharacter => continue, + }; + if (mem.eql(u8, name, "ndots")) { + rc.ndots = std.math.min(value, 15); + } else if (mem.eql(u8, name, "attempts")) { + rc.attempts = std.math.min(value, 10); + } else if (mem.eql(u8, name, "timeout")) { + rc.timeout = std.math.min(value, 60); + } + } + } else if (mem.eql(u8, token, "nameserver")) { + const ip_txt = line_it.next() orelse continue; + try linuxLookupNameFromNumericUnspec(&rc.ns, ip_txt, 53); + } else if (mem.eql(u8, token, "domain") or mem.eql(u8, token, "search")) { + try rc.search.replaceContents(line_it.rest()); + } + } + + if (rc.ns.len == 0) { + return linuxLookupNameFromNumericUnspec(&rc.ns, "127.0.0.1", 53); } } -test "std.net.parseIp6" { - const addr = try parseIp6("FF01:0:0:0:0:0:0:FB"); - assert(addr.addr[0] == 0xff); - assert(addr.addr[1] == 0x01); - assert(addr.addr[2] == 0x00); +fn linuxLookupNameFromNumericUnspec( + addrs: *std.ArrayList(LookupAddr), + name: []const u8, + port: u16, +) !void { + const addr = try IpAddress.parse(name, port); + (try addrs.addOne()).* = LookupAddr{ .addr = addr }; } -pub fn connectUnixSocket(path: []const u8) !std.fs.File { - const opt_non_block = if (std.event.Loop.instance != null) os.SOCK_NONBLOCK else 0; - const sockfd = try os.socket( - os.AF_UNIX, - os.SOCK_STREAM | os.SOCK_CLOEXEC | opt_non_block, - 0, - ); - errdefer os.close(sockfd); +fn resMSendRc( + queries: []const []const u8, + answers: [][]u8, + answer_bufs: []const []u8, + rc: ResolvConf, +) !void { + const timeout = 1000 * rc.timeout; + const attempts = rc.attempts; + + var sl: os.socklen_t = @sizeOf(os.sockaddr_in); + var family: os.sa_family_t = os.AF_INET; + + var ns_list = std.ArrayList(IpAddress).init(rc.ns.allocator); + defer ns_list.deinit(); + + try ns_list.resize(rc.ns.len); + const ns = ns_list.toSlice(); + + for (rc.ns.toSliceConst()) |iplit, i| { + ns[i] = iplit.addr; + assert(ns[i].getPort() == 53); + if (iplit.addr.any.family != os.AF_INET) { + sl = @sizeOf(os.sockaddr_in6); + family = os.AF_INET6; + } + } - var sock_addr = os.sockaddr{ - .un = os.sockaddr_un{ - .family = os.AF_UNIX, - .path = undefined, + // Get local address and open/bind a socket + var sa: IpAddress = undefined; + @memset(@ptrCast([*]u8, &sa), 0, @sizeOf(IpAddress)); + sa.any.family = family; + const flags = os.SOCK_DGRAM | os.SOCK_CLOEXEC | os.SOCK_NONBLOCK; + const fd = os.socket(family, flags, 0) catch |err| switch (err) { + error.AddressFamilyNotSupported => blk: { + // Handle case where system lacks IPv6 support + if (family == os.AF_INET6) { + family = os.AF_INET; + break :blk try os.socket(os.AF_INET, flags, 0); + } + return err; }, + else => |e| return e, }; + defer os.close(fd); + try os.bind(fd, &sa.any, sl); - if (path.len > @typeOf(sock_addr.un.path).len) return error.NameTooLong; - mem.copy(u8, sock_addr.un.path[0..], path); - const size = @intCast(u32, @sizeOf(os.sa_family_t) + path.len); - if (std.event.Loop.instance) |loop| { - try os.connect_async(sockfd, &sock_addr, size); - try loop.linuxWaitFd(sockfd, os.EPOLLIN | os.EPOLLOUT | os.EPOLLET); - try os.getsockoptError(sockfd); - } else { - try os.connect(sockfd, &sock_addr, size); + // Past this point, there are no errors. Each individual query will + // yield either no reply (indicated by zero length) or an answer + // packet which is up to the caller to interpret. + + // Convert any IPv4 addresses in a mixed environment to v4-mapped + // TODO + //if (family == AF_INET6) { + // setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &(int){0}, sizeof 0); + // for (i=0; i<nns; i++) { + // if (ns[i].sin.sin_family != AF_INET) continue; + // memcpy(ns[i].sin6.sin6_addr.s6_addr+12, + // &ns[i].sin.sin_addr, 4); + // memcpy(ns[i].sin6.sin6_addr.s6_addr, + // "\0\0\0\0\0\0\0\0\0\0\xff\xff", 12); + // ns[i].sin6.sin6_family = AF_INET6; + // ns[i].sin6.sin6_flowinfo = 0; + // ns[i].sin6.sin6_scope_id = 0; + // } + //} + + var pfd = [1]os.pollfd{os.pollfd{ + .fd = fd, + .events = os.POLLIN, + .revents = undefined, + }}; + const retry_interval = timeout / attempts; + var next: u32 = 0; + var t2: u64 = std.time.milliTimestamp(); + var t0 = t2; + var t1 = t2 - retry_interval; + + var servfail_retry: usize = undefined; + + outer: while (t2 - t0 < timeout) : (t2 = std.time.milliTimestamp()) { + if (t2 - t1 >= retry_interval) { + // Query all configured nameservers in parallel + var i: usize = 0; + while (i < queries.len) : (i += 1) { + if (answers[i].len == 0) { + var j: usize = 0; + while (j < ns.len) : (j += 1) { + _ = os.sendto(fd, queries[i], os.MSG_NOSIGNAL, &ns[j].any, sl) catch undefined; + } + } + } + t1 = t2; + servfail_retry = 2 * queries.len; + } + + // Wait for a response, or until time to retry + const clamped_timeout = std.math.min(u31(std.math.maxInt(u31)), t1 + retry_interval - t2); + const nevents = os.poll(&pfd, clamped_timeout) catch 0; + if (nevents == 0) continue; + + while (true) { + var sl_copy = sl; + const rlen = os.recvfrom(fd, answer_bufs[next], 0, &sa.any, &sl_copy) catch break; + + // Ignore non-identifiable packets + if (rlen < 4) continue; + + // Ignore replies from addresses we didn't send to + var j: usize = 0; + while (j < ns.len and !ns[j].eql(sa)) : (j += 1) {} + if (j == ns.len) continue; + + // Find which query this answer goes with, if any + var i: usize = next; + while (i < queries.len and (answer_bufs[next][0] != queries[i][0] or + answer_bufs[next][1] != queries[i][1])) : (i += 1) + {} + + if (i == queries.len) continue; + if (answers[i].len != 0) continue; + + // Only accept positive or negative responses; + // retry immediately on server failure, and ignore + // all other codes such as refusal. + switch (answer_bufs[next][3] & 15) { + 0, 3 => {}, + 2 => if (servfail_retry != 0) { + servfail_retry -= 1; + _ = os.sendto(fd, queries[i], os.MSG_NOSIGNAL, &ns[j].any, sl) catch undefined; + }, + else => continue, + } + + // Store answer in the right slot, or update next + // available temp slot if it's already in place. + answers[i].len = rlen; + if (i == next) { + while (next < queries.len and answers[next].len != 0) : (next += 1) {} + } else { + mem.copy(u8, answer_bufs[i], answer_bufs[next][0..rlen]); + } + + if (next == queries.len) break :outer; + } } +} - return std.fs.File.openHandle(sockfd); +fn dnsParse( + r: []const u8, + ctx: var, + comptime callback: var, +) !void { + // This implementation is ported from musl libc. + // A more idiomatic "ziggy" implementation would be welcome. + if (r.len < 12) return error.InvalidDnsPacket; + if ((r[3] & 15) != 0) return; + var p = r.ptr + 12; + var qdcount = r[4] * usize(256) + r[5]; + var ancount = r[6] * usize(256) + r[7]; + if (qdcount + ancount > 64) return error.InvalidDnsPacket; + while (qdcount != 0) { + qdcount -= 1; + while (@ptrToInt(p) - @ptrToInt(r.ptr) < r.len and p[0] -% 1 < 127) p += 1; + if (p[0] > 193 or (p[0] == 193 and p[1] > 254) or @ptrToInt(p) > @ptrToInt(r.ptr) + r.len - 6) + return error.InvalidDnsPacket; + p += usize(5) + @boolToInt(p[0] != 0); + } + while (ancount != 0) { + ancount -= 1; + while (@ptrToInt(p) - @ptrToInt(r.ptr) < r.len and p[0] -% 1 < 127) p += 1; + if (p[0] > 193 or (p[0] == 193 and p[1] > 254) or @ptrToInt(p) > @ptrToInt(r.ptr) + r.len - 6) + return error.InvalidDnsPacket; + p += usize(1) + @boolToInt(p[0] != 0); + const len = p[8] * usize(256) + p[9]; + if (@ptrToInt(p) + len > @ptrToInt(r.ptr) + r.len) return error.InvalidDnsPacket; + try callback(ctx, p[1], p[10 .. 10 + len], r); + p += 10 + len; + } } + +fn dnsParseCallback(ctx: dpc_ctx, rr: u8, data: []const u8, packet: []const u8) !void { + switch (rr) { + os.RR_A => { + if (data.len != 4) return error.InvalidDnsARecord; + const new_addr = try ctx.addrs.addOne(); + new_addr.* = LookupAddr{ + // TODO slice [0..4] to make this *[4]u8 without @ptrCast + .addr = IpAddress.initIp4(@ptrCast(*const [4]u8, data.ptr).*, ctx.port), + }; + }, + os.RR_AAAA => { + if (data.len != 16) return error.InvalidDnsAAAARecord; + const new_addr = try ctx.addrs.addOne(); + new_addr.* = LookupAddr{ + // TODO slice [0..16] to make this *[16]u8 without @ptrCast + .addr = IpAddress.initIp6(@ptrCast(*const [16]u8, data.ptr).*, ctx.port, 0, 0), + }; + }, + os.RR_CNAME => { + var tmp: [256]u8 = undefined; + // Returns len of compressed name. strlen to get canon name. + _ = try os.dn_expand(packet, data, &tmp); + const canon_name = mem.toSliceConst(u8, &tmp); + if (isValidHostName(canon_name)) { + try ctx.canon.replaceContents(canon_name); + } + }, + else => return, + } +} + +pub const TcpServer = struct { + /// Copied from `Options` on `init`. + kernel_backlog: u32, + + /// `undefined` until `listen` returns successfully. + listen_address: IpAddress, + + sockfd: ?os.fd_t, + + pub const Options = 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 + /// seeing "Connection refused". + kernel_backlog: u32 = 128, + }; + + /// After this call succeeds, resources have been acquired and must + /// be released with `deinit`. + pub fn init(options: Options) TcpServer { + return TcpServer{ + .sockfd = null, + .kernel_backlog = options.kernel_backlog, + .listen_address = undefined, + }; + } + + /// Release all resources. The `TcpServer` memory becomes `undefined`. + pub fn deinit(self: *TcpServer) void { + self.close(); + self.* = undefined; + } + + pub fn listen(self: *TcpServer, address: IpAddress) !void { + const nonblock = if (std.io.is_async) os.SOCK_NONBLOCK else 0; + const sock_flags = os.SOCK_STREAM | os.SOCK_CLOEXEC | nonblock; + const sockfd = try os.socket(os.AF_INET, sock_flags, os.PROTO_tcp); + self.sockfd = sockfd; + errdefer { + os.close(sockfd); + self.sockfd = null; + } + + var socklen = address.getOsSockLen(); + try os.bind(sockfd, &address.any, socklen); + try os.listen(sockfd, self.kernel_backlog); + try os.getsockname(sockfd, &self.listen_address.any, &socklen); + } + + /// Stop listening. It is still necessary to call `deinit` after stopping listening. + /// Calling `deinit` will automatically call `close`. It is safe to call `close` when + /// not listening. + pub fn close(self: *TcpServer) void { + if (self.sockfd) |fd| { + os.close(fd); + self.sockfd = null; + self.listen_address = undefined; + } + } + + pub const AcceptError = error{ + ConnectionAborted, + + /// The per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + + /// The system-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + + /// Not enough free memory. This often means that the memory allocation is limited + /// by the socket buffer limits, not by the system memory. + SystemResources, + + ProtocolFailure, + + /// Firewall rules forbid connection. + BlockedByFirewall, + } || os.UnexpectedError; + + /// If this function succeeds, the returned `fs.File` is a caller-managed resource. + pub fn accept(self: *TcpServer) AcceptError!fs.File { + const nonblock = if (std.io.is_async) os.SOCK_NONBLOCK else 0; + const accept_flags = nonblock | os.SOCK_CLOEXEC; + var accepted_addr: IpAddress = undefined; + var adr_len: os.socklen_t = @sizeOf(IpAddress); + if (os.accept4(self.sockfd.?, &accepted_addr.any, &adr_len, accept_flags)) |fd| { + return fs.File.openHandle(fd); + } else |err| switch (err) { + // We only give SOCK_NONBLOCK when I/O mode is async, in which case this error + // is handled by os.accept4. + error.WouldBlock => unreachable, + else => |e| return e, + } + } +}; |
