Skip to content

fix(core): always poll WireGuard stats regardless of Doze mode#1177

Merged
zaneschepke merged 1 commit intowgtunnel:masterfrom
naonak:fix/tunnel-freeze-root-causes
Mar 7, 2026
Merged

fix(core): always poll WireGuard stats regardless of Doze mode#1177
zaneschepke merged 1 commit intowgtunnel:masterfrom
naonak:fix/tunnel-freeze-root-causes

Conversation

@naonak
Copy link
Copy Markdown
Contributor

@naonak naonak commented Feb 26, 2026

WireGuard statistics are read from the kernel (local memory, no network required), so skipping the poll during Doze mode was unnecessary and harmful: tunStateFlow would stop updating, preventing HandshakeRestartHandler from detecting a stale handshake during device sleep.

The ping monitor retains the idle-mode check (pings need network access, which Android blocks during Doze).

Problem

When Android enters Doze mode (screen off, device idle), the startWgStatsPoll loop in TunnelMonitoringHandler was skipping stat updates due to this guard:

if (!powerManager.isDeviceIdleMode) {
    val stats = getStatistics(tunnelId)
    updateTunnelStatus(tunnelId, null, stats, null, null)
}

This caused tunStateFlow to stop being updated, which in turn caused HandshakeRestartHandler to stall — it monitors tunStateFlow to detect stale handshakes (no handshake in ~3.5 min). With no new stats flowing in, the handler could never observe the stale condition and never triggered a restart (#1176).

The result: tunnels appeared Up in the UI but passed no traffic, and stayed frozen for the entire duration of Doze mode. Reported in #1036.

Why removing this check is safe

WireGuard tunnel statistics are read from kernel memory — specifically the WireGuard netlink interface. This is a local, synchronous operation that requires no network access. Doze mode restricts network I/O and wakelocks, but does not prevent reading kernel state. Polling stats every 1 second while idle is negligible CPU work and has no meaningful battery impact.

The ping loop correctly retains its isDeviceIdleMode check, since ICMP pings genuinely require network access that Doze blocks.

Fix

Remove the idle guard from startWgStatsPoll:

while (isActive) {
    ensureActive()
    val stats = getStatistics(tunnelId)
    ensureActive()
    updateTunnelStatus(tunnelId, null, stats, null, null)
    delay(STATS_DELAY)
}

tunStateFlow now stays current at all times. HandshakeRestartHandler can detect a stale handshake within ~1 second even during Doze, and will trigger a tunnel restart. WireGuard handles the reconnection gracefully once network is available again.

Testing

# Force Doze
adb shell dumpsys deviceidle force-idle

# Simulate stale handshake (remove peer on server side or block UDP port)
# Verify restart is logged within HANDSHAKE_TIMEOUT + ~1s
adb logcat | grep -E "stale|restart|HandshakeRestart"

# Exit Doze
adb shell dumpsys deviceidle unforce

WireGuard statistics are read from the kernel (local memory, no network
required), so skipping the poll during Doze mode was unnecessary and
harmful: tunStateFlow would stop updating, preventing HandshakeRestartHandler
from detecting a stale handshake during device sleep.

The ping monitor retains the idle-mode check (pings need network access,
which Android blocks during Doze).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@naonak
Copy link
Copy Markdown
Contributor Author

naonak commented Feb 26, 2026

#1036 #1176

@zaneschepke
Copy link
Copy Markdown
Collaborator

Hello,

The stats are not completely no cost and are only read from the kernel in kernel mode, not userspace (which most users are using). This does add a small overhead + jni for CPU in the background but it is a small amount and this probably causes more trouble than it is worth. I agree this should be changes so I'll merge this.

Thanks!

@zaneschepke zaneschepke merged commit 2d9c5ec into wgtunnel:master Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants