From f4af02e827b97a051bd12b813d4e09fcc41b45c3 Mon Sep 17 00:00:00 2001 From: will Date: Fri, 20 Mar 2026 10:39:30 +0000 Subject: [PATCH 1/4] net: add an add_even_if_unreachable argument to AddLocal Prepare for letting explicitly configured local addresses bypass the reachable-net filter without tying that behavior to LOCAL_MANUAL score. Pass an `add_even_if_unreachable` bool through `AddLocal()`, defaulting to `false`. --- src/net.cpp | 6 +++--- src/net.h | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index d046c2a2722..c903afc653f 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -274,7 +274,7 @@ void ClearLocal() } // learn a new local address -bool AddLocal(const CService& addr_, int nScore) +bool AddLocal(const CService& addr_, int nScore, bool add_even_if_unreachable) { CService addr{MaybeFlipIPv6toCJDNS(addr_)}; @@ -304,9 +304,9 @@ bool AddLocal(const CService& addr_, int nScore) return true; } -bool AddLocal(const CNetAddr &addr, int nScore) +bool AddLocal(const CNetAddr& addr, int nScore, bool add_even_if_unreachable) { - return AddLocal(CService(addr, GetListenPort()), nScore); + return AddLocal(CService(addr, GetListenPort()), nScore, add_even_if_unreachable); } void RemoveLocal(const CService& addr) diff --git a/src/net.h b/src/net.h index 928d518eda6..62e97b48944 100644 --- a/src/net.h +++ b/src/net.h @@ -164,8 +164,8 @@ enum std::optional GetLocalAddrForPeer(CNode& node); void ClearLocal(); -bool AddLocal(const CService& addr, int nScore = LOCAL_NONE); -bool AddLocal(const CNetAddr& addr, int nScore = LOCAL_NONE); +bool AddLocal(const CService& addr, int nScore = LOCAL_NONE, bool add_even_if_unreachable = false); +bool AddLocal(const CNetAddr& addr, int nScore = LOCAL_NONE, bool add_even_if_unreachable = false); void RemoveLocal(const CService& addr); bool SeenLocal(const CService& addr); bool IsLocal(const CService& addr); From 8c87e32bd3937251d6f30295cc1924048e5b74d1 Mon Sep 17 00:00:00 2001 From: will Date: Fri, 20 Mar 2026 10:39:41 +0000 Subject: [PATCH 2/4] net: let -externalip bypass -onlynet When -onlynet excludes a network, AddLocal() rejects local addresses from that network. This also rejects addresses explicitly configured via -externalip, so a configuration like -onlynet=ipv4 -externalip= never advertises the onion address. Set the AddLocal() unreachable-net override only when adding -externalip addresses so the bypass remains limited to explicit user configuration. --- src/init.cpp | 2 +- src/net.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index c53e5ed634c..03b9cbf51cc 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1808,7 +1808,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) for (const std::string& strAddr : args.GetArgs("-externalip")) { const std::optional addrLocal{Lookup(strAddr, GetListenPort(), fNameLookup)}; if (addrLocal.has_value() && addrLocal->IsValid()) - AddLocal(addrLocal.value(), LOCAL_MANUAL); + AddLocal(addrLocal.value(), LOCAL_MANUAL, /*add_even_if_unreachable=*/true); else return InitError(ResolveErrMsg("externalip", strAddr)); } diff --git a/src/net.cpp b/src/net.cpp index c903afc653f..ef158180c67 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -284,7 +284,7 @@ bool AddLocal(const CService& addr_, int nScore, bool add_even_if_unreachable) if (!fDiscover && nScore < LOCAL_MANUAL) return false; - if (!g_reachable_nets.Contains(addr)) + if (!g_reachable_nets.Contains(addr) && !add_even_if_unreachable) return false; if (fLogIPs) { From 657a5aa3f3d0e8533d3d7dadfcc3b2908be83601 Mon Sep 17 00:00:00 2001 From: will Date: Fri, 20 Mar 2026 10:39:45 +0000 Subject: [PATCH 3/4] test: cover -externalip bypassing -onlynet Add unit coverage for the -onlynet/-externalip interaction. Check that an unreachable address still fails with normal AddLocal() arguments, while the same address succeeds when the unreachable-net override is set for explicit local-address configuration. --- src/test/net_tests.cpp | 43 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 32801d97b8b..495e6f7d04a 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -1590,4 +1590,47 @@ BOOST_AUTO_TEST_CASE(private_broadcast_version_does_not_update_addrman_services) m_node.peerman->FinalizeNode(node); } +BOOST_AUTO_TEST_CASE(addlocal_onlynet_externalip) +{ + // Test that `-externalip` addresses bypass `-onlynet`, but score alone does + // not. + + CAddress addr_onion; + BOOST_REQUIRE(addr_onion.SetSpecial("pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion")); + BOOST_REQUIRE(addr_onion.IsValid()); + BOOST_REQUIRE(addr_onion.IsTor()); + + const auto reachable_nets_at_start{g_reachable_nets.All()}; + const bool discover_orig{fDiscover}; + + // Simulate using -onlynet=ipv4 -externalip= + g_reachable_nets.RemoveAll(); + g_reachable_nets.Add(NET_IPV4); + fDiscover = false; + + // Now AddLocal with a non-manual score should fail for an unreachable network. + BOOST_CHECK(!AddLocal(addr_onion, LOCAL_BIND)); + BOOST_CHECK(!IsLocal(addr_onion)); + + BOOST_CHECK(!AddLocal(addr_onion, LOCAL_MANUAL)); + BOOST_CHECK(!IsLocal(addr_onion)); + + // Whereas AddLocal for -externalip should succeed. + BOOST_CHECK(AddLocal(addr_onion, LOCAL_MANUAL, /*add_even_if_unreachable=*/true)); + BOOST_CHECK(IsLocal(addr_onion)); + + // Normal AddLocal on a reachable network still works. + const CNetAddr addr_ipv4{LookupHost("1.2.3.4", false).value()}; + BOOST_CHECK(AddLocal(addr_ipv4, LOCAL_MANUAL)); + BOOST_CHECK(IsLocal(CService{addr_ipv4, GetListenPort()})); + + RemoveLocal(CService{addr_ipv4, GetListenPort()}); + RemoveLocal(addr_onion); + g_reachable_nets.RemoveAll(); + for (const auto& net : reachable_nets_at_start) { + g_reachable_nets.Add(net); + } + fDiscover = discover_orig; +} + BOOST_AUTO_TEST_SUITE_END() From dab7f2c984bdf10ed65fec6e7b602486f853c456 Mon Sep 17 00:00:00 2001 From: will Date: Fri, 20 Mar 2026 11:52:35 +0000 Subject: [PATCH 4/4] test: cover -externalip/onlynet interaction in functional test Extend p2p_addr_selfannouncement to restart the node with -onlynet=ipv4 -externalip= and verify the onion address appears in localaddresses despite its network being unreachable. --- test/functional/p2p_addr_selfannouncement.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/functional/p2p_addr_selfannouncement.py b/test/functional/p2p_addr_selfannouncement.py index 631ecb4c00e..01221332511 100755 --- a/test/functional/p2p_addr_selfannouncement.py +++ b/test/functional/p2p_addr_selfannouncement.py @@ -23,6 +23,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_greater_than IP_TO_ANNOUNCE = "42.42.42.42" +ONION_ADDR = "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion" ONE_DAY = 60 * 60 * 24 @@ -77,6 +78,7 @@ class AddrSelfAnnouncementTest(BitcoinTestFramework): self.self_announcement_test(outbound=False, addrv2=True) self.self_announcement_test(outbound=True, addrv2=False) self.self_announcement_test(outbound=True, addrv2=True) + self.test_externalip_bypasses_onlynet() @staticmethod def inbound_connection_open_assertions(addr_receiver): @@ -150,5 +152,17 @@ class AddrSelfAnnouncementTest(BitcoinTestFramework): self.nodes[0].disconnect_p2ps() + def test_externalip_bypasses_onlynet(self): + self.log.info("Test that -externalip onion is advertised despite -onlynet=ipv4") + self.restart_node(0, extra_args=["-onlynet=ipv4", f"-externalip={ONION_ADDR}"]) + + netinfo = self.nodes[0].getnetworkinfo() + + onion_net = next(n for n in netinfo["networks"] if n["name"] == "onion") + assert not onion_net["reachable"], "onion should not be reachable under -onlynet=ipv4" + + addrs = [a["address"] for a in netinfo["localaddresses"]] + assert ONION_ADDR in addrs, f"onion address missing from localaddresses: {addrs}" + if __name__ == '__main__': AddrSelfAnnouncementTest(__file__).main()