Skip to content

Commit

Permalink
implement IPv6 scoped addresses (RFC4007)
Browse files Browse the repository at this point in the history
 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.

Scoped identifiers are only valid for link-local (`fe80::`) addresses,
e.g. `fe80::1%eth0`

References:
  - https://datatracker.ietf.org/doc/html/rfc4007

TODO (to be resolved during the PR discussion):
 - discuss the preprocessor option for retrieving the constant value
   `IF_NAMESIZE` (No clue how do that, haven't found any event of this
   constant ever changing in the past, might not be worth the trouble?)
 - 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

I'm happy to adjust/rebase if needed based on feedback or other merges
into `master` in the mean time.
  • Loading branch information
foxxx0 committed Dec 10, 2024
1 parent cc2bc1c commit dfe9ed6
Show file tree
Hide file tree
Showing 20 changed files with 242 additions and 8 deletions.
58 changes: 58 additions & 0 deletions spec/std/socket/address_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,63 @@ 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 loopback interface index by name" 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 %}
address = Socket::IPAddress.new("fe80::1111%#{loopback_iface}", 0)
address.address.should eq "fe80::1111"
address.scope_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
buf = uninitialized StaticArray(UInt8, LibC::IF_NAMESIZE)
LibC.if_indextoname(1, buf)
ifname = String.new(buf.to_unsafe)
{% if flag?(:windows) %}
ifname.should eq "loopback_0"
{% elsif flag?(:darwin) || flag?(:bsd) || flag?(:solaris) %}
ifname.should eq "lo0"
{% else %}
ifname.should eq "lo"
{% end %}
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 +320,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/arm-linux-gnueabihf/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 dfe9ed6

Please sign in to comment.