diff --git a/spec/std/socket/address_spec.cr b/spec/std/socket/address_spec.cr index e89151844f3c..13869ad336af 100644 --- a/spec/std/socket/address_spec.cr +++ b/spec/std/socket/address_spec.cr @@ -126,6 +126,87 @@ describe Socket::IPAddress do Socket::IPAddress.new("::ffff:0:0", 443).address.should eq "::ffff:0.0.0.0" end + describe "#zone_id" do + # loopback interface "lo" is supposed to *always* be the first interface and + # enumerated with index 1 + loopback_iface = {% if flag?(:windows) %} + "loopback_0" + {% elsif flag?(:darwin) || flag?(:bsd) || flag?(:solaris) %} + "lo0" + {% else %} + "lo" + {% end %} + + it "parses link-local IPv6 with interface scope" do + address = Socket::IPAddress.new("fe80::3333:4444%3", 8081) + address.address.should eq "fe80::3333:4444" + address.zone_id.should eq 3 + address.inspect.should eq "Socket::IPAddress([fe80::3333:4444%3]:8081)" + end + + it "looks up loopback interface index by name" do + address = Socket::IPAddress.new("fe80::1111%#{loopback_iface}", 0) + address.address.should eq "fe80::1111" + address.zone_id.should eq 1 + end + + it "looks up loopback interface name by index" do + # loopback interface "lo" is supposed to *always* be the first interface and + # enumerated with index 1 + address = Socket::IPAddress.new("fe80::1111%#{loopback_iface}", 0) + address.link_local_interface.should eq loopback_iface + end + + it "fails interface name lookup for non-existent interfaces" do + exc_suff = {% if flag?(:windows) %} + "" + {% elsif flag?(:darwin) || flag?(:bsd) %} + ": Device not configured" + {% else %} + ": No such device or address" + {% end %} + expect_raises(Socket::Error, "Failed to look up interface name for index 333#{exc_suff}") do + Socket::IPAddress.new("fe80::d00d:1%333", 0).link_local_interface + end + end + + it "interface name lookup returns nil in unsupported cases" do + Socket::IPAddress.new("fd03::3333", 0).link_local_interface.should be_nil + Socket::IPAddress.new("192.168.10.10", 0).link_local_interface.should be_nil + Socket::IPAddress.new("169.254.0.3", 0).link_local_interface.should be_nil + Socket::IPAddress.new("fe80::4545", 0).link_local_interface.should be_nil + end + + it "fails link-local zone identifier on non-LL v6 addrs" do + expect_raises(Socket::Error, "Zoned/scoped IPv6 addresses are only allowed for link-local (supplied 'fd00::abcd%5' is not within fe80::/10)") do + Socket::IPAddress.new("fd00::abcd%5", 443) + end + end + + it "fails link-local zone identifier on v4 addrs" do + expect_raises(Socket::Error, "Invalid IP address: 169.254.11.11%eth0") do + Socket::IPAddress.new("169.254.11.11%eth0", 0) + end + + expect_raises(Socket::Error, "Invalid IP address: 192.168.11.11%3") do + Socket::IPAddress.new("192.168.11.11%3", 0) + end + end + + it "fails on invalid link-local zone identifier" do + expect_raises(Socket::Error, "Invalid IPv6 link-local zone index '0' in address 'fe80::c0ff:ee%0'") do + Socket::IPAddress.new("fe80::c0ff:ee%0", port: 0) + end + end + + it "fails on non-existent link-local zone interface" do + # looking up an interface index obviously requires for said interface device to exist + expect_raises(Socket::Error, "IPv6 link-local zone interface 'zzzzzzzzzzzzzzz' not found (in address 'fe80::0f0f:abcd%zzzzzzzzzzzzzzz')") do + Socket::IPAddress.new("fe80::0f0f:abcd%zzzzzzzzzzzzzzz", port: 0) + end + end + end + describe ".parse" do it "parses IPv4" do address = Socket::IPAddress.parse "ip://192.168.0.1:8081" @@ -222,6 +303,8 @@ describe Socket::IPAddress do it { Socket::IPAddress.parse_v6_fields?("0::ffff:c0a8:5e4").should eq UInt16.static_array(0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x5e4) } it { Socket::IPAddress.parse_v6_fields?("::0::ffff:c0a8:5e4").should be_nil } it { Socket::IPAddress.parse_v6_fields?("c0a8").should be_nil } + it { Socket::IPAddress.parse_v6_fields?("fe80::a:b%eth0").should eq UInt16.static_array(0xfe80, 0, 0, 0, 0, 0, 0xa, 0xb) } + it { Socket::IPAddress.parse_v6_fields?("fe80:0:0:0:ffff:c0a8:5e4%lo").should eq UInt16.static_array(0xfe80, 0, 0, 0, 0xffff, 0xc0a8, 0x5e4, 0) } end describe ".v4" do @@ -263,6 +346,7 @@ describe Socket::IPAddress do Socket::IPAddress.v6(0xfe80, 0, 0, 0, 0x4860, 0x4860, 0x4860, 0x1234, port: 55001).should eq Socket::IPAddress.new("fe80::4860:4860:4860:1234", 55001) Socket::IPAddress.v6(0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xfffe, port: 65535).should eq Socket::IPAddress.new("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe", 65535) Socket::IPAddress.v6(0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0001, port: 0).should eq Socket::IPAddress.new("::ffff:192.168.0.1", 0) + Socket::IPAddress.v6(0xfe80, 0, 0, 0, 0x5971, 0x5971, 0x5971, 0xabcd, port: 44444, zone_id: 3).should eq Socket::IPAddress.new("fe80::5971:5971:5971:abcd%3", 44444) end it "raises on out of bound field" do diff --git a/src/lib_c/aarch64-android/c/net/if.cr b/src/lib_c/aarch64-android/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/aarch64-android/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/aarch64-darwin/c/net/if.cr b/src/lib_c/aarch64-darwin/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/aarch64-darwin/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/aarch64-linux-gnu/c/net/if.cr b/src/lib_c/aarch64-linux-gnu/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/aarch64-linux-gnu/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/aarch64-linux-musl/c/net/if.cr b/src/lib_c/aarch64-linux-musl/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/aarch64-linux-musl/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/arm-linux-gnueabihf/c/net/if.cr b/src/lib_c/arm-linux-gnueabihf/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/arm-linux-gnueabihf/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/i386-linux-gnu/c/net/if.cr b/src/lib_c/i386-linux-gnu/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/i386-linux-gnu/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/i386-linux-musl/c/net/if.cr b/src/lib_c/i386-linux-musl/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/i386-linux-musl/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/x86_64-darwin/c/net/if.cr b/src/lib_c/x86_64-darwin/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/x86_64-darwin/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/x86_64-dragonfly/c/net/if.cr b/src/lib_c/x86_64-dragonfly/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/x86_64-dragonfly/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/x86_64-freebsd/c/net/if.cr b/src/lib_c/x86_64-freebsd/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/x86_64-freebsd/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/x86_64-linux-gnu/c/net/if.cr b/src/lib_c/x86_64-linux-gnu/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/x86_64-linux-gnu/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/x86_64-linux-musl/c/net/if.cr b/src/lib_c/x86_64-linux-musl/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/x86_64-linux-musl/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/x86_64-netbsd/c/net/if.cr b/src/lib_c/x86_64-netbsd/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/x86_64-openbsd/c/net/if.cr b/src/lib_c/x86_64-openbsd/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/x86_64-openbsd/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/x86_64-solaris/c/net/if.cr b/src/lib_c/x86_64-solaris/c/net/if.cr new file mode 100644 index 000000000000..c33747bc9150 --- /dev/null +++ b/src/lib_c/x86_64-solaris/c/net/if.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + IF_NAMESIZE = 16 + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/lib_c/x86_64-windows-msvc/c/netioapi.cr b/src/lib_c/x86_64-windows-msvc/c/netioapi.cr new file mode 100644 index 000000000000..0d4e80c09e67 --- /dev/null +++ b/src/lib_c/x86_64-windows-msvc/c/netioapi.cr @@ -0,0 +1,12 @@ +require "./in6addr" +require "./inaddr" +require "./stdint" + +@[Link("iphlpapi")] +lib LibC + NDIS_IF_MAX_STRING_SIZE = 256 + IF_NAMESIZE = LibC::NDIS_IF_MAX_STRING_SIZE + 1 # need one more byte for terminating '\0' + + fun if_nametoindex(ifname : Char*) : UInt + fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char* +end diff --git a/src/socket/address.cr b/src/socket/address.cr index c07505ad43ab..d78ba35621fc 100644 --- a/src/socket/address.cr +++ b/src/socket/address.cr @@ -90,6 +90,7 @@ class Socket BROADCAST6 = "ff0X::1" getter port : Int32 + getter zone_id : Int32 @addr : LibC::In6Addr | LibC::InAddr @@ -100,21 +101,44 @@ class Socket # Raises `Socket::Error` if *address* does not contain a valid IP address or # the port number is out of range. # + # Scoped/Zoned IPv6 link-local addresses are supported per RFC4007, e.g. + # `fe80::abcd%eth0` but will always use their numerical interface index + # in the `#inspect` representation. The interface name can be retrieved later + # using `#link_local_interface` on the `IPAddress` object. + # # ``` # require "socket" # # Socket::IPAddress.new("127.0.0.1", 8080) # => Socket::IPAddress(127.0.0.1:8080) # Socket::IPAddress.new("fe80::2ab2:bdff:fe59:8e2c", 1234) # => Socket::IPAddress([fe80::2ab2:bdff:fe59:8e2c]:1234) + # Socket::IPAddress.new("fe80::4567:8:9%eth0", 443) # => Socket::IPAddress([fe80::4567:8:9%2]:443) # ``` def self.new(address : String, port : Int32) raise Error.new("Invalid port number: #{port}") unless IPAddress.valid_port?(port) if v4_fields = parse_v4_fields?(address) addr = v4(v4_fields, port.to_u16!) - elsif v6_fields = parse_v6_fields?(address) - addr = v6(v6_fields, port.to_u16!) else - raise Error.new("Invalid IP address: #{address}") + v6_fields_tpl = parse_v6_fields?(address.to_slice) + raise Error.new("Invalid IP address: #{address}") if v6_fields_tpl.nil? + v6_fields, zone_slice = v6_fields_tpl + zone_id = 0 + unless zone_slice.nil? + # `zone_id` is only relevant for link-local addresses, i.e. beginning with "fe80:". + if v6_fields[0] != 0xfe80 + raise Error.new("Zoned/scoped IPv6 addresses are only allowed for link-local (supplied '#{address}' is not within fe80::/10)") + end + # Scope/Zone can be given either as a network interface name or directly as the interface index. + # When given a name we need to find the corresponding interface index. + zone = String.new(zone_slice) + if zone_id = zone.to_i? + raise Error.new("Invalid IPv6 link-local zone index '#{zone}' in address '#{address}'") unless zone_id.positive? + else + zone_id = LibC.if_nametoindex(zone).to_i + raise Error.new("IPv6 link-local zone interface '#{zone}' not found (in address '#{address}')") unless zone_id.positive? + end + end + addr = v6(v6_fields, port.to_u16!, zone_id) end addr @@ -247,12 +271,20 @@ class Socket # Socket::IPAddress.parse_v6_fields?("a:0b:00c:000d:E:F::") # => UInt16.static_array(10, 11, 12, 13, 14, 15, 0, 0) # Socket::IPAddress.parse_v6_fields?("::ffff:192.168.1.1") # => UInt16.static_array(0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0101) # Socket::IPAddress.parse_v6_fields?("1::2::") # => nil + # Socket::IPAddress.parse_v6_fields?("fe80::a:b%eth0") # => StaticArray[65152, 0, 0, 0, 0, 0, 10, 11] # ``` def self.parse_v6_fields?(str : String) : UInt16[8]? - parse_v6_fields?(str.to_slice) + parse_v6_fields?(str.to_slice).try &.[0] end - private def self.parse_v6_fields?(bytes : Bytes) + # This private method additionally supports IPv6 scoped addresses and + # returns its fields plus the zone/scope subslice (if present). + # Invalid addresses will immediately return just `nil` while correctly + # formatted addresses return a tuple. + # + # The format of IPv6 scoped addresses follows + # [RFC 4007, section 11](https://datatracker.ietf.org/doc/html/rfc4007#section-11). + private def self.parse_v6_fields?(bytes : Bytes) : Tuple(UInt16[8], Slice(UInt8)?)? # port of https://git.musl-libc.org/cgit/musl/tree/src/network/inet_pton.c?id=7e13e5ae69a243b90b90d2f4b79b2a150f806335 ptr = bytes.to_unsafe finish = ptr + bytes.size @@ -265,6 +297,7 @@ class Socket fields = StaticArray(UInt16, 8).new(0) brk = -1 need_v4 = false + zone_slice = nil i = 0 while true @@ -296,6 +329,10 @@ class Socket return nil if i == 7 unless ptr < finish && ptr.value === ':' + if (ptr < finish && ptr.value === '%') + zone_slice = Bytes.new(ptr + 1, finish - ptr - 1) + break + end return nil if !(ptr < finish && ptr.value === '.') || (i < 6 && brk < 0) need_v4 = true i &+= 1 @@ -320,7 +357,7 @@ class Socket fields[7] = x2.to_u16! << 8 | x3 end - fields + {fields, zone_slice} end private def self.from_hex(ch : UInt8) @@ -364,14 +401,15 @@ class Socket 0 <= field <= 0xff ? field.to_u8! : raise Error.new("Invalid IPv4 field: #{field}") end - # Returns the IPv6 address with the given address *fields* and *port* - # number. - def self.v6(fields : UInt16[8], port : UInt16) : self + # Returns the IPv6 address with the given address *fields*, *port* number + # and scope identifier. + def self.v6(fields : UInt16[8], port : UInt16, zone_id : Int32 = 0) : self fields.map! { |field| endian_swap(field) } addr = LibC::SockaddrIn6.new( sin6_family: LibC::AF_INET6, sin6_port: endian_swap(port), sin6_addr: ipv6_from_addr16(fields), + sin6_scope_id: zone_id, ) new(pointerof(addr), sizeof(typeof(addr))) end @@ -379,10 +417,10 @@ class Socket # Returns the IPv6 address `[x0:x1:x2:x3:x4:x5:x6:x7]:port`. # # Raises `Socket::Error` if any field or the port number is out of range. - def self.v6(x0 : Int, x1 : Int, x2 : Int, x3 : Int, x4 : Int, x5 : Int, x6 : Int, x7 : Int, *, port : Int) : self + def self.v6(x0 : Int, x1 : Int, x2 : Int, x3 : Int, x4 : Int, x5 : Int, x6 : Int, x7 : Int, *, port : Int, zone_id : Int32 = 0) : self fields = StaticArray[x0, x1, x2, x3, x4, x5, x6, x7].map { |field| to_v6_field(field) } port = valid_port?(port) ? port.to_u16! : raise Error.new("Invalid port number: #{port}") - v6(fields, port) + v6(fields, port, zone_id) end private def self.to_v6_field(field) @@ -435,12 +473,14 @@ class Socket protected def initialize(sockaddr : LibC::SockaddrIn6*, @size) @family = Family::INET6 @addr = sockaddr.value.sin6_addr + @zone_id = sockaddr.value.sin6_scope_id.to_i @port = IPAddress.endian_swap(sockaddr.value.sin6_port).to_i end protected def initialize(sockaddr : LibC::SockaddrIn*, @size) @family = Family::INET @addr = sockaddr.value.sin_addr + @zone_id = 0 @port = IPAddress.endian_swap(sockaddr.value.sin_port).to_i end @@ -605,6 +645,7 @@ class Socket in LibC::In6Addr io << '[' address_to_s(io, addr) + io << '%' << @zone_id if @zone_id.positive? io << ']' << ':' << port end end @@ -717,6 +758,11 @@ class Socket sockaddr.value.sin6_family = family sockaddr.value.sin6_port = IPAddress.endian_swap(port.to_u16!) sockaddr.value.sin6_addr = addr + if @family == Family::INET6 && link_local? + sockaddr.value.sin6_scope_id = @zone_id + else + sockaddr.value.sin6_scope_id = 0 + end sockaddr.as(LibC::Sockaddr*) end @@ -728,6 +774,36 @@ class Socket sockaddr.as(LibC::Sockaddr*) end + # Returns the interface name for a scoped/zoned link-local IPv6 address. + # This only works on properly initialized link-local IPv6 address objects. + # In any other case this will return nil. + # + # The OS tracks the zone via a numerical interface index as enumerated + # by the kernel. To keep our abstraction class in line, we also only + # keep the interface index around. + # + # This helper method exists to look up the interface name based on the + # associated zone_id property. + def link_local_interface : String | Nil + {% unless LibC.has_method?(:if_nametoindex) %} + raise NotImplementedError.new "Socket::Address.link_local_interface" + {% end %} + return nil if @zone_id.zero? + return nil unless (@family == Socket::Family::INET6 && link_local?) + buf = uninitialized StaticArray(UInt8, LibC::IF_NAMESIZE) + result = LibC.if_indextoname(@zone_id, buf) + if result.null? + message = "Failed to look up interface name for index #{@zone_id}" + {% if flag?(:windows) %} + # In windows it is not possible to determine an error code here + raise Error.new(message) + {% else %} + raise Error.from_errno(message) + {% end %} + end + String.new(buf.to_unsafe) + end + protected def self.endian_swap(x : Int::Primitive) : Int::Primitive {% if IO::ByteFormat::NetworkEndian != IO::ByteFormat::SystemEndian %} x.byte_swap diff --git a/src/socket/common.cr b/src/socket/common.cr index 19f1700a2cbd..91f2b5ebde39 100644 --- a/src/socket/common.cr +++ b/src/socket/common.cr @@ -1,6 +1,7 @@ {% if flag?(:win32) %} require "c/ws2tcpip" require "c/afunix" + require "c/netioapi" {% elsif flag?(:wasi) %} require "c/arpa/inet" require "c/netinet/in" @@ -8,6 +9,7 @@ require "c/arpa/inet" require "c/sys/un" require "c/netinet/in" + require "c/net/if" {% end %} class Socket < IO