Commit d776a6bbbe

Andrew Kelley <andrew@ziglang.org>
2025-09-22 06:13:26
Io.net: rework IPv6 parsing and printing
extract pure functional logic into pure functions and then layer the scope crap on top properly the formatting code incorrectly didn't do the reverse operation (if_indextoname). fix that with some TODO panics
1 parent 5089352
Changed files (3)
lib/std/Io/net/HostName.zig
@@ -105,11 +105,11 @@ pub fn lookup(host_name: HostName, io: Io, options: LookupOptions) LookupError!L
             {
                 var i: usize = 0;
                 if (options.family != .ip6) {
-                    options.addresses_buffer[i] = .{ .ip4 = .localhost(options.port) };
+                    options.addresses_buffer[i] = .{ .ip4 = .loopback(options.port) };
                     i += 1;
                 }
                 if (options.family != .ip4) {
-                    options.addresses_buffer[i] = .{ .ip6 = .localhost(options.port) };
+                    options.addresses_buffer[i] = .{ .ip6 = .loopback(options.port) };
                     i += 1;
                 }
                 const canon_name = "localhost";
@@ -166,7 +166,7 @@ fn sortLookupResults(options: LookupOptions, result: LookupResult) !LookupResult
             switch (a) {
                 .ip6 => |ip6| {
                     da6.bytes = ip6.bytes;
-                    da6.scope_id = ip6.scope_id;
+                    da6.interface = ip6.interface;
                 },
                 .ip4 => |ip4| {
                     da6.bytes[0..12].* = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff".*;
lib/std/Io/net.zig
@@ -26,10 +26,12 @@ pub const IpAddress = union(enum) {
     pub const Family = @typeInfo(IpAddress).@"union".tag_type.?;
 
     /// Parse the given IP address string into an `IpAddress` value.
+    ///
+    /// This is a pure function but it cannot handle IPv6 addresses that have
+    /// scope ids ("%foo" at the end). To also handle those, `resolve` must be
+    /// called instead.
     pub fn parse(name: []const u8, port: u16) !IpAddress {
-        if (Ip4Address.parse(name, port)) |ip4| {
-            return .{ .ip4 = ip4 };
-        } else |err| switch (err) {
+        if (parseIp4(name, port)) |ip4| return ip4 else |err| switch (err) {
             error.Overflow,
             error.InvalidEnd,
             error.InvalidCharacter,
@@ -38,26 +40,41 @@ pub const IpAddress = union(enum) {
             => {},
         }
 
-        if (Ip6Address.parse(name, port)) |ip6| {
-            return .{ .ip6 = ip6 };
-        } else |err| switch (err) {
+        return parseIp6(name, port);
+    }
+
+    pub fn parseIp4(text: []const u8, port: u16) Ip4Address.ParseError!IpAddress {
+        return .{ .ip4 = try Ip4Address.parse(text, port) };
+    }
+
+    /// This is a pure function but it cannot handle IPv6 addresses that have
+    /// scope ids ("%foo" at the end). To also handle those, `resolveIp6` must be
+    /// called instead.
+    pub fn parseIp6(text: []const u8, port: u16) Ip6Address.ParseError!IpAddress {
+        return .{ .ip6 = try Ip6Address.parse(text, port) };
+    }
+
+    /// This function requires an `Io` parameter because it must query the operating
+    /// system to convert interface name to index. For example, in
+    /// "fe80::e0e:76ff:fed4:cf22%eno1", "eno1" must be resolved to an index by
+    /// creating a socket and then using an `ioctl` syscall.
+    ///
+    /// For a pure function that cannot handle scopes, see `parse`.
+    pub fn resolve(io: Io, text: []const u8, port: u16) !IpAddress {
+        if (parseIp4(text, port)) |ip4| return ip4 else |err| switch (err) {
             error.Overflow,
             error.InvalidEnd,
             error.InvalidCharacter,
             error.Incomplete,
-            error.InvalidIpv4Mapping,
+            error.NonCanonical,
             => {},
         }
 
-        return error.InvalidIpAddressFormat;
+        return resolveIp6(io, text, port);
     }
 
-    pub fn parseIp6(buffer: []const u8, port: u16) Ip6Address.ParseError!IpAddress {
-        return .{ .ip6 = try Ip6Address.parse(buffer, port) };
-    }
-
-    pub fn parseIp4(buffer: []const u8, port: u16) Ip4Address.ParseError!IpAddress {
-        return .{ .ip4 = try Ip4Address.parse(buffer, port) };
+    pub fn resolveIp6(io: Io, text: []const u8, port: u16) Ip6Address.ResolveError!IpAddress {
+        return .{ .ip6 = try Ip6Address.resolve(io, text, port) };
     }
 
     /// Returns the port in native endian.
@@ -74,6 +91,19 @@ pub const IpAddress = union(enum) {
         }
     }
 
+    /// Includes the optional scope ("%foo" at the end) in IPv6 addresses.
+    ///
+    /// See `format` for an alternative that omits scopes and does
+    /// not require an `Io` parameter.
+    pub fn formatResolved(a: IpAddress, io: Io, w: *Io.Writer) Ip6Address.FormatError!void {
+        switch (a) {
+            .ip4 => |x| return x.format(w),
+            .ip6 => |x| return x.formatResolved(io, w),
+        }
+    }
+
+    /// See `formatResolved` for an alternative that additionally prints the optional
+    /// scope at the end of IPv6 addresses and requires an `Io` parameter.
     pub fn format(a: IpAddress, w: *Io.Writer) Io.Writer.Error!void {
         switch (a) {
             inline .ip4, .ip6 => |x| return x.format(w),
@@ -99,11 +129,12 @@ pub const IpAddress = union(enum) {
     }
 };
 
+/// An IPv4 address in binary memory layout.
 pub const Ip4Address = struct {
     bytes: [4]u8,
     port: u16,
 
-    pub fn localhost(port: u16) Ip4Address {
+    pub fn loopback(port: u16) Ip4Address {
         return .{
             .bytes = .{ 127, 0, 0, 1 },
             .port = port,
@@ -162,21 +193,14 @@ pub const Ip4Address = struct {
     }
 };
 
+/// An IPv6 address in binary memory layout.
 pub const Ip6Address = struct {
     /// Native endian
     port: u16,
     /// Big endian
     bytes: [16]u8,
-    flowinfo: u32 = 0,
-    scope_id: u32 = 0,
-
-    pub const ParseError = error{
-        Overflow,
-        InvalidCharacter,
-        InvalidEnd,
-        InvalidIpv4Mapping,
-        Incomplete,
-    };
+    flow: u32 = 0,
+    interface: Interface = .none,
 
     pub const Policy = struct {
         addr: [16]u8,
@@ -186,192 +210,205 @@ pub const Ip6Address = struct {
         label: u8,
     };
 
-    pub fn localhost(port: u16) Ip6Address {
+    pub fn loopback(port: u16) Ip6Address {
         return .{
             .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
             .port = port,
         };
     }
 
-    pub fn parse(buffer: []const u8, port: u16) ParseError!Ip6Address {
-        var result: Ip6Address = .{
-            .port = port,
-            .bytes = undefined,
+    /// An IPv6 address but with `Interface` as a name rather than index.
+    pub const Unresolved = struct {
+        /// Big endian
+        bytes: [16]u8,
+        interface_name: ?Interface.Name,
+
+        pub const Parsed = union(enum) {
+            success: Unresolved,
+            invalid_byte: usize,
+            unexpected_end,
         };
-        var ip_slice: *[16]u8 = &result.bytes;
 
-        var tail: [16]u8 = undefined;
+        pub fn parse(buffer: []const u8) Parsed {
+            if (buffer.len < 2) return .unexpected_end;
+            var parts: [8]u16 = @splat(0);
+            var parts_i: usize = 0;
+            var i: usize = 0;
+            var digit_i: usize = 0;
+            const State = union(enum) { digit, colon, end };
+            state: switch (State.digit) {
+                .digit => c: switch (buffer[i]) {
+                    'a'...'f' => |c| {
+                        const digit = c - 'a';
+                        parts[parts_i] = parts[parts_i] * 16 + digit;
+                        if (digit_i == 3) {
+                            digit_i = 0;
+                            parts_i += 1;
+                            i += 1;
+                            if (parts.len - parts_i == 0) continue :state .end;
+                            continue :state .colon;
+                        }
+                        digit_i += 1;
+                        if (buffer.len - i == 0) return .unexpected_end;
+                        i += 1;
+                        continue :c buffer[i];
+                    },
+                    'A'...'F' => |c| continue :c c + ('a' - 'A'),
+                    '0'...'9' => |c| continue :c c + ('a' - '0'),
+                    ':' => @panic("TODO"),
+                    else => return .{ .invalid_byte = i },
+                },
+                .colon => @panic("TODO"),
+                .end => @panic("TODO"),
+            }
+        }
+
+        pub const FromAddressError = Interface.NameError;
 
-        var x: u16 = 0;
-        var saw_any_digits = false;
-        var index: u8 = 0;
-        var scope_id = false;
-        var abbrv = false;
-        for (buffer, 0..) |c, i| {
-            if (scope_id) {
-                if (c >= '0' and c <= '9') {
-                    const digit = c - '0';
-                    {
-                        const ov = @mulWithOverflow(result.scope_id, 10);
-                        if (ov[1] != 0) return error.Overflow;
-                        result.scope_id = ov[0];
-                    }
-                    {
-                        const ov = @addWithOverflow(result.scope_id, digit);
-                        if (ov[1] != 0) return error.Overflow;
-                        result.scope_id = ov[0];
+        pub fn fromAddress(a: *const Ip6Address, io: Io) FromAddressError!Unresolved {
+            if (a.interface.isNone()) return .{
+                .bytes = a.bytes,
+                .interface_name = null,
+            };
+            return .{
+                .bytes = a.bytes,
+                .interface_name = try a.interface.name(io),
+            };
+        }
+
+        pub fn format(u: *const Unresolved, w: *Io.Writer) Io.Writer.Error!void {
+            const bytes = &u.bytes;
+            if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff })) {
+                try w.print("::ffff:{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] });
+            } else {
+                const parts: [8]u16 = .{
+                    std.mem.readInt(u16, bytes[0..2], .big),
+                    std.mem.readInt(u16, bytes[2..4], .big),
+                    std.mem.readInt(u16, bytes[4..6], .big),
+                    std.mem.readInt(u16, bytes[6..8], .big),
+                    std.mem.readInt(u16, bytes[8..10], .big),
+                    std.mem.readInt(u16, bytes[10..12], .big),
+                    std.mem.readInt(u16, bytes[12..14], .big),
+                    std.mem.readInt(u16, bytes[14..16], .big),
+                };
+
+                // Find the longest zero run
+                var longest_start: usize = 8;
+                var longest_len: usize = 0;
+                var current_start: usize = 0;
+                var current_len: usize = 0;
+
+                for (parts, 0..) |part, i| {
+                    if (part == 0) {
+                        if (current_len == 0) {
+                            current_start = i;
+                        }
+                        current_len += 1;
+                        if (current_len > longest_len) {
+                            longest_start = current_start;
+                            longest_len = current_len;
+                        }
+                    } else {
+                        current_len = 0;
                     }
-                } else {
-                    return error.InvalidCharacter;
                 }
-            } else if (c == ':') {
-                if (!saw_any_digits) {
-                    if (abbrv) return error.InvalidCharacter; // ':::'
-                    if (i != 0) abbrv = true;
-                    @memset(ip_slice[index..], 0);
-                    ip_slice = tail[0..];
-                    index = 0;
-                    continue;
-                }
-                if (index == 14) {
-                    return error.InvalidEnd;
-                }
-                ip_slice[index] = @as(u8, @truncate(x >> 8));
-                index += 1;
-                ip_slice[index] = @as(u8, @truncate(x));
-                index += 1;
 
-                x = 0;
-                saw_any_digits = false;
-            } else if (c == '%') {
-                if (!saw_any_digits) {
-                    return error.InvalidCharacter;
-                }
-                scope_id = true;
-                saw_any_digits = false;
-            } else if (c == '.') {
-                if (!abbrv or ip_slice[0] != 0xff or ip_slice[1] != 0xff) {
-                    // must start with '::ffff:'
-                    return error.InvalidIpv4Mapping;
+                // Only compress if the longest zero run is 2 or more
+                if (longest_len < 2) {
+                    longest_start = 8;
+                    longest_len = 0;
                 }
-                const start_index = std.mem.lastIndexOfScalar(u8, buffer[0..i], ':').? + 1;
-                const addr = (Ip4Address.parse(buffer[start_index..], 0) catch {
-                    return error.InvalidIpv4Mapping;
-                }).bytes;
-                ip_slice = result.bytes[0..];
-                ip_slice[10] = 0xff;
-                ip_slice[11] = 0xff;
-
-                ip_slice[12] = addr[0];
-                ip_slice[13] = addr[1];
-                ip_slice[14] = addr[2];
-                ip_slice[15] = addr[3];
-                return result;
-            } else {
-                const digit = try std.fmt.charToDigit(c, 16);
-                {
-                    const ov = @mulWithOverflow(x, 16);
-                    if (ov[1] != 0) return error.Overflow;
-                    x = ov[0];
-                }
-                {
-                    const ov = @addWithOverflow(x, digit);
-                    if (ov[1] != 0) return error.Overflow;
-                    x = ov[0];
+
+                try w.writeAll("[");
+                var i: usize = 0;
+                var abbrv = false;
+                while (i < parts.len) : (i += 1) {
+                    if (i == longest_start) {
+                        // Emit "::" for the longest zero run
+                        if (!abbrv) {
+                            try w.writeAll(if (i == 0) "::" else ":");
+                            abbrv = true;
+                        }
+                        i += longest_len - 1; // Skip the compressed range
+                        continue;
+                    }
+                    if (abbrv) {
+                        abbrv = false;
+                    }
+                    try w.print("{x}", .{parts[i]});
+                    if (i != parts.len - 1) {
+                        try w.writeAll(":");
+                    }
                 }
-                saw_any_digits = true;
             }
+            if (u.interface_name) |n| try w.print("%{s}", .{n.toSlice()});
         }
+    };
 
-        if (!saw_any_digits and !abbrv) {
-            return error.Incomplete;
-        }
-        if (!abbrv and index < 14) {
-            return error.Incomplete;
-        }
+    pub const ParseError = error{
+        /// If this is returned, more detailed diagnostics can be obtained by
+        /// calling `Ip6Address.Parsed.init`.
+        ParseFailed,
+        /// If this is returned, the IPv6 address had a scope id on it ("%foo"
+        /// at the end) which requires calling `resolve`.
+        UnresolvedScope,
+    };
 
-        if (index == 14) {
-            ip_slice[14] = @as(u8, @truncate(x >> 8));
-            ip_slice[15] = @as(u8, @truncate(x));
-            return result;
-        } else {
-            ip_slice[index] = @as(u8, @truncate(x >> 8));
-            index += 1;
-            ip_slice[index] = @as(u8, @truncate(x));
-            index += 1;
-            @memcpy(result.bytes[16 - index ..][0..index], ip_slice[0..index]);
-            return result;
+    /// This is a pure function but it cannot handle IPv6 addresses that have
+    /// scope ids ("%foo" at the end). To also handle those, `resolve` must be
+    /// called instead.
+    pub fn parse(buffer: []const u8, port: u16) ParseError!Ip6Address {
+        switch (Unresolved.parse(buffer)) {
+            .success => |p| return .{
+                .bytes = p.bytes,
+                .port = port,
+                .interface = if (p.interface_name != null) return error.UnresolvedScope else .none,
+            },
+            else => return error.ParseFailed,
         }
+        return .{ .ip6 = try Ip6Address.parse(buffer, port) };
     }
 
-    pub fn format(a: Ip6Address, w: *Io.Writer) Io.Writer.Error!void {
-        const bytes = &a.bytes;
-        if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff })) {
-            try w.print("[::ffff:{d}.{d}.{d}.{d}]:{d}", .{
-                bytes[12], bytes[13], bytes[14], bytes[15], a.port,
-            });
-            return;
-        }
-        const parts: [8]u16 = .{
-            std.mem.readInt(u16, bytes[0..2], .big),
-            std.mem.readInt(u16, bytes[2..4], .big),
-            std.mem.readInt(u16, bytes[4..6], .big),
-            std.mem.readInt(u16, bytes[6..8], .big),
-            std.mem.readInt(u16, bytes[8..10], .big),
-            std.mem.readInt(u16, bytes[10..12], .big),
-            std.mem.readInt(u16, bytes[12..14], .big),
-            std.mem.readInt(u16, bytes[14..16], .big),
+    pub const ResolveError = error{
+        /// If this is returned, more detailed diagnostics can be obtained by
+        /// calling the `Parsed.init` function.
+        ParseFailed,
+    } || Interface.Name.ResolveError;
+
+    /// This function requires an `Io` parameter because it must query the operating
+    /// system to convert interface name to index. For example, in
+    /// "fe80::e0e:76ff:fed4:cf22%eno1", "eno1" must be resolved to an index by
+    /// creating a socket and then using an `ioctl` syscall.
+    pub fn resolve(io: Io, buffer: []const u8, port: u16) ResolveError!Ip6Address {
+        return switch (Unresolved.parse(buffer)) {
+            .success => |p| return .{
+                .bytes = p.bytes,
+                .port = port,
+                .interface = if (p.interface_name) |n| try n.resolve(io) else .none,
+            },
+            else => return error.ParseFailed,
         };
+    }
 
-        // Find the longest zero run
-        var longest_start: usize = 8;
-        var longest_len: usize = 0;
-        var current_start: usize = 0;
-        var current_len: usize = 0;
+    pub const FormatError = Io.Writer.Error || Unresolved.FromAddressError;
 
-        for (parts, 0..) |part, i| {
-            if (part == 0) {
-                if (current_len == 0) {
-                    current_start = i;
-                }
-                current_len += 1;
-                if (current_len > longest_len) {
-                    longest_start = current_start;
-                    longest_len = current_len;
-                }
-            } else {
-                current_len = 0;
-            }
-        }
-
-        // Only compress if the longest zero run is 2 or more
-        if (longest_len < 2) {
-            longest_start = 8;
-            longest_len = 0;
-        }
+    /// Includes the optional scope ("%foo" at the end).
+    ///
+    /// See `format` for an alternative that omits scopes and does
+    /// not require an `Io` parameter.
+    pub fn formatResolved(a: Ip6Address, io: Io, w: *Io.Writer) FormatError!void {
+        const u: Unresolved = try .fromAddress(io);
+        try w.print("[{f}]:{d}", .{ u, a.port });
+    }
 
-        try w.writeAll("[");
-        var i: usize = 0;
-        var abbrv = false;
-        while (i < parts.len) : (i += 1) {
-            if (i == longest_start) {
-                // Emit "::" for the longest zero run
-                if (!abbrv) {
-                    try w.writeAll(if (i == 0) "::" else ":");
-                    abbrv = true;
-                }
-                i += longest_len - 1; // Skip the compressed range
-                continue;
-            }
-            if (abbrv) {
-                abbrv = false;
-            }
-            try w.print("{x}", .{parts[i]});
-            if (i != parts.len - 1) {
-                try w.writeAll(":");
-            }
-        }
-        try w.print("]:{d}", .{a.port});
+    /// See `formatResolved` for an alternative that additionally prints the optional
+    /// scope at the end of addresses and requires an `Io` parameter.
+    pub fn format(a: Ip6Address, w: *Io.Writer) Io.Writer.Error!void {
+        const u: Unresolved = .{
+            .bytes = a.bytes,
+            .interface_name = null,
+        };
+        try w.print("[{f}]:{d}", .{ u, a.port });
     }
 
     pub fn eql(a: Ip6Address, b: Ip6Address) bool {
@@ -471,11 +508,64 @@ pub const Ip6Address = struct {
     };
 };
 
+pub const Interface = struct {
+    /// Value 0 indicates `none`.
+    index: u32,
+
+    pub const none: Interface = .{ .index = 0 };
+
+    pub const Name = struct {
+        bytes: [max_len:0]u8,
+
+        pub const max_len = std.posix.IFNAMESIZE - 1;
+
+        pub fn toSlice(n: *const Name) []const u8 {
+            return std.mem.sliceTo(&n.bytes, 0);
+        }
+
+        pub fn fromSlice(bytes: []const u8) error{NameTooLong}!Name {
+            if (bytes.len > max_len) return error.NameTooLong;
+            var result: Name = undefined;
+            @memcpy(result.bytes[0..bytes.len], bytes);
+            result.bytes[bytes.len] = 0;
+            return result;
+        }
+
+        pub const ResolveError = error{
+            InterfaceNotFound,
+            AccessDenied,
+            SystemResources,
+        } || Io.UnexpectedError || Io.Cancelable;
+
+        /// Corresponds to "if_nametoindex" in libc.
+        pub fn resolve(n: []const u8, io: Io) ResolveError!Interface {
+            return io.vtable.netInterfaceNameResolve(io.userdata, n);
+        }
+    };
+
+    pub const NameError = Io.UnexpectedError || Io.Cancelable;
+
+    /// Asserts not `none`.
+    ///
+    /// Corresponds to "if_indextoname" in libc.
+    pub fn name(i: Interface, io: Io) NameError!Name {
+        assert(i.index != 0);
+        return io.vtable.netInterfaceName(io.userdata, i);
+    }
+
+    pub fn isNone(i: Interface) bool {
+        return i.index == 0;
+    }
+};
+
+/// 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 {
-    /// Underlying platform-defined type which may or may not be
-    /// interchangeable with a file system file descriptor.
     handle: Handle,
 
+    /// Underlying platform-defined type which may or may not be
+    /// interchangeable with a file system file descriptor.
     pub const Handle = switch (native_os) {
         .windows => std.windows.ws2_32.SOCKET,
         else => std.posix.fd_t,
@@ -583,17 +673,6 @@ pub const Server = struct {
     }
 };
 
-pub const InterfaceIndexError = error{
-    InterfaceNotFound,
-    AccessDenied,
-    SystemResources,
-} || Io.UnexpectedError || Io.Cancelable;
-
-/// Otherwise known as "if_nametoindex".
-pub fn interfaceIndex(io: Io, name: []const u8) InterfaceIndexError!u32 {
-    return io.vtable.netInterfaceIndex(io.userdata, name);
-}
-
 test {
     _ = HostName;
 }
lib/std/Io/Threaded.zig
@@ -135,7 +135,8 @@ pub fn io(pool: *Pool) Io {
                 else => netWritePosix,
             },
             .netClose = netClose,
-            .netInterfaceIndex = netInterfaceIndex,
+            .netInterfaceNameResolve = netInterfaceNameResolve,
+            .netInterfaceName = netInterfaceName,
         },
     };
 }
@@ -1123,16 +1124,11 @@ fn netClose(userdata: ?*anyopaque, stream: Io.net.Stream) void {
     return net_stream.close();
 }
 
-fn netInterfaceIndex(userdata: ?*anyopaque, name: []const u8) Io.net.InterfaceIndexError!u32 {
+fn netInterfaceNameResolve(userdata: ?*anyopaque, name: Io.net.Interface.Name) Io.net.Interface.Name.ResolveError!Io.net.Interface {
     const pool: *Pool = @ptrCast(@alignCast(userdata));
     try pool.checkCancel();
 
     if (native_os == .linux) {
-        if (name.len >= posix.IFNAMESIZE) return error.InterfaceNotFound;
-        var ifr: posix.ifreq = undefined;
-        @memcpy(ifr.ifrn.name[0..name.len], name);
-        ifr.ifrn.name[name.len] = 0;
-
         const rc = posix.system.socket(posix.AF.UNIX, posix.SOCK.DGRAM | posix.SOCK.CLOEXEC, 0);
         const sock_fd: posix.fd_t = switch (posix.errno(rc)) {
             .SUCCESS => @intCast(rc),
@@ -1145,10 +1141,15 @@ fn netInterfaceIndex(userdata: ?*anyopaque, name: []const u8) Io.net.InterfaceIn
         };
         defer posix.close(sock_fd);
 
+        var ifr: posix.ifreq = .{
+            .ifrn = .{ .name = @bitCast(name.bytes) },
+            .ifru = undefined,
+        };
+
         while (true) {
             try pool.checkCancel();
             switch (posix.errno(posix.system.ioctl(sock_fd, posix.SIOCGIFINDEX, @intFromPtr(&ifr)))) {
-                .SUCCESS => return @bitCast(ifr.ifru.ivalue),
+                .SUCCESS => return .{ .index = @bitCast(ifr.ifru.ivalue) },
                 .INVAL => |err| return badErrno(err), // Bad parameters.
                 .NOTTY => |err| return badErrno(err),
                 .NXIO => |err| return badErrno(err),
@@ -1162,28 +1163,39 @@ fn netInterfaceIndex(userdata: ?*anyopaque, name: []const u8) Io.net.InterfaceIn
         }
     }
 
-    if (native_os.isDarwin()) {
-        if (name.len >= posix.IFNAMESIZE) return error.InterfaceNotFound;
-        var if_name: [posix.IFNAMESIZE:0]u8 = undefined;
-        @memcpy(if_name[0..name.len], name);
-        if_name[name.len] = 0;
-        const if_slice = if_name[0..name.len :0];
-        const index = std.c.if_nametoindex(if_slice);
+    if (native_os == .windows) {
+        const index = std.os.windows.ws2_32.if_nametoindex(&name.bytes);
         if (index == 0) return error.InterfaceNotFound;
-        return @bitCast(index);
+        return .{ .index = index };
     }
 
-    if (native_os == .windows) {
-        if (name.len >= posix.IFNAMESIZE) return error.InterfaceNotFound;
-        var interface_name: [posix.IFNAMESIZE:0]u8 = undefined;
-        @memcpy(interface_name[0..name.len], name);
-        interface_name[name.len] = 0;
-        const index = std.os.windows.ws2_32.if_nametoindex(@as([*:0]const u8, &interface_name));
+    if (builtin.link_libc) {
+        const index = std.c.if_nametoindex(&name.bytes);
         if (index == 0) return error.InterfaceNotFound;
-        return index;
+        return .{ .index = @bitCast(index) };
+    }
+
+    @panic("unimplemented");
+}
+
+fn netInterfaceName(userdata: ?*anyopaque, interface: Io.net.Interface) Io.net.Interface.NameError!Io.net.Interface.Name {
+    const pool: *Pool = @ptrCast(@alignCast(userdata));
+    try pool.checkCancel();
+
+    if (native_os == .linux) {
+        _ = interface;
+        @panic("TODO");
+    }
+
+    if (native_os == .windows) {
+        @panic("TODO");
+    }
+
+    if (builtin.link_libc) {
+        @panic("TODO");
     }
 
-    @compileError("std.net.if_nametoindex unimplemented for this OS");
+    @panic("unimplemented");
 }
 
 const PosixAddress = extern union {
@@ -1231,8 +1243,8 @@ fn address6FromPosix(in6: *posix.sockaddr.in6) Io.net.Ip6Address {
     return .{
         .port = std.mem.bigToNative(u16, in6.port),
         .bytes = in6.addr,
-        .flowinfo = in6.flowinfo,
-        .scope_id = in6.scope_id,
+        .flow = in6.flowinfo,
+        .interface = .{ .index = in6.scope_id },
     };
 }
 
@@ -1246,9 +1258,9 @@ fn address4ToPosix(a: Io.net.Ip4Address) posix.sockaddr.in {
 fn address6ToPosix(a: Io.net.Ip6Address) posix.sockaddr.in6 {
     return .{
         .port = std.mem.nativeToBig(u16, a.port),
-        .flowinfo = a.flowinfo,
+        .flowinfo = a.flow,
         .addr = a.bytes,
-        .scope_id = a.scope_id,
+        .scope_id = a.interface.index,
     };
 }