diff options
| author | selsta <selsta@sent.at> | 2026-03-25 20:37:18 +0100 |
|---|---|---|
| committer | selsta <selsta@sent.at> | 2026-04-01 18:06:10 +0200 |
| commit | 7f2cfe9294ec1fa7bece2a8069114c1b6985650f (patch) | |
| tree | 06a5d19a645f700b02b9bdcacaff915c7bd9974a | |
| parent | dd0fb6c7552330a088033b0cfba0ba570bbbfd15 (diff) | |
| download | monzero-core-7f2cfe9294ec1fa7bece2a8069114c1b6985650f.tar.gz monzero-core-7f2cfe9294ec1fa7bece2a8069114c1b6985650f.tar.xz monzero-core-7f2cfe9294ec1fa7bece2a8069114c1b6985650f.zip | |
zmq: add restricted rpc mode
| -rw-r--r-- | src/daemon/command_line_args.h | 12 | ||||
| -rw-r--r-- | src/daemon/daemon.cpp | 51 | ||||
| -rw-r--r-- | src/daemon/main.cpp | 2 | ||||
| -rw-r--r-- | src/rpc/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/rpc/daemon_handler.cpp | 20 | ||||
| -rw-r--r-- | src/rpc/daemon_handler.h | 3 | ||||
| -rw-r--r-- | src/rpc/zmq_restricted_methods.cpp | 73 | ||||
| -rw-r--r-- | src/rpc/zmq_restricted_methods.h | 45 | ||||
| -rw-r--r-- | tests/unit_tests/zmq_rpc.cpp | 18 |
9 files changed, 217 insertions, 9 deletions
diff --git a/src/daemon/command_line_args.h b/src/daemon/command_line_args.h index 96fddc02d..a0b15d021 100644 --- a/src/daemon/command_line_args.h +++ b/src/daemon/command_line_args.h @@ -136,6 +136,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" diff --git a/src/daemon/daemon.cpp b/src/daemon/daemon.cpp index 043e73501..e069d1b65 100644 --- a/src/daemon/daemon.cpp +++ b/src/daemon/daemon.cpp @@ -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"); } diff --git a/src/daemon/main.cpp b/src/daemon/main.cpp index 38b99ff49..39ef04772 100644 --- a/src/daemon/main.cpp +++ b/src/daemon/main.cpp @@ -158,6 +158,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); daemonizer::init_options(hidden_options, visible_options); diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt index edfc70067..f12da48d3 100644 --- a/src/rpc/CMakeLists.txt +++ b/src/rpc/CMakeLists.txt @@ -49,6 +49,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) @@ -81,6 +82,7 @@ set(daemon_rpc_server_private_headers message.h daemon_messages.h daemon_handler.h + zmq_restricted_methods.h zmq_server.h) diff --git a/src/rpc/daemon_handler.cpp b/src/rpc/daemon_handler.cpp index c1e4c10bf..671889ddd 100644 --- a/src/rpc/daemon_handler.cpp +++ b/src/rpc/daemon_handler.cpp @@ -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) @@ -930,13 +933,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()); diff --git a/src/rpc/daemon_handler.h b/src/rpc/daemon_handler.h index 74885cf30..f0ddb2967 100644 --- a/src/rpc/daemon_handler.h +++ b/src/rpc/daemon_handler.h @@ -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 diff --git a/src/rpc/zmq_restricted_methods.cpp b/src/rpc/zmq_restricted_methods.cpp new file mode 100644 index 000000000..eb58fd3d2 --- /dev/null +++ b/src/rpc/zmq_restricted_methods.cpp @@ -0,0 +1,73 @@ +// 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> + +namespace cryptonote +{ +namespace rpc +{ + namespace + { + const std::array<boost::string_ref, 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 boost::string_ref 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 "} + last->to_string() + }; + } +} // rpc +} // cryptonote diff --git a/src/rpc/zmq_restricted_methods.h b/src/rpc/zmq_restricted_methods.h new file mode 100644 index 000000000..28f2dd0a1 --- /dev/null +++ b/src/rpc/zmq_restricted_methods.h @@ -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 <stdexcept> +#include <boost/utility/string_ref.hpp> + +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(boost::string_ref method) noexcept; + + //! Throws std::logic_error if the internal method table is not sorted. + void check_blocked_methods_sorted(); +} // rpc +} // cryptonote diff --git a/tests/unit_tests/zmq_rpc.cpp b/tests/unit_tests/zmq_rpc.cpp index c98f0011b..38b5f083f 100644 --- a/tests/unit_tests/zmq_rpc.cpp +++ b/tests/unit_tests/zmq_rpc.cpp @@ -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>; |
