test: support `get_bind_addrs` and `feature_bind_extra` on macOS & BSD #34256

pull l0rinc wants to merge 2 commits into bitcoin:master from l0rinc:l0rinc/test-macos-bind-netutil changing 3 files +68 −37
  1. l0rinc commented at 9:42 PM on January 11, 2026: contributor

    Problem

    Some functional tests are shown as skipped when running on macOS & BSD because test_framework/netutil.py only implemented the Linux-specific logic for checking which TCP sockets a node is listening on.

    Fix

    Add macOS and BSD implementations in test/functional/test_framework/netutil.py so tests can query:

    • which TCP sockets a node is listening on (get_bind_addrs(), via lsof)
    • a non-loopback interface address (all_interfaces(), via ifconfig)

    Then enable the previously Linux-only tests by switching to a shared POSIX platform guard.

    Commands

    <details> <summary><code>get_bind_addrs()</code> (<code>lsof</code> + regex)</summary>

    Command used

    lsof -nP -a -p <pid> -iTCP -sTCP:LISTEN -Ftn
    

    Flags

    • -D: device cache warnings
    • -n: no hostname resolution
    • -P: no service/port-name resolution
    • -a: AND all conditions
    • -p <pid>: filter by process ID
    • -iTCP: TCP sockets only
    • -sTCP:LISTEN: listening sockets only
    • -Ftn: machine-readable output (fields: type t, name n)

    Regex parser

    t(IPv[46])\nn(\*|\[.+?]|[^:]+):(\d+)
    

    Captured groups

    • group 1: IPv4 / IPv6 (used to disambiguate *)
    • group 2: host (*, [::1], 127.0.0.1, ...)
    • group 3: port

      </details>

    <details> <summary><code>all_interfaces()</code> (<code>ifconfig</code> + regex)</summary>

    Command used

    ifconfig -au
    

    Regex parsing

    Interface blocks:

    (?m)^(?P<iface>\S+):(?P<block>[^\n]*(?:\n[ \t]+[^\n]*)*)
    

    IPv4 extraction within each block:

    inet (\S+)
    

    </details>

    Notes

    The only remaining platform skips on macOS are the USDT/BPF tracing tests (interface_usdt_*.py).

  2. DrahtBot added the label Tests on Jan 11, 2026
  3. DrahtBot commented at 9:42 PM on January 11, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34256.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Stale ACK bensig, willcl-ark, Sjors, ismaelsadeeq

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. in test/functional/test_framework/netutil.py:91 in 9b3a39cf60
      92 | -    bind_addrs = []
      93 | -    for conn in netstat('tcp') + netstat('tcp6'):
      94 | -        if conn[3] == STATE_LISTEN and conn[4] in inodes:
      95 | -            bind_addrs.append(conn[1])
      96 | -    return bind_addrs
      97 | +    if sys.platform.startswith("linux"):
    


    Sjors commented at 4:14 AM on January 12, 2026:

    In 9b3a39cf60dbad453874d3474dc21ecb960ffcfc test: support get_bind_addrs on macOS nit: you can get rid of startswith, since our minimum Python version is well after 3.3 which removed "linux2" and "linux3": https://docs.python.org/3.3/whatsnew/3.3.html#porting-python-code

    Replace sys.platform == ‘linux2’ with sys.platform.startswith(‘linux’), or directly sys.platform == ‘linux’ if you don’t need to support older Python versions.


    l0rinc commented at 7:47 AM on January 12, 2026:

    Thanks, done, added you as coauthor


    Sjors commented at 1:05 PM on February 23, 2026:

    e70de0a406c072474b17d7c8ccff57fc952fae4b: it still says startswith


    Sjors commented at 1:13 PM on February 23, 2026:

    Nvm, you added a separate commit for this.

  5. in test/functional/test_framework/netutil.py:104 in 9b3a39cf60 outdated
     105 | +        import re
     106 | +        import subprocess
     107 | +        output = subprocess.check_output(["lsof", "-nP", "-a", "-p", str(pid), "-iTCP", "-sTCP:LISTEN", "-Ftn"], text=True)
     108 | +        return [
     109 | +            (addr_to_hex(("::" if sock_type == "IPv6" else "0.0.0.0") if host == "*" else host.strip("[]")), int(port))
     110 | +            for sock_type, host, port in re.findall(r"t(IPv[46])\nn(\*|\[.+?]|[^:]+):(\d+)", output)
    


    Sjors commented at 4:22 AM on January 12, 2026:

    (delete wrong commit reference)

    Review note, this is what the command outputs (without -Ftv):

    % lsof -nP -a -p 92306 -iTCP -sTCP:LISTEN
    COMMAND    PID  USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
    bitcoind 92306 sjors   10u  IPv6 0x41f09f8c114a7707      0t0  TCP [::1]:18443 (LISTEN)
    bitcoind 92306 sjors   11u  IPv4 0x9b8ff0d88c43ff6f      0t0  TCP 127.0.0.1:18443 (LISTEN)
    bitcoind 92306 sjors   19u  IPv4 0xd8de1cae67469884      0t0  TCP *:18444 (LISTEN)
    bitcoind 92306 sjors   21u  IPv4 0x33676f2f9e2adc58      0t0  TCP 127.0.0.1:18445 (LISTEN)
    bitcoind 92306 sjors   22u  IPv6 0x776a345d97d44893      0t0  TCP *:18444 (LISTEN)
    

    And with -Ftv:

    p92306
    f10
    tIPv6
    n[::1]:18443
    f11
    tIPv4
    n127.0.0.1:18443
    f19
    tIPv4
    n*:18444
    f21
    tIPv4
    n127.0.0.1:18445
    f22
    tIPv6
    n*:18444
    

    I didn't study the regex in much detail, I guess it works...


    l0rinc commented at 7:47 AM on January 12, 2026:

    Please see the collapsed sections in the PR description for more details. Let me know if you think more explanation is needed anywhere.


    willcl-ark commented at 9:48 AM on February 5, 2026:

    Instead of the double ternary inside a list comp here, this could perhaps be written in slightly more human-readable python, as:

    results = []
    for sock_type, host, port in re.findall(r"t(IPv[46])\nn(\*|\[.+?]|[^:]+):(\d+)", output):
        if host == "*":
            addr = "::" if sock_type == "IPv6" else "0.0.0.0"
        else:
            addr = host.strip("[]")
        results.append((addr_to_hex(addr), int(port)))
    return results
    

    I also did not study this regex precisely, but have observed that it works on my macbook.


    l0rinc commented at 10:15 AM on February 5, 2026:

    If I need to touch it again, I'll consider avoiding the nesting, thanks for the hint. Was going for compactness here, but maybe I overshot.


    l0rinc commented at 11:48 AM on February 26, 2026:

    I haven't applied this in the end, but if you feel strongly about it, let me know


    l0rinc commented at 3:25 PM on March 12, 2026:

    I kept the nesting for now, let me know if you feel strongly about it

  6. Sjors commented at 4:24 AM on January 12, 2026: member

    Concept ACK, still need to look at the third and fourth commit.

    Tested that the two functional tests now run on macOS. I also introduced some regressions to see if they're caught.

    Nit: commit messages contain literal \n instead of newline.

  7. l0rinc force-pushed on Jan 12, 2026
  8. bensig commented at 9:54 PM on January 12, 2026: contributor

    ACK a843fde881820a70b72f0be0ff6e9602bb7ae1dc

  9. DrahtBot requested review from Sjors on Jan 12, 2026
  10. in test/functional/test_framework/netutil.py:111 in 2296a19c96
     105 | @@ -106,33 +106,44 @@ def get_bind_addrs(pid):
     106 |      else:
     107 |          raise NotImplementedError(f"get_bind_addrs is not supported on {sys.platform}")
     108 |  
     109 | -# from: https://code.activestate.com/recipes/439093/
     110 |  def all_interfaces():
     111 |      '''
     112 |      Return all interfaces that are up
    


    Sjors commented at 8:58 AM on January 13, 2026:

    Let's clarify that this only returns IPv4 interfaces (only obvious to people who recognise AF_INET in the Linux code).


  11. in test/functional/test_framework/netutil.py:143 in 2296a19c96 outdated
     161 | +        import subprocess
     162 | +        output = subprocess.check_output(["ifconfig", "-au"], text=True)
     163 | +        return [
     164 | +            (m["iface"].encode(), ip)
     165 | +            for m in re.finditer(r"(?m)^(?P<iface>\S+):(?P<block>[^\n]*(?:\n[ \t]+[^\n]*)*)", output)
     166 | +            for ip in re.findall(r"inet (\S+)", m["block"])
    


    Sjors commented at 3:16 PM on January 13, 2026:

    In 2296a19c96294eca9f376249a1745cd077fb1f6a test: support all_interfaces on macOS: I don't mind keeping the regex approach, but something like this is easier to read:

    # Get list of "up" interfaces
    ifaces = subprocess.check_output(["ifconfig", "-lu"], text=True).split()
    return [
        (iface.encode(), res.strip()) for iface in ifaces
        if (res := subprocess.run(["ipconfig", "getifaddr", iface], text=True).stdout) is not None
    ]
    

    Sjors commented at 8:05 PM on January 13, 2026:

    Actually the above short version doesn't work, it just returns an empty result, which the test seems to be happy with.

    Back to the more verbose version:

    results = []
    for iface in ifaces:
        try:
            res = subprocess.check_output(
                ["ipconfig", "getifaddr", iface],
                text=True
            ).strip()
        except subprocess.CalledProcessError:
            continue
        if res:
            results.append((iface.encode(), res))
    
    return results
    

    However it's still not identical to your version, because it doesn't the return the loopback IP. That's because ipconfig getifaddr lo0 returns nothing. Might be a matter of tweaking the getifaddr command, so the man page doesn't suggest any obvious way.


    l0rinc commented at 12:24 PM on January 16, 2026:
  12. DrahtBot requested review from Sjors on Jan 13, 2026
  13. l0rinc force-pushed on Jan 16, 2026
  14. ismaelsadeeq commented at 1:53 PM on February 4, 2026: member

    Tested ACK 67002b0b2cb4e2520c8f48cd048e3522d9c76c32 on macOS M2 Pro Tahoe 26.1.

    Confirmed that the previously skipped networking tests now run successfully:

    • feature_bind_extra.py
    • rpc_bind.py --ipv4
    • rpc_bind.py --ipv6
    • rpc_bind.py --nonloopback

    Before: 21 skipped tests (including the 4 above) After: 16 skipped tests (only USDT/tracing + compatibility tests remain skipped)

    Only did testing in this round.

  15. willcl-ark approved
  16. willcl-ark commented at 9:54 AM on February 5, 2026: member

    tACK 13a791c85bbcbaa0c2bf7312a7b295dfd0a9f927

    This looks ok to me. Left a comment on breaking up the large list comp with two ternaries, in case you touch again.

    I was trying to think of cleaner approaches, but couldn't think of any outside of adding psutil as a test dependency, which I think we can avoid while we only need this single function.

    All tests pass on an M3 Pro running MacOS Tahoe 26.2 with lsof 4.91

  17. DrahtBot requested review from willcl-ark on Feb 5, 2026
  18. willcl-ark commented at 10:09 AM on February 5, 2026: member

    I have noticed that above I ACKed the commits I had rebased onto master.

    ACK 67002b0b2cb4e2520c8f48cd048e3522d9c76c32

    (after checking git range-diff 13a791c85bbcbaa0c2bf7312a7b295dfd0a9f927...67002b0b2cb4e2520c8f48cd048e3522d9c76c32)

  19. in test/functional/test_framework/test_framework.py:910 in 67002b0b2c
     906 | @@ -907,6 +907,11 @@ def skip_if_platform_not_linux(self):
     907 |          if platform.system() != "Linux":
     908 |              raise SkipTest("not on a Linux system")
     909 |  
     910 | +    def skip_if_platform_not_linux_or_mac(self):
    


    fanquake commented at 10:16 AM on February 5, 2026:

    Why can't this use skip_if_platform_not_posix?


    l0rinc commented at 12:42 PM on February 5, 2026:

    Valid question, I remember the CI giving me some problems, let me recheck https://github.com/l0rinc/bitcoin/pull/115


    fanquake commented at 1:51 PM on February 5, 2026:

    l0rinc commented at 2:45 PM on February 5, 2026:

    It does work for Mac, I already tried that locally, I remembered that it enabled something that I didn't want on CI - let's see if the PR passes and I'll adjust it here if it does, thanks for the hint.


    l0rinc commented at 11:49 PM on February 5, 2026:

    Changed it to use the posix helper, not sure what went wrong originally, but seems to be working now, thanks: https://github.com/bitcoin/bitcoin/compare/67002b0b2cb4e2520c8f48cd048e3522d9c76c32..10e1e38c75312c5d5219c1afd1232df252aaaa9a

  20. l0rinc force-pushed on Feb 5, 2026
  21. l0rinc requested review from ismaelsadeeq on Feb 6, 2026
  22. l0rinc requested review from fanquake on Feb 6, 2026
  23. ismaelsadeeq commented at 5:14 PM on February 9, 2026: member

    re-tACK 10e1e38c75312c5d5219c1afd1232df252aaaa9a

    Previously skipped tests are now running as reported in my comment here #34256#pullrequestreview-3751197721 after the recent push

  24. Sjors commented at 1:14 PM on February 23, 2026: member

    ACK 10e1e38c75312c5d5219c1afd1232df252aaaa9a

  25. l0rinc commented at 1:38 PM on February 27, 2026: contributor

    rfm?

  26. fanquake commented at 10:28 AM on March 6, 2026: member

    @willcl-ark want to take another look here?

  27. in test/functional/rpc_bind.py:20 in 9b6aa99710 outdated
      16 | @@ -17,8 +17,7 @@ def set_test_params(self):
      17 |          self.supports_cli = False
      18 |  
      19 |      def skip_test_if_missing_module(self):
      20 | -        # due to OS-specific network stats queries, this test works only on Linux
      21 | -        self.skip_if_platform_not_linux()
      22 | +        self.skip_if_platform_not_posix()
    


    willcl-ark commented at 1:28 PM on March 11, 2026:

    nit: the commit message still states

    allow rpc_bind to run there as well by switching to skip_if_platform_not_linux_or_mac().


    l0rinc commented at 2:34 PM on March 11, 2026:

    Indeed, will @ismaelsadeeq and @Sjors reack if I adjust it?

  28. in test/functional/test_framework/netutil.py:146 in 10e1e38c75 outdated
     172 | +            (m["iface"].encode(), ip)
     173 | +            for m in re.finditer(r"(?m)^(?P<iface>\S+):(?P<block>[^\n]*(?:\n[ \t]+[^\n]*)*)", output)
     174 | +            for ip in re.findall(r"inet (\S+)", m["block"])
     175 | +        ]
     176 | +    else:
     177 | +        raise NotImplementedError(f"all_interfaces is not supported on {sys.platform}")
    


    willcl-ark commented at 1:56 PM on March 11, 2026:

    How will this work on *BSD? We dropped the gate from if: linux to if: posix, but then only handle linux and darwin here. So will BSD now run these tests and always hit this exception?


    l0rinc commented at 2:33 PM on March 11, 2026:

    Not sure, but this only affects the tests - is anyone running them on BSD? If it ever becomes a priority, we can adjust the tests.


    willcl-ark commented at 2:35 PM on March 11, 2026:

    They are being run in a few places I know of, e.g. here https://github.com/hebasto/bitcoin-core-nightly/actions/runs/22936550559/workflow#L97


    maflcko commented at 2:48 PM on March 11, 2026:

    Yeah, also the docs recommend to run the tests:

    To run the test suite (recommended), you will need to have Python 3 installed:

    See doc/build-freebsd.md


    l0rinc commented at 3:10 PM on March 11, 2026:

    So that was the reason for #34256 (review). Did we remove a BSD CI task since then? I remember a similar failure, but it seems I forgot the reason. I'll revert that change, thanks.


    fanquake commented at 3:12 PM on March 11, 2026:

    Did we remove a BSD CI task since then?

    No.


    fanquake commented at 3:13 PM on March 11, 2026:

    So will BSD now run these tests and always hit this exception?

    Why can't this just skip, for an unimplemented platform? Rather than re-adding more redundant code?


    l0rinc commented at 3:21 PM on March 11, 2026:

    Yes, that's what the origin skip_if_platform_not_linux_or_mac did - I just completely forgot about BSD


    fanquake commented at 3:22 PM on March 11, 2026:

    I mean skip in the test, not introduce a new function, to differentiate a subset of posix (for a single functional test).


    l0rinc commented at 12:48 PM on March 13, 2026:

    Thanks for the feedback. The PR is simpler now, and the tests are passing on the 3 BSD versions as well, without any significant change to the original implementation.

  29. in test/functional/test_framework/netutil.py:143 in 10e1e38c75 outdated
     169 | +        import subprocess
     170 | +        output = subprocess.check_output(["ifconfig", "-au"], text=True)
     171 | +        return [
     172 | +            (m["iface"].encode(), ip)
     173 | +            for m in re.finditer(r"(?m)^(?P<iface>\S+):(?P<block>[^\n]*(?:\n[ \t]+[^\n]*)*)", output)
     174 | +            for ip in re.findall(r"inet (\S+)", m["block"])
    


    willcl-ark commented at 1:57 PM on March 11, 2026:

    Agree it's a (slight) shame about all the regex needed, but don't know a better way to do it either without a large rewrite to ctypes or pulling in psutil (as Sjors already noted).

  30. DrahtBot requested review from willcl-ark on Mar 11, 2026
  31. l0rinc force-pushed on Mar 12, 2026
  32. l0rinc commented at 3:19 PM on March 12, 2026: contributor
  33. l0rinc referenced this in commit 8132885af0 on Mar 12, 2026
  34. l0rinc renamed this:
    test: support `get_bind_addrs` and `feature_bind_extra` on macOS
    test: support `get_bind_addrs` and `feature_bind_extra` on macOS & BSD
    on Mar 13, 2026
  35. ismaelsadeeq commented at 3:49 PM on March 30, 2026: member

    Partial (Because I did not test on BSD platform) re-Tested ACK b57910b21e on macOS M2 Pro (Tahoe 26.1).

    Confirmed that the networking-related tests enabled by this PR continue to run successfully on macOS:

    These tests feature_bind_extra.py rpc_bind.py --ipv4 rpc_bind.py --ipv6 rpc_bind.py --nonloopback

    were skipped on master f6e6fad0d9

    These tests execute and pass on this PR b57910b21e63237b69bb06e1e8e9cc9631814922

  36. l0rinc commented at 8:10 AM on April 6, 2026: contributor

    @willcl-ark, @Sjors, @fanquake, I have addressed the concerns, please let me know if anything else is needed.

    Edit:

    Because I did not test on BSD platform

    Rebased to enable the CI's new BSD job

  37. 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 <will8clark@gmail.com>
    Co-authored-by: fanquake <fanquake@gmail.com>
    7236a05503
  38. 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 <sjors@sprovoost.nl>
    1950da94fc
  39. l0rinc force-pushed on Apr 6, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-04-22 18:12 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me