From b0f353af9096434ae6dfba4496bf3bed30a8f396 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sat, 10 Oct 2026 09:17:36 +0700 Subject: [PATCH] android: whitelist - working recheck backoff, fewer probe rounds - scheduled rechecks are forced: the 15/30 s steps were below FRESH_MS, so the unforced recheck returned early and the backoff chain died - signal-strength-only capability changes are no longer network events (each one reset the backoff and launched a check) - tunnel state transitions force a new round at most every 15 s Co-Authored-By: Claude Opus 5.5 --- .../logic/connectivity/WhiteListManager.kt | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/connectivity/WhiteListManager.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/connectivity/WhiteListManager.kt index 22f05fecf..1b339e544 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/connectivity/WhiteListManager.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/connectivity/WhiteListManager.kt @@ -44,6 +44,7 @@ import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import org.strongswan.android.logic.VpnStateService +import java.util.concurrent.ConcurrentHashMap /** Whether the ISP currently blocks direct access to foreign hosts. */ enum class WhiteListStatus @@ -177,6 +178,9 @@ class WhiteListManager private constructor(private val context: Context) : VpnSt private val _state = MutableStateFlow(WhiteListState()) val state: StateFlow = _state + /* last classification-relevant capabilities per network (see capabilityKey) */ + private val lastCapabilities = ConcurrentHashMap() + /* event trigger: the underlying network changed, the ISP may classify differently now */ private val networkCallback = object : ConnectivityManager.NetworkCallback() { @@ -186,12 +190,19 @@ class WhiteListManager private constructor(private val context: Context) : VpnSt } override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) - { - onNetworkEvent() + { /* fired on every signal-strength change too: only a change that + * can alter the ISP's classification counts as an event (it also + * resets the recheck backoff) */ + val key = capabilityKey(capabilities) + if (lastCapabilities.put(network, key) != key) + { + onNetworkEvent() + } } override fun onLost(network: Network) { + lastCapabilities.remove(network) onNetworkEvent() } } @@ -238,6 +249,7 @@ class WhiteListManager private constructor(private val context: Context) : VpnSt recheckJob?.cancel() recheckJob = null unregisterNetworkCallback() + lastCapabilities.clear() service?.unregisterListener(this) try { @@ -364,7 +376,10 @@ class WhiteListManager private constructor(private val context: Context) : VpnSt return } backoffAttempt = 0 - scope.launch { runCheck(force = true) } + /* a flapping tunnel (CONNECTING -> DISCONNECTING -> DISABLED -> ...) + * must not trigger a fresh direct probe round on every transition */ + val force = SystemClock.elapsedRealtime() - lastCheckElapsed >= TRANSITION_MIN_GAP_MS + scope.launch { runCheck(force = force) } } private fun onNetworkEvent() @@ -453,7 +468,9 @@ class WhiteListManager private constructor(private val context: Context) : VpnSt recheckJob?.cancel() recheckJob = scope.launch { delay(delayMs) - runCheck(force = false) + /* forced: the first backoff steps are shorter than FRESH_MS, an + * unforced recheck returned early and ended the chain */ + runCheck(force = true) } } @@ -598,6 +615,22 @@ class WhiteListManager private constructor(private val context: Context) : VpnSt /** Cached results younger than this are not re-probed. */ private const val FRESH_MS = 60_000L + /** State transitions force a new round only if the last one is at least this old. */ + private const val TRANSITION_MIN_GAP_MS = 15_000L + + /** The parts of the capabilities that matter for the classification. */ + @JvmStatic + internal fun capabilityKey(caps: NetworkCapabilities): String + { + val transports = intArrayOf(NetworkCapabilities.TRANSPORT_WIFI, NetworkCapabilities.TRANSPORT_CELLULAR, + NetworkCapabilities.TRANSPORT_ETHERNET, NetworkCapabilities.TRANSPORT_VPN) + .filter { caps.hasTransport(it) } + val capabilities = intArrayOf(NetworkCapabilities.NET_CAPABILITY_INTERNET, + NetworkCapabilities.NET_CAPABILITY_VALIDATED, NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + .filter { caps.hasCapability(it) } + return "$transports/$capabilities" + } + /** * A cached ACTIVE status stops suppressing reconnects once no recheck * has confirmed it for this long (fail open). Kept above the backoff