From f0df3f5986f4db452b4a882089d0c428f0c41e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Mon, 20 Apr 2026 15:24:41 +0200 Subject: [PATCH] Bind process to default network and ignore initial callback burst Pin the process's outgoing sockets to the current default Android Network via ConnectivityManager.bindProcessToNetwork so fresh dials after a WiFi/cellular switch do not stall on TCP SYN retransmits through the departing interface. Skip the initial onAvailable burst fired right after registering the NetworkCallback. That burst reflects current state, not a transition, and was triggering a spurious EngineRestarter restart that cancelled the in-flight login on cold start. --- .../ConcreteNetworkAvailabilityListener.java | 20 +++++++++- .../tool/networks/NetworkChangeDetector.java | 39 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/tool/src/main/java/io/netbird/client/tool/networks/ConcreteNetworkAvailabilityListener.java b/tool/src/main/java/io/netbird/client/tool/networks/ConcreteNetworkAvailabilityListener.java index 8c8cdbc..1542ad6 100644 --- a/tool/src/main/java/io/netbird/client/tool/networks/ConcreteNetworkAvailabilityListener.java +++ b/tool/src/main/java/io/netbird/client/tool/networks/ConcreteNetworkAvailabilityListener.java @@ -4,8 +4,13 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class ConcreteNetworkAvailabilityListener implements NetworkAvailabilityListener { + // Grace window after subscribing a listener during which Android's initial + // onAvailable burst is treated as state seeding, not as a transition. + private static final long INITIAL_BURST_GRACE_MS = 3000; + private final Map availableNetworkTypes; private NetworkToggleListener listener; + private volatile long listenerSubscribedAt = 0; public ConcreteNetworkAvailabilityListener() { this.availableNetworkTypes = new ConcurrentHashMap<>(); @@ -38,16 +43,27 @@ public class ConcreteNetworkAvailabilityListener implements NetworkAvailabilityL } private void notifyListener() { - if (listener != null) { - listener.onNetworkTypeChanged(); + NetworkToggleListener l = listener; + if (l == null) { + return; } + // Skip Android's initial onAvailable burst that fires right after the + // NetworkCallback is registered; that is the current state, not a + // transition, and must not trigger an engine restart. + long subscribedAt = listenerSubscribedAt; + if (subscribedAt != 0 && System.currentTimeMillis() - subscribedAt < INITIAL_BURST_GRACE_MS) { + return; + } + l.onNetworkTypeChanged(); } public void subscribe(NetworkToggleListener listener) { this.listener = listener; + this.listenerSubscribedAt = System.currentTimeMillis(); } public void unsubscribe() { this.listener = null; + this.listenerSubscribedAt = 0; } } diff --git a/tool/src/main/java/io/netbird/client/tool/networks/NetworkChangeDetector.java b/tool/src/main/java/io/netbird/client/tool/networks/NetworkChangeDetector.java index f02a09c..8fdceca 100644 --- a/tool/src/main/java/io/netbird/client/tool/networks/NetworkChangeDetector.java +++ b/tool/src/main/java/io/netbird/client/tool/networks/NetworkChangeDetector.java @@ -13,11 +13,13 @@ public class NetworkChangeDetector { private static final String LOGTAG = NetworkChangeDetector.class.getSimpleName(); private final ConnectivityManager connectivityManager; private ConnectivityManager.NetworkCallback networkCallback; + private ConnectivityManager.NetworkCallback defaultNetworkCallback; private volatile NetworkAvailabilityListener listener; public NetworkChangeDetector(ConnectivityManager connectivityManager) { this.connectivityManager = connectivityManager; initNetworkCallback(); + initDefaultNetworkCallback(); } private void checkNetworkCapabilities(Network network, Consumer operation) { @@ -58,10 +60,37 @@ public class NetworkChangeDetector { }; } + private void initDefaultNetworkCallback() { + defaultNetworkCallback = new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(@NonNull Network network) { + Log.d(LOGTAG, "default network became " + network + ", binding process to it"); + try { + if (!connectivityManager.bindProcessToNetwork(network)) { + Log.w(LOGTAG, "bindProcessToNetwork returned false for " + network); + } + } catch (Exception e) { + Log.e(LOGTAG, "bindProcessToNetwork failed", e); + } + } + + @Override + public void onLost(@NonNull Network network) { + Log.d(LOGTAG, "default network " + network + " lost, clearing process binding"); + try { + connectivityManager.bindProcessToNetwork(null); + } catch (Exception e) { + Log.e(LOGTAG, "bindProcessToNetwork(null) failed", e); + } + } + }; + } + public void registerNetworkCallback() { NetworkRequest.Builder builder = new NetworkRequest.Builder(); builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); connectivityManager.registerNetworkCallback(builder.build(), networkCallback); + connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback); } public void unregisterNetworkCallback() { @@ -70,6 +99,16 @@ public class NetworkChangeDetector { } catch (Exception e) { Log.e(LOGTAG, "failed to unregister network callback", e); } + try { + connectivityManager.unregisterNetworkCallback(defaultNetworkCallback); + } catch (Exception e) { + Log.e(LOGTAG, "failed to unregister default network callback", e); + } + try { + connectivityManager.bindProcessToNetwork(null); + } catch (Exception e) { + Log.e(LOGTAG, "bindProcessToNetwork(null) on unregister failed", e); + } } public void subscribe(NetworkAvailabilityListener listener) {