mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 08:47:33 +00:00
Merge pull request #3275 from vitorpamplona/fix/tor-circuit-dead-warm-reset
fix(tor): warm-reset on dead-circuit self-heal instead of wiping state
This commit is contained in:
@@ -313,13 +313,20 @@ class TorManager(
|
||||
* the per-relay success/failure outcome and the Tor-routing of each url) detects the
|
||||
* all-failing condition and pokes us here — analogous to [onNetworkChange].
|
||||
*
|
||||
* Recovery mirrors the post-Active stuck-Connecting path: drop the client and wipe
|
||||
* `arti/state/` (we *did* bootstrap, so a fully-dead exit set behind a healthy-looking
|
||||
* guards.json points at a bad persisted guard/circuit sample worth rebuilding), then bump
|
||||
* [resetEpoch] so the status combine re-enters the INTERNAL branch and runs a full
|
||||
* re-init. Shares [lastSelfHealAtMs]/[SELF_HEAL_COOLDOWN_MS] with the Connecting watchdog
|
||||
* so the two can't thrash — at most one self-heal per cooldown window. If circuits are
|
||||
* still dead after the reset, the cooldown suppresses further resets and the 60s
|
||||
* The failure is exit-side, not entry-side: the dead circuits' *exits* can't reach the
|
||||
* relays (`ExitTimeout` / `RESOLVEFAILED`), while the guards (entry) and the cached
|
||||
* consensus are fine. So recovery is a **warm** [TorBackend.reset]: drop the in-process
|
||||
* client so the next start rebuilds the circuit pool from scratch — a fresh exit draw —
|
||||
* but keep `arti/state/` (the guards were never the problem) and the consensus cache, so
|
||||
* the re-bootstrap is a ~5s warm restart, not a ~60s cold consensus re-download. We
|
||||
* deliberately do NOT [TorBackend.resetWithCleanState] here: exits aren't persisted, so
|
||||
* wiping guards + cache can't improve exit selection, and the 60s cold bootstrap it forces
|
||||
* is exactly the blackout that strands users on the [connectionFailure] dialog. Then bump
|
||||
* [resetEpoch] so the status combine re-enters the INTERNAL branch and re-inits.
|
||||
*
|
||||
* Shares [lastSelfHealAtMs]/[SELF_HEAL_COOLDOWN_MS] with the Connecting watchdog so the
|
||||
* two can't thrash — at most one self-heal per cooldown window. If the fresh circuits are
|
||||
* still dead after the rotation, the cooldown suppresses further resets and the 60s
|
||||
* [connectionFailure] dialog still offers the user the bypass.
|
||||
*/
|
||||
fun onTorCircuitsDead() {
|
||||
@@ -328,9 +335,9 @@ class TorManager(
|
||||
val now = nowMs()
|
||||
if (now - lastSelfHealAtMs < SELF_HEAL_COOLDOWN_MS) return
|
||||
lastSelfHealAtMs = now
|
||||
Log.w("TorManager") { "Tor Active but all circuits failing — self-healing (drop client + wipe state)" }
|
||||
Log.w("TorManager") { "Tor Active but all circuits failing — self-healing (drop client to rotate exits, keep state)" }
|
||||
scope.launch(ioDispatcher) {
|
||||
service.resetWithCleanState()
|
||||
service.reset()
|
||||
resetEpoch.update { it + 1 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,18 +171,22 @@ class TorManagerTest {
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun `onTorCircuitsDead wipes state and re-inits when Active`() =
|
||||
fun `onTorCircuitsDead rotates exits with a warm reset when Active`() =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
val backend = FakeTorBackend()
|
||||
val manager = buildManager(backend = backend, clock = { 1_000_000_000_000L })
|
||||
advanceUntilIdle()
|
||||
backend.setActive(17392)
|
||||
advanceUntilIdle()
|
||||
val resetCountBefore = backend.resetCount
|
||||
|
||||
manager.onTorCircuitsDead()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals("Active-but-failing recovery wipes state", 1, backend.resetWithCleanStateCount)
|
||||
// Exit failures are exit-side: warm reset (keep guards + cache) to rotate exits,
|
||||
// never a state wipe (which only forces a 60s cold bootstrap).
|
||||
assertEquals("Active-but-failing recovery rotates exits warm", resetCountBefore + 1, backend.resetCount)
|
||||
assertEquals("must not wipe state for an exit-side failure", 0, backend.resetWithCleanStateCount)
|
||||
// resetEpoch bump re-enters INTERNAL → start() runs again.
|
||||
assertTrue("re-init should call start() again", backend.startCount >= 2)
|
||||
}
|
||||
@@ -195,10 +199,12 @@ class TorManagerTest {
|
||||
advanceUntilIdle()
|
||||
// Still Connecting (never reached Active).
|
||||
assertEquals(TorServiceStatus.Connecting, manager.status.value)
|
||||
val resetCountBefore = backend.resetCount
|
||||
|
||||
manager.onTorCircuitsDead()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals("no rotation while not Active", resetCountBefore, backend.resetCount)
|
||||
assertEquals(0, backend.resetWithCleanStateCount)
|
||||
}
|
||||
|
||||
@@ -212,10 +218,12 @@ class TorManagerTest {
|
||||
advanceUntilIdle()
|
||||
manager.sessionBypass.value = true
|
||||
advanceUntilIdle()
|
||||
val resetCountBefore = backend.resetCount
|
||||
|
||||
manager.onTorCircuitsDead()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals("no rotation while bypassing", resetCountBefore, backend.resetCount)
|
||||
assertEquals(0, backend.resetWithCleanStateCount)
|
||||
}
|
||||
|
||||
@@ -228,10 +236,11 @@ class TorManagerTest {
|
||||
advanceUntilIdle()
|
||||
backend.setActive(17392)
|
||||
advanceUntilIdle()
|
||||
val resetCountBefore = backend.resetCount
|
||||
|
||||
manager.onTorCircuitsDead()
|
||||
advanceUntilIdle()
|
||||
assertEquals(1, backend.resetWithCleanStateCount)
|
||||
assertEquals(resetCountBefore + 1, backend.resetCount)
|
||||
|
||||
// Backend is Active again (re-init bootstrapped). A second call inside the cooldown
|
||||
// window must be suppressed.
|
||||
@@ -239,7 +248,7 @@ class TorManagerTest {
|
||||
advanceUntilIdle()
|
||||
manager.onTorCircuitsDead()
|
||||
advanceUntilIdle()
|
||||
assertEquals("cooldown should suppress the second self-heal", 1, backend.resetWithCleanStateCount)
|
||||
assertEquals("cooldown should suppress the second self-heal", resetCountBefore + 1, backend.resetCount)
|
||||
|
||||
// Past the cooldown it can fire again.
|
||||
clockNow += TorManager.SELF_HEAL_COOLDOWN_MS + 1_000L
|
||||
@@ -247,7 +256,8 @@ class TorManagerTest {
|
||||
advanceUntilIdle()
|
||||
manager.onTorCircuitsDead()
|
||||
advanceUntilIdle()
|
||||
assertEquals("after cooldown elapses, self-heal fires again", 2, backend.resetWithCleanStateCount)
|
||||
assertEquals("after cooldown elapses, self-heal fires again", resetCountBefore + 2, backend.resetCount)
|
||||
assertEquals("exit-side rotations never wipe state", 0, backend.resetWithCleanStateCount)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user