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

Add Socket::IPaddress.v4, .v6, .v4_mapped_v6 #13422

Merged
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
110 changes: 110 additions & 0 deletions spec/std/socket/address_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,116 @@ describe Socket::IPAddress do
end
end

describe ".v4" do
it "constructs an IPv4 address" do
Socket::IPAddress.v4(0, 0, 0, 0, port: 0).should eq Socket::IPAddress.new("0.0.0.0", 0)
Socket::IPAddress.v4(127, 0, 0, 1, port: 1234).should eq Socket::IPAddress.new("127.0.0.1", 1234)
Socket::IPAddress.v4(192, 168, 0, 1, port: 8081).should eq Socket::IPAddress.new("192.168.0.1", 8081)
Socket::IPAddress.v4(255, 255, 255, 254, port: 65535).should eq Socket::IPAddress.new("255.255.255.254", 65535)
end

it "raises on out of bound field" do
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4(256, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4(0, 256, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4(0, 0, 256, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4(0, 0, 0, 256, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4(-1, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4(0, -1, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4(0, 0, -1, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4(0, 0, 0, -1, port: 0) }
end

it "raises on out of bound port number" do
expect_raises(Socket::Error, "Invalid port number: 65536") { Socket::IPAddress.v4(0, 0, 0, 0, port: 65536) }
expect_raises(Socket::Error, "Invalid port number: -1") { Socket::IPAddress.v4(0, 0, 0, 0, port: -1) }
end

it "constructs from StaticArray" do
Socket::IPAddress.v4(UInt8.static_array(0, 0, 0, 0), 0).should eq Socket::IPAddress.new("0.0.0.0", 0)
Socket::IPAddress.v4(UInt8.static_array(127, 0, 0, 1), 1234).should eq Socket::IPAddress.new("127.0.0.1", 1234)
Socket::IPAddress.v4(UInt8.static_array(192, 168, 0, 1), 8081).should eq Socket::IPAddress.new("192.168.0.1", 8081)
Socket::IPAddress.v4(UInt8.static_array(255, 255, 255, 254), 65535).should eq Socket::IPAddress.new("255.255.255.254", 65535)
end
end

describe ".v6" do
it "constructs an IPv6 address" do
Socket::IPAddress.v6(0, 0, 0, 0, 0, 0, 0, 0, port: 0).should eq Socket::IPAddress.new("::", 0)
Socket::IPAddress.v6(1, 2, 3, 4, 5, 6, 7, 8, port: 8080).should eq Socket::IPAddress.new("1:2:3:4:5:6:7:8", 8080)
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)
end

it "raises on out of bound field" do
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(65536, 0, 0, 0, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(0, 65536, 0, 0, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(0, 0, 65536, 0, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(0, 0, 0, 65536, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(0, 0, 0, 0, 65536, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(0, 0, 0, 0, 0, 65536, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(0, 0, 0, 0, 0, 0, 65536, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(0, 0, 0, 0, 0, 0, 0, 65536, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(-1, 0, 0, 0, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(0, -1, 0, 0, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(0, 0, -1, 0, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(0, 0, 0, -1, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(0, 0, 0, 0, -1, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(0, 0, 0, 0, 0, -1, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(0, 0, 0, 0, 0, 0, -1, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(0, 0, 0, 0, 0, 0, 0, -1, port: 0) }
end

it "raises on out of bound port number" do
expect_raises(Socket::Error, "Invalid port number: 65536") { Socket::IPAddress.v6(0, 0, 0, 0, 0, 0, 0, 0, port: 65536) }
expect_raises(Socket::Error, "Invalid port number: -1") { Socket::IPAddress.v6(0, 0, 0, 0, 0, 0, 0, 0, port: -1) }
end

it "constructs from StaticArray" do
Socket::IPAddress.v6(UInt16.static_array(0, 0, 0, 0, 0, 0, 0, 0), 0).should eq Socket::IPAddress.new("::", 0)
Socket::IPAddress.v6(UInt16.static_array(1, 2, 3, 4, 5, 6, 7, 8), 8080).should eq Socket::IPAddress.new("1:2:3:4:5:6:7:8", 8080)
Socket::IPAddress.v6(UInt16.static_array(0xfe80, 0, 0, 0, 0x4860, 0x4860, 0x4860, 0x1234), 55001).should eq Socket::IPAddress.new("fe80::4860:4860:4860:1234", 55001)
Socket::IPAddress.v6(UInt16.static_array(0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xfffe), 65535).should eq Socket::IPAddress.new("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe", 65535)
Socket::IPAddress.v6(UInt16.static_array(0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0001), 0).should eq Socket::IPAddress.new("::ffff:192.168.0.1", 0)
end
end

describe ".v4_mapped_v6" do
it "constructs an IPv4-mapped IPv6 address" do
# windows returns the former which, while correct, is not canonical
# TODO: implement also `#to_s` in Crystal
{Socket::IPAddress.new("::ffff:0:0", 0), Socket::IPAddress.new("::ffff:0.0.0.0", 0)}.should contain Socket::IPAddress.v4_mapped_v6(0, 0, 0, 0, port: 0)
Socket::IPAddress.v4_mapped_v6(127, 0, 0, 1, port: 1234).should eq Socket::IPAddress.new("::ffff:127.0.0.1", 1234)
Socket::IPAddress.v4_mapped_v6(192, 168, 0, 1, port: 8081).should eq Socket::IPAddress.new("::ffff:192.168.0.1", 8081)
Socket::IPAddress.v4_mapped_v6(255, 255, 255, 254, port: 65535).should eq Socket::IPAddress.new("::ffff:255.255.255.254", 65535)
end

it "raises on out of bound field" do
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4_mapped_v6(256, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4_mapped_v6(0, 256, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4_mapped_v6(0, 0, 256, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4_mapped_v6(0, 0, 0, 256, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4_mapped_v6(-1, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4_mapped_v6(0, -1, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4_mapped_v6(0, 0, -1, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4_mapped_v6(0, 0, 0, -1, port: 0) }
end

it "raises on out of bound port number" do
expect_raises(Socket::Error, "Invalid port number: 65536") { Socket::IPAddress.v4_mapped_v6(0, 0, 0, 0, port: 65536) }
expect_raises(Socket::Error, "Invalid port number: -1") { Socket::IPAddress.v4_mapped_v6(0, 0, 0, 0, port: -1) }
end

it "constructs from StaticArray" do
# windows returns the former which, while correct, is not canonical
# TODO: implement also `#to_s` in Crystal
{Socket::IPAddress.new("::ffff:0:0", 0), Socket::IPAddress.new("::ffff:0.0.0.0", 0)}.should contain Socket::IPAddress.v4_mapped_v6(UInt8.static_array(0, 0, 0, 0), 0)
Comment on lines +225 to +227
Copy link
Member

@straight-shoota straight-shoota May 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor detail, but this expectation is a bit confusing.
How about being more explicit about which result is expected?

Suggested change
# windows returns the former which, while correct, is not canonical
# TODO: implement also `#to_s` in Crystal
{Socket::IPAddress.new("::ffff:0:0", 0), Socket::IPAddress.new("::ffff:0.0.0.0", 0)}.should contain Socket::IPAddress.v4_mapped_v6(UInt8.static_array(0, 0, 0, 0), 0)
# windows returns the former which, while correct, is not canonical
# TODO: implement also `#to_s` in Crystal
Socket::IPAddress.v4_mapped_v6(UInt8.static_array(0, 0, 0, 0), 0).should eq Socket::IPAddress.new({{ flag?(:win32) }} ? "::ffff:0:0" : ::ffff:0.0.0.0", 0)

I'm happy either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the previous Socket.ip? spec, I don't have proof that Windows actually returns the non-canonical IP address on all OS versions, so it is better to err on the side of caution

Socket::IPAddress.v4_mapped_v6(UInt8.static_array(127, 0, 0, 1), 1234).should eq Socket::IPAddress.new("::ffff:127.0.0.1", 1234)
Socket::IPAddress.v4_mapped_v6(UInt8.static_array(192, 168, 0, 1), 8081).should eq Socket::IPAddress.new("::ffff:192.168.0.1", 8081)
Socket::IPAddress.v4_mapped_v6(UInt8.static_array(255, 255, 255, 254), 65535).should eq Socket::IPAddress.new("::ffff:255.255.255.254", 65535)
end
end

it ".valid_v6?" do
Socket::IPAddress.valid_v6?("::1").should be_true
Socket::IPAddress.valid_v6?("x").should be_false
Expand Down
107 changes: 101 additions & 6 deletions src/socket/address.cr
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class Socket
@addr : LibC::In6Addr | LibC::InAddr

def initialize(@address : String, @port : Int32)
raise Error.new("Invalid port number: #{port}") unless 0 <= port <= UInt16::MAX
raise Error.new("Invalid port number: #{port}") unless IPAddress.valid_port?(port)

if addr = IPAddress.address_v6?(address)
@addr = addr
Expand Down Expand Up @@ -144,16 +144,111 @@ class Socket
parse URI.parse(uri)
end

# Returns the IPv4 address with the given address *fields* and *port*
# number.
def self.v4(fields : UInt8[4], port : UInt16) : self
addr_value = UInt32.zero
fields.each_with_index do |field, i|
addr_value = (addr_value << 8) | field
end

addr = LibC::SockaddrIn.new(
sin_family: LibC::AF_INET,
sin_port: endian_swap(port),
sin_addr: LibC::InAddr.new(s_addr: endian_swap(addr_value)),
)
new(pointerof(addr), sizeof(typeof(addr)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HertzDevil I just noticed this now during the review of #13463.
Taking a pointer to a variable on the stack is dangerous. It seems to be working here, but it's fragile.
It probably only works because .new and #initialize don't allocate enough variables to override the (relevant) parts on the stack.
I suppose a safer alternative could perhaps be a yielding variant of #initialize or some other way to directly assign to the memory of the Address struct.
Same applies for v6.

Copy link
Contributor Author

@HertzDevil HertzDevil May 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't a tail call here (I believe you have to opt in explicitly in the LLVM IR), so addr will be valid before new returns. The #initialize bodies exclusively dereference addr and do not attempt to extend its lifetime.

In any case I plan to make SockaddrIn or SockaddrIn6 part of the IPAddress itself so that #to_unsafe never allocates. That would allow addr to be passed by value here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, if LLVM keeps this safe, then it's fine.

end

# Returns the IPv4 address `x0.x1.x2.x3:port`.
#
# Raises `Socket::Error` if any field or the port number is out of range.
def self.v4(x0 : Int, x1 : Int, x2 : Int, x3 : Int, *, port : Int) : self
fields = StaticArray[x0, x1, x2, x3].map { |field| to_v4_field(field) }
port = valid_port?(port) ? port.to_u16! : raise Error.new("Invalid port number: #{port}")
v4(fields, port)
end

private def self.to_v4_field(field)
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
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),
)
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
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)
end

private def self.to_v6_field(field)
0 <= field <= 0xffff ? field.to_u16! : raise Error.new("Invalid IPv6 field: #{field}")
end

# Returns the IPv4-mapped IPv6 address with the given IPv4 address *fields*
# and *port* number.
def self.v4_mapped_v6(fields : UInt8[4], port : UInt16) : self
v6_fields = StaticArray[
0_u16, 0_u16, 0_u16, 0_u16, 0_u16, 0xffff_u16,
fields[0].to_u16! << 8 | fields[1],
fields[2].to_u16! << 8 | fields[3],
]
v6(v6_fields, port)
end

# Returns the IPv4-mapped IPv6 address `[::ffff:x0.x1.x2.x3]:port`.
#
# Raises `Socket::Error` if any field or the port number is out of range.
def self.v4_mapped_v6(x0 : Int, x1 : Int, x2 : Int, x3 : Int, *, port : Int) : self
v4_fields = StaticArray[x0, x1, x2, x3].map { |field| to_v4_field(field) }
port = valid_port?(port) ? port.to_u16! : raise Error.new("Invalid port number: #{port}")
v4_mapped_v6(v4_fields, port)
end

private def self.ipv6_from_addr16(bytes : UInt16[8])
addr = LibC::In6Addr.new
{% if flag?(:darwin) || flag?(:bsd) %}
addr.__u6_addr.__u6_addr16 = bytes
{% elsif flag?(:linux) && flag?(:musl) %}
addr.__in6_union.__s6_addr16 = bytes
{% elsif flag?(:wasm32) %}
bytes.each_with_index do |byte, i|
addr.s6_addr[2 * i] = byte.to_u8!
addr.s6_addr[2 * i + 1] = (byte >> 8).to_u8!
end
{% elsif flag?(:linux) %}
addr.__in6_u.__u6_addr16 = bytes
{% elsif flag?(:win32) %}
addr.u.word = bytes
{% else %}
{% raise "Unsupported platform" %}
{% end %}
addr
end

protected def initialize(sockaddr : LibC::SockaddrIn6*, @size)
@family = Family::INET6
@addr = sockaddr.value.sin6_addr
@port = endian_swap(sockaddr.value.sin6_port).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
@port = endian_swap(sockaddr.value.sin_port).to_i
@port = IPAddress.endian_swap(sockaddr.value.sin_port).to_i
end

# Returns `true` if *address* is a valid IPv4 or IPv6 address.
Expand Down Expand Up @@ -313,20 +408,20 @@ class Socket
private def to_sockaddr_in6(addr)
sockaddr = Pointer(LibC::SockaddrIn6).malloc
sockaddr.value.sin6_family = family
sockaddr.value.sin6_port = endian_swap(port.to_u16!)
sockaddr.value.sin6_port = IPAddress.endian_swap(port.to_u16!)
sockaddr.value.sin6_addr = addr
sockaddr.as(LibC::Sockaddr*)
end

private def to_sockaddr_in(addr)
sockaddr = Pointer(LibC::SockaddrIn).malloc
sockaddr.value.sin_family = family
sockaddr.value.sin_port = endian_swap(port.to_u16!)
sockaddr.value.sin_port = IPAddress.endian_swap(port.to_u16!)
sockaddr.value.sin_addr = addr
sockaddr.as(LibC::Sockaddr*)
end

private def endian_swap(x : UInt16) : UInt16
protected def self.endian_swap(x : Int::Primitive) : Int::Primitive
{% if IO::ByteFormat::NetworkEndian != IO::ByteFormat::SystemEndian %}
x.byte_swap
{% else %}
Expand Down