Repository navigation
fix(net): keep a dedicated onion listener when -bind is given and correct the bind release note - #7786
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 🧰 Additional context used📚 Code guidelines (1)No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (5)
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review. WalkthroughWhen Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~12 minutes Merge Risk: ⚪ Minimal · up to With onion listening enabled, explicit binds must include a dedicated non-wildcard onion bind; this startup requirement is documented and covered by functional-test expectations. No actionable merge-blocking risk remains. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change removes a privacy-sensitive fallback and fails closed when a dedicated onion listener is missing. Existing configurations using explicit binds may require an update before restarting, including nodes without Tor configured. No introduced security finding was identified, but live Tor recovery and deployment behavior were not exercised. Retained concerns Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 3 files. (2 skipped: 2 unsupported.)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Final review complete — no blockers (commit 5f40d56) · triage: low |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
Static verification at f8b56ba confirms that the automatic Tor service targets an onion-classified listener, the explicit-bind opt-out remains valid, and the documentation and regression tests match the implemented behavior. Both reviewers' clean assessments are supported by the source; no actionable in-scope defects or prerequisite claims were identified. No builds or tests were run in this lane: the supplied CI snapshot shows successful container-build and formatting checks, queued manifest jobs, and no completed functional-test results.
Review provenance
Source: reviewer 1: glm-5.3-flash (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
lowbygpt-6.1-sol(effort low) — The diff is a small, contained listener-selection fix in src/init.cpp with focused functional-test updates and a release-note correction, not a large or intricate change to a critical surface. - Phase 1 reviewers:
glm-5.3-flash— general (completed, effort high); agentphase1-reviewer - Phase 1 model:
glm-5.3-flash— zai quota: 5h 99% left, weekly 69% left; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 15% left, 5h 100% left) - Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— dash-core-commit-history (completed, effort medium); agentphase2-reviewer
Since bitcoin#22729 (backported in dashpay#7300), an explicit -bind=<addr> 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) <noreply@anthropic.com>
-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) <noreply@anthropic.com>
f8b56ba to
72b3c71
Compare
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Verified the complete diff at 5f40d56 and found no blocking correctness defects: automatic Tor now requires a dedicated, non-wildcard onion bind when explicit binds are configured. Four non-blocking issues remain concerning the stale PR description, omitted early validation, undocumented partial-backport exclusions, and wildcard-bind release notes. This was static verification only; the supplied CI snapshot shows lint passing, with source builds queued or running and completed build/test validation unavailable.
🟡 4 suggestion(s)
Review provenance
Source: reviewer 1: glm-5.3-flash (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: backport-reviewer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: backport-reviewer); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
lowbygpt-6.1-sol(effort low) — The diff makes a small, contained change to Tor listener initialization and startup validation in src/init.cpp, with focused tests and documentation rather than intricate changes to a critical surface. - Phase 1 reviewers:
glm-5.3-flash— general (completed, effort high); agentphase1-reviewer - Phase 1 model:
glm-5.3-flash— zai quota: 5h 98% left, weekly 99% left; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 15% left, 5h 100% left) - Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— backport-reviewer (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— dash-core-commit-history (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— backport-reviewer (completed, effort medium); agentphase2-reviewer,gpt-6.1-sol— dash-core-commit-history (completed, effort medium); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/init.cpp`:
- [SUGGESTION] src/init.cpp:2679-2689: PR description no longer matches the head commit's behavior
The PR description still says explicit -bind configurations retain the implicit onion listener and fail only if its port is unavailable. At this head, the default listener is added only when both bind vectors are empty; an explicit normal bind without an onion bind instead causes an unconditional startup error when -listenonion is enabled. Wildcard onion binds are also rejected now, although the description says that change is deferred. The documented node-3 regression scenario and its network=='onion' assertion were removed by the final commit. Update the implementation rationale, upstream relationship, breaking changes, and test evidence to describe the final policy, distinguishing earlier implementation results from validation of this head. These differences change the operator migration requirements, not just the wording.
- [SUGGESTION] src/init.cpp:2683-2685: Declared-partial omission: early Tor bind validation
Upstream bitcoin#36170 rejects explicit bind configurations without an =onion suffix in AppInitParameterInteraction, while retaining the defensive check here. Dash includes only this late check: LoadChainstate and wallet loading occur before it. Consequently, a configuration already known to be invalid can spend substantial time loading chainstate and wallets before reporting the dedicated-bind error. Carry the upstream early check, or explain its intentional exclusion from this partial backport. The current check preserves eventual rejection and the privacy fix; this is not a blocker or a missing API prerequisite.
In `test/functional/feature_bind_extra.py`:
- [SUGGESTION] test/functional/feature_bind_extra.py:90-105: Declared-partial omission: two upstream Tor test adaptations
The upstream diff also adds a dedicated onion bind in feature_torcontrol.py::restart_with_mock() and in the Tor-only scenario in p2p_private_broadcast.py. Their containing tests were introduced by bitcoin#34158 (569383356ecd5baa65a87563d776809a63f8f7a3) and bitcoin#29415 (e74d54e04896a86cad4e4b1bd9641afcc3a026c2), respectively, and neither file exists in this Dash base or head. This does not break an existing Dash test or establish a production dependency. However, CONTRIBUTING.md requires Bitcoin backports to explain omitted hunks and tests, and the final commit has only a subject while the PR description describes the superseded implementation. Document these two exclusions and their missing containing tests in the PR description or a backport note, and identify the upstream source revision used. Adding the unrelated prerequisite features is not required to resolve this documentation issue.
In `doc/release-notes-7300.md`:
- [SUGGESTION] doc/release-notes-7300.md:8-16: Release-note adaptation omits wildcard onion binds
The release note explains rejection when no onion bind is supplied, but omits rejection of an existing wildcard onion bind such as -bind=0.0.0.0:<port>=onion. That configuration satisfies the note's stated requirement for an =onion bind yet now fails startup when -listenonion is enabled. Upstream explicitly mentions shared and wildcard binds, and this head's help text and doc/tor.md require a non-wildcard address. Add the wildcard compatibility change here and state that the dedicated onion bind must use a specific, non-wildcard address.
| 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:<port>=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:<port>=onion, or disable the service with -listenonion=0.")); | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: PR description no longer matches the head commit's behavior
The PR description still says explicit -bind configurations retain the implicit onion listener and fail only if its port is unavailable. At this head, the default listener is added only when both bind vectors are empty; an explicit normal bind without an onion bind instead causes an unconditional startup error when -listenonion is enabled. Wildcard onion binds are also rejected now, although the description says that change is deferred. The documented node-3 regression scenario and its network=='onion' assertion were removed by the final commit. Update the implementation rationale, upstream relationship, breaking changes, and test evidence to describe the final policy, distinguishing earlier implementation results from validation of this head. These differences change the operator migration requirements, not just the wording.
source: glm-5.3-flash (phase1-reviewer: general)
| 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:<port>=onion, or disable the service with -listenonion=0.")); |
There was a problem hiding this comment.
🟡 Suggestion: Declared-partial omission: early Tor bind validation
Upstream bitcoin#36170 rejects explicit bind configurations without an =onion suffix in AppInitParameterInteraction, while retaining the defensive check here. Dash includes only this late check: LoadChainstate and wallet loading occur before it. Consequently, a configuration already known to be invalid can spend substantial time loading chainstate and wallets before reporting the dedicated-bind error. Carry the upstream early check, or explain its intentional exclusion from this partial backport. The current check preserves eventual rejection and the privacy fix; this is not a blocker or a missing API prerequisite.
source: gpt-6.1-sol (phase2-reviewer: general, backport-reviewer, dash-core-commit-history)
| 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:<port>=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:<port>=onion, or disable the service with -listenonion=0.", | ||
| ) |
There was a problem hiding this comment.
🟡 Suggestion: Declared-partial omission: two upstream Tor test adaptations
The upstream diff also adds a dedicated onion bind in feature_torcontrol.py::restart_with_mock() and in the Tor-only scenario in p2p_private_broadcast.py. Their containing tests were introduced by bitcoin#34158 (5693833) and bitcoin#29415 (e74d54e), respectively, and neither file exists in this Dash base or head. This does not break an existing Dash test or establish a production dependency. However, CONTRIBUTING.md requires Bitcoin backports to explain omitted hunks and tests, and the final commit has only a subject while the PR description describes the superseded implementation. Document these two exclusions and their missing containing tests in the PR description or a backport note, and identify the upstream source revision used. Adding the unrelated prerequisite features is not required to resolve this documentation issue.
source: glm-5.3-flash (phase1-reviewer: general); gpt-6.1-sol (phase2-reviewer: general, backport-reviewer, dash-core-commit-history)
| * Nodes configured with `-bind` but no specific `-bind=<addr:port>=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=<addr:port>=onion` to accept incoming Tor connections, or set | ||
| `-listenonion=0` to disable automatic onion service creation. |
There was a problem hiding this comment.
🟡 Suggestion: Release-note adaptation omits wildcard onion binds
The release note explains rejection when no onion bind is supplied, but omits rejection of an existing wildcard onion bind such as -bind=0.0.0.0:=onion. That configuration satisfies the note's stated requirement for an =onion bind yet now fails startup when -listenonion is enabled. Upstream explicitly mentions shared and wildcard binds, and this head's help text and doc/tor.md require a non-wildcard address. Add the wildcard compatibility change here and state that the dedicated onion bind must use a specific, non-wildcard address.
source: gpt-6.1-sol (phase2-reviewer: general, backport-reviewer, dash-core-commit-history)
…given and correct the bind release note 5f40d56 partial Merge bitcoin#36170: net: require a dedicated bind for automatic Tor (Konstantin Akimov) 72b3c71 docs: correct when the implicit onion bind is added in release notes (pasta) 5bb8eb3 fix(net): keep a dedicated onion listener when -bind is given (pasta) Pull request description: ## Issue being fixed or feature implemented The backport of bitcoin#22729 (#7300) changed what happens when a node is started with an explicit `-bind=<addr>` but no `-bind=...=onion`. Before, Dash Core always added the dedicated onion listener `127.0.0.1:9996` (`19996` testnet, `19896` regtest) and pointed the automatically created Tor onion service (`-listenonion`, on by default) at it. Now it binds no onion listener and points the onion service at the first `-bind` address. That listener is not in `onion_binds`, so every peer Tor forwards to it is classified by its source address: Tor's loopback address, or the node's own IP when `-bind` is a routable address. It is not classified as an onion peer. As a result, a node that runs an onion service next to a clearnet address (`-externalip`, or a discovered/bound routable address) advertises its clearnet address to peers that reached it through its `.onion` address. That links the two identities, which the privacy-network check in `GetLocal()` is meant to prevent. Those peers also lose the rest of the onion-specific handling, such as `getpeerinfo` `network` and eviction protection for onion peers. The release note added for #7300 has a related problem. It says `-listenonion=0` drops the implicit onion bind, but it never did: the bind is added before `-listenonion` is looked at. An operator whose `127.0.0.1:9996` is taken who follows the note still gets a node that refuses to start. Both changes are only in v24.0.0-rc.1 and rc.2. ### Why this is a real problem Code path at c14104b: - [`src/init.cpp#L2678-L2695`](https://github.com/dashpay/dash/blob/c14104b172a2/src/init.cpp#L2678-L2695): with `vBinds` non-empty and no `=onion` bind, `onion_service_target = connOptions.vBinds.front()` ([L2682](https://github.com/dashpay/dash/blob/c14104b172a2/src/init.cpp#L2682)). It is not pushed into `onion_binds`, and `StartTorControl(onion_service_target)` still runs ([L2694](https://github.com/dashpay/dash/blob/c14104b172a2/src/init.cpp#L2694)). - [`src/net.h#L1291`](https://github.com/dashpay/dash/blob/c14104b172a2/src/net.h#L1291) copies only `onion_binds` into `m_onion_binds`. [`src/net.cpp#L1977`](https://github.com/dashpay/dash/blob/c14104b172a2/src/net.cpp#L1977) sets `inbound_onion` only for listeners in `m_onion_binds`. - [`src/net.cpp#L584-L592`](https://github.com/dashpay/dash/blob/c14104b172a2/src/net.cpp#L584-L592): without `m_inbound_onion`, `ConnectedThroughNetwork()` and `IsConnectedThroughPrivacyNet()` fall back to the peer's source address. The privacy filter in `GetLocal()` ([L186-L193](https://github.com/dashpay/dash/blob/c14104b172a2/src/net.cpp#L186-L193)) then skips the node's own onion address for that peer and lets the clearnet one through. I reproduced this on the unfixed build with a small functional-test-style script. A minimal fake Tor control port accepts `PROTOCOLINFO`/`AUTHENTICATE`/`ADD_ONION` and records the target dashd asks Tor to forward to. A P2P peer then connects to exactly that target, as Tor does for every inbound onion connection, and sends `getaddr`. The node runs with `-bind=127.0.0.1:<port> -listenonion=1 -externalip=1.2.3.4`. Unfixed (c14104b): ``` Tor control commands received: ['PROTOCOLINFO 1', 'AUTHENTICATE', 'GETINFO net/listeners/socks', 'ADD_ONION NEW:ED25519-V3 Port=19899,127.0.0.1:17626'] onion-service target handed to Tor: 127.0.0.1:17626 getnetworkinfo localaddresses: [{'address': '1.2.3.4', 'port': 17626, 'score': 4}, {'address': 'pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion', 'port': 19899, 'score': 4}] Connecting a peer to 127.0.0.1:17626, as Tor would for an inbound onion connection getpeerinfo: addr=127.0.0.1:63442 addrbind=127.0.0.1:17626 network=not_publicly_routable addresses self-advertised to this peer: ['1.2.3.4:17626'] ``` The onion service forwards to the clearnet listener, the forwarded peer is `not_publicly_routable` instead of `onion`, and it is sent the node's clearnet address `1.2.3.4`. In the fixed run the list is empty for two reasons. The privacy filter withholds `1.2.3.4` from an onion peer, and the test `P2PInterface` never sends `sendaddrv2`, so the node cannot send it the v3 onion address either. The fixed run also prints a `wait_until() failed` line after the 10 s wait for addresses; that line is left out of the excerpt above. With this PR: ``` Tor control commands received: ['PROTOCOLINFO 1', 'AUTHENTICATE', 'GETINFO net/listeners/socks', 'ADD_ONION NEW:ED25519-V3 Port=19899,127.0.0.1:19896'] onion-service target handed to Tor: 127.0.0.1:19896 getnetworkinfo localaddresses: [{'address': '1.2.3.4', 'port': 18132, 'score': 4}, {'address': 'pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion', 'port': 19899, 'score': 4}] Connecting a peer to 127.0.0.1:19896, as Tor would for an inbound onion connection getpeerinfo: addr=127.0.0.1:50581 addrbind=127.0.0.1:19896 network=onion addresses self-advertised to this peer: [] ``` <details><summary>Reproduction script (run with <code>WORKTREE=<src root> python3 repro_tor_target.py --configfile=<src root>/test/config.ini</code>)</summary> ```python #!/usr/bin/env python3 """Reproduction: with an explicit -bind (no =onion) and -listenonion=1, which listener does dashd hand to Tor as the onion-service target, and how is a peer that Tor forwards to that target classified? A minimal fake Tor control port answers PROTOCOLINFO/AUTHENTICATE/ADD_ONION and records the target from the ADD_ONION command. The test then connects a P2P peer to exactly that target (what Tor does for every inbound onion connection), prints getpeerinfo's network field, sends getaddr and prints the addresses the node self-advertises to that peer. Run from anywhere: python3 repro_tor_target.py --configfile=<worktree>/test/config.ini """ import os import socket import sys import threading WORKTREE = os.environ["WORKTREE"] sys.path.insert(0, os.path.join(WORKTREE, "test", "functional")) from test_framework.messages import msg_getaddr # noqa: E402 from test_framework.p2p import P2PInterface # noqa: E402 from test_framework.test_framework import BitcoinTestFramework # noqa: E402 from test_framework.util import p2p_port # noqa: E402 ONION_ID = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd" EXTERNAL_IP = "1.2.3.4" class FakeTorControl: def __init__(self): self.srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.srv.bind(("127.0.0.1", 0)) self.srv.listen(1) self.port = self.srv.getsockname()[1] self.commands = [] self.target = None threading.Thread(target=self._serve, daemon=True).start() def _serve(self): conn, _ = self.srv.accept() f = conn.makefile("rwb", buffering=0) while True: line = f.readline() if not line: return cmd = line.decode().strip() self.commands.append(cmd) if cmd.startswith("PROTOCOLINFO"): f.write(b'250-PROTOCOLINFO 1\r\n250-AUTH METHODS=NULL\r\n250-VERSION Tor="0.4.8.12"\r\n250 OK\r\n') elif cmd.startswith("ADD_ONION"): # ADD_ONION NEW:ED25519-V3 Port=<virtport>,<target> self.target = cmd.split("Port=")[1].split(",", 1)[1] f.write(f"250-ServiceID={ONION_ID}\r\n250-PrivateKey=ED25519-V3:AAAA\r\n250 OK\r\n".encode()) else: f.write(b"250 OK\r\n") class AddrCollector(P2PInterface): def __init__(self): super().__init__() self.addrs = [] def on_addr(self, message): self.addrs += [f"{a.ip}:{a.port}" for a in message.addrs] def on_addrv2(self, message): self.addrs += [f"{a.ip}:{a.port}" for a in message.addrs] class ReproTorTarget(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.bind_to_localhost_only = False self.num_nodes = 1 def setup_network(self): self.tor = FakeTorControl() self.extra_args = [[ f"-bind=127.0.0.1:{p2p_port(0)}", "-listenonion=1", f"-torcontrol=127.0.0.1:{self.tor.port}", f"-externalip={EXTERNAL_IP}", "-debug=tor", ]] self.setup_nodes() def run_test(self): node = self.nodes[0] self.log.info(f"dashd args: {self.extra_args[0]}") self.wait_until(lambda: self.tor.target is not None) self.log.info(f"Tor control commands received: {self.tor.commands}") self.log.info(f"onion-service target handed to Tor: {self.tor.target}") self.log.info(f"getnetworkinfo localaddresses: {node.getnetworkinfo()['localaddresses']}") self.generate(node, 1, sync_fun=self.no_op) # leave IBD so self-advertisement happens host, port = self.tor.target.rsplit(":", 1) self.log.info(f"Connecting a peer to {host}:{port}, as Tor would for an inbound onion connection") peer = node.add_p2p_connection(AddrCollector(), dstaddr=host, dstport=int(port)) info = [p for p in node.getpeerinfo() if p["addrbind"] == f"{host}:{port}"][0] self.log.info(f"getpeerinfo: addr={info['addr']} addrbind={info['addrbind']} network={info['network']}") peer.send_and_ping(msg_getaddr()) try: self.wait_until(lambda: len(peer.addrs) > 0, timeout=10) except AssertionError: pass self.log.info(f"addresses self-advertised to this peer: {peer.addrs}") if __name__ == "__main__": ReproTorTarget().main() ``` </details> Release-note claim, unfixed build, with `127.0.0.1:19896` held by another process: ``` $ src/dashd -regtest -datadir=$D -listenonion=0 -port=18555 -rpcport=18556 -printtoconsole=0 Error: Unable to bind to 127.0.0.1:19896 on this computer. Dash Core is probably already running. Error: Failed to listen on any port. Use -listen=0 if you want this. exit=1 ``` ### Why it matters Running an onion service alongside a clearnet address is a supported setup. Operators who also pass an explicit `-bind`, which is common on servers and masternodes, would silently lose the separation between the two once v24.0.0 ships: any peer that connects to the `.onion` address can learn the node's clearnet IP from self-advertisement. Exposure needs a working Tor control connection and a routable local address (`-externalip`, or discovery or a routable `-bind`), so it does not affect every node. Nodes with `-bind` that do not use Tor are unaffected either way. The release-note error is only an operational trap, but it would ship in the v24.0.0 notes. ## What was done? - `src/init.cpp`: drop the branch that used `vBinds.front()` as the onion-service target, so the target is always the first `onion_binds` entry. Without a `-bind=...=onion`, the default onion target is now added to `onion_binds` and used as the Tor target unless `-bind` is given **and** `-listenonion` is disabled. In that one case no onion bind is added and Tor control is not started. `-listenonion` is read after parameter interaction, so `-listen=0` (which soft-sets `-listenonion=0`) is handled as before. The `-bind` help text now mentions the exception. - `doc/release-notes-7300.md`: describe the actual rule. The implicit bind is added unless `-bind` is given together with `-listenonion=0`. `-bind=...=onion` moves it, `-listenonion=0` alone or `-whitebind` alone keeps it, and `-listen=0` disables all binds. - `test/functional/feature_bind_extra.py`: node 2 (explicit `-bind`, no extra port) now says `-listenonion=0` explicitly. Before, it relied on the framework's config default. A new node 3 with explicit `-bind` and `-listenonion=1` must listen on both its `-bind` port and `127.0.0.1:19896`, and a peer connecting to `19896` must show up with `network` `onion`. Node 3's `-torcontrol` points at an unused port so the test never creates an onion service through a Tor instance running on the test host. - `test/functional/feature_proxy.py`: one step starts node 1 with `-listenonion=1` on top of the framework's `bind=127.0.0.1`. With this PR that node would also bind the fixed regtest onion target `127.0.0.1:19896`, which is the port `feature_bind_extra.py` node 3 holds, and since #7300 a failed bind stops startup. Under `test_runner.py -jN` the two tests could then collide. The step now passes `-bind=127.0.0.1:<tor_port(1)>=onion`, the same per-node port the framework uses when it adds the binds itself. I kept this local to the one test rather than teaching `TestNode.start()` a new rule, because it is the only test that enables `-listenonion` together with an explicit bind. ### Why this is the correct minimal fix The root cause is that the onion service can be pointed at a listener that is not tagged as onion. The only listener that can be both the Tor target and correctly tagged is one in `onion_binds`, so the target must always come from `onion_binds`. The alternative of adding `vBinds.front()` to `onion_binds` would tag every clearnet peer on that port as onion, which leaks the onion address to clearnet peers and distorts eviction, so it is not an option. Two other designs would avoid the multi-instance cost described under Breaking Changes. Both are reasonable, and the choice is up to maintainers: - When `-bind` is given without `=onion` and `-listenonion` is not set explicitly, soft-set `-listenonion=0` during parameter interaction. This keeps rc.1's "respect `-bind`" behavior and avoids the extra bind. The cost is that nodes upgrading from v23 with `-bind` and an automatic onion service would silently lose that onion service. - Keep the default onion bind, but treat a failure to bind it as non-fatal and skip Tor control in that case. This brings back v23's tolerance for this one bind and gives up part of #7300's "fail on any bind error" rule. This PR does neither. It keeps v23's behavior for the onion service, plus the stricter bind handling from #7300. bitcoin#22729's goal was to let users avoid the extra `127.0.0.1:9996` bind. This PR keeps that, but only when the user also says they don't want an onion service (`-listenonion=0`). Without an onion service there is nothing to tag. Nothing changes for nodes without `-bind`, with `-bind=...=onion`, or with `-listen=0`. The stricter "fail startup if any bind fails" part of bitcoin#22729 is unchanged too. ### Relation to upstream Bitcoin Core has the same bug. The open upstream fix, bitcoin#36170 ("net: require a dedicated bind for automatic Tor"), has an Approach ACK. It also removes the `vBinds.front()` fallback, but handles the `-bind` + `-listenonion` case differently: instead of adding the default onion bind, it refuses to start until the operator adds `-bind=<addr>=onion` or `-listenonion=0`. That turns every existing `-bind` setup with the default `-listenonion=1` into a startup error, including many masternodes, so this PR keeps v23's default onion bind for that case. The `src/init.cpp` block uses the same layout as bitcoin#36170: the default bind is added up front under one condition, and the Tor target is always `onion_binds[0]` inside the `-listenonion` branch. Backporting bitcoin#36170 later then only changes that condition and adds its errors. bitcoin#36170 also rejects a wildcard `=onion` bind. That is left to the backport, since it is a separate new startup error. This should also go to the v24.0.x release branch, since the behavior was introduced in v24.0.0-rc.1. ## How Has This Been Tested? macOS arm64, `--enable-debug` build, functional tests run outside any sandbox. - Reproduction script above: unfixed build shows the onion target `127.0.0.1:<bind port>`, peer `network=not_publicly_routable`, and the peer is sent `1.2.3.4`. Fixed build shows target `127.0.0.1:19896`, peer `network=onion`, and no clearnet address is sent. - `feature_bind_extra.py` with the new cases. The test is Linux-only (it reads `/proc/net/tcp`), so I ran it locally through a wrapper that swaps `get_bind_addrs()` for an `lsof`-based equivalent and lifts the platform skip. The test itself is unchanged. Nodes 0–2 pass on both builds. - Unfixed (only the `src/init.cpp` hunks reverted): fails at node 3 with `AssertionError: not({('7f000001', 17013)} == {('7f000001', 17013), ('7f000001', 19896)})`. - Fixed: passes, including the `network == "onion"` check. - `feature_proxy.py` with `--nocleanup`, checking the `Bound to` lines in node 1's `debug.log`: - Fixed build without the `feature_proxy.py` change: node 1 binds `127.0.0.1:19896`. This is the collision described above. - Fixed build with the change: node 1 binds only its p2p port and its `tor_port()`, and nothing on `19896`. The test passes. - Unfixed build with the change: same binds, and the test passes. - Release-note cases on the fixed build, with `127.0.0.1:19896` held by another process: `-listenonion=0` alone still fails with `Unable to bind to 127.0.0.1:19896`, as the corrected note says. `-bind=127.0.0.1:18555 -listenonion=0` starts. - `test/functional/test_runner.py feature_proxy.py p2p_eviction.py rpc_net.py` (after the test changes) and earlier `feature_config_args.py`: all passed. `feature_config_args.py` failed once in a `-j4` run with `Unable to start HTTP server` (an RPC port collision with another run on the same host) and passed on rerun. `feature_bind_extra.py`, `feature_bind_port_*.py` and `rpc_bind.py` are skipped on macOS by the runner. CI covers them on Linux. - `test/lint/lint-whitespace.py` and flake8 with the codes enabled in `test/lint/lint-python.py` pass. The `-bind` help line keeps the single-line `AddArg` style of its neighbours rather than the clang-format suggestion. ## Breaking Changes Compared with v24.0.0-rc.1/rc.2 only: a node started with an explicit `-bind` and `-listenonion` enabled (the default) binds `127.0.0.1:9996` (`19996` testnet) again, as v23 did. With the stricter bind handling from #7300, it fails to start if that port is unavailable. This breaks one setup that works on rc.1/rc.2: several `dashd` instances on one host and one chain, each with its own explicit `-bind`, and `-listenonion` left at its default. For example, several masternodes on separate IPs on one server. On rc.1/rc.2 each instance binds only its own `-bind` address. With this PR every instance also tries to bind `127.0.0.1:9996`; the first one gets it and every later one fails to start. v23 started all of them because a failed bind was only a warning there, and avoiding this extra bind was the reason for bitcoin#22729. Operators with that setup need one of these per instance: - `-listenonion=0`, if the instance does not need an automatic onion service; or - a distinct `-bind=127.0.0.1:<port>=onion` for each instance. Maintainers should weigh this cost against the self-advertisement problem above before merging. The alternatives listed under "Why this is the correct minimal fix" avoid this cost but make other trade-offs. ## Checklist: - [x] I have performed a self-review of my own code - [x] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) ACKs for top commit: knst: self-ACK 5f40d56 Tree-SHA512: 049a3879405756f81ac5fd5a7f1322cc56a1e78d705dd7774e6e0a144fd6a0da0439d9dd106901c91fb3c9dd8395c0143bfa58c363f0b2ec80ffe43ee123312c (cherry picked from commit d721bef)
fbe6526 Merge #7807: feat(consensus): hold EvoNode shares and multiple payouts behind a new evo_shares deployment (pasta) 57fd2d9 Merge #7776: fix: keep a pending ProUpServTx across a registrar update that keeps the operator key (pasta) ca69801 Merge #7803: fix: return non-zero amount of accounts after sethdseed (pasta) 57a8f07 Merge #7796: backport: bitcoin#26720 (remaining), partial bitcoin#29523: weight-check the knapsack exact-match selection (pasta) 4eb82f6 Merge #7786: fix(net): keep a dedicated onion listener when -bind is given and correct the bind release note (pasta) 70626a0 Merge #7793: backport: partial bitcoin#28868 (wallet: reload wallet if migration exited early, keep mixed watchonly txs) (pasta) 250a3cd Merge #7790: fix: guard cached governance object flags with the object lock (pasta) 700c195 Merge #7787: fix: only sign EHF signals inside the deployment's start/timeout window (pasta) a3b0f3c Merge #7785: fix: restore the txindex requirement when governance validation is enabled (pasta) 2d1735d Merge #7794: fix(wallet): floor the denomination gap threshold so rebalancing cannot oscillate (pasta) d9f6592 Merge #7783: fix: stop fish completion from re-running the typed dash-cli command (pasta) 89d44c8 Merge #7795: backport: bitcoin#34272, partial bitcoin#31650 (pass PSBT by const reference in PSBTInputSignedAndVerified) (pasta) a1d7411 Merge #7779: fix!: report legacy evonode Platform addresses when listdiff changes its address (pasta) 73f6ea2 Merge #7789: fix(qt): report malformed lang/font settings instead of aborting at startup (pasta) 40de493 Merge #7781: fix(rpc): use a consistent tip height in quorum dkgstatus (pasta) 7d1a90d Merge #7791: fix(net): request object votes from nPeersPerHashMax peers (pasta) 51b145c Merge #7778: test: use single-member in functional tests: feature_mnehf, feature_notifications, p2p_instantsend (pasta) Pull request description: ## Issue being fixed or feature implemented Backports for v24.0.0-rc.3. `v24.0.x` was cut at the v24.0.0-rc.2 tag (1e239d4), and develop has since moved to 24.1.0 (#7775), so rc.3 is built from this branch instead of being tagged on develop like rc.1 and rc.2. ## What was done? Cherry-picked (`git cherry-pick -m1 -x`) every develop merge since rc.2 that carries `backport-candidate-24.0.x`, plus #7778, which #7807's test needs. They are in develop merge order: - #7778 test: use single-member in functional tests: feature_mnehf, feature_notifications, p2p_instantsend (test-only. Needed because #7807's `feature_evo_shares_activation.py` depends on its `mine_quorum_single_member()` fix and fails without it.) - #7791 fix(net): request object votes from nPeersPerHashMax peers - #7781 fix(rpc): use a consistent tip height in quorum dkgstatus - #7789 fix(qt): report malformed lang/font settings instead of aborting at startup - #7779 fix!: report legacy evonode Platform addresses when listdiff changes its address - #7795 backport: bitcoin#34272, partial bitcoin#31650 (pass PSBT by const reference in PSBTInputSignedAndVerified) - #7783 fix: stop fish completion from re-running the typed dash-cli command - #7794 fix(wallet): floor the denomination gap threshold so rebalancing cannot oscillate - #7785 fix: restore the txindex requirement when governance validation is enabled - #7787 fix: only sign EHF signals inside the deployment's start/timeout window - #7790 fix: guard cached governance object flags with the object lock - #7793 backport: partial bitcoin#28868 (wallet: reload wallet if migration exited early, keep mixed watchonly txs) - #7786 fix(net): keep a dedicated onion listener when -bind is given and correct the bind release note. Includes partial bitcoin#36170 (5f40d56). Upstream's dedicated-onion-bind changes to `feature_torcontrol.py` and `p2p_private_broadcast.py` are not included, because those tests depend on upstream commits (5693833, e74d54e) that Dash does not have. - #7796 backport: bitcoin#26720 (remaining), partial bitcoin#29523: weight-check the knapsack exact-match selection - #7803 fix: return non-zero amount of accounts after sethdseed - #7776 fix: keep a pending ProUpServTx across a registrar update that keeps the operator key - #7807 feat(consensus): hold EvoNode shares and multiple payouts behind a new evo_shares deployment One pick needed conflict resolution, in a test only, described in its commit message: #7793, `test/functional/wallet_migration.py`. `test_conflict_txs()` comes from bitcoin#28542 (#7762), which is not on this branch, so it is left out. The C++ part of #7793 is identical to develop. Merged since rc.2 but intentionally not included: #7775 (24.1 version bump), #7260 and #7804 (new Qt feature and its CI fix), #7584 (refactor), routine upstream backports #7257, #7759, #7762, #7768, #7788, #7801, and test/CI-only changes #7777 and #7797. No version change is needed. `configure.ac` is already 24.0.0 with `CLIENT_VERSION_IS_RELEASE` false, so the build takes its version string from the `v24.0.0-rc.3` tag, as it did for rc.2. ## How Has This Been Tested? Built on macOS arm64 against depends with `--enable-debug --enable-werror` (dashd, dash-qt, tests). - `make check`: all unit test suites pass, including `test_dash-qt`. - Functional tests, all passing: `feature_evo_shares_activation.py`, `feature_masternode_shares.py`, `feature_mnehf.py`, `feature_governance.py --descriptors`, `feature_governance_objects.py`, `feature_governance_txindex_devnet.py`, `feature_proxy.py`, `feature_notifications.py`, `feature_llmq_connections.py`, `feature_llmq_chainlocks_automatic.py`, `feature_llmq_singlenode.py`, `p2p_instantsend.py`, `rpc_blockchain.py` (v1 and v2 transport), `rpc_netinfo.py`, `rpc_quorum.py`, `rpc_verifychainlock.py`, `wallet_hd.py` (legacy and descriptors), `wallet_migration.py`. - `feature_bind_extra.py` (from #7786) only runs on Linux and was skipped locally, so CI covers it. Every touched file was diffed against develop. The remaining differences come only from develop PRs that are not part of this backport. ## Breaking Changes Carried over from the picked PRs: - #7807: consensus change behind the new `evo_shares` deployment (bit 14). Mainnet and testnet are unaffected until it activates. Devnets with `v24` already active and an EvoNode carrying several owner payouts need to check or reset. New RPC `protx shared_register_prepare_evo`. - #7785: outside regtest, a node with `-txindex=0` and without `-disablegovernance` refuses to start again, as before v23.1.0. - #7786: carries a partial bitcoin#36170. A node started with `-bind` but no specific `-bind=<addr:port>=onion` now refuses to start while `-listenonion` is enabled. That is the default when listening, even without Tor configured. A wildcard onion bind is also refused. Such nodes need `-bind=127.0.0.1:<port>=onion` or `-listenonion=0`. Nodes without `-bind`, including those using only `-whitebind`, keep the default `127.0.0.1:9996` (`19996` testnet) onion target. See `doc/release-notes-7300.md`. - #7779: `protx listdiff` now includes `platform_p2p` and `platform_https` for a legacy-address evonode whose core P2P address changes. ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone 🤖 Generated with [Claude Code](https://claude.com/claude-code) Top commit has no ACKs. Tree-SHA512: 8cb01bdb0234f31b5dd2c105b69673de148922991a7523627395c0e0ccfa22b94be5ffbecc842398d443bc90fd9baf2eee6a9cf6fcdbbd1626eb00b93726a55a
Issue being fixed or feature implemented
The backport of bitcoin#22729 (#7300) changed what happens when a node is started with an explicit
-bind=<addr>but no-bind=...=onion. Before, Dash Core always added the dedicated onion listener127.0.0.1:9996(19996testnet,19896regtest) and pointed the automatically created Tor onion service (-listenonion, on by default) at it. Now it binds no onion listener and points the onion service at the first-bindaddress.That listener is not in
onion_binds, so every peer Tor forwards to it is classified by its source address: Tor's loopback address, or the node's own IP when-bindis a routable address. It is not classified as an onion peer. As a result, a node that runs an onion service next to a clearnet address (-externalip, or a discovered/bound routable address) advertises its clearnet address to peers that reached it through its.onionaddress. That links the two identities, which the privacy-network check inGetLocal()is meant to prevent. Those peers also lose the rest of the onion-specific handling, such asgetpeerinfonetworkand eviction protection for onion peers.The release note added for #7300 has a related problem. It says
-listenonion=0drops the implicit onion bind, but it never did: the bind is added before-listenonionis looked at. An operator whose127.0.0.1:9996is taken who follows the note still gets a node that refuses to start.Both changes are only in v24.0.0-rc.1 and rc.2.
Why this is a real problem
Code path at c14104b:
src/init.cpp#L2678-L2695: withvBindsnon-empty and no=onionbind,onion_service_target = connOptions.vBinds.front()(L2682). It is not pushed intoonion_binds, andStartTorControl(onion_service_target)still runs (L2694).src/net.h#L1291copies onlyonion_bindsintom_onion_binds.src/net.cpp#L1977setsinbound_oniononly for listeners inm_onion_binds.src/net.cpp#L584-L592: withoutm_inbound_onion,ConnectedThroughNetwork()andIsConnectedThroughPrivacyNet()fall back to the peer's source address. The privacy filter inGetLocal()(L186-L193) then skips the node's own onion address for that peer and lets the clearnet one through.I reproduced this on the unfixed build with a small functional-test-style script. A minimal fake Tor control port accepts
PROTOCOLINFO/AUTHENTICATE/ADD_ONIONand records the target dashd asks Tor to forward to. A P2P peer then connects to exactly that target, as Tor does for every inbound onion connection, and sendsgetaddr. The node runs with-bind=127.0.0.1:<port> -listenonion=1 -externalip=1.2.3.4.Unfixed (c14104b):
The onion service forwards to the clearnet listener, the forwarded peer is
not_publicly_routableinstead ofonion, and it is sent the node's clearnet address1.2.3.4.In the fixed run the list is empty for two reasons. The privacy filter withholds
1.2.3.4from an onion peer, and the testP2PInterfacenever sendssendaddrv2, so the node cannot send it the v3 onion address either. The fixed run also prints await_until() failedline after the 10 s wait for addresses; that line is left out of the excerpt above.With this PR:
Reproduction script (run with
WORKTREE=<src root> python3 repro_tor_target.py --configfile=<src root>/test/config.ini)Release-note claim, unfixed build, with
127.0.0.1:19896held by another process:Why it matters
Running an onion service alongside a clearnet address is a supported setup. Operators who also pass an explicit
-bind, which is common on servers and masternodes, would silently lose the separation between the two once v24.0.0 ships: any peer that connects to the.onionaddress can learn the node's clearnet IP from self-advertisement. Exposure needs a working Tor control connection and a routable local address (-externalip, or discovery or a routable-bind), so it does not affect every node. Nodes with-bindthat do not use Tor are unaffected either way. The release-note error is only an operational trap, but it would ship in the v24.0.0 notes.What was done?
src/init.cpp: drop the branch that usedvBinds.front()as the onion-service target, so the target is always the firstonion_bindsentry. Without a-bind=...=onion, the default onion target is now added toonion_bindsand used as the Tor target unless-bindis given and-listenonionis disabled. In that one case no onion bind is added and Tor control is not started.-listenonionis read after parameter interaction, so-listen=0(which soft-sets-listenonion=0) is handled as before. The-bindhelp text now mentions the exception.doc/release-notes-7300.md: describe the actual rule. The implicit bind is added unless-bindis given together with-listenonion=0.-bind=...=onionmoves it,-listenonion=0alone or-whitebindalone keeps it, and-listen=0disables all binds.test/functional/feature_bind_extra.py: node 2 (explicit-bind, no extra port) now says-listenonion=0explicitly. Before, it relied on the framework's config default. A new node 3 with explicit-bindand-listenonion=1must listen on both its-bindport and127.0.0.1:19896, and a peer connecting to19896must show up withnetworkonion. Node 3's-torcontrolpoints at an unused port so the test never creates an onion service through a Tor instance running on the test host.test/functional/feature_proxy.py: one step starts node 1 with-listenonion=1on top of the framework'sbind=127.0.0.1. With this PR that node would also bind the fixed regtest onion target127.0.0.1:19896, which is the portfeature_bind_extra.pynode 3 holds, and since backport: Merge bitcoin/bitcoin#30397, 22729, 29633 #7300 a failed bind stops startup. Undertest_runner.py -jNthe two tests could then collide. The step now passes-bind=127.0.0.1:<tor_port(1)>=onion, the same per-node port the framework uses when it adds the binds itself. I kept this local to the one test rather than teachingTestNode.start()a new rule, because it is the only test that enables-listenoniontogether with an explicit bind.Why this is the correct minimal fix
The root cause is that the onion service can be pointed at a listener that is not tagged as onion. The only listener that can be both the Tor target and correctly tagged is one in
onion_binds, so the target must always come fromonion_binds. The alternative of addingvBinds.front()toonion_bindswould tag every clearnet peer on that port as onion, which leaks the onion address to clearnet peers and distorts eviction, so it is not an option.Two other designs would avoid the multi-instance cost described under Breaking Changes. Both are reasonable, and the choice is up to maintainers:
-bindis given without=onionand-listenonionis not set explicitly, soft-set-listenonion=0during parameter interaction. This keeps rc.1's "respect-bind" behavior and avoids the extra bind. The cost is that nodes upgrading from v23 with-bindand an automatic onion service would silently lose that onion service.This PR does neither. It keeps v23's behavior for the onion service, plus the stricter bind handling from #7300.
bitcoin#22729's goal was to let users avoid the extra
127.0.0.1:9996bind. This PR keeps that, but only when the user also says they don't want an onion service (-listenonion=0). Without an onion service there is nothing to tag. Nothing changes for nodes without-bind, with-bind=...=onion, or with-listen=0. The stricter "fail startup if any bind fails" part of bitcoin#22729 is unchanged too.Relation to upstream
Bitcoin Core has the same bug. The open upstream fix, bitcoin#36170 ("net: require a dedicated bind for automatic Tor"), has an Approach ACK. It also removes the
vBinds.front()fallback, but handles the-bind+-listenonioncase differently: instead of adding the default onion bind, it refuses to start until the operator adds-bind=<addr>=onionor-listenonion=0. That turns every existing-bindsetup with the default-listenonion=1into a startup error, including many masternodes, so this PR keeps v23's default onion bind for that case.The
src/init.cppblock uses the same layout as bitcoin#36170: the default bind is added up front under one condition, and the Tor target is alwaysonion_binds[0]inside the-listenonionbranch. Backporting bitcoin#36170 later then only changes that condition and adds its errors. bitcoin#36170 also rejects a wildcard=onionbind. That is left to the backport, since it is a separate new startup error.This should also go to the v24.0.x release branch, since the behavior was introduced in v24.0.0-rc.1.
How Has This Been Tested?
macOS arm64,
--enable-debugbuild, functional tests run outside any sandbox.127.0.0.1:<bind port>, peernetwork=not_publicly_routable, and the peer is sent1.2.3.4. Fixed build shows target127.0.0.1:19896, peernetwork=onion, and no clearnet address is sent.feature_bind_extra.pywith the new cases. The test is Linux-only (it reads/proc/net/tcp), so I ran it locally through a wrapper that swapsget_bind_addrs()for anlsof-based equivalent and lifts the platform skip. The test itself is unchanged. Nodes 0–2 pass on both builds.src/init.cpphunks reverted): fails at node 3 withAssertionError: not({('7f000001', 17013)} == {('7f000001', 17013), ('7f000001', 19896)}).network == "onion"check.feature_proxy.pywith--nocleanup, checking theBound tolines in node 1'sdebug.log:feature_proxy.pychange: node 1 binds127.0.0.1:19896. This is the collision described above.tor_port(), and nothing on19896. The test passes.127.0.0.1:19896held by another process:-listenonion=0alone still fails withUnable to bind to 127.0.0.1:19896, as the corrected note says.-bind=127.0.0.1:18555 -listenonion=0starts.test/functional/test_runner.py feature_proxy.py p2p_eviction.py rpc_net.py(after the test changes) and earlierfeature_config_args.py: all passed.feature_config_args.pyfailed once in a-j4run withUnable to start HTTP server(an RPC port collision with another run on the same host) and passed on rerun.feature_bind_extra.py,feature_bind_port_*.pyandrpc_bind.pyare skipped on macOS by the runner. CI covers them on Linux.test/lint/lint-whitespace.pyand flake8 with the codes enabled intest/lint/lint-python.pypass. The-bindhelp line keeps the single-lineAddArgstyle of its neighbours rather than the clang-format suggestion.Breaking Changes
Compared with v24.0.0-rc.1/rc.2 only: a node started with an explicit
-bindand-listenonionenabled (the default) binds127.0.0.1:9996(19996testnet) again, as v23 did. With the stricter bind handling from #7300, it fails to start if that port is unavailable.This breaks one setup that works on rc.1/rc.2: several
dashdinstances on one host and one chain, each with its own explicit-bind, and-listenonionleft at its default. For example, several masternodes on separate IPs on one server. On rc.1/rc.2 each instance binds only its own-bindaddress. With this PR every instance also tries to bind127.0.0.1:9996; the first one gets it and every later one fails to start. v23 started all of them because a failed bind was only a warning there, and avoiding this extra bind was the reason for bitcoin#22729.Operators with that setup need one of these per instance:
-listenonion=0, if the instance does not need an automatic onion service; or-bind=127.0.0.1:<port>=onionfor each instance.Maintainers should weigh this cost against the self-advertisement problem above before merging. The alternatives listed under "Why this is the correct minimal fix" avoid this cost but make other trade-offs.
Checklist:
🤖 Generated with Claude Code