http: support unix sockets #36434

pull pinheadmz wants to merge 10 commits into bitcoin:master from pinheadmz:http-unix-sockets changing 20 files +1548 −265
  1. pinheadmz commented at 6:10 PM on October 4, 2026: member

    Closes #5029

    Accept unix socket paths as options for -rpcbind= (server) and -rpcconnect= (bitcoin-cli). Options follow the same pattern as ipcbind:

    • =unix is a magic keyword that means <datadir>/http.sock
    • =unix:relative/path means <datadir>/relative/path
    • =unix:/absolute/path means /absolute/path

    We delete any existing http.sock before calling bind() and do not clean up at shutdown.

    Because access is handled by the filesystem, -rpcallowip is ignored for unix sockets and not required at all if we are only binding to unix sockets.

    Authentication by username/password/cookie is still required.

    A new option --httpunix is added to the test framework and added to test_runner for rpc_bind.py and interface_http.py for CI, but (like --usecli) most other tests should also pass with that option. When set, bitcoind is started with -rpcbind=unix...which overrides tcp://localhost entirely. AuthProxy has been extended to connect directly to the unix socket.

    Try it out!

    bitcoind -regtest -rpcbind=unix
    bitcoin-cli -regtest -rpcconnect=unix getbestblockhash
    

    Refactored code:

    Back in #27375 we added unix socket support but it was isolated to the Proxy class. That created a bit of tech debt which we pay off in this PR. Two new classes are implemented:

    • UnixSocketAddr reflects the CService API for generic unix sockets, without bitcoin-p2p-specific attributes
    • SocketAddr holds a variant of UnixSocketAddr and Cservice so classes like Proxy and HTTPServer can operate on an abstract level, with either socket type.
    • CNetAddr and CService are left untouched -- they are somewhat dirty combinations of generic socket classes and bitcoin p2p code, adding unix sockets to those classes wouldn't make sense for bitcoin p2p operations and that's why this design was chosen.

    Follow-up suggestions:

    • Deduplicate some of the unix socket path resolution code between IPC and HTTP
    • On macos the default unix socket buffer size is only ~8k. In the functional tests we have to bump the python client socket buffer up to ~256k or the "throttle" tests will stall on macos. We could apply this socket option on the server side as well, only on macos... requires some bike shedding.
    • This option replaces TCP but it does not replace HTTP, there may be a more efficient transport protocol.

    Production code and test framework changes designed and authored by my own cranial meatball. Unit tests added by Opus. C++ cleaned up by Fable, security tightened by Kimi-k3 🧠 🤖 🤖 🤖

  2. DrahtBot added the label RPC/REST/ZMQ on Oct 4, 2026
  3. DrahtBot commented at 6:10 PM on October 4, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Concept ACK b-l-u-e, vasild
    Approach ACK winterrdog

    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.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #36716 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36716.svg"></sub> (net: stop monitoring select exceptions outside Windows by Sjors)
    • #36204 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36204.svg"></sub> (http: disconnect clients that never finish a request by janb84)
    • #36187 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36187.svg"></sub> (http: Restore requirement of IPv4 bind success by hodlinator)
    • #35713 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35713.svg"></sub> (Remove boost as a unit test runner by rustaceanrob)
    • #35355 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35355.svg"></sub> (Use atomics for determining whether trace logging is enabled by ajtowns)
    • #31260 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/31260.svg"></sub> (scripted-diff: Type-safe settings retrieval by ryanofsky)
    • #17783 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/17783.svg"></sub> (common: Disallow calling IsArgSet() on ALLOW_LIST options by ryanofsky)
    • #17581 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/17581.svg"></sub> (refactor: Remove settings merge reverse precedence code by ryanofsky)
    • #17580 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/17580.svg"></sub> (refactor: Add ALLOW_LIST flags and enforce usage in CheckArgFlags by ryanofsky)
    • #17493 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/17493.svg"></sub> (util: Forbid ambiguous multiple assignments in config file by ryanofsky)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • src/httpserver.cpp: Lookup("::1", http_port, false)
    • src/httpserver.cpp: Lookup("127.0.0.1", http_port, false)
    • src/httpserver.cpp: Lookup(host, port, false)
    • src/test/httpserver_tests.cpp: CreateSock(0, 0, 0)
    • src/test/httpserver_tests.cpp: CreateSock(0, 0, 0)

    <sup>2026-11-13 16:55:27</sup>

  4. winterrdog commented at 8:57 PM on October 4, 2026: contributor

    concept ACK


    users of other projects that rely on Core have always wanted sth similar to this and implemented workarounds in the meantime:

    there was also an attempt to add this back in 2017:

    unfortunately, that attempt ran into a limitation in libevent's HTTP client. supporting Unix Domain sockets in bitcoin-cli required a libevent patch to pass an already-open Unix socket connection to the HTTP client. this was eventually addressed in https://github.com/libevent/libevent/pull/1722, but by then Core had decided to moved away from libevent. anyway, with libevent dropped, this is no longer an issue

  5. pinheadmz commented at 9:09 PM on October 4, 2026: member

    Don't forget https://github.com/libevent/libevent/issues/1615 ! Its been a long time coming... now that that pesky little refactor is out of the way we can finally get to it ;-)

  6. b-l-u-e commented at 11:10 AM on October 5, 2026: contributor

    concept ACK...i found an issue by running two nodes and noticed that second node can take over a live rpc unix socket path, if two nodes use different datadirs but the same -rpcbind=unix:<path> the second node starts with no error..since it removes the first node socket file and creates its own then after that bitcoin cli talks to the second node.. the first node keeps running but becomes unreachable so with TCP the second bind fails with address already in use

    i reproduced this on regtest

    B=build/bin
    SOCK=/tmp/btc-hijack.sock
    mkdir -p /tmp/nodeA /tmp/nodeB
    ARGS="-regtest -daemon -listen=0 -rpcbind=unix:$SOCK -rpcuser=u -rpcpassword=p"
    CLI="$B/bitcoin-cli -regtest -rpcconnect=unix:$SOCK -rpcuser=u -rpcpassword=p -rpcwait"
    

    1: for node A

    $B/bitcoind -datadir=/tmp/nodeA $ARGS
    $CLI getrpcinfo | grep logpath && stat -c 'inode=%i' $SOCK
    
    output:
    Bitcoin Core starting
    "logpath": "/tmp/nodeA/regtest/debug.log"
    inode=2229678
    

    2: for node B

    $B/bitcoind -datadir=/tmp/nodeB $ARGS; sleep 2
    $CLI getrpcinfo | grep logpath && stat -c 'inode=%i' $SOCK
    
    output:
    Bitcoin Core starting                                  (as you can see node B starts without error)
    "logpath": "/tmp/nodeB/regtest/debug.log"              (cli now reaches B)
    inode=2229647   (and this replaces node A socket file)
    

    3: ss -xlp | grep btc-hijack

    output:
    u_str LISTEN ... /tmp/btc-hijack.sock ... pid=1684462  (this is for B)
    u_str LISTEN ... /tmp/btc-hijack.sock ... pid=1684363  (here A still listening but unreachable)
    

    4: $CLI stop; sleep 2

    output:
    Bitcoin Core stopping                                  (`stop` reached B)
    

    5: pgrep -a bitcoind

    output:
    1684363 build/bin/bitcoind -datadir=/tmp/nodeA ...     (A still running)
    

    6: $CLI -rpcwait=0 getblockcount

    output:
    error: Error while attempting to communicate with server unix:/tmp/btc-hijack.sock (Could not connect to the server)
    
    Make sure the bitcoind server is running and that you are connecting to the correct unix socket path.
    Use "bitcoin-cli -help" for more info.
    
  7. b-l-u-e commented at 5:44 PM on October 5, 2026: contributor

    i noticed a change in bitcoin-cli the host header contains the resolved IP instead of the hostname given in -rpcconnect=localhost the build from master sends Host: localhost but on this PR sends Host: 127.0.0.1

    i reproduced the result by running this py file on terminal 1

    import socket
    
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind(("127.0.0.1", 18555))
    srv.listen()
    print("listening on 127.0.0.1:18555")
    while True:
        conn, _ = srv.accept()
        request = conn.recv(4096).decode(errors="replace")
        for line in request.split("\r\n"):
            if line.lower().startswith("host:"):
                print(line)
        conn.close()
    

    on terminal 2 i ran this command

    build-master/bin/bitcoin-cli -rpcconnect=localhost -rpcport=18555 -rpcuser=u -rpcpassword=p -rpcclienttimeout=3 getblockcount
    build-pr36434/bin/bitcoin-cli       -rpcconnect=localhost -rpcport=18555 -rpcuser=u -rpcpassword=p -rpcclienttimeout=3 getblockcount
    

    and the output was

    python3 /tmp/show_host.py 
    listening on 127.0.0.1:18555
    Host: localhost
    Host: 127.0.0.1
    
  8. DrahtBot added the label Needs rebase on Oct 6, 2026
  9. pinheadmz force-pushed on Oct 8, 2026
  10. pinheadmz commented at 2:59 PM on October 8, 2026: member

    push to 022a6aa69ed423fa66f5747b1f0d6413a01e1645:

    • rebase on master, fix conflicts with #36303 and #34729
    • fix "Host: header in bitcoin-cli (@b-l-u-e)

    Major change is copying the parameter format from ipcbind, so default is to create http.sock in the appropriate network data directory. This should also adderss @b-l-u-e concern about multiple processes messing with each others sockets. Or at least, makes it harder for a user to footgun. Will update the PR description as well.

  11. pinheadmz force-pushed on Oct 8, 2026
  12. pinheadmz commented at 3:13 PM on October 8, 2026: member

    push to 81b8369ac862a7e888606c2220df94d347a7ca59

  13. DrahtBot removed the label Needs rebase on Oct 8, 2026
  14. DrahtBot added the label CI failed on Oct 8, 2026
  15. DrahtBot commented at 4:34 PM on October 8, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task lint: https://github.com/bitcoin/bitcoin/actions/runs/37798972114/job/113385788562</sub> <sub>LLM reason (✨ experimental): CI failed because Ruff’s Python lint found an unnecessary f prefix in test/functional/interface_http.py:758.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  16. pinheadmz force-pushed on Oct 8, 2026
  17. pinheadmz commented at 4:39 PM on October 8, 2026: member

    push to c27a1fec1aa78cb721c31be5511bb1a542f09782

    • fix humiliating lint error 😭
  18. pinheadmz force-pushed on Oct 8, 2026
  19. pinheadmz commented at 7:08 PM on October 8, 2026: member

    push to db638e0f44f0aaffe798a4ec7d4757246de9fb56

    Fix a test in interface_http made flaky by unix sockets and failed last CI run:

    Unix sockets report server closure much faster than TCP which lost a race against the logic where we assert the connection is closed in python by trying to send more data

  20. DrahtBot removed the label CI failed on Oct 8, 2026
  21. DrahtBot added the label Needs rebase on Oct 10, 2026
  22. in doc/release-notes-36434.md:27 in db638e0f44 outdated
      22 | +  The socket file is not removed on shutdown.
      23 | +
      24 | +Tools and Utilities
      25 | +-------------------
      26 | +
      27 | +- `bitcoin-cli` can connect to a node over a unix domain socket with
    


    winterrdog commented at 12:09 PM on October 10, 2026:

    nit: sometimes, i see that you use "unix socket", while on other occasions it is "unix domain socket". is that intentional, or should we choose uniformity, pick one option, and then just run with it all throughout ?

    "unix socket" seems more common in the docs and in the code comments in this PR's diff, so probably the best candidate to go with.


    pinheadmz commented at 6:38 PM on October 28, 2026:

    fair point. since "unix domain sockets" is more correct, I'll change any user-facing docs to use that term. code comments and internal phrasing i think is ok to leave alone.

  23. in doc/release-notes-36434.md:5 in db638e0f44
       0 | @@ -0,0 +1,31 @@
       1 | +Updated settings
       2 | +----------------
       3 | +
       4 | +- `-rpcbind` now accepts a unix domain socket on platforms that support unix
       5 | +  sockets (not Windows), using the same syntax as `-ipcbind`: `-rpcbind=unix`
    


    winterrdog commented at 10:27 PM on October 10, 2026:

    (not Windows)

    TIL: windows has basic support for unix domain sockets (UDS).

    since Windows 10 version 1803 or later, windows has shipped UDS support but limited to SOCK_STREAM sockets only: https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/


    pinheadmz commented at 6:39 PM on October 28, 2026:

    yeah 🤷 we can add windows support for this feature in a follow up

  24. in src/netbase.h:98 in db638e0f44
      95 | +    explicit UnixSocketAddr(std::string path) : m_path(std::move(path)) {}
      96 | +
      97 | +    [[nodiscard]] bool IsValid() const { return IsUnixSocketPath(m_path); }
      98 | +    [[nodiscard]] sa_family_t GetSAFamily() const { return AF_UNIX; }
      99 | +    /** The full address string including the "unix:" prefix.
     100 | +     *  Unix sockets don't have a port but we like to match the CService API. */
    


    winterrdog commented at 11:30 PM on October 10, 2026:

    nit: could you have possibly meant this instead ?

         *  Unix sockets don't have a port but we'd like to match the CService API. */
    

    pinheadmz commented at 7:53 PM on October 28, 2026:

    cleaned up this comment.

  25. in src/netbase.cpp:736 in db638e0f44
     739 | +    } else {
     740 | +        path = std::string(sun->sun_path, len);
     741 | +        // The client path goes into logs and JSONRPCRequest::peerAddr; reject
     742 | +        // control characters a local client could place there to forge log
     743 | +        // lines (e.g. a Linux abstract-namespace name with a \0 prefix).
     744 | +        if (std::any_of(path.begin(), path.end(), [](char c) { return c < 0x20 || c == 0x7F; })) {
    


    winterrdog commented at 1:04 AM on October 11, 2026:

    char is signed on some platforms (e.g. x86), so bytes above 0x7f are interpreted as negative values (they wrap around) and also match c < 0x20. this means a utf-8 path like /tmp/josé.sock could end up being replaced with "unix" in the logs

    taking unsigned char (even uint8_t works OK) in the lambda fixes this (i also switched to std::ranges in case you find that more appealing)

            if (std::ranges::any_of(path, [](unsigned char c) { return c < 0x20 || c == 0x7F; })) {
    

    pinheadmz commented at 7:44 PM on October 28, 2026:

    good suggestion thanks, using std::ranges and unsigned char here

  26. in src/httpserver.cpp:823 in db638e0f44
     818 | +    // if something other than a socket already exists at the path.
     819 | +    if (const auto prepared{to.PreparePath()}; !prepared) {
     820 | +        return util::Unexpected{strprintf("Cannot prepare unix socket path for %s: %s",
     821 | +                                          to.ToStringAddrPort(),
     822 | +                                          prepared.error())};
     823 | +    }
    


    winterrdog commented at 9:32 PM on October 14, 2026:

    why don't we just put this behind an #ifdef HAVE_SOCKADDR_UN guard so we skip this code entirely on platforms that do not support unix sockets ?

    sure! we can never get here with a UnixSocketAddr on those platforms anyway, since those addresses get rejected much earlier. currently this would be a no-op at runtime, but we could avoid compiling code that can never be used on those platforms. seems like we can just sidestep it altogether :)


    pinheadmz commented at 7:38 PM on October 28, 2026:

    yep fair enough, putting behind a compile guard

  27. in src/httpserver.cpp:250 in db638e0f44
     264 | -                return {}; // empty
     265 | +                return {}; // empty, refuse to start with a misconfigured bind
     266 | +            }
     267 | +            const std::optional<CService> addr{Lookup(strRPCBind, port, false)};
     268 | +            if (!addr) {
     269 | +                LogWarning("Could not bind RPC on address %s port %i: Address lookup failed.", strRPCBind, port);
    


    winterrdog commented at 6:30 PM on October 15, 2026:

    strRPCBind can show up in the logs if the optional port is included in the bind address (e.g. 127.0.0.1:8332), leading to logs like this one: Could not bind RPC on address 127.0.0.1:8332 port 8332: Address lookup failed.

                    LogWarning("Could not bind RPC on address %s port %i: Address lookup failed.", host, port);
    

    pinheadmz commented at 7:31 PM on October 28, 2026:

    fixed this along with the GetBindAddresses() refactor.

  28. in src/httpserver.cpp:762 in db638e0f44
     757 | @@ -715,7 +758,9 @@ util::Expected<void, std::string> HTTPServer::BindAndStartListening(const CServi
     758 |          return util::Unexpected{strprintf("Bind address family for %s not supported", to.ToStringAddrPort())};
     759 |      }
     760 |  
     761 | -    std::unique_ptr<Sock> sock{CreateSock(to.GetSAFamily(), SOCK_STREAM, IPPROTO_TCP)};
     762 | +    int protocol{IPPROTO_TCP};
     763 | +    if (to.IsUnix()) protocol = 0;
    


    winterrdog commented at 6:47 PM on October 15, 2026:

    optional style nit: we could make this a const initialised from a ternary expression:

        const int protocol{to.IsUnix() ? 0 : IPPROTO_TCP};
    

    pinheadmz commented at 7:32 PM on October 28, 2026:

    sure ternaries are fun! thanks.

  29. in src/bitcoin-cli.cpp:1230 in db638e0f44
    1253 | +    std::vector<SocketAddr> ret;
    1254 | +    ret.reserve(services.size());
    1255 | +    for (const auto& service : services) {
    1256 | +        ret.emplace_back(service);
    1257 | +    }
    1258 | +    return {host, ret};
    


    winterrdog commented at 6:53 PM on October 15, 2026:

    nit: i think we could std::move both the host string & collection of socket addresses into the return pair object (trivial copy avoidance)

        return {std::move(host), std::move(ret)};
    

    pinheadmz commented at 6:46 PM on October 28, 2026:

    sure, using move here for correctness

  30. in src/httpserver.cpp:237 in db638e0f44
     250 | +                const auto unix_addr{ResolveUnixSocketAddr(strRPCBind, gArgs.GetDataDirNet(), DEFAULT_HTTP_UNIX_SOCKET_NAME)};
     251 | +                if (!unix_addr) {
     252 | +                    LogError("Invalid unix socket path specified in -rpcbind: '%s' (%s)", strRPCBind, unix_addr.error());
     253 | +                    return {}; // empty, refuse to start with a misconfigured bind
     254 | +                }
     255 | +                endpoints.emplace_back(*unix_addr);
    


    winterrdog commented at 7:06 PM on October 15, 2026:

    when i went through GetBindAddresses() and UnixSocketAddr::PreparePath(), i noticed that duplicate unix endpoints can go unnoticed. if two -rpcbind values resolve to the same path (e.g. -rpcbind=unix and -rpcbind=unix:http.sock, or the same value in bitcoin.conf and perhaps on the command line), PreparePath() (later called in HTTPServer::BindAndStartListening) removes the first socket file before the second bind(). therefore, both binds succeed without sending a warning

    is this intentional, or just not something we need to worry about? in the case of TCP, the second bind just fails with EADDRINUSE and gets logged. would we expect the same here, or is silently rebinding just fine ?


    pinheadmz commented at 7:30 PM on October 28, 2026:

    Heh that's kind of a funny edge case. We don't complain if a user sets -rpcport=50000 twice for example. I think it's ok to leave it. I'm more worried about a user setting ipcbind and rpcbind to the same value, but that's a pretty silly mistake

  31. in src/httpserver.cpp:213 in db638e0f44 outdated
     209 | @@ -205,34 +210,72 @@ static void RejectRequest(std::unique_ptr<HTTPRequest> hreq)
     210 |      WriteNoStoreErrorReply(*hreq, HTTP_SERVICE_UNAVAILABLE);
     211 |  }
     212 |  
     213 | -static std::vector<std::pair<std::string, uint16_t>> GetBindAddresses()
     214 | +static std::vector<SocketAddr> GetBindAddresses()
    


    winterrdog commented at 9:02 AM on October 16, 2026:

    while reviewing GetBindAddresses(), i ended up rewriting it to lower the cognitive load a bit and help me get my head around the flow. thought i could share it and see what you think:

    <details><summary>diff</summary>

    diff --git a/src/httpserver.cpp b/src/httpserver.cpp
    index 4b17dc5d89..4d97422f4f 100644
    --- a/src/httpserver.cpp
    +++ b/src/httpserver.cpp
    @@ -212,51 +212,57 @@ static void RejectRequest(std::unique_ptr<HTTPRequest> hreq)
     
     static std::vector<SocketAddr> GetBindAddresses()
     {
    -    uint16_t http_port{static_cast<uint16_t>(gArgs.GetIntArg("-rpcport", BaseParams().RPCPort()))};
    -    std::vector<SocketAddr> endpoints;
    +    const uint16_t http_port{static_cast<uint16_t>(gArgs.GetIntArg("-rpcport", BaseParams().RPCPort()))};
    +    const auto binds{gArgs.GetArgs("-rpcbind")};
    +    const bool has_allowip{!gArgs.GetArgs("-rpcallowip").empty()};
    +    const auto localhost_endpoints{[http_port]() -> std::vector<SocketAddr> {
    +        return {SocketAddr{Lookup("::1", http_port, false).value()},
    +                SocketAddr{Lookup("127.0.0.1", http_port, false).value()}};
    +    }};
    +
    +    // default to loopback when no rpc binds are provided
    +    if (binds.empty()) {
    +        if (has_allowip) {
    +            LogWarning("Option -rpcallowip was specified without -rpcbind; this doesn't usually make sense");
    +        }
    +        return localhost_endpoints();
    +    }
     
    -    // Cleared as soon as any -rpcbind value is an IP address, whether or not it
    -    // resolves, so that a lookup failure can never make a TCP bind request
    -    // look like a unix-socket-only configuration.
    -    bool only_unix_sockets{true};
    -    // Set to true if no addresses were specified, or
    -    // if we need to override a user misconfiguration.
    -    bool force_localhost{false};
    +    std::vector<SocketAddr> endpoints;
    +    // Set before parsing or looking up, so that a failure can never make
    +    // a TCP bind request look like a unix-socket-only configuration.
    +    bool has_ip{false};
     
         // Determine what addresses to bind to.
    -    if (!gArgs.GetArgs("-rpcbind").empty()) {
    +    for (const std::string& bind : binds) {
             // Specific bind addresses.
    -        for (const std::string& strRPCBind : gArgs.GetArgs("-rpcbind")) {
    -            if (IsUnixSocketValue(strRPCBind, /*allow_default=*/true)) {
    -                // UNIX socket is a filesystem path, resolved like -ipcbind
    -                const auto unix_addr{ResolveUnixSocketAddr(strRPCBind, gArgs.GetDataDirNet(), DEFAULT_HTTP_UNIX_SOCKET_NAME)};
    -                if (!unix_addr) {
    -                    LogError("Invalid unix socket path specified in -rpcbind: '%s' (%s)", strRPCBind, unix_addr.error());
    -                    return {}; // empty, refuse to start with a misconfigured bind
    -                }
    -                endpoints.emplace_back(*unix_addr);
    -                continue;
    -            }
    -            // IPv4 or IPv6 address
    -            only_unix_sockets = false;
    -            uint16_t port{http_port};
    -            std::string host;
    -            if (!SplitHostPort(strRPCBind, port, host)) {
    -                LogError("%s\n", InvalidPortErrMsg("-rpcbind", strRPCBind).original);
    +        if (IsUnixSocketValue(bind, /*allow_default=*/true)) {
    +            // UNIX socket is a filesystem path, resolved like -ipcbind
    +            const auto unix_addr{ResolveUnixSocketAddr(bind, gArgs.GetDataDirNet(), DEFAULT_HTTP_UNIX_SOCKET_NAME)};
    +            if (!unix_addr) {
    +                LogError("Invalid unix socket path specified in -rpcbind: '%s' (%s)", bind, unix_addr.error());
                     return {}; // empty, refuse to start with a misconfigured bind
                 }
    -            const std::optional<CService> addr{Lookup(strRPCBind, port, false)};
    -            if (!addr) {
    -                LogWarning("Could not bind RPC on address %s port %i: Address lookup failed.", strRPCBind, port);
    -                continue;
    -            }
    -            if (addr->IsBindAny()) {
    -                LogWarning("The RPC server is not safe to expose to untrusted networks such as the public internet");
    -            }
    -            endpoints.emplace_back(addr.value());
    +            endpoints.emplace_back(*unix_addr);
    +            continue;
             }
    -    } else {
    -        force_localhost = true;
    +        // IPv4 or IPv6 address
    +        has_ip = true;
    +        uint16_t port{http_port};
    +        std::string host;
    +        if (!SplitHostPort(bind, port, host)) {
    +            LogError("%s\n", InvalidPortErrMsg("-rpcbind", bind).original);
    +            return {}; // empty, refuse to start with a misconfigured bind
    +        }
    +        const std::optional<CService> addr{Lookup(bind, port, false)};
    +        if (!addr) {
    +            LogWarning("Could not bind RPC on address %s port %i: Address lookup failed.", host, port);
    +            continue;
    +        }
    +        if (addr->IsBindAny()) {
    +            LogWarning("The RPC server is not safe to expose to untrusted networks such as the public internet");
    +        }
    +        endpoints.emplace_back(addr.value());
         }
     
         // Access to unix sockets is controlled by filesystem permissions, so if we
    @@ -264,19 +270,9 @@ static std::vector<SocketAddr> GetBindAddresses()
         // Otherwise, to prevent misconfiguration and accidental exposure of the HTTP
         // interface, require -rpcallowip whenever an IP address is bound.
         // If the user is misconfigured we restrict HTTP to localhost and log warnings.
    -    if (!only_unix_sockets && gArgs.GetArgs("-rpcallowip").empty()) {
    +    if (has_ip && !has_allowip) {
             LogWarning("Option -rpcbind was ignored because -rpcallowip was not specified, refusing to allow everyone to connect");
    -        force_localhost = true;
    -    }
    -
    -    // Default to loopback, or replace user misconfiguration.
    -    if (force_localhost) {
    -        endpoints.clear();
    -        endpoints.emplace_back(Lookup("::1", http_port, false).value());
    -        endpoints.emplace_back(Lookup("127.0.0.1", http_port, false).value());
    -        if (!gArgs.GetArgs("-rpcallowip").empty()) {
    -            LogWarning("Option -rpcallowip was specified without -rpcbind; this doesn't usually make sense");
    -        }
    +        return localhost_endpoints();
         }
         return endpoints;
     }
    

    </details>


    this is purely a suggestion, so feel free to take or leave it :)


    pinheadmz commented at 7:25 PM on October 28, 2026:

    I think it's a good idea to clean this up now, thanks. Taking the suggestion and tweaking a bit more on top (comments and reducing unreachable code). Adding you as coauthor to that commit.

  32. winterrdog commented at 1:22 PM on October 16, 2026: contributor

    approach ACK


    i am convinced by the implementation here. particularly, i like the choice of std::variant over polymorphism for the SocketAddr class

    after staring at the implementation for a while now, i think the changes are mostly in very good shape, with the introduction of SocketAddr being the cornerstone change that everything else seems to spring from. most of my reviews have therefore been nitpicks, since i am convinced by the overall design choices. the security is tight too 🔒

    i have mostly been looking at the implementation C++ code and have not yet had an extensive look at the tests or their infrastructure code, beyond a quick perusal

    one last question: i saw UnixSocketAddr::SetSockAddr() handles empty and abstract namespace addresses, but we do not support binding or connecting to abstract sockets. do you think it is worth supporting them in the future for linux-specific setups ?


    looks like the PR description is out of sync with the code

    • =unix:relative/path means <datadir>/relative/path/http.sock

    • =unix:/absolute/path means /absolute/path/http.sock

    it says =unix:relative/path resolves to <datadir>/relative/path/http.sock and =unix:/absolute/path to /absolute/path/http.sock, but ResolveUnixSocketAddr() uses the given path as the socket file itself. so unix:rpc.sock turns into <datadir>/rpc.sock, and unix:/tmp/rpc.sock stays /tmp/rpc.sock. only the bare unix (or unix:) keyword uses the default http.sock name

    the docs, release notes and -help text already match the code, so i think the PR description just needs updating to reflect this, so other reviewers do not get tripped up by the mismatch


    i also think this PR would be a good candidate for v33. there has been interest in this for a long time, and i think it is worth tracking as an upcoming feature for the release

  33. netbase: introduce UnixSocketAddr class
    Include unit tests to ensure the API matches CService
    530b7a5a3a
  34. netbase: introduce SocketAddr class 07f4da5248
  35. pinheadmz commented at 7:55 PM on October 28, 2026: member

    push to e34ffb6de9e3fcd635f1e88c29c616a3ef21a2c1

    • Rebase on master, fix conflicts from #36509 and #36159

    • Incorporate lots of great feedback from @winterrdog mostly code clarity and a good catch about signed char's in socket path names

  36. pinheadmz force-pushed on Oct 28, 2026
  37. DrahtBot added the label CI failed on Oct 28, 2026
  38. DrahtBot commented at 9:01 PM on October 28, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task test ancestor commits: https://github.com/bitcoin/bitcoin/actions/runs/40758721978/job/123064520386</sub> <sub>LLM reason (✨ experimental): CI failed to compile test_bitcoin: netbase_tests.cpp calls GetFamily() on Proxy, but that method does not exist.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  39. DrahtBot removed the label Needs rebase on Oct 28, 2026
  40. test: cover existing Proxy API ccfeff2195
  41. netbase: refactor Proxy to use SocketAddr 61c24c437d
  42. http: use SocketAddr instead of CService f0f54a97c1
  43. http: support listening on UNIX domain sockets 517363b2bc
  44. http: allow empty -rpcallowip if all bind addresses are unix sockets
    Co-authored-by: winterrdog <winterrdog@protonmail.ch>
    6ad23a85de
  45. pinheadmz force-pushed on Oct 28, 2026
  46. pinheadmz commented at 11:33 PM on October 28, 2026: member

    push to ccbace1c33

    FIx a test-after-remove rebase mistake that broke ancestor commit CI test

  47. DrahtBot removed the label CI failed on Oct 29, 2026
  48. in doc/JSON-RPC-interface.md:1 in ccbace1c33 outdated


    hodlinator commented at 12:16 PM on October 29, 2026:

    PR desc typo: isloated


    winterrdog commented at 11:02 PM on November 1, 2026:

    sth odd is happening here (platform quirks).

    on FreeBSD, when you consistently run ./build/test/functional/interface_http.py --httpunix, this test case hangs and later throws a TimeoutError error

    <details><summary>log</summary>

    ...
    2026-11-01T22:17:20.881588Z TestFramework (INFO): Checking excessive request size
    2026-11-01T22:17:20.883446Z TestFramework (INFO): Client received expected 400 response before connection was terminated
    2026-11-01T22:17:20.887360Z TestFramework (INFO): Client received expected 400 response before connection was terminated
    2026-11-01T22:18:21.442175Z TestFramework (ERROR): Unexpected exception:
    Traceback (most recent call last):
      File "/home/madman/bitcoin/test/functional/test_framework/test_framework.py", line 147, in main
        self.run_test()
      File "/home/madman/bitcoin/build/test/functional/interface_http.py", line 171, in run_test
        self.check_excessive_request_size()
      File "/home/madman/bitcoin/build/test/functional/interface_http.py", line 324, in check_excessive_request_size
        send_request.result(timeout=5)
      File "/usr/local/lib/python3.11/concurrent/futures/_base.py", line 458, in result
        raise TimeoutError()
    TimeoutError
    2026-11-01T22:18:21.499829Z TestFramework (INFO): Not stopping nodes as test failed. The dangling processes will be cleaned up later.
    2026-11-01T22:18:21.500000Z TestFramework (WARNING): Not cleaning up dir /tmp/bitcoin_func_test_sa92ij23
    2026-11-01T22:18:21.500097Z TestFramework (ERROR): Test failed. Test logging available at /tmp/bitcoin_func_test_sa92ij23/test_framework.log
    2026-11-01T22:18:21.500351Z TestFramework (ERROR): 
    2026-11-01T22:18:21.500534Z TestFramework (ERROR): Hint: Call /home/madman/bitcoin/test/functional/combine_logs.py '/tmp/bitcoin_func_test_sa92ij23' to consolidate all logs
    2026-11-01T22:18:21.500614Z TestFramework (ERROR): 
    2026-11-01T22:18:21.500685Z TestFramework (ERROR): If this failure happened unexpectedly or intermittently, please file a bug and provide a link or upload of the combined log.
    2026-11-01T22:18:21.500801Z TestFramework (ERROR): https://github.com/bitcoin/bitcoin/issues
    2026-11-01T22:18:21.500898Z TestFramework (ERROR): 
    [node 0] Cleaning up leftover process
    

    </details>

    the autopsy

    the test case assumes that when the server rejects a request and closes the connection, a client blocked in sendall() should wake up on an error. this happens on Linux and over TCP (on all supported platforms), but not on FreeBSD with unix sockets. so, a client already blocked in a long poll() is not woken up and it waits out its full socket timeout as a result

    i tried to reproduce this similar scenario without bitcoind i.e. with a plain Python server that never reads and closes after 1 second which caused the client's sendall() to time out after 10 seconds across 4 runs in a deterministic way.

    <details><summary>reproducer script</summary>

    import os, socket, threading, time, tempfile
    
    ##########
    # server
    ##########
    path = os.path.join(tempfile.mkdtemp(), "t.sock")
    srv = socket.socket(socket.AF_UNIX); srv.bind(path); srv.listen()
    
    def server():
        c, _ = srv.accept()
        time.sleep(1)                      # never read, like bitcoind after rejecting
        c.send(b"HTTP/1.1 413\r\n\r\n")
        c.close()                          # close with unread data pending
    
    threading.Thread(target=server, daemon=True).start()
    
    ##########
    # client
    ##########
    cli = socket.socket(socket.AF_UNIX); cli.settimeout(10)
    cli.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1 << 18)
    cli.connect(path)
    t = time.time()
    try:
        cli.sendall(b"0" * (32 << 20)); print("sendall finished")
    except TimeoutError as e:
        print(type(e).__name__, e, f"after {time.time()-t:.1f}s")
    except Exception as e:
        print("error not expected: ", e)
    
    

    </details>


    bitcoind is closing the connection correctly. the issue appears to be that FreeBSD does not wake an already-blocked client in this scenario

    edit: this behaviour does not manifest on netBSD


    winterrdog commented at 12:11 AM on November 2, 2026:

    the same reasoning as before in: https://github.com/bitcoin/bitcoin/pull/36434/changes#r4437730296, it looks like the same issue happens in check_chunked_transfer()

    <details><summary>log</summary>

    ...
    2026-11-02T00:03:15.552181Z TestFramework (INFO): Check HTTP request encoded with chunked transfer
    2026-11-02T00:03:15.721640Z TestFramework (INFO): Check excessive size HTTP request encoded with chunked transfer
    2026-11-02T00:04:15.818517Z TestFramework (ERROR): Unexpected exception:
    Traceback (most recent call last):
      File "/home/madman/bitcoin/test/functional/test_framework/test_framework.py", line 147, in main
        self.run_test()
      File "/home/madman/bitcoin/./build/test/functional/interface_http.py", line 174, in run_test
        self.check_chunked_transfer()
      File "/home/madman/bitcoin/./build/test/functional/interface_http.py", line 441, in check_chunked_transfer
        send_request.result(timeout=5)
      File "/usr/local/lib/python3.11/concurrent/futures/_base.py", line 458, in result
        raise TimeoutError()
    TimeoutError
    2026-11-02T00:04:15.883571Z TestFramework (INFO): Not stopping nodes as test failed. The dangling processes will be cleaned up later.
    2026-11-02T00:04:15.883707Z TestFramework (WARNING): Not cleaning up dir /tmp/bitcoin_func_test_yp2do0xq
    2026-11-02T00:04:15.883758Z TestFramework (ERROR): Test failed. Test logging available at /tmp/bitcoin_func_test_yp2do0xq/test_framework.log
    2026-11-02T00:04:15.883870Z TestFramework (ERROR): 
    2026-11-02T00:04:15.883965Z TestFramework (ERROR): Hint: Call /home/madman/bitcoin/test/functional/combine_logs.py '/tmp/bitcoin_func_test_yp2do0xq' to consolidate all logs
    2026-11-02T00:04:15.884009Z TestFramework (ERROR): 
    2026-11-02T00:04:15.884048Z TestFramework (ERROR): If this failure happened unexpectedly or intermittently, please file a bug and provide a link or upload of the combined log.
    2026-11-02T00:04:15.884080Z TestFramework (ERROR): https://github.com/bitcoin/bitcoin/issues
    2026-11-02T00:04:15.884137Z TestFramework (ERROR): 
    [node 0] Cleaning up leftover process
    

    </details>


    pinheadmz commented at 8:51 PM on November 12, 2026:

    I don't normally use any BSDs but I ran FreeBSD in qemu on my arm64 mac and could not reproduce this test failure. Using this image from https://download.freebsd.org/releases/VM-IMAGES/15.1-RELEASE/aarch64/Latest/FreeBSD-15.1-RELEASE-arm64-aarch64-ufs.qcow2.xz

    checked out this branch at ccbace1c33f30f9fb842451851263a7d83449323

    # cat /etc/os-release 
    NAME=FreeBSD
    VERSION="15.1-RELEASE"
    VERSION_ID="15.1"
    ID=freebsd
    ANSI_COLOR="0;31"
    PRETTY_NAME="FreeBSD 15.1-RELEASE"
    CPE_NAME="cpe:/o:freebsd:freebsd:15.1"
    HOME_URL="https://FreeBSD.org/"
    BUG_REPORT_URL="https://bugs.FreeBSD.org/"
    

    pinheadmz commented at 10:38 PM on November 12, 2026:

    I get the "expected" outcome in this freebsd vm as well: error not expected: [Errno 32] Broken pipe


    winterrdog commented at 10:50 AM on November 13, 2026:

    oh, that is surprising! because even after restarting and clearing the past build folder, it still times out and breaks at the 2 tests i mentioned. the reproducer python script still times out too with TimeoutError timed out after 10.0s :(


    yes, i tested at ccbace1c33f30f9fb842451851263a7d83449323 as well

    # cat /etc/os-release 
    NAME=FreeBSD
    VERSION="15.1-RELEASE"
    VERSION_ID="15.1"
    ID=freebsd
    ANSI_COLOR="0;31"
    PRETTY_NAME="FreeBSD 15.1-RELEASE"
    CPE_NAME="cpe:/o:freebsd:freebsd:15.1"
    HOME_URL="https://FreeBSD.org/"
    BUG_REPORT_URL="https://bugs.FreeBSD.org/"
    

    i am on FreeBSD 15.0 (running via VirtualBox on an x86_64 Linux host):

    $ cat /etc/os-release
    NAME=FreeBSD
    VERSION="15.0-RELEASE-p5"
    VERSION_ID="15.0"
    ID=freebsd
    ANSI_COLOR="0;31"
    PRETTY_NAME="FreeBSD 15.0-RELEASE-p5"
    CPE_NAME="cpe:/o:freebsd:freebsd:15.0"
    HOME_URL="https://FreeBSD.org/"
    BUG_REPORT_URL="https://bugs.FreeBSD.org/"
    
    $ uname -a
    FreeBSD freebsd 15.0-RELEASE-p5 FreeBSD 15.0-RELEASE-p5 releng/15.0-n281018-0730d5233286 GENERIC amd64
    

    anything else about my environment that i might need to provide so that we can come to consensus ?


    EDIT: let me push to 15.1 (due to 15.0's EoL) and then test again, will report back what i find :handshake:


    winterrdog commented at 10:01 PM on November 13, 2026:

    will report back what i find 🤝

    on FreeBSD 15.1-RELEASE, all tests pass just like in your case. also, the reproducer script does not time out and passes with error not expected: [Errno 32] Broken pipe

    <details><summary>log</summary>

    Temporary test directory at /tmp/test_runner_₿_🏃_20261113_145334
    WARNING! Test 'interface_http --http-unix' not found in full test list.
    1/8 - interface_bitcoin_cli.py passed, Duration: 9 s
    2/8 - interface_bitcoin_cli.py --httpunix passed, Duration: 9 s
    3/8 - rpc_bind.py --ipv4 skipped (lsof not available)
    4/8 - rpc_bind.py --ipv6 skipped (lsof not available)
    Remaining jobs: [interface_http.py, interface_http.py --httpunix, rpc_bind.py --httpunix, rpc_bind.py --nonloopback]
    5/8 - rpc_bind.py --httpunix skipped (lsof not available)
    Remaining jobs: [interface_http.py, interface_http.py --httpunix, rpc_bind.py --nonloopback]
    6/8 - rpc_bind.py --nonloopback skipped (lsof not available)
    Remaining jobs: [interface_http.py, interface_http.py --httpunix]
    7/8 - interface_http.py passed, Duration: 52 s                                       
    Remaining jobs: [interface_http.py --httpunix]
    8/8 - interface_http.py --httpunix passed, Duration: 57 s
    
    TEST                                | STATUS    | DURATION
    
    interface_bitcoin_cli.py            | ✓ Passed  | 9 s
    interface_bitcoin_cli.py --httpunix | ✓ Passed  | 9 s
    interface_http.py                   | ✓ Passed  | 52 s
    interface_http.py --httpunix        | ✓ Passed  | 57 s
    rpc_bind.py --httpunix              | ○ Skipped | 0 s
    rpc_bind.py --ipv4                  | ○ Skipped | 0 s
    rpc_bind.py --ipv6                  | ○ Skipped | 0 s
    rpc_bind.py --nonloopback           | ○ Skipped | 0 s
    
    ALL                                 | ✓ Passed  | 127 s (accumulated) 
    Runtime: 57 s
    

    </details>


    the issue ?

    it turns out this was a FreeBSD 15.0 upstream kernel bug, not a problem with the PR

    the issue was that when the server closed a unix-domain socket, the kernel did not wake up a client that was waiting to send, so the client just sat there until its timeout. this is now fixed in 15.1, thus passing all the --httpunix tests on my end


    all good! this review comment, together with the next one below it, can be marked as resolved

  49. hodlinator commented at 12:29 PM on October 29, 2026: contributor

    What is the benefit of allowing HTTP over unix sockets compared to using IPC over unix sockets? executeRpc() is already exposed through IPC. I guess the REST interface is only exposed through HTTP, not IPC.

  50. pinheadmz commented at 1:26 PM on October 29, 2026: member

    What is the benefit of allowing HTTP over unix sockets compared to using IPC over unix sockets?

    You get the security advantage and possibly an efficiency advantage with a very low bar for entry. IPC requires capnproto, unix sockets only require platform support. IPC is only exposed in multiprocess mode.

    See also #32297 (comment)

  51. vasild commented at 3:56 PM on November 2, 2026: contributor

    Concept ACK

  52. in test/functional/test_framework/netutil.py:60 in ccbace1c33
      55 | +    def connect(self):
      56 | +        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
      57 | +        self.sock.settimeout(self.timeout)
      58 | +        # Increase the socket buffer to ~256 kB, more like a TCP socket.
      59 | +        # On macos the default unix socket buffer size is only ~8 kB.
      60 | +        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1 << 18)
    


    winterrdog commented at 8:49 PM on November 2, 2026:

    on netBSD, this fails with OSError: [Errno 55] No buffer space available

    the error is caused by setting SO_SNDBUF to 256 KiB (1 << 18), which netBSD rejects because its default socket buffer limit, kern.sbmax, is also 256 KiB. 128 KiB (1 << 17) works just fine, and so does 256 KiB provided kern.sbmax is raised above 256 KiB

    we could just lower SO_SNDBUF to 128 KiB so that interface_http.py --httpunix passes end to end on netBSD too. the only thing i am not so sure about is whether 128 KiB is enough for the macOS throttle stall that motivated the 256 KiB in the first place

    alternatively, we could keep 256 KiB and fall back to 128 KiB on OSError, but it would be preferable to just keep this simple if 128 KiB is enough

    what do you think ?


    [!NOTE] by default, unix domain sockets have 8 KiB send/receive buffers on netBSD too, same as macOS (checked on netBSD 11.0).


    pinheadmz commented at 4:55 PM on November 13, 2026:

    Really great catch here thank you, I'll take your suggestion to use 1 << 17 which I tested on both NetBSD and macos.

  53. Enable unix sockets in test_framework: cover interface_http & rpc_bind
    Most functional tests will pass with --httpunix, which uses unix
    sockets instead of TCP for all RPC commands in the test. For now,
    only require interface_http, rpc_bind --httpunix in CI to cover
    the specific changes from this branch.
    743f0c3843
  54. bitcoin-cli: support connecting on UNIX domain sockets c93bc85796
  55. doc: add unix socket options to help and docs c01d87c0b2
  56. pinheadmz force-pushed on Nov 13, 2026
  57. pinheadmz commented at 4:58 PM on November 13, 2026: member

    push to c01d87c0b2aecfc10b3b9230304d8dfe4d05b7ed

    • Get the functional test to pass on NetBsd by using a smaller socket buffer size (thanks to @winterrdog)

    That issue was also caught by running @hebasto nightly BSD tests on this branch:

    NetBSD (failed) FreeBSD (passed) OpenBSD (passed)

    I'll re-run the extra CI tests with this new fix in place

  58. pinheadmz commented at 6:38 PM on November 13, 2026: member

    hebasto-nightly CI passed on all 3 BSDs after lowering the buffer size in the test:


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-11-15 02:51 UTC

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