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 <[email protected]>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-10-10 09:17:36 +07:00
1 parent 514b15b1ca
commit b0f353af90
1 file changed
+37 -4
@@ -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<WhiteListState> = _state
/* last classification-relevant capabilities per network (see capabilityKey) */
private val lastCapabilities = ConcurrentHashMap<Network, String>()
/* 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