From 5bb8eb3bb5e19a8e1ba9970819e20a17e520c389 Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 3 Oct 2026 23:27:05 -0400 Subject: [PATCH 1/3] fix(net): keep a dedicated onion listener when -bind is given Since bitcoin#22729 (backported in #7300), an explicit -bind= without =onion made the automatically created Tor onion service forward to the first -bind address instead of the dedicated 127.0.0.1:9996 listener. Connections arriving there are not in onion_binds, so peers reaching the node over its onion address were classified by their source address (Tor's loopback, or the node's own IP) rather than as onion. GetLocal() then offered them the node's clearnet addresses in self-advertisement, linking the onion service to the clearnet endpoint. Only skip the default onion bind when -bind is given and -listenonion is disabled, which keeps the ability to avoid the extra bind. Otherwise always add it to onion_binds and use it as the onion-service target. feature_proxy.py starts a node with -listenonion=1 on top of the framework's bind=127.0.0.1; give it an explicit =onion bind on its tor_port() so it does not now also bind the fixed regtest onion target port that feature_bind_extra.py uses. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/init.cpp | 16 +++++++--------- test/functional/feature_bind_extra.py | 23 ++++++++++++++++++++--- test/functional/feature_proxy.py | 5 ++++- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 4683a409b123..b3a0ec3ec2c0 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -653,7 +653,7 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-addnode=", strprintf("Add a node to connect to and attempt to keep the connection open (see the addnode RPC help for more info). This option can be specified multiple times to add multiple nodes; connections are limited to %u at a time and are counted separately from the -maxconnections limit.", MAX_ADDNODE_CONNECTIONS), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); 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)", defaultBaseParams->OnionServiceTargetPort(), testnetBaseParams->OnionServiceTargetPort(), devnetBaseParams->OnionServiceTargetPort(), regtestBaseParams->OnionServiceTargetPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, 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; not added if -bind is given and -listenonion=0)", 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); @@ -2675,17 +2675,15 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) } } - CService onion_service_target; - if (!connOptions.onion_binds.empty()) { - onion_service_target = connOptions.onion_binds.front(); - } else if (!connOptions.vBinds.empty()) { - onion_service_target = connOptions.vBinds.front(); - } else { - onion_service_target = DefaultOnionServiceTarget(); - connOptions.onion_binds.push_back(onion_service_target); + // Never point the onion service at a -bind=... address: incoming Tor connections there + // would not be tagged as such and would be mixed with clearnet ones. + if (connOptions.onion_binds.empty() && + (connOptions.vBinds.empty() || args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION))) { + connOptions.onion_binds.push_back(DefaultOnionServiceTarget()); } if (args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)) { + 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."), diff --git a/test/functional/feature_bind_extra.py b/test/functional/feature_bind_extra.py index ed2328b76f7f..2e21dd19c297 100755 --- a/test/functional/feature_bind_extra.py +++ b/test/functional/feature_bind_extra.py @@ -11,6 +11,7 @@ addr_to_hex, get_bind_addrs, ) +from test_framework.p2p import P2PInterface from test_framework.test_framework import ( BitcoinTestFramework, ) @@ -27,7 +28,7 @@ def set_test_params(self): # Avoid any -bind= on the command line. Force the framework to avoid # adding -bind=127.0.0.1. self.bind_to_localhost_only = False - self.num_nodes = 3 + self.num_nodes = 4 def skip_test_if_missing_module(self): # Due to OS-specific network stats queries, we only run on Linux. @@ -60,15 +61,27 @@ def setup_network(self): ) port += 2 - # Node2, no -bind=...=onion, thus no extra port for Tor target. + # Node2, no -bind=...=onion and -listenonion=0, thus no extra port for Tor target. self.expected.append( [ - [f"-bind=127.0.0.1:{port}"], + [f"-bind=127.0.0.1:{port}", "-listenonion=0"], [(loopback_ipv4, port)] ], ) port += 1 + # Node3, no -bind=...=onion but -listenonion=1, thus the default Tor target + # 127.0.0.1:19896 (regtest) is bound in addition, so that incoming Tor + # connections are not mixed with the ones on -bind=... Point -torcontrol at + # an unused port so that no onion service is created via a local Tor. + self.expected.append( + [ + [f"-bind=127.0.0.1:{port}", "-listenonion=1", f"-torcontrol=127.0.0.1:{port + 1}"], + [(loopback_ipv4, port), (loopback_ipv4, 19896)] + ], + ) + port += 2 + self.extra_args = list(map(lambda e: e[0], self.expected)) self.setup_nodes() @@ -87,5 +100,9 @@ def run_test(self): binds = set(filter(lambda e: e[1] != rpc_port(i), binds)) assert_equal(binds, set(expected_services)) + self.log.info("Checking that a connection to the default Tor target of node 3 is tagged as onion") + self.nodes[3].add_p2p_connection(P2PInterface(), dstport=19896) + assert_equal([peer["network"] for peer in self.nodes[3].getpeerinfo()], ["onion"]) + if __name__ == '__main__': BindExtraTest().main() diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index 8f88f28675e8..a730d3903838 100755 --- a/test/functional/feature_proxy.py +++ b/test/functional/feature_proxy.py @@ -49,6 +49,7 @@ from test_framework.util import ( assert_equal, p2p_port, + tor_port, ) from test_framework.netutil import test_ipv6_local, test_unix_socket @@ -436,7 +437,9 @@ def networks_dict(d): self.nodes[1].assert_start_raises_init_error(expected_msg=msg) self.log.info("Test passing -onlynet=onion without -proxy or -onion but with -listenonion=1 is ok") - self.start_node(1, extra_args=["-onlynet=onion", "-listenonion=1"]) + # The framework's bind=127.0.0.1 would otherwise make the node also bind the + # fixed default onion target port, which other tests may be using. + self.start_node(1, extra_args=["-onlynet=onion", "-listenonion=1", f"-bind=127.0.0.1:{tor_port(1)}=onion"]) self.stop_node(1) self.log.info("Test passing unknown network to -onlynet raises expected init error") From 72b3c713a5447e0a825a392b1766596fceba1a65 Mon Sep 17 00:00:00 2001 From: pasta Date: Sat, 3 Oct 2026 23:27:05 -0400 Subject: [PATCH 2/3] docs: correct when the implicit onion bind is added in release notes -listenonion=0 on its own never dropped the 127.0.0.1:9996 bind. Describe the actual rule: it is skipped only when -bind is given together with -listenonion=0, and -whitebind alone does not affect it. Co-Authored-By: Claude Opus 5.5 (1M context) --- doc/release-notes-7300.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/release-notes-7300.md b/doc/release-notes-7300.md index a9d63a16eaf2..d5c45bd98045 100644 --- a/doc/release-notes-7300.md +++ b/doc/release-notes-7300.md @@ -3,6 +3,10 @@ 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. Use - `-bind=:=onion` to move the onion bind, or `-listenonion=0` to - drop it, if the default address is not available on your system. + v23 and earlier started as long as at least one bind succeeded. + +* The implicit onion-service bind is added whenever no `-bind=...=onion` is + given, unless `-bind` is given and `-listenonion=0`. Use + `-bind=:=onion` to move it. To drop it, combine an explicit + `-bind=` with `-listenonion=0`. `-listenonion=0` alone or `-whitebind` + alone does not drop it, and `-listen=0` disables all binds. From 5f40d56f4ddb3d14a2e91569056ca0cb8d0d196c Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 6 Oct 2026 02:41:05 +0700 Subject: [PATCH 3/3] partial Merge bitcoin/bitcoin#36170: net: require a dedicated bind for automatic Tor --- doc/release-notes-7300.md | 14 ++++++---- doc/tor.md | 3 +++ src/init.cpp | 14 ++++++---- test/functional/feature_bind_extra.py | 38 +++++++++++++-------------- test/functional/feature_proxy.py | 2 -- 5 files changed, 40 insertions(+), 31 deletions(-) diff --git a/doc/release-notes-7300.md b/doc/release-notes-7300.md index d5c45bd98045..bbae7e647268 100644 --- a/doc/release-notes-7300.md +++ b/doc/release-notes-7300.md @@ -5,8 +5,12 @@ 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. -* The implicit onion-service bind is added whenever no `-bind=...=onion` is - given, unless `-bind` is given and `-listenonion=0`. Use - `-bind=:=onion` to move it. To drop it, combine an explicit - `-bind=` with `-listenonion=0`. `-listenonion=0` alone or `-whitebind` - alone does not drop it, and `-listen=0` disables all binds. +* 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. diff --git a/doc/tor.md b/doc/tor.md index 3cd32aee785f..d617a58e510d 100644 --- a/doc/tor.md +++ b/doc/tor.md @@ -83,6 +83,9 @@ 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. + To see verbose Tor information in the dashd debug log, pass `-debug=tor`. ### Control Port diff --git a/src/init.cpp b/src/init.cpp index b3a0ec3ec2c0..f8627f51fc19 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -653,7 +653,8 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-addnode=", strprintf("Add a node to connect to and attempt to keep the connection open (see the addnode RPC help for more info). This option can be specified multiple times to add multiple nodes; connections are limited to %u at a time and are counted separately from the -maxconnections limit.", MAX_ADDNODE_CONNECTIONS), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); 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; not added if -bind is given and -listenonion=0)", defaultBaseParams->OnionServiceTargetPort(), testnetBaseParams->OnionServiceTargetPort(), devnetBaseParams->OnionServiceTargetPort(), regtestBaseParams->OnionServiceTargetPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, 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); 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); @@ -2675,14 +2676,17 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) } } - // Never point the onion service at a -bind=... address: incoming Tor connections there - // would not be tagged as such and would be mixed with clearnet ones. - if (connOptions.onion_binds.empty() && - (connOptions.vBinds.empty() || args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION))) { + if (connOptions.onion_binds.empty() && connOptions.vBinds.empty()) { connOptions.onion_binds.push_back(DefaultOnionServiceTarget()); } 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.")); + } + 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 " diff --git a/test/functional/feature_bind_extra.py b/test/functional/feature_bind_extra.py index 2e21dd19c297..5470c3df9c15 100755 --- a/test/functional/feature_bind_extra.py +++ b/test/functional/feature_bind_extra.py @@ -11,7 +11,6 @@ addr_to_hex, get_bind_addrs, ) -from test_framework.p2p import P2PInterface from test_framework.test_framework import ( BitcoinTestFramework, ) @@ -28,7 +27,7 @@ def set_test_params(self): # Avoid any -bind= on the command line. Force the framework to avoid # adding -bind=127.0.0.1. self.bind_to_localhost_only = False - self.num_nodes = 4 + self.num_nodes = 3 def skip_test_if_missing_module(self): # Due to OS-specific network stats queries, we only run on Linux. @@ -61,27 +60,15 @@ def setup_network(self): ) port += 2 - # Node2, no -bind=...=onion and -listenonion=0, thus no extra port for Tor target. + # Node2, no -bind=...=onion, thus no extra port for Tor target. self.expected.append( [ - [f"-bind=127.0.0.1:{port}", "-listenonion=0"], + [f"-bind=127.0.0.1:{port}"], [(loopback_ipv4, port)] ], ) port += 1 - # Node3, no -bind=...=onion but -listenonion=1, thus the default Tor target - # 127.0.0.1:19896 (regtest) is bound in addition, so that incoming Tor - # connections are not mixed with the ones on -bind=... Point -torcontrol at - # an unused port so that no onion service is created via a local Tor. - self.expected.append( - [ - [f"-bind=127.0.0.1:{port}", "-listenonion=1", f"-torcontrol=127.0.0.1:{port + 1}"], - [(loopback_ipv4, port), (loopback_ipv4, 19896)] - ], - ) - port += 2 - self.extra_args = list(map(lambda e: e[0], self.expected)) self.setup_nodes() @@ -100,9 +87,22 @@ def run_test(self): binds = set(filter(lambda e: e[1] != rpc_port(i), binds)) assert_equal(binds, set(expected_services)) - self.log.info("Checking that a connection to the default Tor target of node 3 is tagged as onion") - self.nodes[3].add_p2p_connection(P2PInterface(), dstport=19896) - assert_equal([peer["network"] for peer in self.nodes[3].getpeerinfo()], ["onion"]) + self.log.info("Test -listenonion with a normal bind and no dedicated onion bind") + 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 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"]) + + self.log.info("Test wildcard onion bind with -listenonion=1") + 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.", + ) if __name__ == '__main__': BindExtraTest().main() diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index a730d3903838..d249b74660b2 100755 --- a/test/functional/feature_proxy.py +++ b/test/functional/feature_proxy.py @@ -437,8 +437,6 @@ def networks_dict(d): self.nodes[1].assert_start_raises_init_error(expected_msg=msg) self.log.info("Test passing -onlynet=onion without -proxy or -onion but with -listenonion=1 is ok") - # The framework's bind=127.0.0.1 would otherwise make the node also bind the - # fixed default onion target port, which other tests may be using. self.start_node(1, extra_args=["-onlynet=onion", "-listenonion=1", f"-bind=127.0.0.1:{tor_port(1)}=onion"]) self.stop_node(1)