fix(net): make new IPv6 sockets IPv6-only as FreeBSD does

On Linux, socket() and sceNetSocket() create AF_INET6 sockets in the
host's default dual-stack mode (net.ipv6.bindv6only is 0), so a wildcard
IPv6 bind also claims the IPv4 port. A title that keeps an IPv4 and an
IPv6 socket on the same port fails: with 0.0.0.0:P bound, binding [::]:P
returns EADDRINUSE (48), and an IPv6 socket bound to [::] also receives
IPv4 traffic from v4-mapped peers.

FreeBSD makes every new IPv6 socket IPv6-only. sys/netinet6/in6_proto.c:

	VNET_DEFINE(int, ip6_v6only) = 1;

sys/netinet/in_pcb.c, in_pcballoc():

	if (INP_SOCKAF(so) == AF_INET6) {
		inp->inp_vflag |= INP_IPV6PROTO;
		if (V_ip6_v6only)
			inp->inp_flags |= IN6P_IPV6_V6ONLY;
	}

and sys/netinet6/in6_pcb.c, in6_pcbbind(), looks for an IPv4 owner of
the port only when the flag is clear:

	if ((inp->inp_flags & IN6P_IPV6_V6ONLY) == 0 &&
	    IN6_IS_ADDR_UNSPECIFIED(&sin6->sin6_addr)) {

socket() and sceNetSocket() now set IPV6_V6ONLY on every IPv6 socket
they create. Windows already defaults to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
milescoviello 2026-10-12 07:52:27 +00:00
parent c36fd0a0eb
commit 98f6639e9e
4 changed files with 59 additions and 0 deletions

View File

@ -528,6 +528,13 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol)
if (!initialize_sockets()) return fail(5);
const NativeSocket native = ::socket(family == NET_AF_INET ? AF_INET : AF_INET6, type, protocol);
if (native == INVALID_NATIVE_SOCKET) return fail(native_error());
const int ipv6_only = 1;
if (family == NET_AF_INET6 &&
::setsockopt(native, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast<const char*>(&ipv6_only), sizeof(ipv6_only)) != 0) {
const int error = native_error();
close_socket(native);
return fail(error);
}
std::shared_ptr<NativeSocketHandle> handle;
try {
handle = std::make_shared<NativeSocketHandle>(native);

View File

@ -506,6 +506,9 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) {
const auto native = ::socket(family == 2 ? AF_INET : AF_INET6, nativeType, protocol);
if (native == Invalid) return Fail(NativeError());
Socket guard(native, family, plainType);
const int ipv6Only = 1;
if (family == 28 && ::setsockopt(native, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast<const char*>(&ipv6Only), sizeof(ipv6Only)))
return Fail(NativeError());
try {
auto socket = std::make_shared<Socket>(native, family, plainType);
guard.value = Invalid;

View File

@ -338,6 +338,25 @@ static void CheckUnspecifiedIpv6() {
Require(sceNetSocketClose(receiver) == 0);
}
static void CheckIpv6SocketsAreIpv6Only() {
const int inet = sceNetSocket("ipv4-any", 2, 2, 17);
Require(inet >= 0);
std::array<std::uint8_t, 16> any{16, 2};
Require(sceNetBind_nid_postfix(inet, any.data(), any.size()) == 0);
std::uint32_t size = any.size();
Require(sceNetGetsockname(inet, any.data(), &size) == 0 && size == any.size());
const int inet6 = sceNetSocket("ipv6-any-same-port", 28, 2, 17);
Require(inet6 >= 0);
std::array<std::uint8_t, 28> any6{28, 28, any[2], any[3]};
if (sceNetBind_nid_postfix(inet6, any6.data(), any6.size()) != 0) {
std::fprintf(stderr, "sceNetBind of [::]:%d failed with errno %d while an IPv4 socket holds 0.0.0.0:%d\n",
(any[2] << 8) | any[3], *sceNetErrnoLoc(), (any[2] << 8) | any[3]);
std::abort();
}
Require(sceNetSocketClose(inet6) == 0);
Require(sceNetSocketClose(inet) == 0);
}
int main() {
const auto epoch = std::chrono::steady_clock::time_point{};
Require(KernelSocketSelect::DeadlineAfter(epoch, 2, 250000) == epoch + std::chrono::milliseconds(2250));
@ -359,6 +378,7 @@ int main() {
CheckAddressText(28, "1234:5678:9abc:def0:1234:5678:9abc:def0");
CheckLinkLocalAllNodesIpv6();
CheckUnspecifiedIpv6();
if (!ipv6Unavailable) CheckIpv6SocketsAreIpv6Only();
const int closed = sceNetSocket("closed-by-kernel", 2, 2, 17);
Require(closed >= 0);

View File

@ -1,6 +1,7 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@ -47,8 +48,36 @@ static void StreamReceiveReportsNoSender() {
for (const int descriptor : {client, accepted, listener}) Require(close_nid_postfix(descriptor) == 0);
}
static void ListenersShareAPortAcrossFamilies() {
const int inet = socket_nid_postfix(2, 1, 0);
Require(inet >= 0);
std::array<std::uint8_t, 16> any{16, 2};
Require(bind_nid_postfix(inet, any.data(), any.size()) == 0 && listen_nid_postfix(inet, 4) == 0);
std::uint32_t size = any.size();
Require(getsockname_nid_postfix(inet, any.data(), &size) == 0 && size == any.size());
const int inet6 = socket_nid_postfix(28, 1, 0);
if (inet6 < 0) {
Require(*__error_nid_postfix() == 47);
Require(close_nid_postfix(inet) == 0);
return;
}
std::array<std::uint8_t, 28> any6{28, 28, any[2], any[3]};
if (bind_nid_postfix(inet6, any6.data(), any6.size()) != 0) {
if (*__error_nid_postfix() != 49) {
std::fprintf(stderr, "bind of [::]:%d failed with errno %d while an IPv4 listener holds 0.0.0.0:%d\n",
(any[2] << 8) | any[3], *__error_nid_postfix(), (any[2] << 8) | any[3]);
std::abort();
}
} else {
Require(listen_nid_postfix(inet6, 4) == 0);
}
Require(close_nid_postfix(inet6) == 0);
Require(close_nid_postfix(inet) == 0);
}
int main() {
StreamReceiveReportsNoSender();
ListenersShareAPortAcrossFamilies();
const int listener = socket_nid_postfix(2, 1, 6);
Require(listener >= 0);
std::array<std::uint8_t, 16> address{16, 2, 0, 0, 127, 0, 0, 1};