http: Introduce HTTPServer class and implement binding to listening socket
What changed, and why it matters
This commit adds a new internal HTTPServer class to Bitcoin Core that handles low-level network socket binding and listening for the HTTP RPC server. It is a code refactoring/introduction change with no obvious security bug. The new code is covered by unit tests that use mock sockets, so it does not open real network ports during testing.
No security action required. Review as normal code-quality change.
Security signals we found
No security-relevant signal: this is a structural/refactoring commit introducing a new server class.
Code is unit-tested with mocked sockets, reducing test-side attack surface.
No input parsing, authentication, authorization, cryptography, or consensus logic is modified.
Evidence from the diff
The patch introduces HTTPServer::BindAndStartListening(), adapted from CConnMan::BindListenPort(), and HTTPServer::StopListening(). It uses the existing Sock abstraction, sets SO_REUSEADDR and IPV6_V6ONLY, binds, and listens. A new SocketTestingSetup test fixture mocks CreateSock() via DynSock so tests do not perform real I/O. No vulnerability, bugfix, or security-hardening change is evident in the diff.
Changed components
src/httpserver.cppsrc/httpserver.hsrc/test/httpserver_tests.cppsrc/test/util/setup_common.cppsrc/test/util/setup_common.hInspect captured patch +178 / −1
diff --git a/src/httpserver.cpp b/src/httpserver.cpp
index 0b3d5d06..310907ba 100644
--- a/src/httpserver.cpp
+++ b/src/httpserver.cpp
@@ -2,6 +2,8 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+#include <bitcoin-build-config.h> // IWYU pragma: keep
+
#include <httpserver.h>
#include <chainparamsbase.h>
@@ -15,6 +17,7 @@
#include <sync.h>
#include <util/check.h>
#include <util/signalinterrupt.h>
+#include <util/sock.h>
#include <util/strencodings.h>
#include <util/threadnames.h>
#include <util/threadpool.h>
@@ -46,6 +49,9 @@
#include <support/events.h>
+//! Explicit alias for setting socket option methods.
+static constexpr int SOCKET_OPTION_TRUE{1};
+
using common::InvalidPortErrMsg;
using http_libevent::HTTPRequest;
@@ -889,4 +895,85 @@ bool HTTPRequest::LoadBody(LineReader& reader)
return true;
}
+
+util::Expected<void, std::string> HTTPServer::BindAndStartListening(const CService& to)
+{
+ // Create socket for listening for incoming connections
+ sockaddr_storage storage;
+ auto sa = reinterpret_cast<sockaddr*>(&storage);
+ socklen_t len{sizeof(storage)};
+ if (!to.GetSockAddr(sa, &len)) {
+ return util::Unexpected{strprintf("Bind address family for %s not supported", to.ToStringAddrPort())};
+ }
+
+ std::unique_ptr<Sock> sock{CreateSock(to.GetSAFamily(), SOCK_STREAM, IPPROTO_TCP)};
+ if (!sock) {
+ return util::Unexpected{strprintf("Cannot create %s listen socket: %s",
+ to.ToStringAddrPort(),
+ NetworkErrorString(WSAGetLastError()))};
+ }
+
+ // 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) {
+ LogDebug(BCLog::HTTP,
+ "Cannot set SO_REUSEADDR on %s listen socket: %s, continuing anyway",
+ to.ToStringAddrPort(),
+ NetworkErrorString(WSAGetLastError()));
+ }
+
+ // 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.
+ if (to.IsIPv6()) {
+#ifdef IPV6_V6ONLY
+ if (sock->SetSockOpt(IPPROTO_IPV6, IPV6_V6ONLY, &SOCKET_OPTION_TRUE, sizeof(SOCKET_OPTION_TRUE)) == SOCKET_ERROR) {
+ LogDebug(BCLog::HTTP,
+ "Cannot set IPV6_V6ONLY on %s listen socket: %s, continuing anyway",
+ to.ToStringAddrPort(),
+ NetworkErrorString(WSAGetLastError()));
+ }
+#endif
+#ifdef WIN32
+ int prot_level{PROTECTION_LEVEL_UNRESTRICTED};
+ if (sock->SetSockOpt(IPPROTO_IPV6,
+ IPV6_PROTECTION_LEVEL,
+ &prot_level,
+ sizeof(prot_level)) == SOCKET_ERROR) {
+ LogDebug(BCLog::HTTP,
+ "Cannot set IPV6_PROTECTION_LEVEL on %s listen socket: %s, continuing anyway",
+ to.ToStringAddrPort(),
+ NetworkErrorString(WSAGetLastError()));
+ }
+#endif
+ }
+
+ if (sock->Bind(sa, len) == SOCKET_ERROR) {
+ const int err{WSAGetLastError()};
+ if (err == WSAEADDRINUSE) {
+ return util::Unexpected{strprintf("Unable to bind to %s on this computer. %s is probably already running.",
+ to.ToStringAddrPort(),
+ CLIENT_NAME)};
+ } else {
+ return util::Unexpected{strprintf("Unable to bind to %s on this computer (bind returned error %s)",
+ to.ToStringAddrPort(),
+ NetworkErrorString(err))};
+ }
+ }
+
+ // Listen for incoming connections
+ if (sock->Listen(SOMAXCONN) == SOCKET_ERROR) {
+ return util::Unexpected{strprintf("Cannot listen on %s: %s",
+ to.ToStringAddrPort(),
+ NetworkErrorString(WSAGetLastError()))};
+ }
+
+ m_listen.emplace_back(std::move(sock));
+
+ return {};
+}
+
+void HTTPServer::StopListening()
+{
+ m_listen.clear();
+}
} // namespace http_bitcoin
diff --git a/src/httpserver.h b/src/httpserver.h
index 6bf18274..ba93cbb8 100644
--- a/src/httpserver.h
+++ b/src/httpserver.h
@@ -6,13 +6,18 @@
#define BITCOIN_HTTPSERVER_H
#include <functional>
+#include <memory>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
+#include <vector>
+#include <netaddress.h>
#include <rpc/protocol.h>
#include <util/byte_units.h>
+#include <util/expected.h>
+#include <util/sock.h>
#include <util/strencodings.h>
#include <util/string.h>
@@ -297,6 +302,33 @@ public:
bool LoadBody(LineReader& reader);
/// @}
};
+
+class HTTPServer
+{
+public:
+ /**
+ * Bind to a new address:port, start listening and add the listen socket to `m_listen`.
+ * @param[in] to Where to bind.
+ * @returns {} or the reason for failure.
+ */
+ util::Expected<void, std::string> BindAndStartListening(const CService& to);
+
+ /**
+ * Stop listening by closing all listening sockets.
+ */
+ void StopListening();
+
+ /**
+ * Get the number of sockets the server is bound to and listening on
+ */
+ size_t GetListeningSocketCount() const { return m_listen.size(); }
+
+private:
+ /**
+ * List of listening sockets.
+ */
+ std::vector<std::shared_ptr<Sock>> m_listen;
+};
} // namespace http_bitcoin
#endif // BITCOIN_HTTPSERVER_H
diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp
index e2b3f0c9..1114df05 100644
--- a/src/test/httpserver_tests.cpp
+++ b/src/test/httpserver_tests.cpp
@@ -13,11 +13,12 @@
using http_bitcoin::HTTPHeaders;
using http_bitcoin::HTTPRequest;
using http_bitcoin::HTTPResponse;
+using http_bitcoin::HTTPServer;
using http_bitcoin::MAX_BODY_SIZE;
using http_bitcoin::MAX_HEADERS_SIZE;
using util::LineReader;
-BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup)
+BOOST_FIXTURE_TEST_SUITE(httpserver_tests, SocketTestingSetup)
BOOST_AUTO_TEST_CASE(test_query_parameters)
{
@@ -372,4 +373,27 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
BOOST_CHECK(!req.LoadBody(reader));
}
}
+
+BOOST_AUTO_TEST_CASE(http_server_socket_tests)
+{
+ HTTPServer server;
+
+ {
+ // We can only bind to NET_IPV4 and NET_IPV6
+ CService onion_address{Lookup("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
+ auto result{server.BindAndStartListening(onion_address)};
+ BOOST_REQUIRE(!result);
+ BOOST_CHECK_EQUAL(result.error(), "Bind address family for aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion:0 not supported");
+ }
+
+ // This VALID address won't actually get used because we stubbed CreateSock()
+ CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()};
+
+ // Init state
+ BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 0);
+ // Bind to mock Listening Socket
+ BOOST_REQUIRE(server.BindAndStartListening(addr_bind));
+ // We are bound and listening
+ BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 1);
+}
BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp
index 8097494d..2ba63efd 100644
--- a/src/test/util/setup_common.cpp
+++ b/src/test/util/setup_common.cpp
@@ -639,6 +639,25 @@ std::vector<CTransactionRef> TestChain100Setup::PopulateMempool(FastRandomContex
return mempool_transactions;
}
+SocketTestingSetup::SocketTestingSetup()
+{
+ // "back up" the current CreateSock() so we can restore it after the test
+ m_create_sock_orig = CreateSock;
+
+ CreateSock = [this](int, int, int) {
+ // This is a mock Listening Socket that a server can "bind" to and
+ // listen to for incoming connections. We won't need to access its I/O
+ // pipes because we don't read or write directly to it. It will return
+ // Connected Sockets from the queue via its Accept() method.
+ return std::make_unique<DynSock>(std::make_shared<DynSock::Pipes>(), &m_accepted_sockets);
+ };
+};
+
+SocketTestingSetup::~SocketTestingSetup()
+{
+ CreateSock = m_create_sock_orig;
+}
+
/**
* @returns a real block (0000000000013b8ab2cd513b0261a14096412195a72a0c4827d229dcc7e0f7af)
* with 9 txs.
diff --git a/src/test/util/setup_common.h b/src/test/util/setup_common.h
index d03f5bae..7a8f6b70 100644
--- a/src/test/util/setup_common.h
+++ b/src/test/util/setup_common.h
@@ -13,6 +13,7 @@
#include <node/context.h> // IWYU pragma: export
#include <primitives/transaction.h>
#include <random.h>
+#include <test/util/net.h>
#include <test/util/random.h>
#include <test/util/time.h>
#include <util/chaintype.h> // IWYU pragma: export
@@ -249,6 +250,20 @@ std::unique_ptr<T> MakeNoLogFileContext(const ChainType chain_type = ChainType::
return std::make_unique<T>(chain_type, opts);
}
+class SocketTestingSetup : public BasicTestingSetup
+{
+public:
+ explicit SocketTestingSetup();
+ ~SocketTestingSetup();
+
+private:
+ //! Save the original value of CreateSock here and restore it when the test ends.
+ decltype(CreateSock) m_create_sock_orig;
+
+ //! Queue of connected sockets returned by listening socket (represents network interface)
+ DynSock::Queue m_accepted_sockets;
+};
+
CBlock getBlock13b8a();
#endif // BITCOIN_TEST_UTIL_SETUP_COMMON_H
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.