Summary
Self-hosted NetBird. On iOS, the session reaches Connected in ~1.3 s, but for the next 10–14 s the VPN status-bar icon flickers 3–4 times and the tunnel is effectively unusable. Desktop clients (macOS/Windows) on the same account, against the same management/signal/relay, connect in 1–2 s. Not observed on Android.
The cause we measured: the client performs one tunnel network settings application per IP routed resource (~0.2 s each) instead of batching them into a single application. Domain resources are unaffected.
What the device logs show (macOS Console streaming from the iPhone)
Timeline from nesessionmanager / SpringBoard / mDNSResponder during one connect (network map with 64 IP routed resources):
Received a start command → status changed to connected in 1.3 s — this part is fine.
- Then, for the next 14 s, tunnel network settings are applied 191 times (~13–15 applies/s). Each cycle on the tun interface:
utunX: removing DNS / removing IPv4 / removing IPv6 / removing proxies → setting DNS / setting IPv4 …. IPv4 config is present in only 64 of the 191 applies — most of that window the interface has no IPv4 at all, so routed traffic fails.
mDNSResponder processes ~85 network-config changes per second during the window.
SpringBoard receives ~65 VPN state-change notifications → the status-bar VPN icon visibly flickers.
Controlled experiments (same phone, test user, varying the policy contents)
| Network map contents |
"provider set tunnel configuration" events |
Churn duration |
| 64 IP resources (subnets/hosts) |
64 |
~14 s |
| 46 IP resources (single network) |
47 (= 46 + 1 initial) |
~11 s |
| 11 IP resources |
~2 icon redraws |
< 1 s |
| 113 domain resources only |
no churn |
none — connects instantly |
The effect is linear in the number of subnet/host resources: ~0.2 s of tunnel instability per resource. With a realistic enterprise network map (~50–60 subnets) every connect costs 10+ seconds of icon flicker and broken routing. Domain resources, even 113 of them, cause no churn at all — so the per-item reapply happens on the IP-route path specifically.
Also ruled out: immediate reconnect is equally slow (not extension cold start), Wi-Fi and LTE behave the same (not DNS64/IPv6), and two independent self-hosted installs behind different load balancers show identical behavior (not the control plane).
Expected behavior
All routes/DNS from the network map applied in one (or a few debounced) setTunnelNetworkSettings calls, matching the effective behavior of the desktop and Android clients.
Environment
- Self-hosted NetBird, two identical installs (nginx and hardware LB in front) — same behavior on both
- iOS app from the App Store, NetworkExtension bundle version 0.3.3
- Observed on iOS 26.5.2 (primary test device), multiple iPhones in the fleet affected
- Keycloak as IdP
Debug bundle
Uploaded from the app (Settings → Troubleshoot). Upload key: d89ee892b8bb157f842d017aa9b67b59bd879fab8c73b4dce741397cc45df89d/c150e5b8-6974-4254-ae04-13e6bc8de1df
Happy to provide more logs or test a build.
Summary
Self-hosted NetBird. On iOS, the session reaches Connected in ~1.3 s, but for the next 10–14 s the VPN status-bar icon flickers 3–4 times and the tunnel is effectively unusable. Desktop clients (macOS/Windows) on the same account, against the same management/signal/relay, connect in 1–2 s. Not observed on Android.
The cause we measured: the client performs one tunnel network settings application per IP routed resource (~0.2 s each) instead of batching them into a single application. Domain resources are unaffected.
What the device logs show (macOS Console streaming from the iPhone)
Timeline from
nesessionmanager/SpringBoard/mDNSResponderduring one connect (network map with 64 IP routed resources):Received a start command→status changed to connectedin 1.3 s — this part is fine.utunX: removing DNS / removing IPv4 / removing IPv6 / removing proxies→setting DNS / setting IPv4 …. IPv4 config is present in only 64 of the 191 applies — most of that window the interface has no IPv4 at all, so routed traffic fails.mDNSResponderprocesses ~85 network-config changes per second during the window.SpringBoardreceives ~65 VPN state-change notifications → the status-bar VPN icon visibly flickers.Controlled experiments (same phone, test user, varying the policy contents)
The effect is linear in the number of subnet/host resources: ~0.2 s of tunnel instability per resource. With a realistic enterprise network map (~50–60 subnets) every connect costs 10+ seconds of icon flicker and broken routing. Domain resources, even 113 of them, cause no churn at all — so the per-item reapply happens on the IP-route path specifically.
Also ruled out: immediate reconnect is equally slow (not extension cold start), Wi-Fi and LTE behave the same (not DNS64/IPv6), and two independent self-hosted installs behind different load balancers show identical behavior (not the control plane).
Expected behavior
All routes/DNS from the network map applied in one (or a few debounced)
setTunnelNetworkSettingscalls, matching the effective behavior of the desktop and Android clients.Environment
Debug bundle
Uploaded from the app (Settings → Troubleshoot). Upload key: d89ee892b8bb157f842d017aa9b67b59bd879fab8c73b4dce741397cc45df89d/c150e5b8-6974-4254-ae04-13e6bc8de1df
Happy to provide more logs or test a build.