Merge bitcoin/bitcoin#34346: test: use IP_PORTRANGE_HIGH on FreeBSD for dynamic port allocation

2845f10a2be0fee13b2772d24e948052243782b8 test: extend FreeBSD ephemeral port range fix to P2P listeners (node)
34bed0ed8c449a3834927cec3447dbe6c74edf3d test: use IP_PORTRANGE_HIGH on FreeBSD for dynamic port allocation (woltx)

Pull request description:

  Reopening #34336. I’ve now tested it on FreeBSD and confirmed it works.

  On FreeBSD, the default ephemeral port range (10000-65535) overlaps with the test framework's static port range (11000-26000), possibly causing intermittent "address already in use" failures when tests use dynamic port allocation (`port=0`).

  This PR adds a helper that sets `IP_PORTRANGE_HIGH` via `setsockopt()` before binding, requesting ports from 49152-65535 instead, which avoids the overlap, as suggested in https://github.com/bitcoin/bitcoin/issues/34331#issuecomment-3767161843 by @maflcko .

  From FreeBSD's [sys/netinet/in.h](https://cgit.freebsd.org/src/tree/sys/netinet/in.h):
    ```c
    #define IP_PORTRANGE         19
    #define IP_PORTRANGE_HIGH    1
    #define IPPORT_EPHEMERALFIRST 10000  /* default range start */
    #define IPPORT_HIFIRSTAUTO   49152   /* high range start */
  ```

  See also: FreeBSD https://man.freebsd.org/cgi/man.cgi?query=ip&sektion=4 man page.

  Fixes #34331

ACKs for top commit:
  vasild:
    ACK 2845f10a2be0fee13b2772d24e948052243782b8
  hebasto:
    ACK 2845f10a2be0fee13b2772d24e948052243782b8, I have reviewed the code and it looks OK.

Tree-SHA512: ce501ce3e8a4023e07bad572df2b85d6829becf133813e4529aebba83e4eba59fa8b48e9d2197ebbb226adaf3054fad720775a787244d6b38c0078ee086102f4
This commit is contained in:
Hennadii Stepanov 2026-01-29 11:33:29 +00:00
commit f7e0c3d3d3
No known key found for this signature in database
GPG Key ID: 410108112E7EA81F
3 changed files with 46 additions and 3 deletions

View File

@ -181,3 +181,22 @@ def format_addr_port(addr, port):
return f"[{addr}]:{port}" return f"[{addr}]:{port}"
else: else:
return f"{addr}:{port}" 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)

View File

@ -22,9 +22,11 @@ P2PTxInvStore: A p2p interface class that inherits from P2PDataStore, and keeps
import asyncio import asyncio
from collections import defaultdict from collections import defaultdict
import ipaddress
from io import BytesIO from io import BytesIO
import logging import logging
import platform import platform
import socket
import struct import struct
import sys import sys
import threading import threading
@ -76,6 +78,9 @@ from test_framework.messages import (
MAGIC_BYTES, MAGIC_BYTES,
sha256, sha256,
) )
from test_framework.netutil import (
set_ephemeral_port_range,
)
from test_framework.util import ( from test_framework.util import (
assert_not_equal, assert_not_equal,
MAX_NODES, MAX_NODES,
@ -793,8 +798,22 @@ class NetworkThread(threading.Thread):
# connections, we can accomplish this by providing different # connections, we can accomplish this by providing different
# `proto` functions # `proto` functions
listener = await cls.network_event_loop.create_server(peer_protocol, addr, port) if port == 0:
port = listener.sockets[0].getsockname()[1] # 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)) logger.debug("Listening server on %s:%d should be started" % (addr, port))
cls.listeners[(addr, port)] = listener cls.listeners[(addr, port)] = listener

View File

@ -11,7 +11,8 @@ import queue
import logging import logging
from .netutil import ( from .netutil import (
format_addr_port format_addr_port,
set_ephemeral_port_range,
) )
logger = logging.getLogger("TestFramework.socks5") logger = logging.getLogger("TestFramework.socks5")
@ -202,6 +203,10 @@ class Socks5Server():
self.conf = conf self.conf = conf
self.s = socket.socket(conf.af) self.s = socket.socket(conf.af)
self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 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) self.s.bind(conf.addr)
# When port=0, the OS assigns an available port. Update 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. # to reflect the actual bound address so callers can use it.