From 7236a05503516603f024f1383491b8c99ef9a1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sun, 11 Jan 2026 20:32:57 +0100 Subject: [PATCH 1/2] test: enable `feature_bind_extra` on macOS and BSD `feature_bind_extra` checks `-bind` and `-whitebind` by comparing the node's listening sockets with the expected addresses. Add `get_bind_addrs` support for macOS, FreeBSD, NetBSD, and OpenBSD using `lsof`, and switch the test to the POSIX platform guard so it runs there too. On FreeBSD, pass `-Di` to avoid device-cache warnings on stderr that the functional test runner treats as failures. Co-authored-by: willcl-ark Co-authored-by: fanquake --- test/functional/feature_bind_extra.py | 3 +-- test/functional/test_framework/netutil.py | 33 ++++++++++++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/test/functional/feature_bind_extra.py b/test/functional/feature_bind_extra.py index ad4c4b138e4..91f846d6f69 100755 --- a/test/functional/feature_bind_extra.py +++ b/test/functional/feature_bind_extra.py @@ -32,8 +32,7 @@ class BindExtraTest(BitcoinTestFramework): self.num_nodes = 3 def skip_test_if_missing_module(self): - # Due to OS-specific network stats queries, we only run on Linux. - self.skip_if_platform_not_linux() + self.skip_if_platform_not_posix() def setup_network(self): loopback_ipv4 = addr_to_hex("127.0.0.1") diff --git a/test/functional/test_framework/netutil.py b/test/functional/test_framework/netutil.py index 5504029a766..c7ab756a0c8 100644 --- a/test/functional/test_framework/netutil.py +++ b/test/functional/test_framework/netutil.py @@ -2,7 +2,7 @@ # Copyright (c) 2014-present The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Linux network utilities. +"""Linux, macOS, and BSD network utilities. Roughly based on https://web.archive.org/web/20190424172231/http://voorloopnul.com/blog/a-python-netstat-in-less-than-100-lines-of-code/ by Ricardo Pascal """ @@ -88,12 +88,31 @@ def get_bind_addrs(pid): ''' Get bind addresses as (host,port) tuples for process pid. ''' - inodes = get_socket_inodes(pid) - bind_addrs = [] - for conn in netstat('tcp') + netstat('tcp6'): - if conn[3] == STATE_LISTEN and conn[4] in inodes: - bind_addrs.append(conn[1]) - return bind_addrs + if sys.platform == 'linux': + inodes = get_socket_inodes(pid) + bind_addrs = [] + for conn in netstat('tcp') + netstat('tcp6'): + if conn[3] == STATE_LISTEN and conn[4] in inodes: + bind_addrs.append(conn[1]) + return bind_addrs + elif sys.platform.startswith(("darwin", "freebsd", "netbsd", "openbsd")): + import re + import subprocess + output = subprocess.check_output(["lsof", + *(["-Di"] if sys.platform.startswith("freebsd") else []), # Ignore device cache to avoid stderr warnings. + "-nP", # Keep hosts and ports numeric. + "-a", # Require all filters to match. + "-p", str(pid), # Limit results to the target pid. + "-iTCP", # Only inspect TCP sockets. + "-sTCP:LISTEN", # Only keep listening sockets. + "-Ftn", # Emit machine-readable type and name fields. + ], text=True) + return [ + (addr_to_hex(("::" if sock_type == "IPv6" else "0.0.0.0") if host == "*" else host.strip("[]")), int(port)) + for sock_type, host, port in re.findall(r"t(IPv[46])\nn(\*|\[.+?]|[^:]+):(\d+)", output) + ] + else: + raise NotImplementedError(f"get_bind_addrs is not supported on {sys.platform}") # from: https://code.activestate.com/recipes/439093/ def all_interfaces(): From 1950da94fce864d1add43c3196007e4c56607d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sun, 11 Jan 2026 20:33:36 +0100 Subject: [PATCH 2/2] test: enable `rpc_bind` on macOS and BSD `rpc_bind` uses `all_interfaces` to find a non-loopback IPv4 address and `get_bind_addrs` to verify the node's listening sockets. Add `all_interfaces` support for macOS, FreeBSD, NetBSD, and OpenBSD using `ifconfig -au`, switch the test to the POSIX platform guard so it runs there too, and fail early if no IPv4 interfaces are returned. Co-authored-by: Sjors Provoost --- test/functional/rpc_bind.py | 8 ++-- test/functional/test_framework/netutil.py | 57 ++++++++++++++--------- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/test/functional/rpc_bind.py b/test/functional/rpc_bind.py index 3c6e3e4b378..bff8ec244d6 100755 --- a/test/functional/rpc_bind.py +++ b/test/functional/rpc_bind.py @@ -17,8 +17,7 @@ class RPCBindTest(BitcoinTestFramework): self.supports_cli = False def skip_test_if_missing_module(self): - # due to OS-specific network stats queries, this test works only on Linux - self.skip_if_platform_not_linux() + self.skip_if_platform_not_posix() def setup_network(self): self.add_nodes(self.num_nodes, None) @@ -105,8 +104,11 @@ class RPCBindTest(BitcoinTestFramework): raise SkipTest("This test requires ipv6 support.") self.log.info("Check for non-loopback interface") + interfaces = all_interfaces() + if not interfaces: + raise AssertionError("all_interfaces() returned no IPv4 interfaces") self.non_loopback_ip = None - for name,ip in all_interfaces(): + for name,ip in interfaces: if ip != '127.0.0.1': self.non_loopback_ip = ip break diff --git a/test/functional/test_framework/netutil.py b/test/functional/test_framework/netutil.py index c7ab756a0c8..85209322196 100644 --- a/test/functional/test_framework/netutil.py +++ b/test/functional/test_framework/netutil.py @@ -114,33 +114,44 @@ def get_bind_addrs(pid): else: raise NotImplementedError(f"get_bind_addrs is not supported on {sys.platform}") -# from: https://code.activestate.com/recipes/439093/ def all_interfaces(): ''' - Return all interfaces that are up + Return all IPv4 interfaces that are up. ''' - import fcntl # Linux only, so only import when required + if sys.platform == 'linux': + import fcntl # Linux only, so only import when required - is_64bits = sys.maxsize > 2**32 - struct_size = 40 if is_64bits else 32 - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - max_possible = 8 # initial value - while True: - bytes = max_possible * struct_size - names = array.array('B', b'\0' * bytes) - outbytes = struct.unpack('iL', fcntl.ioctl( - s.fileno(), - 0x8912, # SIOCGIFCONF - struct.pack('iL', bytes, names.buffer_info()[0]) - ))[0] - if outbytes == bytes: - max_possible *= 2 - else: - break - namestr = names.tobytes() - return [(namestr[i:i+16].split(b'\0', 1)[0], - socket.inet_ntoa(namestr[i+20:i+24])) - for i in range(0, outbytes, struct_size)] + is_64bits = sys.maxsize > 2**32 + struct_size = 40 if is_64bits else 32 + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + max_possible = 8 # initial value + while True: + bytes = max_possible * struct_size + names = array.array('B', b'\0' * bytes) + outbytes = struct.unpack('iL', fcntl.ioctl( + s.fileno(), + 0x8912, # SIOCGIFCONF + struct.pack('iL', bytes, names.buffer_info()[0]) + ))[0] + if outbytes == bytes: + max_possible *= 2 + else: + break + namestr = names.tobytes() + return [(namestr[i:i+16].split(b'\0', 1)[0], + socket.inet_ntoa(namestr[i+20:i+24])) + for i in range(0, outbytes, struct_size)] + elif sys.platform.startswith(("darwin", "freebsd", "netbsd", "openbsd")): + import re + import subprocess + output = subprocess.check_output(["ifconfig", "-au"], text=True) + return [ + (m["iface"].encode(), ip) + for m in re.finditer(r"(?m)^(?P\S+):(?P[^\n]*(?:\n[ \t]+[^\n]*)*)", output) + for ip in re.findall(r"inet (\S+)", m["block"]) + ] + else: + raise NotImplementedError(f"all_interfaces is not supported on {sys.platform}") def addr_to_hex(addr): '''