zmq: add restricted rpc mode

This commit is contained in:
selsta
2026-03-25 20:37:18 +01:00
parent b9998fc9e1
commit 1498737f7f
9 changed files with 219 additions and 9 deletions

View File

@@ -133,6 +133,18 @@ namespace daemon_args
, "Address for ZMQ pub - tcp://ip:port or ipc://path"
};
const command_line::arg_descriptor<bool> arg_restricted_zmq_rpc = {
"restricted-zmq-rpc"
, "Restrict ZMQ RPC to view-only / non-sensitive methods"
, false
};
const command_line::arg_descriptor<bool> arg_confirm_zmq_rpc_external_bind = {
"confirm-zmq-rpc-external-bind"
, "Confirm zmq-rpc-bind-ip value is NOT a loopback (local) IP"
, false
};
const command_line::arg_descriptor<bool> arg_zmq_rpc_disabled = {
"no-zmq"
, "Disable ZMQ RPC server"

View File

@@ -31,6 +31,7 @@
#include <memory>
#include <stdexcept>
#include <boost/algorithm/string/split.hpp>
#include <boost/asio/ip/address.hpp>
#include "misc_log_ex.h"
#include "daemon/daemon.h"
#include "rpc/daemon_handler.h"
@@ -58,10 +59,39 @@ using namespace epee;
namespace daemonize {
namespace
{
void verify_zmq_rpc_bind(const boost::program_options::variables_map& vm)
{
std::string bind_ip = command_line::get_arg(vm, daemon_args::arg_zmq_rpc_bind_ip);
if (bind_ip.empty())
return;
// ZMQ bind input already accepts bracketed IPv6 literals, but
// boost::asio::ip::make_address does not.
if (bind_ip.size() >= 2 && bind_ip.front() == '[' && bind_ip.back() == ']')
bind_ip = bind_ip.substr(1, bind_ip.size() - 2);
boost::system::error_code ec{};
const auto parsed_ip = boost::asio::ip::make_address(bind_ip, ec);
if (ec)
throw std::runtime_error{"Invalid IP address given for --" + std::string(daemon_args::arg_zmq_rpc_bind_ip.name)};
if (!parsed_ip.is_loopback() && !command_line::get_arg(vm, daemon_args::arg_confirm_zmq_rpc_external_bind))
{
throw std::runtime_error{
std::string{"--"} + daemon_args::arg_zmq_rpc_bind_ip.name +
" permits inbound unencrypted external connections. Consider SSH tunnel or SSL proxy instead. Override with --" +
daemon_args::arg_confirm_zmq_rpc_external_bind.name
};
}
}
}
struct zmq_internals
{
explicit zmq_internals(t_core& core, t_p2p& p2p)
: rpc_handler{core.get(), p2p.get()}
explicit zmq_internals(t_core& core, t_p2p& p2p, const bool restricted)
: rpc_handler{core.get(), p2p.get(), restricted}
, server{rpc_handler}
{}
@@ -104,7 +134,10 @@ public:
if (!command_line::get_arg(vm, daemon_args::arg_zmq_rpc_disabled))
{
zmq.reset(new zmq_internals{core, p2p});
verify_zmq_rpc_bind(vm);
const bool restricted = command_line::get_arg(vm, daemon_args::arg_restricted_zmq_rpc);
zmq.reset(new zmq_internals{core, p2p, restricted});
const std::string zmq_port = command_line::get_arg(vm, daemon_args::arg_zmq_rpc_bind_port);
const std::string zmq_address = command_line::get_arg(vm, daemon_args::arg_zmq_rpc_bind_ip);
@@ -133,12 +166,20 @@ public:
{
MWARNING("WARN: --zmq-rpc-bind-port has no effect because --no-zmq was specified");
}
else if (command_line::get_arg(vm, daemon_args::arg_zmq_rpc_bind_ip) !=
if (command_line::get_arg(vm, daemon_args::arg_zmq_rpc_bind_ip) !=
daemon_args::arg_zmq_rpc_bind_ip.default_value)
{
MWARNING("WARN: --zmq-rpc-bind-ip has no effect because --no-zmq was specified");
}
else if (!command_line::get_arg(vm, daemon_args::arg_zmq_pub).empty())
if (command_line::get_arg(vm, daemon_args::arg_confirm_zmq_rpc_external_bind))
{
MWARNING("WARN: --confirm-zmq-rpc-external-bind has no effect because --no-zmq was specified");
}
if (command_line::get_arg(vm, daemon_args::arg_restricted_zmq_rpc))
{
MWARNING("WARN: --restricted-zmq-rpc has no effect because --no-zmq was specified");
}
if (!command_line::get_arg(vm, daemon_args::arg_zmq_pub).empty())
{
MWARNING("WARN: --zmq-pub has no effect because --no-zmq was specified");
}

View File

@@ -157,6 +157,8 @@ int main(int argc, char const * argv[])
command_line::add_arg(core_settings, daemon_args::arg_zmq_rpc_bind_ip);
command_line::add_arg(core_settings, daemon_args::arg_zmq_rpc_bind_port);
command_line::add_arg(core_settings, daemon_args::arg_zmq_pub);
command_line::add_arg(core_settings, daemon_args::arg_confirm_zmq_rpc_external_bind);
command_line::add_arg(core_settings, daemon_args::arg_restricted_zmq_rpc);
command_line::add_arg(core_settings, daemon_args::arg_zmq_rpc_disabled);
command_line::add_arg(core_settings, daemonizer::arg_non_interactive);

View File

@@ -47,6 +47,7 @@ set(rpc_pub_sources zmq_pub.cpp)
set(daemon_rpc_server_sources
daemon_handler.cpp
zmq_restricted_methods.cpp
zmq_pub.cpp
zmq_server.cpp)
@@ -79,6 +80,7 @@ set(daemon_rpc_server_private_headers
message.h
daemon_messages.h
daemon_handler.h
zmq_restricted_methods.h
zmq_server.h)

View File

@@ -27,6 +27,7 @@
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "daemon_handler.h"
#include "rpc/zmq_restricted_methods.h"
#include <algorithm>
#include <cstring>
@@ -109,12 +110,14 @@ namespace rpc
};
} // anonymous
DaemonHandler::DaemonHandler(cryptonote::core& c, t_p2p& p2p)
: m_core(c), m_p2p(p2p)
DaemonHandler::DaemonHandler(cryptonote::core& c, t_p2p& p2p, bool restricted)
: m_core(c), m_p2p(p2p), m_restricted(restricted)
{
const auto last_sorted = std::is_sorted_until(std::begin(handlers), std::end(handlers));
if (last_sorted != std::end(handlers))
throw std::logic_error{std::string{"ZMQ JSON-RPC handlers map is not properly sorted, see "} + last_sorted->method_name};
check_blocked_methods_sorted();
}
void DaemonHandler::handle(const GetHeight::Request& req, GetHeight::Response& res)
@@ -921,13 +924,24 @@ namespace rpc
epee::byte_slice DaemonHandler::handle(std::string&& request)
{
MDEBUG("Handling RPC request: " << request);
if (m_restricted)
MDEBUG("Handling RPC request");
else
MDEBUG("Handling RPC request: " << request);
try
{
FullMessage req_full(std::move(request), true);
const std::string request_type = req_full.getRequestType();
if (m_restricted && is_blocked_in_restricted_mode(request_type))
{
Message fail;
fail.status = Message::STATUS_FAILED;
fail.error_details = "\"" + request_type + "\" is not available in restricted mode.";
return FullMessage::getResponse(fail, req_full.getID());
}
const auto matched_handler = std::lower_bound(std::begin(handlers), std::end(handlers), request_type);
if (matched_handler == std::end(handlers) || matched_handler->method_name != request_type)
return BAD_REQUEST(request_type, req_full.getID());

View File

@@ -51,7 +51,7 @@ class DaemonHandler : public RpcHandler
{
public:
DaemonHandler(cryptonote::core& c, t_p2p& p2p);
DaemonHandler(cryptonote::core& c, t_p2p& p2p, bool restricted = false);
~DaemonHandler() { }
@@ -143,6 +143,7 @@ class DaemonHandler : public RpcHandler
cryptonote::core& m_core;
t_p2p& m_p2p;
bool m_restricted;
};
} // namespace rpc

View File

@@ -0,0 +1,75 @@
// Copyright (c) 2016-2026, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "rpc/zmq_restricted_methods.h"
#include <algorithm>
#include <array>
#include <string>
#include <string_view>
namespace cryptonote
{
namespace rpc
{
namespace
{
constexpr std::array<std::string_view, 9> blocked_in_restricted_mode{{
"flush_txpool",
"get_peer_list",
"mining_status",
"relay_tx",
"save_bc",
"set_log_categories",
"set_log_level",
"start_mining",
"stop_mining"
}};
}
bool is_blocked_in_restricted_mode(const std::string_view method) noexcept
{
return std::binary_search(
blocked_in_restricted_mode.begin(),
blocked_in_restricted_mode.end(),
method
);
}
void check_blocked_methods_sorted()
{
const auto last =
std::is_sorted_until(blocked_in_restricted_mode.begin(), blocked_in_restricted_mode.end());
if (last != blocked_in_restricted_mode.end())
throw std::logic_error{
std::string{"ZMQ restricted-method map is not properly sorted, see "} + std::string{*last}
};
}
} // rpc
} // cryptonote

View File

@@ -0,0 +1,45 @@
// Copyright (c) 2016-2026, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <string_view>
#include <stdexcept>
namespace cryptonote
{
namespace rpc
{
//! Returns true when `method` must be rejected while ZMQ RPC runs in
//! restricted mode. Keep this list in sync with daemon RPC method
bool is_blocked_in_restricted_mode(std::string_view method) noexcept;
//! Throws std::logic_error if the internal method table is not sorted.
void check_blocked_methods_sorted();
} // rpc
} // cryptonote

View File

@@ -39,6 +39,7 @@
#include "net/zmq.h"
#include "rpc/message.h"
#include "rpc/zmq_pub.h"
#include "rpc/zmq_restricted_methods.h"
#include "rpc/zmq_server.h"
#include "serialization/json_object.h"
@@ -69,6 +70,23 @@ TEST(ZmqFullMessage, Request)
EXPECT_STREQ("foo", parsed.getRequestType().c_str());
}
TEST(ZmqRestrictedMethods, BasicCoverage)
{
EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("flush_txpool"));
EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("get_peer_list"));
EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("mining_status"));
EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("relay_tx"));
EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("save_bc"));
EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("set_log_categories"));
EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("set_log_level"));
EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("start_mining"));
EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("stop_mining"));
EXPECT_FALSE(cryptonote::rpc::is_blocked_in_restricted_mode("get_height"));
EXPECT_FALSE(cryptonote::rpc::is_blocked_in_restricted_mode("get_info"));
EXPECT_FALSE(cryptonote::rpc::is_blocked_in_restricted_mode("send_raw_tx"));
}
namespace
{
using published_json = std::pair<std::string, rapidjson::Document>;