Skip to content

Commit

Permalink
implement IPv6 scoped addresses (RFC4007)
Browse files Browse the repository at this point in the history
https://datatracker.ietf.org/doc/html/rfc4007

Only valid for IPv6 link-local (`fe80::`) addresses.
Examples:
 - `fe80::1%eth0`

 In order to translate interface names in such scoped addresses the
 required `LibC` binding to `if_nametoindex()` has been added.
 This method obviously only works for interfaces (devices) that are
 actually present on the system.

 The binding for the reverse operation `if_indextoname()` has also been
 added, although its usage is a bit more cumbersome due to LibC::Char*
 buffer handling. The necessary buffer length has been placed into the
 constant `LibC::IF_NAMESIZE`, which appears to be `16u8` on unix-like
 systems and `257u16` on windows.
 This could potentially be reworked via a preprocessor block at
 compile-time as indicated by some folks over on discord, I currently do
 not know how to achieve that though.

TODO:
 - clarify whether scope_id parsing should raise `ArgumentError` or
   `Socket::Error`
 - clarify whether `Socket::Address` spec `#scope_id` `looks up
   interface name by index` should remain in the `Socket::Address` spec,
   since it only calls the `LibC` binding
  • Loading branch information
foxxx0 committed Dec 9, 2024
1 parent 6df6b3d commit 5d31547
Show file tree
Hide file tree
Showing 19 changed files with 220 additions and 8 deletions.
45 changes: 45 additions & 0 deletions spec/std/socket/address_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,50 @@ describe Socket::IPAddress do
Socket::IPAddress.new("::ffff:0:0", 443).address.should eq "::ffff:0.0.0.0"
end

describe "#scope_id" do
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.scope_id.should eq 3
end

it "ignores link-local scope identifier on non-LL addrs" do
address = Socket::IPAddress.new("fd00::abcd%5", 443)
address.address.should eq "fd00::abcd"
address.scope_id.should eq 0
end

it "looks up interface index by name" do
# loopback interface "lo" is supposed to *always* be the first interface and
# enumerated with index 1
address = Socket::IPAddress.new("fe80::1111%lo", 0)
address.address.should eq "fe80::1111"
address.scope_id.should eq 1
end

it "looks up interface name by index" do
# loopback interface "lo" is supposed to *always* be the first interface and
# enumerated with index 1
buf = uninitialized StaticArray(UInt8, LibC::IF_NAMESIZE)
LibC.if_indextoname(1, buf)
ifname = String.new(buf.to_unsafe)
ifname.should eq "lo"
end

it "fails on invalid link-local scope identifier" do
expect_raises(ArgumentError, "Invalid IPv6 link-local scope 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 scope interface" do
# looking up an interface index obviously requires for said interface device to exist
expect_raises(ArgumentError, "IPv6 link-local scope 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"
Expand Down Expand Up @@ -263,6 +307,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, scope_id: 3).should eq Socket::IPAddress.new("fe80::5971:5971:5971:abcd%3", 44444)
end

it "raises on out of bound field" do
Expand Down
9 changes: 9 additions & 0 deletions src/lib_c/aarch64-android/c/net/if.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../netinet/in"
require "../stdint"

lib LibC
IF_NAMESIZE = 16u8

fun if_nametoindex(ifname : Char*) : UInt
fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char*
end
9 changes: 9 additions & 0 deletions src/lib_c/aarch64-darwin/c/net/if.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../netinet/in"
require "../stdint"

lib LibC
IF_NAMESIZE = 16u8

fun if_nametoindex(ifname : Char*) : UInt
fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char*
end
9 changes: 9 additions & 0 deletions src/lib_c/aarch64-linux-gnu/c/net/if.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../netinet/in"
require "../stdint"

lib LibC
IF_NAMESIZE = 16u8

fun if_nametoindex(ifname : Char*) : UInt
fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char*
end
9 changes: 9 additions & 0 deletions src/lib_c/aarch64-linux-musl/c/net/if.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../netinet/in"
require "../stdint"

lib LibC
IF_NAMESIZE = 16u8

fun if_nametoindex(ifname : Char*) : UInt
fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char*
end
9 changes: 9 additions & 0 deletions src/lib_c/i386-linux-gnu/c/net/if.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../netinet/in"
require "../stdint"

lib LibC
IF_NAMESIZE = 16u8

fun if_nametoindex(ifname : Char*) : UInt
fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char*
end
9 changes: 9 additions & 0 deletions src/lib_c/i386-linux-musl/c/net/if.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../netinet/in"
require "../stdint"

lib LibC
IF_NAMESIZE = 16u8

fun if_nametoindex(ifname : Char*) : UInt
fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char*
end
9 changes: 9 additions & 0 deletions src/lib_c/x86_64-darwin/c/net/if.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../netinet/in"
require "../stdint"

lib LibC
IF_NAMESIZE = 16u8

fun if_nametoindex(ifname : Char*) : UInt
fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char*
end
9 changes: 9 additions & 0 deletions src/lib_c/x86_64-dragonfly/c/net/if.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../netinet/in"
require "../stdint"

lib LibC
IF_NAMESIZE = 16u8

fun if_nametoindex(ifname : Char*) : UInt
fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char*
end
9 changes: 9 additions & 0 deletions src/lib_c/x86_64-freebsd/c/net/if.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../netinet/in"
require "../stdint"

lib LibC
IF_NAMESIZE = 16u8

fun if_nametoindex(ifname : Char*) : UInt
fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char*
end
9 changes: 9 additions & 0 deletions src/lib_c/x86_64-linux-gnu/c/net/if.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../netinet/in"
require "../stdint"

lib LibC
IF_NAMESIZE = 16u8

fun if_nametoindex(ifname : Char*) : UInt
fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char*
end
9 changes: 9 additions & 0 deletions src/lib_c/x86_64-linux-musl/c/net/if.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../netinet/in"
require "../stdint"

lib LibC
IF_NAMESIZE = 16u8

fun if_nametoindex(ifname : Char*) : UInt
fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char*
end
9 changes: 9 additions & 0 deletions src/lib_c/x86_64-netbsd/c/net/if.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../netinet/in"
require "../stdint"

lib LibC
IF_NAMESIZE = 16u8

fun if_nametoindex(ifname : Char*) : UInt
fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char*
end
9 changes: 9 additions & 0 deletions src/lib_c/x86_64-openbsd/c/net/if.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../netinet/in"
require "../stdint"

lib LibC
IF_NAMESIZE = 16u8

fun if_nametoindex(ifname : Char*) : UInt
fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char*
end
9 changes: 9 additions & 0 deletions src/lib_c/x86_64-solaris/c/net/if.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../netinet/in"
require "../stdint"

lib LibC
IF_NAMESIZE = 16u8

fun if_nametoindex(ifname : Char*) : UInt
fun if_indextoname(ifindex : UInt, ifname : LibC::Char*) : LibC::Char*
end
Empty file.
12 changes: 12 additions & 0 deletions src/lib_c/x86_64-windows-msvc/c/netioapi.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
require "./in6addr"
require "./inaddr"
require "./stdint"

@[Link("iphlpapi")]
lib LibC
NDIS_IF_MAX_STRING_SIZE = 256u16
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
42 changes: 34 additions & 8 deletions src/socket/address.cr
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class Socket
BROADCAST6 = "ff0X::1"

getter port : Int32
getter scope_id : UInt32

@addr : LibC::In6Addr | LibC::InAddr

Expand All @@ -109,10 +110,27 @@ class Socket
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_part, _, scope_part = address.partition('%')
if v4_fields = parse_v4_fields?(addr_part)
addr = v4(v4_fields, port.to_u16!)
elsif v6_fields = parse_v6_fields?(address)
addr = v6(v6_fields, port.to_u16!)
elsif v6_fields = parse_v6_fields?(addr_part)
# `scope_id` is only relevant for link-local addresses, i.e. beginning with "fe80:".
scope_id = 0u32
if v6_fields[0] == 0xfe80 && !scope_part.empty?
# Scope 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.
# TODO: clarify whether this should be an ArgumentError or a Socket::Error
if scope_part.to_u32?
scope_id_parsed = scope_part.to_u32
raise ArgumentError.new("Invalid IPv6 link-local scope index '#{scope_part}' in address '#{address}'") unless scope_id_parsed.positive?
scope_id = scope_id_parsed
else
scope_id_parsed = LibC.if_nametoindex(scope_part).not_nil!
raise ArgumentError.new("IPv6 link-local scope interface '#{scope_part}' not found (in address '#{address}').") unless scope_id_parsed.positive?
scope_id = scope_id_parsed
end
end
addr = v6(v6_fields, port.to_u16!, scope_id)
else
raise Error.new("Invalid IP address: #{address}")
end
Expand Down Expand Up @@ -364,25 +382,26 @@ 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, scope_id : UInt32 = 0u32) : 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: scope_id,
)
new(pointerof(addr), sizeof(typeof(addr)))
end

# 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, scope_id : UInt32 = 0u32) : 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, scope_id)
end

private def self.to_v6_field(field)
Expand Down Expand Up @@ -435,12 +454,14 @@ class Socket
protected def initialize(sockaddr : LibC::SockaddrIn6*, @size)
@family = Family::INET6
@addr = sockaddr.value.sin6_addr
@scope_id = sockaddr.value.sin6_scope_id
@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
@scope_id = 0u32
@port = IPAddress.endian_swap(sockaddr.value.sin_port).to_i
end

Expand Down Expand Up @@ -717,6 +738,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 = @scope_id
else
sockaddr.value.sin6_scope_id = 0
end
sockaddr.as(LibC::Sockaddr*)
end

Expand Down
3 changes: 3 additions & 0 deletions src/socket/common.cr
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
{% if flag?(:win32) %}
require "c/ws2tcpip"
require "c/afunix"
require "c/netioapi"
{% elsif flag?(:wasi) %}
require "c/arpa/inet"
require "c/netinet/in"
require "c/net/if"
{% else %}
require "c/arpa/inet"
require "c/sys/un"
require "c/netinet/in"
require "c/net/if"
{% end %}

class Socket < IO
Expand Down

0 comments on commit 5d31547

Please sign in to comment.