Skip to content

net: Reduce local network activity when networkactive=0#34486

Open
willcl-ark wants to merge 4 commits intobitcoin:masterfrom
willcl-ark:respect-networkactive
Open

net: Reduce local network activity when networkactive=0#34486
willcl-ark wants to merge 4 commits intobitcoin:masterfrom
willcl-ark:respect-networkactive

Conversation

@willcl-ark
Copy link
Member

@willcl-ark willcl-ark commented Feb 2, 2026

Fixes #34190

When networkactive=0 is set, NAT-PMP port mapping and Tor control connections
still run in the background, mapping ports and logging retry attempts despite
the node being "inactive."

This wires both subsystems to CConnman::SetNetworkActive so they start and stop
with the network state:

  • mapport: EnableMapPort is injected as a callback via CConnman::Options.
    SetNetworkActive and SetMapPortEnabled both gate on network state.

  • torcontrol: is injected similarly. The callback dispatches onto the tor event loop via
    event_base_once to respect thread ownership. EVLOOP_NO_EXIT_ON_EMPTY is
    used to keep the event loop alive without keepalive timers, so the controller starts disconnected and waits for CConnman::Start() to signal.

When -networkactive=0 is set, NAT-PMP port mapping was still running and
attempting gateway queries. Move mapport lifecycle management into
CConnman, which already owns SetNetworkActive, so it can control mapport
based on both the -natpmp setting and network state.

EnableMapPort is injected as a callback via CConnman::Options to avoid a
circular dependency between net and mapport.
Add functional test to verify that PCP/NAT-PMP port mapping respects the
networkactive state:

- Does not run when started with -networkactive=0
- Starts when network is activated via setnetworkactive RPC
- Stops when network is deactivated
- Resumes when network is reactivated
@DrahtBot DrahtBot added the P2P label Feb 2, 2026
@willcl-ark
Copy link
Member Author

willcl-ark commented Feb 2, 2026

This is an alternative to #34467. The key difference is that #34467 gates mapport and tor control at init time with if (networkactive) guards, which means those subsystems are permanently disabled for the lifetime of the process. setnetworkactive true via RPC won't start them. Boot-time and runtime behavior would now differ.

This PR instead wires both subsystems into CConnman::SetNetworkActive so they follow the network state through the full lifecycle: starting with -networkactive=0 and later calling setnetworkactive true works as expected.

I'm unsure how this will conflict with the work currently being done to remove libevent.

@DrahtBot
Copy link
Contributor

DrahtBot commented Feb 2, 2026

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept ACK sedited, fanquake, Jhackman2019, brunoerg

If your review is incorrectly listed, please copy-paste <!--meta-tag:bot-skip--> into the comment that the bot should ignore.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #34741 (refactor: Return std::optional from GetNameProxy/GetProxy by maflcko)
  • #34520 (refactor: Add [[nodiscard]] to functions returning bool+mutable ref by maflcko)
  • #34467 (net: don't perform network activity when networkactive=0 by saksham-1304)
  • #34158 (torcontrol: Remove libevent usage by fjahr)
  • #30951 (net: option to disallow v1 connection on ipv4 and ipv6 peers by stratospher)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@sedited
Copy link
Contributor

sedited commented Feb 2, 2026

Concept ACK

Copy link

@bradleystachurski bradleystachurski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 35da4e0

Tested on Linux: started with -networkactive=0 -natpmp=1 -debug=net -debug=tor, confirmed no portmap/tor activity at startup, toggled via setnetworkactive, confirmed mapport thread start/stop and tor connection attempts follow network state.

Verified feature_mapport.py fails when the m_mapport callback is removed from SetNetworkActive.

nit: TorController::SetNetworkActive doesn't reset reconnect_timeout, so re-enabling network after backoff has grown continues from the stale value. Verified this fixes it:

diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp
index d5aeb55a72..934499d72b 100644
--- a/src/torcontrol.cpp
+++ b/src/torcontrol.cpp
@@ -677,6 +677,8 @@ void TorController::SetNetworkActive(bool set_active)
         // Disconnect if currently connected
         conn.Disconnect();
     }
+    // Reset backoff when re-enabling network
+    reconnect_timeout = RECONNECT_TIMEOUT_START;
     // Connect handles both cases: connects if active, reschedules if inactive
     Connect();
 }

When -networkactive=0 is set, Tor control connection attempts were still
running and logging retry messages. Wire tor control lifecycle to
CConnman similar to mapport: inject EnableTorControl as a callback via
CConnman::Options, called from Start() and SetNetworkActive().

TorController starts disconnected and waits for the callback from
CConnman::Start() before connecting.

The callback dispatches onto the tor control event loop via
event_base_once rather than calling TorController::SetNetworkActive
directly, since TorController state and libevent structures are owned by
the event loop thread. This also means the gTorController pointer cannot
be observed after destruction: once event_base_dispatch returns, no
further callbacks execute.
@willcl-ark willcl-ark force-pushed the respect-networkactive branch from 35da4e0 to 745aed3 Compare February 3, 2026 09:26
@willcl-ark
Copy link
Member Author

Thanks @bradleystachurski that's a nice suggestion which I've taken in 745aed3

I also reworked the if logic slightly to make the flow of this function clearer (and not always call Connect()).

@fanquake
Copy link
Member

fanquake commented Feb 3, 2026

Concept ACK

@willcl-ark
Copy link
Member Author

I will address the LLM linter suggestion if I push again.

@Jhackman2019
Copy link

Jhackman2019 commented Feb 7, 2026

Tested this on my Pi 5 (ARM64, Debian Bookworm). Builds clean, all the networking-related tests pass:

feature_mapport.py                  PASSED
feature_proxy.py                    PASSED
p2p_addr_relay.py                   PASSED
p2p_disconnect_ban.py --v1transport PASSED
p2p_disconnect_ban.py --v2transport PASSED
p2p_dns_seeds.py                    PASSED

Had to clear my test/cache first since I had a stale cache from an autotools build — after that everything was smooth.

Concept ACK

@brunoerg
Copy link
Contributor

Concept ACK

Copy link
Contributor

@brunoerg brunoerg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tested up to 396b56a: I checked that portmap activity follows network activity.

if (!reconnect)
return;

if (!m_network_active) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way to test this condition? I've tried several scenarios manually, but haven't been able to achieve it.

However, checking that it is not connected to any Tor control port and then will retry was easier, could also check it on a functional test, e.g:

diff --git a/test/functional/p2p_private_broadcast.py b/test/functional/p2p_private_broadcast.py
index 4943841790..b163020e85 100755
--- a/test/functional/p2p_private_broadcast.py
+++ b/test/functional/p2p_private_broadcast.py
@@ -433,9 +433,11 @@ class P2PPrivateBroadcast(BitcoinTestFramework):
             # the RPC should throw.
             "-torcontrol=127.0.0.1:1",
             "-listenonion",
+            "-debug=tor",
         ])
-        assert_raises_rpc_error(-1, "none of the Tor or I2P networks is reachable",
-                                tx_originator.sendrawtransaction, hexstring=txs[0]["hex"], maxfeerate=0.1)
+        with tx_originator.assert_debug_log(['Not connected to Tor control port'], timeout=5):
+            assert_raises_rpc_error(-1, "none of the Tor or I2P networks is reachable",
+                                    tx_originator.sendrawtransaction, hexstring=txs[0]["hex"], maxfeerate=0.1)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanksf or the review!

Yes I think this is hard to reach in a functional test, as conn.Disconnect() frees the bufferevent without firing the callback, so this guard only triggers if a libevent disconnect event was already queued when SetNetworkActive(false) runs.

It's mainly intended to be defensive against an event-loop ordering edge case, and even if it wasn't here, Connect() has its own m_network_active check, but this guard also prevents a misleading "retrying" log message.

Your p2p_private_broadcast.py change seems like good coverage for the reworked connection initiation path. Happy to include it.

Verify that the tor controller attempts reconnection to the control port
by asserting the retry log message when started with an unreachable
`-torcontrol` address.

Co-authored-by: brunoerg <brunoerg@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bitcoind shouldn't perform network activity if the option networkactive=0, e.g. [addrman] Selected IP:PORT from tried

7 participants