From 1a5380ff5d65b4cc129de2f77b37dc30a1c0663a Mon Sep 17 00:00:00 2001 From: Vitaly Lavrov Date: Thu, 8 Oct 2026 22:27:06 +0200 Subject: [PATCH] Fix IPv6 socket addresses on Native `toSockaddrIn6` never copied the address bytes (`while (i < 0)`), so every IPv6 address was passed to the kernel as `::`. Also replace fs2's own sockaddr bindings with Scala Native's `posix.netinet.in`/`sys.un`, which handle the BSD `sa_len`/`sa_family` layout. Without that, IPv6 bind fails on macOS with EAFNOSUPPORT. --- .../scala/fs2/io/internal/SocketHelpers.scala | 10 +-- .../main/scala/fs2/io/internal/netinet.scala | 90 ------------------- .../main/scala/fs2/io/internal/sysun.scala | 48 ---------- .../io/net/FdPollingUnixSocketsProvider.scala | 4 +- .../test/scala/fs2/io/net/SocketSuite.scala | 16 ++++ 5 files changed, 23 insertions(+), 145 deletions(-) delete mode 100644 io/native/src/main/scala/fs2/io/internal/netinet.scala delete mode 100644 io/native/src/main/scala/fs2/io/internal/sysun.scala diff --git a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala index d4d08a917e..96bd13f96a 100644 --- a/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala +++ b/io/native/src/main/scala/fs2/io/internal/SocketHelpers.scala @@ -44,15 +44,15 @@ import scala.scalanative.posix.unistd._ import scala.scalanative.posix.sys.socket._ import scala.scalanative.posix.sys.socketOps._ import scala.scalanative.posix.netinet.in._ +import scala.scalanative.posix.netinet.inOps._ import scala.scalanative.posix.arpa.inet._ +import scala.scalanative.posix.sys.un._ +import scala.scalanative.posix.sys.unOps._ import scala.scalanative.unsafe._ import scala.scalanative.unsigned._ import NativeUtil._ -import netinetinOps._ import syssocket._ -import sysun._ -import sysunOps._ private[io] object SocketHelpers { @@ -338,8 +338,8 @@ private[io] object SocketHelpers { val bytes = address.host.toBytes var i = 0 - while (i < 0) { - addr.sin6_addr.s6_addr(i) = bytes(i).toUByte + while (i < 16) { + addr.at4.s6_addr(i) = bytes(i).toUByte i += 1 } } diff --git a/io/native/src/main/scala/fs2/io/internal/netinet.scala b/io/native/src/main/scala/fs2/io/internal/netinet.scala deleted file mode 100644 index fdb1dc4afc..0000000000 --- a/io/native/src/main/scala/fs2/io/internal/netinet.scala +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package fs2.io.internal - -import scalanative.unsafe._ -import scalanative.posix.inttypes._ -import scalanative.posix.sys.socket._ - -private[io] object netinetin { - import Nat._ - type _16 = Digit2[_1, _6] - - type in_port_t = uint16_t - - type in_addr = CStruct1[uint32_t] - - type sockaddr_in = CStruct4[ - sa_family_t, - in_port_t, - in_addr, - CArray[Byte, _8] - ] - - type in6_addr = CStruct1[CArray[CUnsignedChar, _16]] - - type sockaddr_in6 = CStruct5[ - sa_family_t, - in_port_t, - uint32_t, - in6_addr, - uint32_t - ] - -} - -private[io] object netinetinOps { - import netinetin._ - - implicit final class in_addrOps(val in_addr: in_addr) extends AnyVal { - def s_addr: uint32_t = in_addr._1 - def s_addr_=(s_addr: uint32_t): Unit = in_addr._1 = s_addr - } - - implicit final class sockaddr_inOps(val sockaddr_in: Ptr[sockaddr_in]) extends AnyVal { - def sin_family: sa_family_t = sockaddr_in._1 - def sin_family_=(sin_family: sa_family_t): Unit = sockaddr_in._1 = sin_family - def sin_port: in_port_t = sockaddr_in._2 - def sin_port_=(sin_port: in_port_t): Unit = sockaddr_in._2 = sin_port - def sin_addr: in_addr = sockaddr_in._3 - def sin_addr_=(sin_addr: in_addr) = sockaddr_in._3 = sin_addr - } - - implicit final class in6_addrOps(val in6_addr: in6_addr) extends AnyVal { - def s6_addr: CArray[uint8_t, _16] = in6_addr._1 - def s6_addr_=(s6_addr: CArray[uint8_t, _16]): Unit = in6_addr._1 = s6_addr - } - - implicit final class sockaddr_in6Ops(val sockaddr_in6: Ptr[sockaddr_in6]) extends AnyVal { - def sin6_family: sa_family_t = sockaddr_in6._1 - def sin6_family_=(sin6_family: sa_family_t): Unit = sockaddr_in6._1 = sin6_family - def sin6_port: in_port_t = sockaddr_in6._2 - def sin6_port_=(sin6_port: in_port_t): Unit = sockaddr_in6._2 = sin6_port - def sin6_flowinfo: uint32_t = sockaddr_in6._3 - def sin6_flowinfo_=(sin6_flowinfo: uint32_t): Unit = sockaddr_in6._3 = sin6_flowinfo - def sin6_addr: in6_addr = sockaddr_in6._4 - def sin6_addr_=(sin6_addr: in6_addr) = sockaddr_in6._4 = sin6_addr - def sin6_scope_id: uint32_t = sockaddr_in6._5 - def sin6_scope_id_=(sin6_scope_id: uint32_t): Unit = sockaddr_in6._5 = sin6_scope_id - } - -} diff --git a/io/native/src/main/scala/fs2/io/internal/sysun.scala b/io/native/src/main/scala/fs2/io/internal/sysun.scala deleted file mode 100644 index 951a1ca346..0000000000 --- a/io/native/src/main/scala/fs2/io/internal/sysun.scala +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package fs2.io.internal - -import scala.scalanative.posix.sys.socket._ -import scala.scalanative.unsafe._ - -private[io] object sysun { - import Nat._ - type _108 = Digit3[_1, _0, _8] - - type sockaddr_un = CStruct2[ - sa_family_t, - CArray[CChar, _108] - ] - -} - -private[io] object sysunOps { - import sysun._ - - implicit final class sockaddr_unOps(val sockaddr_un: Ptr[sockaddr_un]) extends AnyVal { - def sun_family: sa_family_t = sockaddr_un._1 - def sun_family_=(sun_family: sa_family_t): Unit = sockaddr_un._1 = sun_family - def sun_path: CArray[CChar, _108] = sockaddr_un._2 - def sun_path_=(sun_path: CArray[CChar, _108]): Unit = sockaddr_un._2 = sun_path - } - -} diff --git a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala index 2f9ef13e4c..8a2d9d5224 100644 --- a/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala +++ b/io/native/src/main/scala/fs2/io/net/FdPollingUnixSocketsProvider.scala @@ -32,12 +32,12 @@ import fs2.io.file.Files import fs2.io.internal.NativeUtil._ import fs2.io.internal.SocketHelpers import fs2.io.internal.syssocket.{connect => uconnect, bind => ubind, _} -import fs2.io.internal.sysun._ -import fs2.io.internal.sysunOps._ import scala.scalanative.meta.LinktimeInfo import scala.scalanative.posix.string._ import scala.scalanative.posix.sys.socket.{accept => _, _} +import scala.scalanative.posix.sys.un._ +import scala.scalanative.posix.sys.unOps._ import scala.scalanative.posix.unistd._ import scala.scalanative.unsafe._ import scala.scalanative.unsigned._ diff --git a/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala index fe2e7c9e69..148e013057 100644 --- a/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala +++ b/io/shared/src/test/scala/fs2/io/net/SocketSuite.scala @@ -343,5 +343,21 @@ class SocketSuite extends Fs2Suite with SocketSuitePlatform { assertEquals(received, expected) } } + + test("ipv6 - bind, connect and echo over loopback") { + val msg = Chunk.array("fs2.rocks".getBytes) + Network[IO].bind(SocketAddress(ip"::1", Port.Wildcard)).use { serverSocket => + IO(assertEquals(serverSocket.address.asIpUnsafe.host, ip"::1")) *> + Network[IO].connect(serverSocket.address).use { client => + serverSocket.accept.head + .evalMap { server => + IO(assertEquals(server.peerAddress, client.address)) *> + client.write(msg) *> server.readN(msg.size).assertEquals(msg) + } + .compile + .drain + } + } + } } }