Merge pull request #2269 from vitorpamplona/claude/fix-relay-connectivity-pJ29t

Improve Tor bootstrap resilience and WebSocket connection handling
This commit is contained in:
Vitor Pamplona
2026-04-12 14:16:34 -04:00
committed by GitHub
4 changed files with 55 additions and 4 deletions
@@ -46,10 +46,16 @@ class OkHttpClientFactory(
.addNetworkInterceptor(keyDecryptor) .addNetworkInterceptor(keyDecryptor)
.build() .build()
private var lastProxy: Proxy? = null
fun buildHttpClient( fun buildHttpClient(
proxy: Proxy?, proxy: Proxy?,
timeoutSeconds: Int, timeoutSeconds: Int,
): OkHttpClient { ): OkHttpClient {
if (proxy != lastProxy) {
rootClient.connectionPool.evictAll()
lastProxy = proxy
}
val seconds = if (proxy != null) timeoutSeconds * 3 else timeoutSeconds val seconds = if (proxy != null) timeoutSeconds * 3 else timeoutSeconds
return rootClient return rootClient
.newBuilder() .newBuilder()
@@ -37,6 +37,7 @@ class OkHttpClientFactoryForRelays(
const val DEFAULT_IS_MOBILE: Boolean = false const val DEFAULT_IS_MOBILE: Boolean = false
const val DEFAULT_TIMEOUT_ON_WIFI_SECS: Int = 10 const val DEFAULT_TIMEOUT_ON_WIFI_SECS: Int = 10
const val DEFAULT_TIMEOUT_ON_MOBILE_SECS: Int = 30 const val DEFAULT_TIMEOUT_ON_MOBILE_SECS: Int = 30
const val WEBSOCKET_PING_INTERVAL_SECS: Long = 120
private fun isEmulator(): Boolean = private fun isEmulator(): Boolean =
Build.FINGERPRINT.startsWith("generic") || Build.FINGERPRINT.startsWith("generic") ||
@@ -76,10 +77,16 @@ class OkHttpClientFactoryForRelays(
.addInterceptor(DefaultContentTypeInterceptor(userAgent)) .addInterceptor(DefaultContentTypeInterceptor(userAgent))
.build() .build()
private var lastProxy: Proxy? = null
fun buildHttpClient( fun buildHttpClient(
proxy: Proxy?, proxy: Proxy?,
timeoutSeconds: Int, timeoutSeconds: Int,
): OkHttpClient { ): OkHttpClient {
if (proxy != lastProxy) {
rootClient.connectionPool.evictAll()
lastProxy = proxy
}
val seconds = if (proxy != null) timeoutSeconds * 3 else timeoutSeconds val seconds = if (proxy != null) timeoutSeconds * 3 else timeoutSeconds
return rootClient return rootClient
.newBuilder() .newBuilder()
@@ -87,6 +94,7 @@ class OkHttpClientFactoryForRelays(
.connectTimeout(Duration.ofSeconds(seconds.toLong())) .connectTimeout(Duration.ofSeconds(seconds.toLong()))
.readTimeout(Duration.ofSeconds(seconds.toLong() * 3)) .readTimeout(Duration.ofSeconds(seconds.toLong() * 3))
.writeTimeout(Duration.ofSeconds(seconds.toLong() * 3)) .writeTimeout(Duration.ofSeconds(seconds.toLong() * 3))
.pingInterval(Duration.ofSeconds(WEBSOCKET_PING_INTERVAL_SECS))
.build() .build()
} }
@@ -54,6 +54,34 @@ class TorService(
private val _status = MutableStateFlow<TorServiceStatus>(TorServiceStatus.Off) private val _status = MutableStateFlow<TorServiceStatus>(TorServiceStatus.Off)
val status: StateFlow<TorServiceStatus> = _status.asStateFlow() val status: StateFlow<TorServiceStatus> = _status.asStateFlow()
private fun artiDataDir() = File(context.filesDir, "arti")
/**
* Clears the Arti cache directory (consensus, relay descriptors) while
* preserving the state directory (guard selection). This forces a fresh
* consensus download on the next bootstrap, preventing stale cached data
* from causing circuit failures.
*/
private fun clearArtiCache() {
val cacheDir = File(artiDataDir(), "cache")
if (cacheDir.exists()) {
cacheDir.deleteRecursively()
Log.d("TorService") { "Cleared Arti cache directory" }
}
}
/**
* Clears all Arti persistent data (state + cache). Used as a last resort
* when initialization fails, to recover from corrupted state.
*/
private fun clearAllArtiData() {
val dataDir = artiDataDir()
if (dataDir.exists()) {
dataDir.deleteRecursively()
Log.d("TorService") { "Cleared all Arti data" }
}
}
/** /**
* Initialize the TorClient (once) and start the SOCKS proxy. * Initialize the TorClient (once) and start the SOCKS proxy.
* Must be called from a coroutine on [Dispatchers.IO]. * Must be called from a coroutine on [Dispatchers.IO].
@@ -90,12 +118,21 @@ class TorService(
} }
} }
val dataDir = File(context.filesDir, "arti").absolutePath // Clear cached consensus/descriptors so Arti bootstraps with
// fresh network data, preventing stale guards/circuits.
clearArtiCache()
val dataDir = artiDataDir().absolutePath
Log.d("TorService") { "Initializing Arti with data dir: $dataDir" } Log.d("TorService") { "Initializing Arti with data dir: $dataDir" }
val initResult = ArtiNative.initialize(dataDir) var initResult = ArtiNative.initialize(dataDir)
if (initResult != 0) { if (initResult != 0) {
Log.e("TorService") { "Failed to initialize Arti: error $initResult" } Log.e("TorService") { "Failed to initialize Arti: error $initResult, clearing data and retrying" }
clearAllArtiData()
initResult = ArtiNative.initialize(dataDir)
}
if (initResult != 0) {
Log.e("TorService") { "Failed to initialize Arti on retry: error $initResult" }
initialized.set(false) initialized.set(false)
_status.value = TorServiceStatus.Off _status.value = TorServiceStatus.Off
return@withContext return@withContext
@@ -228,7 +228,7 @@ open class BasicRelayClient(
} }
fun dontTryAgainForALongTime() { fun dontTryAgainForALongTime() {
delayToConnectInSeconds = TimeUtils.ONE_DAY delayToConnectInSeconds = TimeUtils.FIVE_MINUTES
} }
override fun sendOrConnectAndSync(cmd: Command) { override fun sendOrConnectAndSync(cmd: Command) {