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)
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