From f728b6b11100fae1e27f7a0ef92a5930fa8cffb3 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 16 May 2025 15:08:51 -0400 Subject: [PATCH 1/3] init: Configure reachable networks before we start the RPC server We need to determine if CJDNS is reachable before we parse any IPv6 addresses (for example, by the -rpcallowip setting) or an RFC4193 address might get flipped to CJDNS, which can not be used with subnets --- src/init.cpp | 50 ++++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 7ac0bbeb2b0..ac900cb1ba5 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1392,6 +1392,32 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) // Check port numbers if (!CheckHostPortOptions(args)) return false; + // Configure reachable networks before we start the RPC server. + // This is necessary for -rpcallowip to distinguish CJDNS from other RFC4193 + const auto onlynets = args.GetArgs("-onlynet"); + if (!onlynets.empty()) { + g_reachable_nets.RemoveAll(); + for (const std::string& snet : onlynets) { + enum Network net = ParseNetwork(snet); + if (net == NET_UNROUTABLE) + return InitError(strprintf(_("Unknown network specified in -onlynet: '%s'"), snet)); + g_reachable_nets.Add(net); + } + } + + if (!args.IsArgSet("-cjdnsreachable")) { + if (!onlynets.empty() && g_reachable_nets.Contains(NET_CJDNS)) { + return InitError( + _("Outbound connections restricted to CJDNS (-onlynet=cjdns) but " + "-cjdnsreachable is not provided")); + } + g_reachable_nets.Remove(NET_CJDNS); + } + // Now g_reachable_nets.Contains(NET_CJDNS) is true if: + // 1. -cjdnsreachable is given and + // 2.1. -onlynet is not given or + // 2.2. -onlynet=cjdns is given + /* Start the RPC server already. It will be started in "warmup" mode * and not really process calls already (but it will signify connections * that the server is there and will be ready later). Warmup mode will @@ -1504,30 +1530,6 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) strSubVersion.size(), MAX_SUBVERSION_LENGTH)); } - const auto onlynets = args.GetArgs("-onlynet"); - if (!onlynets.empty()) { - g_reachable_nets.RemoveAll(); - for (const std::string& snet : onlynets) { - enum Network net = ParseNetwork(snet); - if (net == NET_UNROUTABLE) - return InitError(strprintf(_("Unknown network specified in -onlynet: '%s'"), snet)); - g_reachable_nets.Add(net); - } - } - - if (!args.IsArgSet("-cjdnsreachable")) { - if (!onlynets.empty() && g_reachable_nets.Contains(NET_CJDNS)) { - return InitError( - _("Outbound connections restricted to CJDNS (-onlynet=cjdns) but " - "-cjdnsreachable is not provided")); - } - g_reachable_nets.Remove(NET_CJDNS); - } - // Now g_reachable_nets.Contains(NET_CJDNS) is true if: - // 1. -cjdnsreachable is given and - // 2.1. -onlynet is not given or - // 2.2. -onlynet=cjdns is given - // Requesting DNS seeds entails connecting to IPv4/IPv6, which -onlynet options may prohibit: // If -dnsseed=1 is explicitly specified, abort. If it's left unspecified by the user, we skip // the DNS seeds by adjusting -dnsseed in InitParameterInteraction. From c02bd3c1875abd877a0dc73fb8866c883b7fcd32 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 16 May 2025 14:57:48 -0400 Subject: [PATCH 2/3] config: Explain RFC4193 and CJDNS interaction in help and init error --- src/httpserver.cpp | 2 +- src/init.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index bd2dec19b97..f5686bbd2e6 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -227,7 +227,7 @@ static bool InitHTTPAllowList() const CSubNet subnet{LookupSubNet(strAllow)}; if (!subnet.IsValid()) { uiInterface.ThreadSafeMessageBox( - Untranslated(strprintf("Invalid -rpcallowip subnet specification: %s. Valid are a single IP (e.g. 1.2.3.4), a network/netmask (e.g. 1.2.3.4/255.255.255.0) or a network/CIDR (e.g. 1.2.3.4/24).", strAllow)), + Untranslated(strprintf("Invalid -rpcallowip subnet specification: %s. Valid values are a single IP (e.g. 1.2.3.4), a network/netmask (e.g. 1.2.3.4/255.255.255.0), a network/CIDR (e.g. 1.2.3.4/24), all ipv4 (0.0.0.0/0), or all ipv6 (::/0). RFC4193 is allowed only if -cjdnsreachable=0.", strAllow)), "", CClientUIInterface::MSG_ERROR); return false; } diff --git a/src/init.cpp b/src/init.cpp index ac900cb1ba5..5f6582b8355 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -650,7 +650,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) argsman.AddArg("-blockversion=", "Override block version to test forking scenarios", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::BLOCK_CREATION); argsman.AddArg("-rest", strprintf("Accept public REST requests (default: %u)", DEFAULT_REST_ENABLE), ArgsManager::ALLOW_ANY, OptionsCategory::RPC); - argsman.AddArg("-rpcallowip=", "Allow JSON-RPC connections from specified source. Valid values for are a single IP (e.g. 1.2.3.4), a network/netmask (e.g. 1.2.3.4/255.255.255.0), a network/CIDR (e.g. 1.2.3.4/24), all ipv4 (0.0.0.0/0), or all ipv6 (::/0). This option can be specified multiple times", ArgsManager::ALLOW_ANY, OptionsCategory::RPC); + argsman.AddArg("-rpcallowip=", "Allow JSON-RPC connections from specified source. Valid values for are a single IP (e.g. 1.2.3.4), a network/netmask (e.g. 1.2.3.4/255.255.255.0), a network/CIDR (e.g. 1.2.3.4/24), all ipv4 (0.0.0.0/0), or all ipv6 (::/0). RFC4193 is allowed only if -cjdnsreachable=0. This option can be specified multiple times", ArgsManager::ALLOW_ANY, OptionsCategory::RPC); argsman.AddArg("-rpcauth=", "Username and HMAC-SHA-256 hashed password for JSON-RPC connections. The field comes in the format: :$. A canonical python script is included in share/rpcauth. The client then connects normally using the rpcuser=/rpcpassword= pair of arguments. This option can be specified multiple times", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::RPC); argsman.AddArg("-rpcbind=[:port]", "Bind to given address to listen for JSON-RPC connections. Do not expose the RPC server to untrusted networks such as the public internet! This option is ignored unless -rpcallowip is also passed. Port is optional and overrides -rpcport. Use [host]:port notation for IPv6. This option can be specified multiple times (default: 127.0.0.1 and ::1 i.e., localhost)", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::RPC); argsman.AddArg("-rpcdoccheck", strprintf("Throw a non-fatal error at runtime if the documentation for an RPC is incorrect (default: %u)", DEFAULT_RPC_DOC_CHECK), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC); From 12ff4be9c724c752fe67835419be3ff4d0e65990 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 16 May 2025 14:25:42 -0400 Subject: [PATCH 3/3] test: ensure -rpcallowip is compatible with RFC4193 --- test/functional/rpc_bind.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/functional/rpc_bind.py b/test/functional/rpc_bind.py index 69afd45b9af..c259608c940 100755 --- a/test/functional/rpc_bind.py +++ b/test/functional/rpc_bind.py @@ -6,6 +6,7 @@ from test_framework.netutil import all_interfaces, addr_to_hex, get_bind_addrs, test_ipv6_local from test_framework.test_framework import BitcoinTestFramework, SkipTest +from test_framework.test_node import ErrorMatch from test_framework.util import assert_equal, assert_raises_rpc_error, get_rpc_proxy, rpc_port, rpc_url class RPCBindTest(BitcoinTestFramework): @@ -75,6 +76,25 @@ class RPCBindTest(BitcoinTestFramework): node.getnetworkinfo() self.stop_nodes() + def run_invalid_allowip_test(self): + ''' + Check parameter interaction with -rpcallowip and -cjdnsreachable. + RFC4193 addresses are fc00::/7 like CJDNS but have an optional + "local" L bit making them fd00:: which should always be OK. + ''' + self.log.info("Allow RFC4193 when compatible with CJDNS options") + # Don't rpcallow RFC4193 with L-bit=0 if CJDNS is enabled + self.nodes[0].assert_start_raises_init_error( + ["-rpcallowip=fc00:db8:c0:ff:ee::/80","-cjdnsreachable"], + "Invalid -rpcallowip subnet specification", + match=ErrorMatch.PARTIAL_REGEX) + # OK to rpcallow RFC4193 with L-bit=1 if CJDNS is enabled + self.start_node(0, ["-rpcallowip=fd00:db8:c0:ff:ee::/80","-cjdnsreachable"]) + self.stop_nodes() + # OK to rpcallow RFC4193 with L-bit=0 if CJDNS is not enabled + self.start_node(0, ["-rpcallowip=fc00:db8:c0:ff:ee::/80"]) + self.stop_nodes() + def run_test(self): if sum([self.options.run_ipv4, self.options.run_ipv6, self.options.run_nonloopback]) > 1: raise AssertionError("Only one of --ipv4, --ipv6 and --nonloopback can be set") @@ -101,6 +121,7 @@ class RPCBindTest(BitcoinTestFramework): self.run_invalid_bind_test(['127.0.0.1'], ['127.0.0.1:notaport', '127.0.0.1:-18443', '127.0.0.1:0', '127.0.0.1:65536']) if self.options.run_ipv6: self.run_invalid_bind_test(['[::1]'], ['[::1]:notaport', '[::1]:-18443', '[::1]:0', '[::1]:65536']) + self.run_invalid_allowip_test() if not self.options.run_ipv4 and not self.options.run_ipv6: self._run_nonloopback_tests()