From fc74e6f09fd1ad84de02de8a80b1d6882eb0b7e0 Mon Sep 17 00:00:00 2001 From: LionThunderFingers <42521162+LionThunderFingers@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:51:11 +0200 Subject: [PATCH] net: Check an assumed onion inbound's source address under -v2onlyclearnet Without a -bind=...=onion listener, an inbound connection on the first -bind is treated as onion, and for a bind that is not bind-any every inbound connection there is. -v2onlyclearnet then exempted all of them from its inbound v1 check, so a clearnet peer could stay connected over v1 (knots#427). Keep that assumption everywhere else, but for the v1 check also judge an assumed onion inbound by its source address. Tor reaches the node from this host or a private address, which stays exempt; any other public source address is a clearnet peer. Inbound connections on an explicit -bind=...=onion listener are unchanged. Fixes #427 --- src/net.cpp | 6 ++++ src/net.h | 5 +++ src/net_processing.cpp | 6 +++- src/test/net_peer_connection_tests.cpp | 49 ++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/net.cpp b/src/net.cpp index a953080d19c3..1ea16de37640 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1801,6 +1801,10 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, return std::find(m_onion_binds.begin(), m_onion_binds.end(), addr_bind) != m_onion_binds.end(); } }(); + // Without a -bind=...=onion listener, an onion inbound is an assumption about a listener that + // clearnet peers can reach too. A connection from the bind address itself comes from this host + // (such as a Tor daemon whose onion service points at that address), so it is not in doubt. + const bool inbound_onion_assumed{inbound_onion && m_onion_binds.empty() && CNetAddr{addr} != CNetAddr{addr_bind}}; // Tor inbound connections do not reveal the peer's actual network address. // Therefore do not apply address-based whitelist permissions to them. @@ -1884,6 +1888,7 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, .prefer_evict = discouraged, .recv_flood_size = nReceiveFloodSize, .use_v2transport = use_v2transport, + .inbound_onion_assumed = inbound_onion_assumed, }); pnode->AddRef(); m_msgproc->InitializeNode(*pnode, local_services); @@ -3865,6 +3870,7 @@ CNode::CNode(NodeId idIn, m_addr_name{addrNameIn.empty() ? addr.ToStringAddrPort() : addrNameIn}, m_dest(addrNameIn), m_inbound_onion{inbound_onion}, + m_inbound_onion_assumed{inbound_onion && node_opts.inbound_onion_assumed}, m_prefer_evict{node_opts.prefer_evict}, nKeyedNetGroup{nKeyedNetGroupIn}, m_network_key{network_key}, diff --git a/src/net.h b/src/net.h index 574a60f16a89..087905060e9c 100644 --- a/src/net.h +++ b/src/net.h @@ -668,6 +668,9 @@ struct CNodeOptions bool prefer_evict = false; size_t recv_flood_size{DEFAULT_MAXRECEIVEBUFFER * 1000}; bool use_v2transport = false; + // True if inbound_onion is only assumed: the connection arrived on a listener that also takes clearnet + // connections (no -bind=...=onion is configured), so it may not have come through Tor at all. + bool inbound_onion_assumed{false}; }; /** Information about a peer */ @@ -715,6 +718,8 @@ class CNode const std::string m_dest; //! Whether this peer is an inbound onion, i.e. connected via our Tor onion service. const bool m_inbound_onion; + //! Whether m_inbound_onion is only assumed from a listener shared with clearnet (see CNodeOptions). + const bool m_inbound_onion_assumed; std::atomic nVersion{0}; Mutex m_subver_mutex; /** diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 0c0aff7366ef..3b37f6b0aa73 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -3457,7 +3457,11 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, } const bool is_v1transport = pfrom.m_transport->GetInfo().transport_type == TransportProtocolType::V1; - if (pfrom.IsInboundConn() && is_v1transport && m_connman.RequiresV2Peer(pfrom.ConnectedThroughNetwork())) { + // An onion inbound that is only assumed (no -bind=...=onion) is also checked by its source address: + // Tor reaches us from this host or a private address, so a clearnet source means a clearnet peer. + const bool clearnet_v1{m_connman.RequiresV2Peer(pfrom.ConnectedThroughNetwork()) || + (pfrom.m_inbound_onion_assumed && m_connman.RequiresV2Peer(pfrom.addr.GetNetClass()))}; + if (pfrom.IsInboundConn() && is_v1transport && clearnet_v1) { LogDebug(BCLog::NET, "v1 connection from a clearnet peer not allowed (-v2onlyclearnet), %s", pfrom.DisconnectMsg(fLogIPs)); pfrom.fDisconnect = true; return; diff --git a/src/test/net_peer_connection_tests.cpp b/src/test/net_peer_connection_tests.cpp index bdc8722d3b34..93a6628c4bd2 100644 --- a/src/test/net_peer_connection_tests.cpp +++ b/src/test/net_peer_connection_tests.cpp @@ -228,6 +228,55 @@ BOOST_AUTO_TEST_CASE(v2onlyclearnet_inbound_v1) BOOST_CHECK(!node.fDisconnect); peerman->FinalizeNode(node); } + + // Without a -bind=...=onion listener every inbound on the first -bind is assumed to be onion + // (knots#427). A v1 peer from a clearnet source address is still a clearnet peer: disconnected. + { + CNode node{++id, + /*sock=*/std::make_shared(""), + addr, + /*nKeyedNetGroupIn=*/0, + /*nLocalHostNonceIn=*/0, + CAddress{}, + /*addrNameIn=*/"", + ConnectionType::INBOUND, + /*inbound_onion=*/true, + /*network_key=*/0, + CNodeOptions{.inbound_onion_assumed = true}}; + BOOST_REQUIRE_EQUAL(node.ConnectedThroughNetwork(), Network::NET_ONION); + connman->Handshake(node, + /*successfully_connected=*/false, + /*remote_services=*/ServiceFlags(NODE_NETWORK | NODE_WITNESS), + /*local_services=*/ServiceFlags(NODE_NETWORK | NODE_WITNESS), + /*version=*/PROTOCOL_VERSION, + /*relay_txs=*/true); + BOOST_CHECK(node.fDisconnect); + peerman->FinalizeNode(node); + } + + // The same assumed onion inbound from a local address, as from a Tor daemon on this host, is kept. + { + CNode node{++id, + /*sock=*/std::make_shared(""), + CAddress{ip(0x0100007f), NODE_NONE}, + /*nKeyedNetGroupIn=*/0, + /*nLocalHostNonceIn=*/0, + CAddress{}, + /*addrNameIn=*/"", + ConnectionType::INBOUND, + /*inbound_onion=*/true, + /*network_key=*/0, + CNodeOptions{.inbound_onion_assumed = true}}; + BOOST_REQUIRE_EQUAL(node.ConnectedThroughNetwork(), Network::NET_ONION); + connman->Handshake(node, + /*successfully_connected=*/true, + /*remote_services=*/ServiceFlags(NODE_NETWORK | NODE_WITNESS), + /*local_services=*/ServiceFlags(NODE_NETWORK | NODE_WITNESS), + /*version=*/PROTOCOL_VERSION, + /*relay_txs=*/true); + BOOST_CHECK(!node.fDisconnect); + peerman->FinalizeNode(node); + } } BOOST_AUTO_TEST_SUITE_END()