Android: 4s ICE keepalive prevents the device from ever reaching deep sleep (~8x idle battery drain) #7185
Unanswered
Klos54
asked this question in
Issue Triage
Replies: 1 comment
|
Great findings! This is a big problem with the netbird-mobile apps. Thanks for investigating, hope some developer sees it an can provice a fix, this would be awesome! |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Before posting
Affected area
Client / Agent, Mobile app
Deployment type
Self-hosted - advanced/custom deployment
Operating system or environment
Linux, Android
NetBird version and upgrade status
netbird: v0.76.3-18-gdb9fcf39e
Client: JetBird (third-party Android front-end built on client/android)
Device: OnePlus 9 Pro (LE2123), Android 16, LineageOS-based, rooted (KernelSU Next)
Management: self-hosted, same version
Peers: 2 (this phone + one Linux routing peer on the same LAN)
Connection: direct, host/host ICE candidate pair, no relay
Did this work before?
Not sure
Regression details
N/A — I have no older netbird version to compare against on this device.
My baseline is the plain WireGuard Android app on the same phone, same night
pattern, two days earlier: 10.8 mAh/h idle versus 89 mAh/h with netbird.
Summary
On Android, the unconditional 4-second ICE keepalive (iceKeepAliveDefault in
client/internal/peer/ice/agent.go) wakes the device roughly every 4 seconds
while the screen is off. The CPU never stays suspended, Doze never becomes
effective, and idle battery drain goes from ~11 mAh/h to ~89 mAh/h — a factor
of 8.
NB_ICE_KEEP_ALIVE_INTERVAL_SEC can override it, but that key is not exported in
client/android/env_list.go, so Android front-ends are not given the knob.
Current behavior
With the client connected, screen off, unplugged and no user activity, the
phone suspends and resumes continuously instead of sleeping.
Kernel suspend counter (/sys/power/suspend_stats/success) over a ~3 minute
window, screen off and unplugged, same device and session:
client running, Rosenpass enabled -> 847 suspends / 1854 s = 1 every 2.2 s
client running, Rosenpass disabled -> 38 suspends / 180 s = 1 every 4.7 s
client stopped (am force-stop) -> 1 suspend / 180 s = sleeps through
Each cycle is ~320 ms awake. Every wake carries the same kernel reason, 1:1
with the suspend count, from dumpsys batterystats --history:
+running wake_reason=0:"Abort: Pending Wakeup Sources: [timerfd]"
-running
+running wake_reason=0:"Abort: Pending Wakeup Sources: [timerfd]"
-running
Over a full night: 34 % awake time with the screen off (3 h 53 out of 11 h 21),
against roughly 6 % on the same device without the client. Doze never reaches
deep idle - dumpsys deviceidle reports mState=ACTIVE after 12 minutes with the
screen off.
ftrace on power:wakeup_source_activate filtered to [timerfd], captured during
the loop, shows the wakeup source taken from ep_poll_callback and drained by
system_server's AlarmManager thread. So Android's own alarm/epoll machinery is
being spun; the VPN app is not holding a wakelock itself:
-0 wakeup_source_activate: [timerfd]
=> __pm_stay_awake
=> ep_poll_callback
AlarmManager-2716 wakeup_source_activate: [timerfd]
=> __pm_stay_awake
=> ep_send_events_proc
The ~4.7 s cadence with Rosenpass off matches iceKeepAliveDefault = 4 s.
Rosenpass adds a second beat on top, roughly halving the interval.
I have not proven the exact path from the ICE keepalive to the AlarmManager
timerfd. What is established: the cadence matches the configured keepalive, and
stopping the client removes the wakes entirely.
Expected behavior
An idle Android peer with no traffic should let the device reach deep sleep for
minutes at a time. A 4-second keepalive is a reasonable desktop default for
holding a NAT binding, but on a battery-powered handset it is far more
aggressive than needed - WireGuard's own PersistentKeepalive default, which
netbird itself uses for the data plane (defaultWgKeepAlive = 25 * time.Second
in client/internal/peer/endpoint.go), is 25 seconds and is empirically low
enough for the device to sleep normally.
Steps to reproduce
adb shell su -c 'cat /sys/power/suspend_stats/success'
Observed: hundreds of suspend/resume cycles with the client running, versus
one with it stopped.
Environment and topology
one 0.0.0.0/0 exit-node route advertised by Peer B, distributed to Peer A
(the behaviour is identical with the route disabled)
Self-hosted details, if available
Not exercised in this reproduction - the peers connect host/host, directly
on the LAN, with no relay in the path.
throughout (handshakes fine, no reconnects)
Logs, status output, or debug evidence
Related issues or discussions
net.InterfaceByName polling on non-Linux platforms)
path, but the same underlying theme - a fixed-interval poll that is fine on a
desktop and expensive on a constrained platform. Memory leak after 0.40.0 update #3678 was fixed by making
the Linux watcher event-driven; this report is about the ICE keepalive, which
is still a fixed interval everywhere.
Impact
is unconditional and there is no way to change it from an Android front-end
third of its standby battery. Enough that I switched back to plain WireGuard.
rate but still leaves a wake every ~5 s. A front-end could set
NB_ICE_KEEP_ALIVE_INTERVAL_SEC by hardcoding the string, since
exportEnvList() calls os.Setenv on arbitrary keys - but the key is not in
client/android/env_list.go, so it is not discoverable, and it requires
rebuilding the app.
Additional context
Relevant code:
client/internal/peer/ice/agent.go
iceKeepAliveDefault = 4 * time.Second
iceDisconnectedTimeoutDefault = 6 * time.Second
iceFailedTimeoutDefault = 6 * time.Second
agentConfig := &ice.AgentConfig{
...
KeepaliveInterval: &iceKeepAlive,
DisconnectedTimeout: &iceDisconnectedTimeout,
...
}
pion sends a STUN binding indication on the selected pair every
KeepaliveInterval. There is no adaptation to GOOS == "android", to screen
state, or to the platform idle mode.
client/android/env_list.go exports EnvKeyNBForceRelay, EnvKeyNBLazyConn and
EnvKeyNBInactivityThreshold, but none of the ICE tuning keys.
Suggested fixes, roughly in order of how much they help:
Adapt the keepalive on mobile. Something in the 15-25 s range on
android/ios would match what WireGuard considers sufficient to hold a NAT
mapping, at 6x fewer wakes.
Export the existing knob to Android:
// EnvKeyNBICEKeepAlive Exported for Android java client to tune the ICE keepalive
EnvKeyNBICEKeepAlive = ice.EnvICEKeepAliveIntervalSec
Suspend or stretch keepalives while the platform reports idle. The Android
front-end knows when the device enters Doze; a hook to pause on idle and
resume on wake would remove the drain rather than reduce it.
Secondary: enabling Rosenpass roughly doubles the wake rate on top of the ICE
keepalive. The same idle adaptation would probably be worth applying there.
All reactions