From 1eb1dfe31725cd2167b6bf55c8987f16ad87cae9 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sat, 10 Oct 2026 09:12:22 +0700 Subject: [PATCH] android: RU list - no reconnect for an unchanged list, failure backoff - same SHA-256 as the applied list: only bump the timestamp, no parse, save or tunnel reconnect (it tore a healthy tunnel down daily) - a failed download is not retried on every cold start (3 h backoff) - the saved list is read and parsed off the main thread at startup - switching the split off in system mode reconnects to drop the routes Co-Authored-By: Claude Opus 5.5 --- .../android/logic/routing/RuIpListManager.kt | 46 ++++++++++++++++--- .../logic/routing/RuIpListManagerTest.kt | 16 +++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/routing/RuIpListManager.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/routing/RuIpListManager.kt index 92c3e972a..910acc50f 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/routing/RuIpListManager.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/routing/RuIpListManager.kt @@ -155,8 +155,10 @@ class RuIpListManager private constructor(private val context: Context) * classification set is hot-swapped in native code instead */ CharonVpnService.pushUserspaceRoutingData(context) } - else if (enabled) - { + else if (enabled || excludedV4.isNotEmpty() || excludedV6.isNotEmpty()) + { /* routes only change when the TUN is re-established - also when + * the split is switched OFF, or the RU ranges keep bypassing the + * tunnel until the next manual reconnect */ scheduleReconnect() } } @@ -183,7 +185,17 @@ class RuIpListManager private constructor(private val context: Context) return try { val (text, sha256) = withContext(Dispatchers.IO) { fetchList() } - val parsed = withContext(Dispatchers.IO) { parse(text) } + if (sha256 == prefs.getString(PREF_SHA256, null) && + (excludedV4.isNotEmpty() || excludedV6.isNotEmpty())) + { /* identical list: nothing to parse, save or re-apply - above + * all no reconnect that tears down a healthy tunnel daily */ + val now = System.currentTimeMillis() + prefs.edit().putLong(PREF_LAST_UPDATE, now).remove(PREF_LAST_FAIL).apply() + publish(status = RuIpListStatus.UP_TO_DATE, lastUpdatedMs = now) + Log.i(TAG, "RU geoip list unchanged") + return true + } + val parsed = withContext(Dispatchers.Default) { parse(text) } if (parsed.v4.isEmpty() && parsed.v6.isEmpty()) { throw IOException("downloaded list contains no CIDRs") @@ -195,6 +207,7 @@ class RuIpListManager private constructor(private val context: Context) prefs.edit() .putLong(PREF_LAST_UPDATE, now) .putString(PREF_SHA256, sha256) + .remove(PREF_LAST_FAIL) .apply() publish( status = RuIpListStatus.UP_TO_DATE, @@ -213,6 +226,8 @@ class RuIpListManager private constructor(private val context: Context) catch (e: Exception) { Log.w(TAG, "RU geoip list update failed", e) + /* remembered so a blocked source is not retried on every cold start */ + prefs.edit().putLong(PREF_LAST_FAIL, System.currentTimeMillis()).apply() publish(status = RuIpListStatus.ERROR) false } @@ -224,14 +239,16 @@ class RuIpListManager private constructor(private val context: Context) /* ------------------------------ internals ------------------------------ */ - private fun loadFromDisk() + private suspend fun loadFromDisk() { val file = listFile() if (file.isFile) { try { - val parsed = parse(file.readText()) + /* thousands of CIDRs: read and parse off the main thread (this + * runs at every process start) */ + val parsed = withContext(Dispatchers.Default) { parse(file.readText()) } excludedV4 = parsed.v4 excludedV6 = parsed.v6 publish( @@ -257,8 +274,8 @@ class RuIpListManager private constructor(private val context: Context) { return } - val last = prefs.getLong(PREF_LAST_UPDATE, 0L) - if (System.currentTimeMillis() - last >= Constants.RUSPLIT_CHECK_INTERVAL_MS) + if (isAutoCheckDue(System.currentTimeMillis(), prefs.getLong(PREF_LAST_UPDATE, 0L), + prefs.getLong(PREF_LAST_FAIL, 0L))) { scope.launch { updateNow() } } @@ -403,6 +420,21 @@ class RuIpListManager private constructor(private val context: Context) private const val FILE_NAME = "ru.list" private const val PREF_LAST_UPDATE = "rusplit_last_update" private const val PREF_SHA256 = "rusplit_sha256" + private const val PREF_LAST_FAIL = "rusplit_last_fail" + /* after a failed download wait this long before the next automatic try */ + internal const val FAIL_RETRY_MS = 3 * 60 * 60 * 1000L + + /** Whether the automatic refresh is due (pure, unit-testable). */ + @JvmStatic + internal fun isAutoCheckDue(now: Long, lastUpdate: Long, lastFail: Long): Boolean + { + if (now - lastUpdate < Constants.RUSPLIT_CHECK_INTERVAL_MS) + { + return false + } + /* clock moved backwards: don't let a "future" failure block forever */ + return lastFail <= 0L || lastFail > now || now - lastFail >= FAIL_RETRY_MS + } private const val RECONNECT_DEBOUNCE_MS = 2000L private const val CONNECT_TIMEOUT_MS = 10_000 private const val READ_TIMEOUT_MS = 30_000 diff --git a/src/frontends/android/app/src/test/java/org/strongswan/android/logic/routing/RuIpListManagerTest.kt b/src/frontends/android/app/src/test/java/org/strongswan/android/logic/routing/RuIpListManagerTest.kt index 5e5c31f09..9e92b7bda 100644 --- a/src/frontends/android/app/src/test/java/org/strongswan/android/logic/routing/RuIpListManagerTest.kt +++ b/src/frontends/android/app/src/test/java/org/strongswan/android/logic/routing/RuIpListManagerTest.kt @@ -290,4 +290,20 @@ class RuIpListManagerTest assertThat(fit).isNotNull() assertThat(RuIpListManager.subtractAndSplit(ranges4, fit!!.first, 6000)).isNotNull() } + + /* ---------------- auto check ---------------- */ + + @Test + fun autoCheckWaitsForTheIntervalAndBacksOffAfterFailures() + { + val day = org.strongswan.android.utils.Constants.RUSPLIT_CHECK_INTERVAL_MS + val now = 10 * day + assertThat(RuIpListManager.isAutoCheckDue(now, now - day / 2, 0L)).isFalse() + assertThat(RuIpListManager.isAutoCheckDue(now, now - day, 0L)).isTrue() + /* a recent failure blocks retries on every cold start */ + assertThat(RuIpListManager.isAutoCheckDue(now, 0L, now - 60_000)).isFalse() + assertThat(RuIpListManager.isAutoCheckDue(now, 0L, now - RuIpListManager.FAIL_RETRY_MS)).isTrue() + /* a failure stamped in the future (clock change) never blocks */ + assertThat(RuIpListManager.isAutoCheckDue(now, 0L, now + 60_000)).isTrue() + } }