From af65069fd15bbfca5c93b6099a9cc1d5b8de30bc Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Fri, 4 Sep 2026 21:51:32 +0200 Subject: [PATCH 1/2] windows: Use SO_EXCLUSIVEADDRUSE over SO_REUSEADDR The latter allows other processes to bind to the same socket and intercept traffic on this platform. --- src/httpserver.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index 9bb89863afc..6e186d151d3 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -722,6 +722,17 @@ util::Expected HTTPServer::BindAndStartListening(const CServi NetworkErrorString(WSAGetLastError()))}; } +#ifdef WIN32 + // Prevent another application from binding to the same address and port and + // intercepting RPC credentials. + // SO_REUSEADDR on Windows is non-exclusive so another process could bind to + // the same port. + if (sock->SetSockOpt(SOL_SOCKET, SO_EXCLUSIVEADDRUSE, &SOCKET_OPTION_TRUE, sizeof(SOCKET_OPTION_TRUE)) == SOCKET_ERROR) { + return util::Unexpected{strprintf("Cannot set SO_EXCLUSIVEADDRUSE on %s listen socket: %s", + to.ToStringAddrPort(), + NetworkErrorString(WSAGetLastError()))}; + } +#else // Allow binding if the port is still in TIME_WAIT state after // the program was closed and restarted. if (sock->SetSockOpt(SOL_SOCKET, SO_REUSEADDR, &SOCKET_OPTION_TRUE, sizeof(SOCKET_OPTION_TRUE)) == SOCKET_ERROR) { @@ -730,6 +741,7 @@ util::Expected HTTPServer::BindAndStartListening(const CServi to.ToStringAddrPort(), NetworkErrorString(WSAGetLastError())); } +#endif // some systems don't have IPV6_V6ONLY but are always v6only; others do have the option // and enable it by default or not. Try to enable it, if possible. From bcb09b3f4aec73b5e17d1266ec21983d7add118c Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Fri, 4 Sep 2026 21:52:48 +0200 Subject: [PATCH 2/2] qa: Verify HTTP listen port exclusivity --- test/functional/interface_http.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/functional/interface_http.py b/test/functional/interface_http.py index 38a1f2b55dd..a63cc7aa24c 100755 --- a/test/functional/interface_http.py +++ b/test/functional/interface_http.py @@ -134,6 +134,7 @@ class HTTPBasicsTest (BitcoinTestFramework): self.node.reuse_http_connections = False self.check_default_connection() + self.check_socket_exclusivity() self.check_keepalive_connection() self.check_close_connection() self.check_excessive_request_size() @@ -173,6 +174,18 @@ class HTTPBasicsTest (BitcoinTestFramework): assert conn.sock_closed() + def check_socket_exclusivity(self): + self.log.info("Checking that another process cannot bind the HTTP listen port") + url = urllib.parse.urlparse(self.node.url) + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as competing_listener: + competing_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # Ill-configured sockets permit port reuse unless the original + # listener requested exclusive address use. + assert_raises( + OSError, + lambda: competing_listener.bind((url.hostname, url.port))) + + def check_keepalive_connection(self): self.log.info("Checking keep-alive connection persistence") conn = BitcoinHTTPConnection(self.node)