diff --git a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java index 703cbd4d..bf9a4cd3 100644 --- a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java +++ b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java @@ -163,6 +163,19 @@ public synchronized void stop() { goClient.stop(); } + /** + * Notify the Go client that the underlying network changed (WiFi/cellular). + * This triggers an immediate re-sync of NetworkAddresses with the management + * server for posture check re-evaluation. + */ + public void notifyNetworkChanged() { + try { + goClient.onUnderlyingNetworkChanged(); + } catch (Exception e) { + Log.e(LOGTAG, "notifyNetworkChanged: " + e.getMessage()); + } + } + public PeerInfoArray peersInfo() { return goClient.peersList(); } diff --git a/tool/src/main/java/io/netbird/client/tool/VPNService.java b/tool/src/main/java/io/netbird/client/tool/VPNService.java index 548ae3ce..cf29834c 100644 --- a/tool/src/main/java/io/netbird/client/tool/VPNService.java +++ b/tool/src/main/java/io/netbird/client/tool/VPNService.java @@ -76,6 +76,12 @@ public void onCreate() { networkChangeDetector = new NetworkChangeDetector( (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE)); networkChangeDetector.subscribe(networkAvailabilityListener); + // Notify Go layer on any network change for posture check re-evaluation + networkChangeDetector.setNetworkChangedCallback(() -> { + if (engineRunner != null) { + engineRunner.notifyNetworkChanged(); + } + }); networkChangeDetector.registerNetworkCallback(); // Register broadcast receiver for stopping engine (e.g., during profile switch) 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 8c8cdbce..4f6fefc3 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 @@ -13,26 +13,24 @@ public ConcreteNetworkAvailabilityListener() { @Override public void onNetworkAvailable(@Constants.NetworkType int networkType) { - boolean isWifiAvailable = Boolean.TRUE.equals(availableNetworkTypes.get(Constants.NetworkType.WIFI)); + Boolean wasAvailable = availableNetworkTypes.put(networkType, true); - availableNetworkTypes.put(networkType, true); - - // if wifi is available and wasn't before, notifies listener. - // Android prioritizes wifi over mobile data network by default. - if (!isWifiAvailable && networkType == Constants.NetworkType.WIFI) { + // Always notify on any network change so the engine re-syncs + // NetworkAddresses with the management server. This ensures + // posture checks see the current network (e.g. WiFi subnet) + // immediately after a network switch. + if (!Boolean.TRUE.equals(wasAvailable)) { notifyListener(); } } @Override public void onNetworkLost(@Constants.NetworkType int networkType) { - boolean isMobileAvailable = Boolean.TRUE.equals(availableNetworkTypes.get(Constants.NetworkType.MOBILE)); - availableNetworkTypes.remove(networkType); - // if wifi is lost and mobile data is available, notifies listener. - // No use to notify it if there's no other type of network available. - if (isMobileAvailable && networkType == Constants.NetworkType.WIFI) { + // Notify on any network loss if another network is still available, + // so the engine re-syncs with updated NetworkAddresses. + if (!availableNetworkTypes.isEmpty()) { notifyListener(); } } 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 f02a09c9..b9edaedf 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 @@ -14,12 +14,23 @@ public class NetworkChangeDetector { private final ConnectivityManager connectivityManager; private ConnectivityManager.NetworkCallback networkCallback; private volatile NetworkAvailabilityListener listener; + private volatile Runnable networkChangedCallback; + private volatile boolean initialCallbackReceived; public NetworkChangeDetector(ConnectivityManager connectivityManager) { this.connectivityManager = connectivityManager; initNetworkCallback(); } + /** + * Set a callback that fires on every network availability/loss event, + * regardless of type. Used to notify the Go layer about underlying + * network changes for posture check re-evaluation. + */ + public void setNetworkChangedCallback(Runnable callback) { + this.networkChangedCallback = callback; + } + private void checkNetworkCapabilities(Network network, Consumer operation) { var capabilities = connectivityManager.getNetworkCapabilities(network); if (capabilities == null) return; @@ -37,16 +48,36 @@ private void initNetworkCallback() { networkCallback = new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(@NonNull Network network) { + // Skip the very first onAvailable after registerNetworkCallback(). + // Android fires this immediately for each already-connected network — + // it is an initial state report, not an actual network change. + // Note: if both WiFi and cellular are connected at registration time, + // Android fires onAvailable for each, but we only skip the first one. + // The second fires a spurious notification, which is acceptable because + // the EngineRestarter debounces rapid network change callbacks anyway. + if (!initialCallbackReceived) { + initialCallbackReceived = true; + Log.d(LOGTAG, "ignoring initial onAvailable (not a real network change)"); + return; + } + Log.d(LOGTAG, "onAvailable: " + network); NetworkAvailabilityListener localListener = listener; - if (localListener == null) return; - checkNetworkCapabilities(network, localListener::onNetworkAvailable); + if (localListener != null) { + checkNetworkCapabilities(network, localListener::onNetworkAvailable); + } + Runnable cb = networkChangedCallback; + if (cb != null) cb.run(); } @Override public void onLost(@NonNull Network network) { + Log.d(LOGTAG, "onLost: " + network); NetworkAvailabilityListener localListener = listener; - if (localListener == null) return; - checkNetworkCapabilities(network, localListener::onNetworkLost); + if (localListener != null) { + checkNetworkCapabilities(network, localListener::onNetworkLost); + } + Runnable cb = networkChangedCallback; + if (cb != null) cb.run(); } @Override @@ -59,6 +90,7 @@ public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapa } public void registerNetworkCallback() { + initialCallbackReceived = false; NetworkRequest.Builder builder = new NetworkRequest.Builder(); builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); connectivityManager.registerNetworkCallback(builder.build(), networkCallback); diff --git a/tool/src/test/java/io/netbird/client/tool/ConcreteNetworkAvailabilityListenerUnitTest.java b/tool/src/test/java/io/netbird/client/tool/ConcreteNetworkAvailabilityListenerUnitTest.java index 809ceb11..b1dc0176 100644 --- a/tool/src/test/java/io/netbird/client/tool/ConcreteNetworkAvailabilityListenerUnitTest.java +++ b/tool/src/test/java/io/netbird/client/tool/ConcreteNetworkAvailabilityListenerUnitTest.java @@ -53,11 +53,11 @@ public void shouldNotifyListenerNetworkUpgraded() { var networkChangeDetector = new MockNetworkChangeDetector(networkAvailabilityListener); // Act: - networkChangeDetector.activateMobile(); - networkChangeDetector.activateWifi(); + networkChangeDetector.activateMobile(); // new network type -> notify + networkChangeDetector.activateWifi(); // new network type -> notify - // Assert: - assertEquals(1, networkToggleListener.totalTimesNetworkTypeChanged); + // Assert: both mobile and wifi becoming available trigger notifications + assertEquals(2, networkToggleListener.totalTimesNetworkTypeChanged); } @Test @@ -70,16 +70,16 @@ public void shouldNotifyListenerNetworkDowngraded() { var networkChangeDetector = new MockNetworkChangeDetector(networkAvailabilityListener); // Act: - networkChangeDetector.activateMobile(); - networkChangeDetector.activateWifi(); // upgraded, network changes. - networkChangeDetector.deactivateWifi(); // downgraded, network changes. + networkChangeDetector.activateMobile(); // new network type -> notify + networkChangeDetector.activateWifi(); // new network type -> notify + networkChangeDetector.deactivateWifi(); // lost, mobile still available -> notify - // Assert: - assertEquals(2, networkToggleListener.totalTimesNetworkTypeChanged); + // Assert: each event triggers a notification + assertEquals(3, networkToggleListener.totalTimesNetworkTypeChanged); } @Test - public void shouldNotNotifyListenerNetworkDidNotUpgrade() { + public void shouldNotifyListenerOnAnyNetworkChange() { // Assemble: var networkToggleListener = new MockNetworkToggleListener(); var networkAvailabilityListener = new ConcreteNetworkAvailabilityListener(); @@ -92,11 +92,11 @@ public void shouldNotNotifyListenerNetworkDidNotUpgrade() { networkToggleListener.resetCounter(); - networkChangeDetector.activateMobile(); - networkChangeDetector.deactivateMobile(); + networkChangeDetector.activateMobile(); // new network type -> notify + networkChangeDetector.deactivateMobile(); // lost, wifi still available -> notify - // Assert: - assertEquals(0, networkToggleListener.totalTimesNetworkTypeChanged); + // Assert: every network change notifies for posture check re-evaluation + assertEquals(2, networkToggleListener.totalTimesNetworkTypeChanged); } @Test