-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: Guard mnauth by local masternode service #4974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,9 +19,22 @@ | |
| void CMNAuth::PushMNAUTH(CNode& peer, CConnman& connman, const CActiveMasternodeManager& mn_activeman) | ||
| { | ||
| CMNAuth mnauth; | ||
| if (mn_activeman.GetProTxHash().IsNull()) { | ||
| const uint256 pro_tx_hash{mn_activeman.GetProTxHash()}; | ||
| if (pro_tx_hash.IsNull()) { | ||
| return; | ||
| } | ||
| if (peer.IsInboundConn()) { | ||
| const CService expected_service{mn_activeman.GetService()}; | ||
| const CService connected_service{static_cast<const CService&>(peer.addrBind)}; | ||
| if (expected_service.GetPort() != connected_service.GetPort() || | ||
| expected_service.GetNetwork() != peer.ConnectedThroughNetwork()) { | ||
| LogPrint(BCLog::NET_NETCONN, /* Continued */ | ||
| "CMNAuth::%s -- Not sending MNAUTH on unexpected local service, expected=%s, connected=%s, " | ||
| "peer=%d\n", | ||
| __func__, expected_service.ToStringAddrPort(), connected_service.ToStringAddrPort(), peer.GetId()); | ||
| return; | ||
| } | ||
| } | ||
|
Comment on lines
+26
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: Same-port multi-bind IPv4 listeners are still indistinguishable; For non-onion inbound peers,
This is not a regression — strict address equality would re-break the NAT case the PR is designed to fix, and the consensus-critical privacy concern (onion deanonymization) is correctly handled because source: ['claude', 'codex'] 🤖 Fix this with AI agents |
||
|
|
||
| const auto receivedMNAuthChallenge = peer.GetReceivedMNAuthChallenge(); | ||
| if (receivedMNAuthChallenge.IsNull()) { | ||
|
|
@@ -39,7 +52,7 @@ void CMNAuth::PushMNAUTH(CNode& peer, CConnman& connman, const CActiveMasternode | |
| } | ||
| const uint256 signHash{::SerializeHash(std::make_tuple(mn_activeman.GetPubKey(), receivedMNAuthChallenge, peer.IsInboundConn(), nOurNodeVersion))}; | ||
|
|
||
| mnauth.proRegTxHash = mn_activeman.GetProTxHash(); | ||
| mnauth.proRegTxHash = pro_tx_hash; | ||
|
|
||
| // all clients uses basic BLS | ||
| mnauth.sig = mn_activeman.Sign(signHash, false); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| #!/usr/bin/env python3 | ||
| # Copyright (c) 2026 The Dash Core developers | ||
| # Distributed under the MIT software license, see the accompanying | ||
| # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||
|
|
||
| """ | ||
| Test MNAUTH emission on the registered masternode service only. | ||
| """ | ||
|
|
||
| import platform | ||
|
|
||
| from test_framework.test_framework import ( | ||
| DashTestFramework, | ||
| MasternodeInfo, | ||
| ) | ||
| from test_framework.util import ( | ||
| assert_equal, | ||
| p2p_port, | ||
| ) | ||
|
|
||
|
|
||
| class P2PMNAUTHTest(DashTestFramework): | ||
| def add_options(self, parser): | ||
| self.add_wallet_options(parser) | ||
|
|
||
| def set_test_params(self): | ||
| # Disable the framework's implicit `bind=127.0.0.1` so all binds for the | ||
| # masternode are visible in this file rather than coming from the conf. | ||
| self.bind_to_localhost_only = False | ||
| self.alt_port = p2p_port(10) | ||
| self.mn_port = p2p_port(2) | ||
| # NAT-style variant requires a non-127.0.0.1 loopback bind; only Linux | ||
| # routes 127.0.0.0/8 to lo by default. | ||
| self.nat_capable = platform.system() == "Linux" | ||
|
|
||
| mn_args = [ | ||
| f"-bind=127.0.0.1:{self.mn_port}", | ||
| f"-bind=127.0.0.1:{self.alt_port}", | ||
| f"-externalip=127.0.0.1:{self.mn_port}", | ||
| ] | ||
| if self.nat_capable: | ||
| mn_args.append(f"-bind=127.0.0.2:{self.mn_port}") | ||
|
|
||
| self.set_dash_test_params(3, 1, extra_args=[[], [], mn_args]) | ||
|
|
||
| def run_test(self): | ||
| masternode: MasternodeInfo = self.mninfo[0] | ||
| masternode_node = masternode.get_node(self) | ||
| connector = self.nodes[1] | ||
| use_v2transport = self.options.v2transport | ||
|
|
||
| expected_addr = f"127.0.0.1:{masternode.nodePort}" | ||
| alternate_addr = f"127.0.0.1:{self.alt_port}" | ||
|
|
||
| self.wait_until(lambda: masternode_node.masternode("status")["state"] == "READY") | ||
| assert_equal(masternode_node.masternode("status")["service"], expected_addr) | ||
|
|
||
| self.log.info(f"Connect to the registered masternode service over {'v2' if use_v2transport else 'v1'} and expect MNAUTH") | ||
| with connector.assert_debug_log([f"Masternode probe successful for {masternode.proTxHash}"]): | ||
| assert_equal(connector.masternode("connect", expected_addr, use_v2transport), "successfully connected") | ||
|
|
||
| self.log.info(f"Connect to the alternate bind over {'v2' if use_v2transport else 'v1'} and expect no MNAUTH") | ||
| with masternode_node.assert_debug_log(["Not sending MNAUTH on unexpected local service"]): | ||
| with connector.assert_debug_log(["connection is a masternode probe but first received message is not MNAUTH"]): | ||
| assert_equal(connector.masternode("connect", alternate_addr, use_v2transport), "successfully connected") | ||
|
|
||
| if self.nat_capable: | ||
|
Comment on lines
+64
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💬 Nitpick: Negative cases assert only on log strings Both new tests verify suppression by matching log lines ( source: ['claude'] |
||
| nat_addr = f"127.0.0.2:{self.mn_port}" | ||
| self.log.info(f"Connect to a different loopback IP on the registered port over {'v2' if use_v2transport else 'v1'} (NAT-style: addrBind address differs from advertised externalip, ports match) and expect MNAUTH") | ||
| with connector.assert_debug_log([f"Masternode probe successful for {masternode.proTxHash}"]): | ||
| assert_equal(connector.masternode("connect", nat_addr, use_v2transport), "successfully connected") | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| P2PMNAUTHTest().main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| #!/usr/bin/env python3 | ||
| # Copyright (c) 2026 The Dash Core developers | ||
| # Distributed under the MIT software license, see the accompanying | ||
| # file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||
|
|
||
| """ | ||
| Test that MNAUTH is suppressed when an inbound arrives over a different network class | ||
| than the masternode's registered service. Specifically: an IPv4-registered masternode | ||
| must not emit MNAUTH on a Tor-tagged inbound, even when the local port matches — that | ||
| would deanonymize the masternode by linking its onion endpoint to its IPv4 identity. | ||
|
|
||
| Set up: bind the masternode's listener at its registered port with `=onion`, so that any | ||
| inbound arriving there is treated by dashd as a Tor connection (m_inbound_onion=true). | ||
| Port match is then guaranteed and only the network differs, isolating the network check. | ||
| """ | ||
|
|
||
| from test_framework.test_framework import ( | ||
| DashTestFramework, | ||
| MasternodeInfo, | ||
| ) | ||
| from test_framework.util import ( | ||
| assert_equal, | ||
| p2p_port, | ||
| ) | ||
|
|
||
|
|
||
| class P2PMNAUTHOnionTest(DashTestFramework): | ||
| def add_options(self, parser): | ||
| self.add_wallet_options(parser) | ||
|
|
||
| def set_test_params(self): | ||
| self.bind_to_localhost_only = False | ||
| self.mn_port = p2p_port(2) | ||
| self.set_dash_test_params(3, 1, extra_args=[ | ||
| [], | ||
| [], | ||
| [ | ||
| f"-bind=127.0.0.1:{self.mn_port}=onion", | ||
| f"-externalip=127.0.0.1:{self.mn_port}", | ||
| ], | ||
| ]) | ||
|
|
||
| def run_test(self): | ||
| masternode: MasternodeInfo = self.mninfo[0] | ||
| masternode_node = masternode.get_node(self) | ||
| connector = self.nodes[1] | ||
| use_v2transport = self.options.v2transport | ||
|
|
||
| expected_addr = f"127.0.0.1:{masternode.nodePort}" | ||
|
|
||
| self.wait_until(lambda: masternode_node.masternode("status")["state"] == "READY") | ||
| assert_equal(masternode_node.masternode("status")["service"], expected_addr) | ||
|
|
||
| self.log.info(f"Probe the MN over {'v2' if use_v2transport else 'v1'} via its onion-tagged listener; ports match, network differs, expect no MNAUTH") | ||
| with masternode_node.assert_debug_log(["Not sending MNAUTH on unexpected local service"]): | ||
| with connector.assert_debug_log(["connection is a masternode probe but first received message is not MNAUTH"]): | ||
| assert_equal(connector.masternode("connect", expected_addr, use_v2transport), "successfully connected") | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| P2PMNAUTHOnionTest().main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💬 Nitpick: Redundant
static_cast<const CService&>andconnected_servicelocalCAddresspublicly inherits fromCService(src/protocol.h:398), sopeer.addrBind.GetPort()andpeer.addrBind.ToStringAddrPort()already invoke theCServicemembers directly. The intermediateconnected_serviceand the explicit slicing cast add noise without changing behavior.💡 Suggested change
source: ['claude']