From f2b78d38a81be2da3f00fdaf99e4d5be3c3ce043 Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 31 Oct 2026 18:44:53 +0000 Subject: [PATCH 1/2] fix(net): don't refuse to start over the automatic onion service unless -listenonion=1 Since #7786, a node with -bind but no specific -bind==onion refuses to start while -listenonion is enabled. -listenonion is on by default, so this hits every node with bind= and no Tor (dashmate evonodes, docker, multi-masternode hosts), which all started on v23. Keep the privacy fix: Tor is still never pointed at a listener that isn't tagged as onion. But without an onion bind, the default -listenonion now connects to Tor without a target: it still authenticates and discovers the onion SOCKS proxy, but skips ADD_ONION and logs once why no onion service was created. Only an explicit -listenonion=1 keeps that startup error. A wildcard =onion bind stays a startup error while -listenonion is enabled, by default too, also next to a specific one. Writing =onion asks for Tor, and a wildcard listener can't honor it: an accepted socket reports its concrete local address, which never matches the wildcard entry, so peers that Tor forwards there would be treated as clearnet peers and could be told our clearnet address. Setups without Tor don't write =onion, so they are not affected. With -listenonion=0 a wildcard =onion bind is still accepted. Co-Authored-By: Claude Opus 5.5 --- doc/release-notes-7300.md | 26 +++++++++++------ doc/tor.md | 10 +++++-- src/init.cpp | 29 +++++++++++------- src/torcontrol.cpp | 16 +++++++--- src/torcontrol.h | 11 ++++--- test/functional/feature_bind_extra.py | 42 +++++++++++++++++++-------- 6 files changed, 92 insertions(+), 42 deletions(-) diff --git a/doc/release-notes-7300.md b/doc/release-notes-7300.md index bbae7e647268..7c2dc7d8c625 100644 --- a/doc/release-notes-7300.md +++ b/doc/release-notes-7300.md @@ -5,12 +5,20 @@ P2P and network changes onion-service bind (`127.0.0.1:9996` by default) cannot be set up. Dash Core v23 and earlier started as long as at least one bind succeeded. -* Nodes configured with `-bind` but no specific `-bind==onion` now - refuse to start when `-listenonion` is enabled. This includes nodes without - Tor configured, since `-listenonion` is enabled by default when listening. - Shared binds cannot distinguish Tor-forwarded connections from direct - connections, which can grant Tor peers unintended IP-based whitelist - permissions. Nodes without `-bind`, including those using only `-whitebind`, - continue to get the default onion target. Users should add a specific - `-bind==onion` to accept incoming Tor connections, or set - `-listenonion=0` to disable automatic onion service creation. +* Nodes configured with `-bind` but no specific `-bind==onion` no + longer create an automatic Tor onion service. The service would have to + forward to a shared bind, which cannot tell Tor-forwarded connections from + direct ones, so onion peers would be treated as clearnet peers (and could be + told the node's clearnet address or get IP-based whitelist permissions). + These nodes still start, and still use Tor for outbound onion connections if + its control port is reachable; the debug log notes that no onion service was + created. Nodes without `-bind`, including those using only `-whitebind`, + continue to get the default onion target. Add a specific + `-bind==onion` to accept incoming Tor connections. Only an + explicit `-listenonion=1` without an onion bind makes startup fail. + +* A wildcard onion bind such as `-bind=0.0.0.0:=onion` is a startup error + unless `-listenonion=0` is set, also with the default `-listenonion`. The node + can't tell which connections to a wildcard listener came through Tor, so Tor + peers would be treated as clearnet peers. Dash Core v23 accepted it. Use a + specific address such as `-bind=127.0.0.1:=onion` instead. diff --git a/doc/tor.md b/doc/tor.md index d617a58e510d..c94118c4c6df 100644 --- a/doc/tor.md +++ b/doc/tor.md @@ -83,8 +83,14 @@ it requires a Tor connection to work. It can be explicitly disabled with `-listenonion=0`. If it is not disabled, it can be configured using the `-torcontrol` and `-torpassword` settings. -When enabled with an explicit `-bind`, the automatic service requires a non-wildcard onion bind such as `-bind=127.0.0.1:=onion`. -Without `-bind`, Dash Core adds the default onion bind. +The automatic service forwards incoming connections to a dedicated, +non-wildcard onion bind. Without `-bind`, Dash Core adds the default onion bind +(`127.0.0.1:9996` on mainnet). With an explicit `-bind`, add one such as +`-bind=127.0.0.1:=onion`; otherwise Dash Core still uses Tor for outbound +onion connections but creates no onion service. Setting `-listenonion=1` +explicitly without an onion bind is a startup error. A wildcard onion bind such +as `-bind=0.0.0.0:=onion` is a startup error unless `-listenonion=0` is +set, because Dash Core can't recognize the Tor peers connecting to it. To see verbose Tor information in the dashd debug log, pass `-debug=tor`. diff --git a/src/init.cpp b/src/init.cpp index 0ae176913494..d5cf633e5401 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -664,7 +664,7 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-allowprivatenet", strprintf("Allow RFC1918 addresses to be relayed and connected to (default: %u)", DEFAULT_ALLOWPRIVATENET), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-bantime=", strprintf("Default duration (in seconds) of manually configured bans (default: %u)", DEFAULT_MISBEHAVING_BANTIME), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-bind=[:][=onion]", strprintf("Bind to given address and always listen on it (default: 0.0.0.0). Use [host]:port notation for IPv6. Append =onion to tag any incoming connections to that address and port as incoming Tor connections (default: 127.0.0.1:%u=onion, testnet: 127.0.0.1:%u=onion, devnet: 127.0.0.1:%u=onion, regtest: 127.0.0.1:%u=onion). " - "The default onion bind is added only when no -bind is specified. When -listenonion is enabled and -bind is specified, a non-wildcard =onion bind is required, and wildcard =onion binds are rejected.", defaultBaseParams->OnionServiceTargetPort(), testnetBaseParams->OnionServiceTargetPort(), devnetBaseParams->OnionServiceTargetPort(), regtestBaseParams->OnionServiceTargetPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); + "The default onion bind is added only when no -bind is specified. While -listenonion is enabled, a wildcard =onion bind is an error. The automatic Tor onion service needs an =onion bind; without one, no onion service is created, and an explicit -listenonion=1 is an error.", defaultBaseParams->OnionServiceTargetPort(), testnetBaseParams->OnionServiceTargetPort(), devnetBaseParams->OnionServiceTargetPort(), regtestBaseParams->OnionServiceTargetPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); argsman.AddArg("-cjdnsreachable", "If set, then this host is configured for CJDNS (connecting to fc00::/8 addresses would lead us to the CJDNS network, see doc/cjdns.md) (default: 0)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-connect=", "Connect only to the specified node; -noconnect disables automatic connections (the rules for this peer are the same as for -addnode). This option can be specified multiple times to connect to multiple nodes.", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); argsman.AddArg("-discover", "Discover own IP addresses (default: 1 when listening and no -externalip or -proxy)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); @@ -674,7 +674,7 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-fixedseeds", strprintf("Allow fixed seeds if DNS seeds don't provide peers (default: %u)", DEFAULT_FIXEDSEEDS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-forcednsseed", strprintf("Always query for peer addresses via DNS lookup (default: %u)", DEFAULT_FORCEDNSSEED), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-listen", strprintf("Accept connections from outside (default: %u if no -proxy, -connect or -maxconnections=0)", DEFAULT_LISTEN), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); - argsman.AddArg("-listenonion", strprintf("Automatically create Tor onion service (default: %d)", DEFAULT_LISTEN_ONION), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); + argsman.AddArg("-listenonion", strprintf("Automatically create Tor onion service if Tor is available and an onion bind exists (see -bind). If set to 1 explicitly, fail to start without one (default: %d)", DEFAULT_LISTEN_ONION), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-maxconnections=", strprintf("Maintain at most connections to peers (temporary service connections excluded) (default: %u). This limit does not apply to connections manually added via -addnode or the addnode RPC, which have a separate limit of %u.", DEFAULT_MAX_PEER_CONNECTIONS, MAX_ADDNODE_CONNECTIONS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-maxreceivebuffer=", strprintf("Maximum per-connection receive buffer, *1000 bytes (default: %u)", DEFAULT_MAXRECEIVEBUFFER), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-maxsendbuffer=", strprintf("Maximum per-connection memory usage for the send buffer, *1000 bytes (default: %u)", DEFAULT_MAXSENDBUFFER), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); @@ -2706,19 +2706,26 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) } if (args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)) { - if (connOptions.onion_binds.empty()) { - return InitError(_("The automatic Tor onion service requires a dedicated onion bind. Use a specific address such as -bind=127.0.0.1:=onion, or disable the service with -listenonion=0.")); - } + // A wildcard listener can't tag the peers Tor forwards to it as onion. if (std::ranges::any_of(connOptions.onion_binds, [](auto& b) { return b.IsBindAny(); })) { return InitError(_("The automatic Tor onion service cannot use a wildcard onion bind because the Tor daemon wouldn't be able to forward incoming connections to us. Use a specific address such as -bind=127.0.0.1:=onion, or disable the service with -listenonion=0.")); } - const CService& onion_service_target{connOptions.onion_binds[0]}; - if (connOptions.onion_binds.size() > 1) { - InitWarning(strprintf(_("More than one onion bind address is provided. Using %s " - "for the automatically created Tor onion service."), - onion_service_target.ToStringAddrPort())); + if (connOptions.onion_binds.empty()) { + // Tor must only forward to a listener that tags its peers as onion. Without one, the + // default -listenonion still uses Tor for outbound onion connections. + if (args.IsArgSet("-listenonion")) { + return InitError(_("The automatic Tor onion service requires a dedicated onion bind. Use a specific address such as -bind=127.0.0.1:=onion, or disable the service with -listenonion=0.")); + } + StartTorControl(std::nullopt); + } else { + const CService& onion_service_target{connOptions.onion_binds[0]}; + if (connOptions.onion_binds.size() > 1) { + InitWarning(strprintf(_("More than one onion bind address is provided. Using %s " + "for the automatically created Tor onion service."), + onion_service_target.ToStringAddrPort())); + } + StartTorControl(onion_service_target); } - StartTorControl(onion_service_target); } if (connOptions.bind_on_any) { diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index 2c6fce13517b..59682fe352b1 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -312,7 +312,7 @@ std::map ParseTorReplyMapping(const std::string &s) return mapping; } -TorController::TorController(struct event_base* _base, const std::string& tor_control_center, const CService& target): +TorController::TorController(struct event_base* _base, const std::string& tor_control_center, const std::optional& target): base(_base), m_tor_control_center(tor_control_center), conn(base), reconnect(true), reconnect_timeout(RECONNECT_TIMEOUT_START), m_target(target) @@ -457,13 +457,21 @@ void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply& _conn.Command("GETINFO net/listeners/socks", std::bind(&TorController::get_socks_cb, this, std::placeholders::_1, std::placeholders::_2)); } + if (!m_target) { + if (!m_logged_no_target) { + LogPrintf("tor: Not creating an onion service because there is no dedicated onion bind. Use -bind=127.0.0.1:=onion to create one.\n"); + m_logged_no_target = true; + } + return; + } + // Finally - now create the service if (private_key.empty()) { // No private key, generate one private_key = "NEW:ED25519-V3"; // Explicitly request key type - see issue #9214 } // Request onion service, redirect port. // Note that the 'virtual' port is always the default port to avoid decloaking nodes using other ports. - _conn.Command(strprintf("ADD_ONION %s Port=%i,%s", private_key, Params().GetDefaultPort(), m_target.ToStringAddrPort()), + _conn.Command(strprintf("ADD_ONION %s Port=%i,%s", private_key, Params().GetDefaultPort(), m_target->ToStringAddrPort()), std::bind(&TorController::add_onion_cb, this, std::placeholders::_1, std::placeholders::_2)); } else { LogPrintf("tor: Authentication failed\n"); @@ -660,14 +668,14 @@ void TorController::reconnect_cb(evutil_socket_t fd, short what, void *arg) static struct event_base *gBase; static std::thread torControlThread; -static void TorControlThread(CService onion_service_target) +static void TorControlThread(std::optional onion_service_target) { TorController ctrl(gBase, gArgs.GetArg("-torcontrol", DEFAULT_TOR_CONTROL), onion_service_target); event_base_dispatch(gBase); } -void StartTorControl(CService onion_service_target) +void StartTorControl(std::optional onion_service_target) { assert(!gBase); #ifdef WIN32 diff --git a/src/torcontrol.h b/src/torcontrol.h index 4a0eef223e41..235e4d2b5ec1 100644 --- a/src/torcontrol.h +++ b/src/torcontrol.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -23,7 +24,7 @@ constexpr int DEFAULT_TOR_CONTROL_PORT = 9051; extern const std::string DEFAULT_TOR_CONTROL; static const bool DEFAULT_LISTEN_ONION = true; -void StartTorControl(CService onion_service_target); +void StartTorControl(std::optional onion_service_target); void InterruptTorControl(); void StopTorControl(); @@ -101,12 +102,13 @@ class TorControlConnection /****** Bitcoin specific TorController implementation ********/ /** Controller that connects to Tor control socket, authenticate, then create - * and maintain an ephemeral onion service. + * and maintain an ephemeral onion service. Without a target it only configures + * the onion proxy and creates no onion service. */ class TorController { public: - TorController(struct event_base* base, const std::string& tor_control_center, const CService& target); + TorController(struct event_base* base, const std::string& tor_control_center, const std::optional& target); TorController() : conn{nullptr} { // Used for testing only. } @@ -127,7 +129,8 @@ class TorController struct event *reconnect_ev = nullptr; float reconnect_timeout; CService service; - const CService m_target; + const std::optional m_target; + bool m_logged_no_target{false}; /** Cookie for SAFECOOKIE auth */ std::vector cookie; /** ClientNonce for SAFECOOKIE auth */ diff --git a/test/functional/feature_bind_extra.py b/test/functional/feature_bind_extra.py index 5470c3df9c15..480e8ccd6158 100755 --- a/test/functional/feature_bind_extra.py +++ b/test/functional/feature_bind_extra.py @@ -20,6 +20,8 @@ rpc_port, ) +WILDCARD_ONION_ERROR = "Error: The automatic Tor onion service cannot use a wildcard onion bind because the Tor daemon wouldn't be able to forward incoming connections to us. Use a specific address such as -bind=127.0.0.1:=onion, or disable the service with -listenonion=0." + class BindExtraTest(BitcoinTestFramework): def set_test_params(self): @@ -72,20 +74,23 @@ def setup_network(self): self.extra_args = list(map(lambda e: e[0], self.expected)) self.setup_nodes() + def check_binds(self, i, expected_services): + pid = self.nodes[i].process.pid + binds = set(get_bind_addrs(pid)) + # Remove IPv6 addresses because on some CI environments "::1" is not configured + # on the system (so our test_ipv6_local() would return False), but it is + # possible to bind on "::". This makes it unpredictable whether to expect + # that bitcoind has bound on "::1" (for RPC) and "::" (for P2P). + ipv6_addr_len_bytes = 32 + binds = set(filter(lambda e: len(e[0]) != ipv6_addr_len_bytes, binds)) + # Remove RPC ports. They are not relevant for this test. + binds = set(filter(lambda e: e[1] != rpc_port(i), binds)) + assert_equal(binds, set(expected_services)) + def run_test(self): for i, (args, expected_services) in enumerate(self.expected): self.log.info(f"Checking listening ports of node {i} with {args}") - pid = self.nodes[i].process.pid - binds = set(get_bind_addrs(pid)) - # Remove IPv6 addresses because on some CI environments "::1" is not configured - # on the system (so our test_ipv6_local() would return False), but it is - # possible to bind on "::". This makes it unpredictable whether to expect - # that bitcoind has bound on "::1" (for RPC) and "::" (for P2P). - ipv6_addr_len_bytes = 32 - binds = set(filter(lambda e: len(e[0]) != ipv6_addr_len_bytes, binds)) - # Remove RPC ports. They are not relevant for this test. - binds = set(filter(lambda e: e[1] != rpc_port(i), binds)) - assert_equal(binds, set(expected_services)) + self.check_binds(i, expected_services) self.log.info("Test -listenonion with a normal bind and no dedicated onion bind") self.stop_node(2) @@ -94,6 +99,15 @@ def run_test(self): "Error: The automatic Tor onion service requires a dedicated onion bind. Use a specific address such as -bind=127.0.0.1:=onion, or disable the service with -listenonion=0.", ) + self.log.info("Test -bind without an onion bind starts with the default -listenonion") + self.nodes[2].replace_in_config([("listenonion=0\n", "")]) + self.start_node(2, extra_args=self.extra_args[2] + ["-torcontrol=127.0.0.1:1"]) + self.check_binds(2, self.expected[2][1]) + self.stop_node(2) + + self.log.info("Test wildcard onion bind with the default -listenonion") + self.nodes[2].assert_start_raises_init_error([f"-bind=0.0.0.0:{p2p_port(2)}=onion"], WILDCARD_ONION_ERROR) + self.log.info("Test -bind with dedicated onion bind starts when -listenonion=1") self.restart_node(1, extra_args=self.extra_args[1] + ["-listenonion=1", "-torcontrol=127.0.0.1:1"]) @@ -101,8 +115,12 @@ def run_test(self): self.stop_node(0) self.nodes[0].assert_start_raises_init_error( [f"-bind=0.0.0.0:{p2p_port(0)}=onion", "-listenonion=1", "-torcontrol=127.0.0.1:1"], - "Error: The automatic Tor onion service cannot use a wildcard onion bind because the Tor daemon wouldn't be able to forward incoming connections to us. Use a specific address such as -bind=127.0.0.1:=onion, or disable the service with -listenonion=0.", + WILDCARD_ONION_ERROR, ) + self.log.info("Test wildcard onion bind with -listenonion=0") + self.start_node(0, extra_args=[f"-bind=0.0.0.0:{p2p_port(0)}=onion", "-listenonion=0"]) + self.stop_node(0) + if __name__ == '__main__': BindExtraTest().main() From 8274f68aea17826a7e6920fe32a8f153288b67c6 Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 31 Oct 2026 18:54:00 +0000 Subject: [PATCH 2/2] fix(net): restore the default onion bind as a best-effort listener v23 always bound the default onion target 127.0.0.1: and pointed the automatic onion service at it, also with -bind. #7300 dropped that bind whenever -bind is given, and made every bind failure fatal, so a second instance on the same host (or any node whose default onion port is taken) refused to start, even with -listenonion=0. Add the default onion bind again when there is no -bind=...=onion and -listenonion is enabled, even with -bind, and keep adding it when there are no binds at all. With -bind and -listenonion=0 it is still not added. A failure to bind it is only a warning, unless -listenonion=1 is set explicitly; explicit -bind, -whitebind and =onion binds stay fatal. A default onion bind that failed is dropped from the onion binds, and Tor control, which now starts after connman has bound its listeners, gets it as target only if it is still there. Otherwise Tor control runs without a target, rather than pointing Tor at whatever holds the port (another instance, as on v23, or our own -bind/-whitebind on that address). p2p_permissions.py now gives its whitebind-only node a per-node onion bind, so feature_bind_extra.py is the only test that binds the fixed regtest onion target port 19896. The test framework's p2p_port(), rpc_port() and tor_port() no longer hand it out either: within each of the three port ranges, a node gets PORT_MIN + range * 10000 + n + (20 * seed) % 9979, whose offset is at most 9998, so no port seed and node index ever get offset 9999. A result of 19896 is moved to that last port of its own range. Every other port is unchanged, and the moved one can't collide with any seed's ports. feature_bind_extra.py and interface_zmq.py took extra ports by adding to a p2p_port() result, which could still land on 19896, so they now get each port from p2p_port(). Co-Authored-By: Claude Opus 5.5 --- doc/release-notes-7300.md | 32 +++++++++------- doc/tor.md | 17 +++++---- src/init.cpp | 40 ++++++++++--------- src/net.cpp | 7 +++- src/net.h | 6 +++ test/functional/feature_bind_extra.py | 53 ++++++++++++++++---------- test/functional/interface_zmq.py | 4 +- test/functional/p2p_permissions.py | 3 +- test/functional/test_framework/util.py | 20 ++++++++-- 9 files changed, 114 insertions(+), 68 deletions(-) diff --git a/doc/release-notes-7300.md b/doc/release-notes-7300.md index 7c2dc7d8c625..a3bd6502dac2 100644 --- a/doc/release-notes-7300.md +++ b/doc/release-notes-7300.md @@ -1,21 +1,25 @@ P2P and network changes ----------------------- -* Startup now fails if any configured `-bind`, `-whitebind` or the implicit Tor - onion-service bind (`127.0.0.1:9996` by default) cannot be set up. Dash Core - v23 and earlier started as long as at least one bind succeeded. +* Startup now fails if any configured `-bind` (including `-bind=...=onion`) or + `-whitebind` cannot be set up. Dash Core v23 and earlier started as long as + at least one bind succeeded. The implicit Tor onion-service bind + (`127.0.0.1:9996` by default) is still best effort, as in v23, unless + `-listenonion=1` is set explicitly. -* Nodes configured with `-bind` but no specific `-bind==onion` no - longer create an automatic Tor onion service. The service would have to - forward to a shared bind, which cannot tell Tor-forwarded connections from - direct ones, so onion peers would be treated as clearnet peers (and could be - told the node's clearnet address or get IP-based whitelist permissions). - These nodes still start, and still use Tor for outbound onion connections if - its control port is reachable; the debug log notes that no onion service was - created. Nodes without `-bind`, including those using only `-whitebind`, - continue to get the default onion target. Add a specific - `-bind==onion` to accept incoming Tor connections. Only an - explicit `-listenonion=1` without an onion bind makes startup fail. +* The automatic Tor onion service (`-listenonion`) only forwards to a + dedicated, non-wildcard onion listener that the node actually bound, so peers + arriving through Tor are always recognized as onion peers. Without a + `-bind==onion`, Dash Core adds the default onion bind as v23 did, + also when `-bind` is given; with `-bind` and `-listenonion=0` it no longer + does. If the default onion bind fails, for example because another instance + on the same host holds it, the node logs a warning and creates no onion + service rather than pointing Tor at whatever holds that port. It still + starts and still uses Tor for outbound onion connections if its control port + is reachable; the debug log notes that no onion service was created. With an + explicit `-listenonion=1`, a failed default onion bind makes startup fail. To + run onion services for several instances on one host, give each its own + `-bind=127.0.0.1:=onion`. * A wildcard onion bind such as `-bind=0.0.0.0:=onion` is a startup error unless `-listenonion=0` is set, also with the default `-listenonion`. The node diff --git a/doc/tor.md b/doc/tor.md index c94118c4c6df..9fdf7ef602f0 100644 --- a/doc/tor.md +++ b/doc/tor.md @@ -84,13 +84,16 @@ it requires a Tor connection to work. It can be explicitly disabled with `-torcontrol` and `-torpassword` settings. The automatic service forwards incoming connections to a dedicated, -non-wildcard onion bind. Without `-bind`, Dash Core adds the default onion bind -(`127.0.0.1:9996` on mainnet). With an explicit `-bind`, add one such as -`-bind=127.0.0.1:=onion`; otherwise Dash Core still uses Tor for outbound -onion connections but creates no onion service. Setting `-listenonion=1` -explicitly without an onion bind is a startup error. A wildcard onion bind such -as `-bind=0.0.0.0:=onion` is a startup error unless `-listenonion=0` is -set, because Dash Core can't recognize the Tor peers connecting to it. +non-wildcard onion bind. Unless one is given with `-bind=:=onion`, +Dash Core adds the default onion bind (`127.0.0.1:9996` on mainnet). If that +port is taken, for example by another instance on the same host, no onion +service is created; give each instance its own `-bind=127.0.0.1:=onion`. +With `-bind` and `-listenonion=0`, the default onion bind is not added. If the +default onion bind fails, Dash Core still uses Tor for outbound onion +connections; only an explicit `-listenonion=1` then stops startup. A wildcard +onion bind such as `-bind=0.0.0.0:=onion` is a startup error unless +`-listenonion=0` is set, because Dash Core can't recognize the Tor peers +connecting to it. To see verbose Tor information in the dashd debug log, pass `-debug=tor`. diff --git a/src/init.cpp b/src/init.cpp index d5cf633e5401..38bda8f4212a 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -664,7 +664,7 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-allowprivatenet", strprintf("Allow RFC1918 addresses to be relayed and connected to (default: %u)", DEFAULT_ALLOWPRIVATENET), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-bantime=", strprintf("Default duration (in seconds) of manually configured bans (default: %u)", DEFAULT_MISBEHAVING_BANTIME), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-bind=[:][=onion]", strprintf("Bind to given address and always listen on it (default: 0.0.0.0). Use [host]:port notation for IPv6. Append =onion to tag any incoming connections to that address and port as incoming Tor connections (default: 127.0.0.1:%u=onion, testnet: 127.0.0.1:%u=onion, devnet: 127.0.0.1:%u=onion, regtest: 127.0.0.1:%u=onion). " - "The default onion bind is added only when no -bind is specified. While -listenonion is enabled, a wildcard =onion bind is an error. The automatic Tor onion service needs an =onion bind; without one, no onion service is created, and an explicit -listenonion=1 is an error.", defaultBaseParams->OnionServiceTargetPort(), testnetBaseParams->OnionServiceTargetPort(), devnetBaseParams->OnionServiceTargetPort(), regtestBaseParams->OnionServiceTargetPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); + "Without an =onion bind, the default onion bind is added unless -bind is given and -listenonion is disabled; failing to bind it stops startup only with an explicit -listenonion=1, and otherwise no onion service is created. While -listenonion is enabled, a wildcard =onion bind is an error.", defaultBaseParams->OnionServiceTargetPort(), testnetBaseParams->OnionServiceTargetPort(), devnetBaseParams->OnionServiceTargetPort(), regtestBaseParams->OnionServiceTargetPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); argsman.AddArg("-cjdnsreachable", "If set, then this host is configured for CJDNS (connecting to fc00::/8 addresses would lead us to the CJDNS network, see doc/cjdns.md) (default: 0)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-connect=", "Connect only to the specified node; -noconnect disables automatic connections (the rules for this peer are the same as for -addnode). This option can be specified multiple times to connect to multiple nodes.", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); argsman.AddArg("-discover", "Discover own IP addresses (default: 1 when listening and no -externalip or -proxy)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); @@ -674,7 +674,7 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-fixedseeds", strprintf("Allow fixed seeds if DNS seeds don't provide peers (default: %u)", DEFAULT_FIXEDSEEDS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-forcednsseed", strprintf("Always query for peer addresses via DNS lookup (default: %u)", DEFAULT_FORCEDNSSEED), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-listen", strprintf("Accept connections from outside (default: %u if no -proxy, -connect or -maxconnections=0)", DEFAULT_LISTEN), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); - argsman.AddArg("-listenonion", strprintf("Automatically create Tor onion service if Tor is available and an onion bind exists (see -bind). If set to 1 explicitly, fail to start without one (default: %d)", DEFAULT_LISTEN_ONION), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); + argsman.AddArg("-listenonion", strprintf("Automatically create Tor onion service if Tor is available and the onion bind could be bound (see -bind). If set to 1 explicitly, fail to start if the onion bind cannot be bound (default: %d)", DEFAULT_LISTEN_ONION), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-maxconnections=", strprintf("Maintain at most connections to peers (temporary service connections excluded) (default: %u). This limit does not apply to connections manually added via -addnode or the addnode RPC, which have a separate limit of %u.", DEFAULT_MAX_PEER_CONNECTIONS, MAX_ADDNODE_CONNECTIONS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-maxreceivebuffer=", strprintf("Maximum per-connection receive buffer, *1000 bytes (default: %u)", DEFAULT_MAXRECEIVEBUFFER), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-maxsendbuffer=", strprintf("Maximum per-connection memory usage for the send buffer, *1000 bytes (default: %u)", DEFAULT_MAXSENDBUFFER), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); @@ -2701,30 +2701,24 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) } } - if (connOptions.onion_binds.empty() && connOptions.vBinds.empty()) { + const bool listen_onion{args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)}; + const bool listen_onion_explicit{listen_onion && args.IsArgSet("-listenonion")}; + if (connOptions.onion_binds.empty() && (connOptions.vBinds.empty() || listen_onion)) { connOptions.onion_binds.push_back(DefaultOnionServiceTarget()); + connOptions.onion_bind_optional = !listen_onion_explicit; } - if (args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)) { + std::optional onion_service_target; + if (listen_onion) { // A wildcard listener can't tag the peers Tor forwards to it as onion. if (std::ranges::any_of(connOptions.onion_binds, [](auto& b) { return b.IsBindAny(); })) { return InitError(_("The automatic Tor onion service cannot use a wildcard onion bind because the Tor daemon wouldn't be able to forward incoming connections to us. Use a specific address such as -bind=127.0.0.1:=onion, or disable the service with -listenonion=0.")); } - if (connOptions.onion_binds.empty()) { - // Tor must only forward to a listener that tags its peers as onion. Without one, the - // default -listenonion still uses Tor for outbound onion connections. - if (args.IsArgSet("-listenonion")) { - return InitError(_("The automatic Tor onion service requires a dedicated onion bind. Use a specific address such as -bind=127.0.0.1:=onion, or disable the service with -listenonion=0.")); - } - StartTorControl(std::nullopt); - } else { - const CService& onion_service_target{connOptions.onion_binds[0]}; - if (connOptions.onion_binds.size() > 1) { - InitWarning(strprintf(_("More than one onion bind address is provided. Using %s " - "for the automatically created Tor onion service."), - onion_service_target.ToStringAddrPort())); - } - StartTorControl(onion_service_target); + onion_service_target = connOptions.onion_binds.front(); + if (connOptions.onion_binds.size() > 1) { + InitWarning(strprintf(_("More than one onion bind address is provided. Using %s " + "for the automatically created Tor onion service."), + onion_service_target->ToStringAddrPort())); } } @@ -2787,6 +2781,14 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) return false; } + if (listen_onion) { + // The optional default onion bind may have failed to bind. + if (onion_service_target && !node.connman->HasOnionBind(*onion_service_target)) { + onion_service_target.reset(); + } + StartTorControl(onion_service_target); + } + // ********************************************************* Step 13: finished // At this point, the RPC is "started", but still in warmup, which means it diff --git a/src/net.cpp b/src/net.cpp index 255933b51b35..131d8083899c 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -3986,7 +3986,12 @@ bool CConnman::InitBinds(const Options& options) } } for (const auto& addr_bind : options.onion_binds) { - if (!Bind(addr_bind, BF_REPORT_ERROR | BF_DONT_ADVERTISE, NetPermissionFlags::None)) { + if (options.onion_bind_optional) { + if (!Bind(addr_bind, BF_DONT_ADVERTISE, NetPermissionFlags::None)) { + LogWarning("Unable to bind the default onion service target %s, continuing without it\n", addr_bind.ToStringAddrPort()); + std::erase(m_onion_binds, addr_bind); + } + } else if (!Bind(addr_bind, BF_REPORT_ERROR | BF_DONT_ADVERTISE, NetPermissionFlags::None)) { return false; } } diff --git a/src/net.h b/src/net.h index 59463b88af6e..bef696cc76d5 100644 --- a/src/net.h +++ b/src/net.h @@ -35,6 +35,7 @@ #include #include +#include #include #include #include @@ -1238,6 +1239,9 @@ friend class CNode; std::vector vWhiteBinds; std::vector vBinds; std::vector onion_binds; + /// True if onion_binds holds only the default onion bind and failing to bind it should not + /// stop startup. + bool onion_bind_optional{false}; /// True if the user did not specify -bind= or -whitebind= and thus /// we should bind on `0.0.0.0` (IPv4) and `::` (IPv6). bool bind_on_any; @@ -1315,6 +1319,8 @@ friend class CNode; void Interrupt() EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc); bool GetNetworkActive() const { return fNetworkActive; }; bool GetUseAddrmanOutgoing() const { return m_use_addrman_outgoing; }; + /** Whether we listen on addr and tag the peers connecting there as onion. */ + bool HasOnionBind(const CService& addr) const { return std::ranges::find(m_onion_binds, addr) != m_onion_binds.end(); } void SetNetworkActive(bool active, CMasternodeSync* const mn_sync); bool GetMasternodeThreadActive() const { return m_masternode_thread_active; }; void SetMasternodeThreadActive(bool active) { m_masternode_thread_active = active; }; diff --git a/test/functional/feature_bind_extra.py b/test/functional/feature_bind_extra.py index 480e8ccd6158..270d67f5ae2d 100755 --- a/test/functional/feature_bind_extra.py +++ b/test/functional/feature_bind_extra.py @@ -7,6 +7,8 @@ that bind happens on the expected ports. """ +import socket + from test_framework.netutil import ( addr_to_hex, get_bind_addrs, @@ -14,7 +16,9 @@ from test_framework.test_framework import ( BitcoinTestFramework, ) +from test_framework.test_node import ErrorMatch from test_framework.util import ( + REGTEST_ONION_TARGET_PORT, assert_equal, p2p_port, rpc_port, @@ -38,38 +42,39 @@ def skip_test_if_missing_module(self): def setup_network(self): loopback_ipv4 = addr_to_hex("127.0.0.1") - # Start custom ports by reusing unused p2p ports - port = p2p_port(self.num_nodes) + # Start custom ports by reusing unused p2p ports. Take each one from p2p_port(), + # which never hands out the fixed regtest onion target port. + ports = iter(p2p_port(n) for n in range(self.num_nodes, self.num_nodes + 4)) # Array of tuples [command line arguments, expected bind addresses]. self.expected = [] # Node0, no normal -bind=... with -bind=...=onion, thus only the tor target. + onion = next(ports) self.expected.append( [ - [f"-bind=127.0.0.1:{port}=onion"], - [(loopback_ipv4, port)] + [f"-bind=127.0.0.1:{onion}=onion"], + [(loopback_ipv4, onion)] ], ) - port += 1 # Node1, both -bind=... and -bind=...=onion. + port, onion = next(ports), next(ports) self.expected.append( [ - [f"-bind=127.0.0.1:{port}", f"-bind=127.0.0.1:{port + 1}=onion"], - [(loopback_ipv4, port), (loopback_ipv4, port + 1)] + [f"-bind=127.0.0.1:{port}", f"-bind=127.0.0.1:{onion}=onion"], + [(loopback_ipv4, port), (loopback_ipv4, onion)] ], ) - port += 2 - # Node2, no -bind=...=onion, thus no extra port for Tor target. + # Node2, -bind=... with -listenonion=0 and no -bind=...=onion, thus no extra port for Tor target. + port = next(ports) self.expected.append( [ - [f"-bind=127.0.0.1:{port}"], + [f"-bind=127.0.0.1:{port}", "-listenonion=0"], [(loopback_ipv4, port)] ], ) - port += 1 self.extra_args = list(map(lambda e: e[0], self.expected)) self.setup_nodes() @@ -92,19 +97,27 @@ def run_test(self): self.log.info(f"Checking listening ports of node {i} with {args}") self.check_binds(i, expected_services) - self.log.info("Test -listenonion with a normal bind and no dedicated onion bind") + self.log.info("Test -bind without an onion bind also listens on the default onion target with the default -listenonion") self.stop_node(2) - self.nodes[2].assert_start_raises_init_error( - self.extra_args[2] + ["-listenonion=1", "-torcontrol=127.0.0.1:1"], - "Error: The automatic Tor onion service requires a dedicated onion bind. Use a specific address such as -bind=127.0.0.1:=onion, or disable the service with -listenonion=0.", - ) - - self.log.info("Test -bind without an onion bind starts with the default -listenonion") self.nodes[2].replace_in_config([("listenonion=0\n", "")]) - self.start_node(2, extra_args=self.extra_args[2] + ["-torcontrol=127.0.0.1:1"]) - self.check_binds(2, self.expected[2][1]) + bind_args = [self.extra_args[2][0], "-torcontrol=127.0.0.1:1"] + self.start_node(2, extra_args=bind_args) + self.check_binds(2, self.expected[2][1] + [(addr_to_hex("127.0.0.1"), REGTEST_ONION_TARGET_PORT)]) self.stop_node(2) + with socket.create_server(("127.0.0.1", REGTEST_ONION_TARGET_PORT)): + self.log.info("Test -bind still starts when another process holds the default onion target") + self.start_node(2, extra_args=bind_args) + self.check_binds(2, self.expected[2][1]) + self.stop_node(2) + + self.log.info("Test -listenonion=1 requires the default onion target") + self.nodes[2].assert_start_raises_init_error( + bind_args + ["-listenonion=1"], + f"Error: Unable to bind to 127.0.0.1:{REGTEST_ONION_TARGET_PORT} on this computer.", + match=ErrorMatch.PARTIAL_REGEX, + ) + self.log.info("Test wildcard onion bind with the default -listenonion") self.nodes[2].assert_start_raises_init_error([f"-bind=0.0.0.0:{p2p_port(2)}=onion"], WILDCARD_ONION_ERROR) diff --git a/test/functional/interface_zmq.py b/test/functional/interface_zmq.py index a14089fda676..fc9e34dd4609 100755 --- a/test/functional/interface_zmq.py +++ b/test/functional/interface_zmq.py @@ -542,8 +542,8 @@ def test_multiple_interfaces(self): # chain lengths on node0 and node1; for this test we only need node0, so # we can disable syncing blocks on the setup) subscribers = self.setup_zmq_test([ - ("hashblock", f"tcp://127.0.0.1:{self.zmq_port_base + 1}"), - ("hashblock", f"tcp://127.0.0.1:{self.zmq_port_base + 2}"), + ("hashblock", f"tcp://127.0.0.1:{p2p_port(self.num_nodes + 2)}"), + ("hashblock", f"tcp://127.0.0.1:{p2p_port(self.num_nodes + 3)}"), ], sync_blocks=False) # Generate 1 block in nodes[0] and receive all notifications diff --git a/test/functional/p2p_permissions.py b/test/functional/p2p_permissions.py index 0bddec876e11..8e0e60b952dc 100755 --- a/test/functional/p2p_permissions.py +++ b/test/functional/p2p_permissions.py @@ -62,7 +62,8 @@ def run_test(self): ip_port = "127.0.0.1:{}".format(p2p_port(1)) self.nodes[1].replace_in_config([("bind=127.0.0.1", "whitebind=bloomfilter,forcerelay@" + ip_port)]) self.checkpermission( - ["-whitelist=noban@127.0.0.1"], + # Without a plain -bind, dashd would also bind the fixed default onion target port + ["-whitelist=noban@127.0.0.1", f"-bind=127.0.0.1:{tor_port(1)}=onion"], # Check parameter interaction forcerelay should activate relay ["noban", "bloomfilter", "forcerelay", "relay", "download"]) self.nodes[1].replace_in_config([("whitebind=bloomfilter,forcerelay@" + ip_port, "bind=127.0.0.1")]) diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index da39edee1a57..9678b0ef1516 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -332,6 +332,8 @@ def random_bytes(n): PORT_MIN = int(os.getenv('TEST_RUNNER_PORT_MIN', default=11000)) # The number of ports to "reserve" for p2p, rpc and tor, each PORT_RANGE = 10000 +# Port of dashd's default onion service target on regtest (127.0.0.1:19896) +REGTEST_ONION_TARGET_PORT = 19896 class PortSeed: @@ -364,17 +366,27 @@ def get_rpc_proxy(url: str, node_number: int, *, timeout: int=None, coveragedir: return coverage.AuthServiceProxyWrapper(proxy, url, coverage_logfile) -def p2p_port(n): +def _port(n, range_index): assert n <= MAX_NODES - return PORT_MIN + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + range_start = PORT_MIN + range_index * PORT_RANGE + port = range_start + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + # dashd may bind the regtest default onion service target, so never hand it out. + # No port seed and node index reach the last port of a range, so it can't collide. + if port == REGTEST_ONION_TARGET_PORT: + port = range_start + PORT_RANGE - 1 + return port + + +def p2p_port(n): + return _port(n, 0) def rpc_port(n): - return p2p_port(n) + PORT_RANGE + return _port(n, 1) def tor_port(n): - return p2p_port(n) + PORT_RANGE * 2 + return _port(n, 2) def rpc_url(datadir, i, chain, rpchost=None):