Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement IPv6 scoped addresses (RFC4007) #15263

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading