mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
Stop reporting Tor as connected once its circuits fail
Arti's bootstrap milestones only fire on the way up. "We have found that guard [scrubbed] is usable." latches the session to RUNNING at 100% and calls stopInactivityMonitoring(), and from there nothing downgrades the status except a deliberate stop or a state-file lock conflict. So when the network goes away after bootstrap, Arti logs a continuous stream of "Failed to connect through Tor" / "SOCKS connection error" while the header keeps showing the connected colour, which is gated on running && bootstrapPercent >= 100. isProxyEnabled() agrees, so requests keep being routed into a SOCKS port that cannot reach an exit. Track circuit failures after bootstrap and drop the published status below 100% once they both accumulate and persist. Individual failures are a normal part of Tor, so a burst inside one instant — which is what the report shows, three threads failing in the same millisecond — is ignored; it takes four failures still going twenty seconds later. Downgrading to BOOTSTRAPPING rather than ERROR keeps running = true, so callers fail closed through awaitSelectedRoute instead of leaking to clearnet, the inactivity watchdog re-arms now that bootstrapPercent is below 100, and the existing backoff retry gets to re-establish the session. A successful re-bootstrap clears the tally and restores RUNNING. The policy is kept free of Android and coroutine dependencies so the thresholds are directly testable. Refs #610
This commit is contained in:
parent
5156f7de89
commit
71dd9dfca3
@ -93,6 +93,7 @@ class ArtiTorManager private constructor() {
|
||||
private var inactivityJob: Job? = null
|
||||
private var retryJob: Job? = null
|
||||
private var currentApplication: Application? = null
|
||||
private val circuitHealth = TorCircuitHealthPolicy()
|
||||
|
||||
private enum class LifecycleState { STOPPED, STARTING, RUNNING, STOPPING }
|
||||
|
||||
@ -484,6 +485,7 @@ class ArtiTorManager private constructor() {
|
||||
}
|
||||
retryAttempts = 0
|
||||
bindRetryAttempts = 0
|
||||
circuitHealth.reset()
|
||||
startInactivityMonitoring()
|
||||
}
|
||||
|
||||
@ -498,10 +500,36 @@ class ArtiTorManager private constructor() {
|
||||
running = true
|
||||
)
|
||||
}
|
||||
circuitHealth.reset()
|
||||
stopInactivityMonitoring()
|
||||
completeWaitersIf(TorState.RUNNING)
|
||||
}
|
||||
|
||||
// Bootstrap milestones only fire on the way up, so a session that loses every exit
|
||||
// circuit after reaching RUNNING would otherwise stay pinned at 100% while every
|
||||
// request through it fails.
|
||||
circuitHealth.isCircuitFailure(s) -> {
|
||||
if (currentState != TorState.RUNNING || currentLifecycle != LifecycleState.RUNNING) {
|
||||
return
|
||||
}
|
||||
if (!circuitHealth.onCircuitFailure(System.currentTimeMillis())) {
|
||||
return
|
||||
}
|
||||
circuitHealth.reset()
|
||||
Log.w(TAG, "Tor circuits failing after bootstrap; no longer reporting connected")
|
||||
// Drop below 100% rather than to ERROR: callers fail closed instead of leaking
|
||||
// to clearnet, the indicator stops claiming a working Tor, and the existing
|
||||
// watchdog plus retry path get another chance to re-establish the session.
|
||||
_statusFlow.update {
|
||||
it.copy(
|
||||
state = TorState.BOOTSTRAPPING,
|
||||
bootstrapPercent = 75
|
||||
)
|
||||
}
|
||||
startInactivityMonitoring()
|
||||
currentApplication?.let { scheduleRetry(it) }
|
||||
}
|
||||
|
||||
s.contains("AMEx: state changed to Stopping", ignoreCase = true) -> {
|
||||
if (currentLifecycle != LifecycleState.STOPPING) {
|
||||
return
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
package com.bitchat.android.net
|
||||
|
||||
import com.bitchat.android.util.AppConstants
|
||||
|
||||
/**
|
||||
* Decides when a Tor session that already finished bootstrapping should stop being reported as
|
||||
* connected.
|
||||
*
|
||||
* Arti's bootstrap milestones only fire on the way up. Once a guard is usable the session is
|
||||
* latched to 100%, and losing every exit circuit afterwards produces a stream of SOCKS errors
|
||||
* that no bootstrap milestone contradicts — so the indicator keeps claiming a working Tor.
|
||||
*
|
||||
* Circuit failures are a normal part of Tor operation, so a single burst must not move the
|
||||
* indicator. A downgrade requires [failureThreshold] failures that also span at least
|
||||
* [minSpanMs], which separates "three parallel attempts lost a flaky circuit" from "still
|
||||
* failing twenty seconds later". Progress on the wire clears the tally.
|
||||
*
|
||||
* Kept free of Android and coroutine dependencies so the policy is directly testable.
|
||||
*/
|
||||
internal class TorCircuitHealthPolicy(
|
||||
private val failureThreshold: Int = AppConstants.Tor.CIRCUIT_FAILURE_THRESHOLD,
|
||||
private val minSpanMs: Long = AppConstants.Tor.CIRCUIT_FAILURE_MIN_SPAN_MS,
|
||||
private val windowMs: Long = AppConstants.Tor.CIRCUIT_FAILURE_WINDOW_MS
|
||||
) {
|
||||
private companion object {
|
||||
// Arti emits these once it cannot build or use an exit circuit. Matched as substrings in
|
||||
// the same style as the bootstrap milestones in ArtiTorManager.
|
||||
val FAILURE_MARKERS = listOf(
|
||||
"Failed to connect through Tor",
|
||||
"SOCKS connection error",
|
||||
"Failed to obtain exit circuit"
|
||||
)
|
||||
}
|
||||
|
||||
private var failureCount = 0
|
||||
private var windowStartedAtMs = 0L
|
||||
|
||||
fun isCircuitFailure(line: String): Boolean =
|
||||
FAILURE_MARKERS.any { line.contains(it, ignoreCase = true) }
|
||||
|
||||
/**
|
||||
* Records one circuit failure at [nowMs] and reports whether the session has now failed for
|
||||
* long enough, and often enough, to stop being advertised as connected.
|
||||
*/
|
||||
@Synchronized
|
||||
fun onCircuitFailure(nowMs: Long): Boolean {
|
||||
val elapsed = nowMs - windowStartedAtMs
|
||||
// A stale window, or a clock that moved backwards, starts counting again.
|
||||
if (failureCount == 0 || elapsed > windowMs || elapsed < 0L) {
|
||||
windowStartedAtMs = nowMs
|
||||
failureCount = 1
|
||||
return false
|
||||
}
|
||||
|
||||
failureCount++
|
||||
return failureCount >= failureThreshold && elapsed >= minSpanMs
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun reset() {
|
||||
failureCount = 0
|
||||
windowStartedAtMs = 0L
|
||||
}
|
||||
}
|
||||
@ -115,6 +115,14 @@ object AppConstants {
|
||||
const val INACTIVITY_TIMEOUT_MS: Long = 5_000L
|
||||
const val MAX_RETRY_ATTEMPTS: Int = 5
|
||||
const val STOP_TIMEOUT_MS: Long = 7_000L
|
||||
|
||||
// Post-bootstrap circuit health. A Tor session that has bootstrapped can still lose every
|
||||
// exit circuit — the guard stays usable, so nothing in the bootstrap path notices.
|
||||
// Individual circuit failures are normal and must not move the indicator, so a downgrade
|
||||
// needs both enough failures and enough elapsed time to rule out a momentary blip.
|
||||
const val CIRCUIT_FAILURE_THRESHOLD: Int = 4
|
||||
const val CIRCUIT_FAILURE_MIN_SPAN_MS: Long = 20_000L
|
||||
const val CIRCUIT_FAILURE_WINDOW_MS: Long = 120_000L
|
||||
}
|
||||
|
||||
object UI {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user