From 34bed0ed8c449a3834927cec3447dbe6c74edf3d Mon Sep 17 00:00:00 2001 From: woltx <94266259+w0xlt@users.noreply.github.com> Date: Mon, 19 Jan 2026 01:53:40 -0800 Subject: [PATCH 1/2] test: use IP_PORTRANGE_HIGH on FreeBSD for dynamic port allocation On FreeBSD, the default ephemeral port range (10000-65535) overlaps with the test framework's static port range (11000-26000), causing intermittent "address already in use" failures when tests use dynamic port allocation (port=0). Add a helper function that sets the IP_PORTRANGE/IPV6_PORTRANGE socket option to IP_PORTRANGE_HIGH before binding, which requests ports from the high range (49152-65535) instead. This range does not overlap with the test framework's static ports. Constants from FreeBSD's netinet/in.h and netinet6/in6.h: - IP_PORTRANGE = 19 (for IPv4 sockets) - IPV6_PORTRANGE = 14 (for IPv6 sockets) - IP_PORTRANGE_HIGH = 1 Fixes: bitcoin/bitcoin#34331 Co-Authored-By: Vasil Dimov Co-Authored-By: MarcoFalke <*~=\`'#}+{/-|&$^_@721217.xyz> --- test/functional/test_framework/netutil.py | 19 +++++++++++++++++++ test/functional/test_framework/socks5.py | 7 ++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/test/functional/test_framework/netutil.py b/test/functional/test_framework/netutil.py index 75577b0ca84..5504029a766 100644 --- a/test/functional/test_framework/netutil.py +++ b/test/functional/test_framework/netutil.py @@ -181,3 +181,22 @@ def format_addr_port(addr, port): return f"[{addr}]:{port}" else: return f"{addr}:{port}" + + +def set_ephemeral_port_range(sock): + '''On FreeBSD, set socket to use the high ephemeral port range (49152-65535). + + FreeBSD's default ephemeral port range (10000-65535) overlaps with the test + framework's static port range starting at TEST_RUNNER_PORT_MIN (default=11000). + Using IP_PORTRANGE_HIGH avoids this overlap when binding to port 0 for dynamic + port allocation. + ''' + if sys.platform.startswith('freebsd'): + # Constants from FreeBSD's netinet/in.h and netinet6/in6.h + IP_PORTRANGE = 19 + IPV6_PORTRANGE = 14 + IP_PORTRANGE_HIGH = 1 # Same value for both IPv4 and IPv6 + if sock.family == socket.AF_INET6: + sock.setsockopt(socket.IPPROTO_IPV6, IPV6_PORTRANGE, IP_PORTRANGE_HIGH) + else: + sock.setsockopt(socket.IPPROTO_IP, IP_PORTRANGE, IP_PORTRANGE_HIGH) diff --git a/test/functional/test_framework/socks5.py b/test/functional/test_framework/socks5.py index 711734cf98e..085c5a2e324 100644 --- a/test/functional/test_framework/socks5.py +++ b/test/functional/test_framework/socks5.py @@ -11,7 +11,8 @@ import queue import logging from .netutil import ( - format_addr_port + format_addr_port, + set_ephemeral_port_range, ) logger = logging.getLogger("TestFramework.socks5") @@ -202,6 +203,10 @@ class Socks5Server(): self.conf = conf self.s = socket.socket(conf.af) self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # When using dynamic port allocation (port=0), ensure we don't get a + # port that conflicts with the test framework's static port range. + if conf.addr[1] == 0: + set_ephemeral_port_range(self.s) self.s.bind(conf.addr) # When port=0, the OS assigns an available port. Update conf.addr # to reflect the actual bound address so callers can use it. From 2845f10a2be0fee13b2772d24e948052243782b8 Mon Sep 17 00:00:00 2001 From: node Date: Tue, 20 Jan 2026 15:31:56 -0800 Subject: [PATCH 2/2] test: extend FreeBSD ephemeral port range fix to P2P listeners The previous commit added set_ephemeral_port_range() to avoid port conflicts on FreeBSD by requesting ports from the high ephemeral range (49152-65535) instead of the default range which overlaps with the test framework's static port range. That fix was applied to the SOCKS5 server but not to P2P listeners created via NetworkThread.create_listen_server(). This commit extends the fix to cover P2P listeners as well. When port=0 is requested (dynamic allocation), we now: 1. Manually create a socket with the appropriate address family 2. Call set_ephemeral_port_range() to configure the port range 3. Bind and listen on the socket 4. Pass the pre-configured socket to asyncio's create_server() This ensures that dynamically allocated ports for P2P listeners also come from the high range on FreeBSD, avoiding conflicts with the test framework's static port assignments. Co-Authored-By: Vasil Dimov --- test/functional/test_framework/p2p.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/test/functional/test_framework/p2p.py b/test/functional/test_framework/p2p.py index 986eaf1e88e..8102da7a7f7 100755 --- a/test/functional/test_framework/p2p.py +++ b/test/functional/test_framework/p2p.py @@ -22,9 +22,11 @@ P2PTxInvStore: A p2p interface class that inherits from P2PDataStore, and keeps import asyncio from collections import defaultdict +import ipaddress from io import BytesIO import logging import platform +import socket import struct import sys import threading @@ -76,6 +78,9 @@ from test_framework.messages import ( MAGIC_BYTES, sha256, ) +from test_framework.netutil import ( + set_ephemeral_port_range, +) from test_framework.util import ( assert_not_equal, MAX_NODES, @@ -793,8 +798,22 @@ class NetworkThread(threading.Thread): # connections, we can accomplish this by providing different # `proto` functions - listener = await cls.network_event_loop.create_server(peer_protocol, addr, port) - port = listener.sockets[0].getsockname()[1] + if port == 0: + # Manually create the socket in order to set the range to be + # used for the port before the bind() call. + if ipaddress.ip_address(addr).version == 4: + address_family = socket.AF_INET + else: + address_family = socket.AF_INET6 + s = socket.socket(address_family) + set_ephemeral_port_range(s) + s.bind((addr, 0)) + s.listen() + listener = await cls.network_event_loop.create_server(peer_protocol, sock=s) + port = listener.sockets[0].getsockname()[1] + else: + listener = await cls.network_event_loop.create_server(peer_protocol, addr, port) + logger.debug("Listening server on %s:%d should be started" % (addr, port)) cls.listeners[(addr, port)] = listener