http: check rpcallowip immediately after accepting connection

Instead of sending 403 Forbidden, disconnect as soon as possible.

To facilitate unit testing, this commit includes a refactor
that moves the subnet allow list and relevant methods
into the HTTPServer class instead of file-scope static scope.
This commit is contained in:
Matthew Zipkin
2026-05-23 09:24:21 -04:00
parent 32941e1314
commit d1ed2a6e25
7 changed files with 75 additions and 39 deletions

View File

@@ -67,8 +67,6 @@ struct HTTPPathHandler
/** HTTP module state */
static std::unique_ptr<http_bitcoin::HTTPServer> g_http_server{nullptr};
//! List of subnets to allow RPC connections from
static std::vector<CSubNet> rpc_allow_subnets;
//! Handlers for (sub)paths
static GlobalMutex g_httppathhandlers_mutex;
static std::vector<HTTPPathHandler> pathHandlers GUARDED_BY(g_httppathhandlers_mutex);
@@ -77,23 +75,28 @@ static std::vector<HTTPPathHandler> pathHandlers GUARDED_BY(g_httppathhandlers_m
static ThreadPool g_threadpool_http("http");
static int g_max_queue_depth{100};
namespace http_bitcoin {
/** Check if a network address is allowed to access the HTTP server */
static bool ClientAllowed(const CNetAddr& netaddr)
bool HTTPServer::ClientAllowed(const CNetAddr& netaddr) const
{
if (!netaddr.IsValid())
return false;
for(const CSubNet& subnet : rpc_allow_subnets)
for(const CSubNet& subnet : m_allow_subnets)
if (subnet.Match(netaddr))
return true;
return false;
}
/** Initialize ACL list for HTTP server */
static bool InitHTTPAllowList()
bool HTTPServer::InitHTTPAllowList()
{
rpc_allow_subnets.clear();
rpc_allow_subnets.emplace_back(LookupHost("127.0.0.1", false).value(), 8); // always allow IPv4 local subnet
rpc_allow_subnets.emplace_back(LookupHost("::1", false).value()); // always allow IPv6 localhost
// Must be run before StartSocketThreads() because ThreadSocketHandler()
// will check m_allow_subnets from the I/O thread.
Assume(!m_thread_socket_handler.joinable());
m_allow_subnets.clear();
m_allow_subnets.emplace_back(LookupHost("127.0.0.1", false).value(), 8); // always allow IPv4 local subnet
m_allow_subnets.emplace_back(LookupHost("::1", false).value()); // always allow IPv6 localhost
for (const std::string& strAllow : gArgs.GetArgs("-rpcallowip")) {
const CSubNet subnet{LookupSubNet(strAllow)};
if (!subnet.IsValid()) {
@@ -102,14 +105,15 @@ static bool InitHTTPAllowList()
CClientUIInterface::MSG_ERROR);
return false;
}
rpc_allow_subnets.push_back(subnet);
m_allow_subnets.push_back(subnet);
}
std::string strAllowed;
for (const CSubNet& subnet : rpc_allow_subnets)
for (const CSubNet& subnet : m_allow_subnets)
strAllowed += subnet.ToString() + " ";
LogDebug(BCLog::HTTP, "Allowing HTTP connections from: %s\n", strAllowed);
return true;
}
} // namespace http_bitcoin
/** HTTP request method as string - use for logging only */
std::string_view RequestMethodString(HTTPRequestMethod m)
@@ -127,14 +131,6 @@ std::string_view RequestMethodString(HTTPRequestMethod m)
static void MaybeDispatchRequestToWorker(std::shared_ptr<HTTPRequest> hreq)
{
// Early address-based allow check
if (!ClientAllowed(hreq->GetPeer())) {
LogDebug(BCLog::HTTP, "HTTP request from %s rejected: Client network is not allowed RPC access\n",
hreq->GetPeer().ToStringAddrPort());
hreq->WriteReply(HTTP_FORBIDDEN);
return;
}
// Early reject unknown HTTP methods
if (hreq->GetRequestMethod() == HTTPRequestMethod::UNKNOWN) {
LogDebug(BCLog::HTTP, "HTTP request from %s rejected: Unknown HTTP request method\n",
@@ -758,6 +754,11 @@ void HTTPServer::StopListening()
void HTTPServer::StartSocketsThreads()
{
// The socket handler reads m_allow_subnets in ClientAllowed(). InitHTTPAllowList()
// must have populated it first; localhost entries are always added, so an empty
// list means it was never called and every connection is rejected.
Assume(!m_allow_subnets.empty());
m_thread_socket_handler = std::thread(&util::TraceThread,
"http",
[this] { ThreadSocketHandler(); });
@@ -792,13 +793,19 @@ std::unique_ptr<Sock> HTTPServer::AcceptConnection(const Sock& listen_sock, CSer
}
// The OS handed us a valid socket but we can't determine its source address.
// In the unlikely event this occurs, the invalid address will be rejected
// by the downstream ClientAllowed() check.
if (!addr.SetSockAddr(sa, len)) {
LogDebug(BCLog::HTTP,
"Unknown socket family");
}
// Early address-based allow check
if (!ClientAllowed(addr)) {
LogDebug(BCLog::HTTP, "Connection from %s rejected: Client network is not allowed HTTP access\n",
addr.ToStringAddrPort());
// Socket destroyed, connection aborted
return {};
}
return sock;
}
@@ -1212,13 +1219,13 @@ bool HTTPRemoteClient::MaybeSendBytesFromBuffer()
bool InitHTTPServer()
{
if (!InitHTTPAllowList()) {
return false;
}
// Create HTTPServer
g_http_server = std::make_unique<HTTPServer>(MaybeDispatchRequestToWorker);
if (!g_http_server->InitHTTPAllowList()) {
return false;
}
g_http_server->SetServerTimeout(std::chrono::seconds(gArgs.GetIntArg("-rpcservertimeout", DEFAULT_HTTP_SERVER_TIMEOUT)));
// Bind HTTP server to specified addresses