diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 5bbb9f734..9a5578bcd 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -372,8 +372,7 @@ dependencies { // Kotlin serialization for the times where we need the Json tree and performance is not that important. implementation(libs.kotlinx.serialization.json) - implementation libs.tor.android - implementation libs.jtorctl + implementation libs.arti.mobile.ex testImplementation libs.junit testImplementation libs.mockk diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt index 220987e65..911ddfa9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt @@ -40,7 +40,6 @@ import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import net.freehaven.tor.control.TorControlCommands import okhttp3.OkHttpClient class RelayProxyClientConnector( @@ -79,24 +78,13 @@ class RelayProxyClientConnector( client.disconnect() } if (it.torStatus is TorServiceStatus.Active) { - try { - it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_DORMANT) - Log.d("ManageRelayServices", "Pausing Tor Activity") - } catch (e: Exception) { - Log.e("ManageRelayServices") { "Failed to signal Tor dormant: ${e.message}" } - } + Log.d("ManageRelayServices", "Connectivity off, Tor idle") } } else if (it.connectivity is ConnectivityStatus.Active && !client.isActive()) { Log.d("ManageRelayServices", "Connectivity On: Resuming Relay Services") if (it.torStatus is TorServiceStatus.Active) { - try { - it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_ACTIVE) - it.torStatus.torControlConnection?.signal(TorControlCommands.SIGNAL_NEWNYM) - Log.d("ManageRelayServices", "Resuming Tor Activity with new nym") - } catch (e: Exception) { - Log.e("ManageRelayServices") { "Failed to signal Tor active: ${e.message}" } - } + Log.d("ManageRelayServices", "Connectivity resumed, Tor active") } // only calls this if the client is not active. Otherwise goes to the else below diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt index 013339885..2c965de36 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt @@ -20,88 +20,84 @@ */ package com.vitorpamplona.amethyst.ui.tor -import android.content.ComponentName import android.content.Context -import android.content.Context.BIND_AUTO_CREATE -import android.content.Intent -import android.content.ServiceConnection -import android.os.IBinder import com.vitorpamplona.quartz.utils.Log +import info.guardianproject.arti.ArtiLogListener +import info.guardianproject.arti.ArtiProxy import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.launch -import org.torproject.jni.TorService -import org.torproject.jni.TorService.LocalBinder -private const val SOCKS_PORT_POLL_INTERVAL_MS = 100L +private const val DEFAULT_SOCKS_PORT = 19050 +private const val MAX_PORT_RETRIES = 3 class TorService( val context: Context, ) { val status = callbackFlow { - Log.d("TorService", "Binding Tor Service") + Log.d("TorService", "Starting Arti Tor Service") trySend(TorServiceStatus.Connecting) - val currentIntent = Intent(context, TorService::class.java) - val serviceConnection: ServiceConnection = - object : ServiceConnection { - override fun onServiceConnected( - name: ComponentName, - service: IBinder, - ) { - launch(Dispatchers.IO) { - try { - // moved torService to a local variable, since we only need it once - val torService = (service as LocalBinder).service + var socksPort = DEFAULT_SOCKS_PORT + var artiProxy: ArtiProxy? = null + var started = false - while (torService.socksPort < 0) { - delay(SOCKS_PORT_POLL_INTERVAL_MS) - } + val logListener = + ArtiLogListener { logLine -> + val text = logLine ?: return@ArtiLogListener + Log.d("TorService") { "Arti: $text" } - val active = TorServiceStatus.Active(torService.socksPort) - active.torControlConnection = torService.torControlConnection - - trySend(active) - Log.d("TorService") { "Tor Service Connected ${torService.socksPort}" } - } catch (e: Exception) { - Log.e("TorService") { "Tor service connection failed: ${e.message}" } - trySend(TorServiceStatus.Off) + when { + text.contains("Sufficiently bootstrapped", ignoreCase = true) || + text.contains("is usable", ignoreCase = true) -> { + if (!started) { + started = true + trySend(TorServiceStatus.Active(socksPort)) + Log.d("TorService") { "Arti bootstrapped on port $socksPort" } } } - } - override fun onServiceDisconnected(name: ComponentName) { - Log.d("TorService", "Tor Service Disconnected") - trySend(TorServiceStatus.Off) + text.contains("state changed to Stopped", ignoreCase = true) -> { + started = false + trySend(TorServiceStatus.Off) + } } } - try { - context.bindService( - currentIntent, - serviceConnection, - BIND_AUTO_CREATE, - ) - } catch (e: Exception) { - Log.e("TorService") { "Failed to bind Tor Service: ${e.message}" } + var lastError: Exception? = null + for (attempt in 0 until MAX_PORT_RETRIES) { + try { + artiProxy = + ArtiProxy + .Builder(context.applicationContext) + .setSocksPort(socksPort) + .setDnsPort(socksPort + 1) + .setLogListener(logListener) + .build() + + artiProxy!!.start() + lastError = null + break + } catch (e: Exception) { + lastError = e + Log.e("TorService") { "Failed to start Arti on port $socksPort (attempt ${attempt + 1}): ${e.message}" } + socksPort++ + } + } + + if (lastError != null) { + Log.e("TorService") { "Failed to start Arti after $MAX_PORT_RETRIES attempts" } trySend(TorServiceStatus.Off) } awaitClose { - Log.d("TorService", "Stopping Tor Service") + Log.d("TorService", "Stopping Arti Tor Service") try { - context.unbindService(serviceConnection) + artiProxy?.stop() } catch (e: Exception) { - Log.d("TorService") { "Failed to unbind Tor Service: ${e.message}" } - } - try { - context.stopService(currentIntent) - } catch (e: Exception) { - Log.d("TorService") { "Failed to stop Tor Service: ${e.message}" } + Log.d("TorService") { "Failed to stop Arti: ${e.message}" } } trySend(TorServiceStatus.Off) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorServiceStatus.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorServiceStatus.kt index 2dd7f31f1..6eec45809 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorServiceStatus.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorServiceStatus.kt @@ -20,15 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.tor -import net.freehaven.tor.control.TorControlConnection - sealed class TorServiceStatus { data class Active( val port: Int, - ) : TorServiceStatus() { - // If internal, it has control. - var torControlConnection: TorControlConnection? = null - } + ) : TorServiceStatus() object Off : TorServiceStatus() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bdaed5d14..a88aff95a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ fragmentKtx = "1.8.9" gms = "4.4.4" jacksonModuleKotlin = "2.21.2" javaKeyring = "1.0.4" -jtorctl = "0.4.5.7" +artiMobileEx = "1.2.3" junit = "4.13.2" kchesslib = "1.0.5" kotlin = "2.3.20" @@ -51,7 +51,6 @@ securityCryptoKtx = "1.1.0" slf4j = "2.0.17" spotless = "8.4.0" tarsosdsp = "2.5" -torAndroid = "0.4.9.5.1" translate = "17.0.3" jetbrainsCompose = "1.10.3" unifiedpush = "3.0.10" @@ -141,7 +140,7 @@ google-mlkit-language-id = { group = "com.google.mlkit", name = "language-id", v google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" } jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jacksonModuleKotlin" } java-keyring = { group = "com.github.javakeyring", name = "java-keyring", version.ref = "javaKeyring" } -jtorctl = { module = "info.guardianproject:jtorctl", version.ref = "jtorctl" } +arti-mobile-ex = { module = "info.guardianproject:arti-mobile-ex", version.ref = "artiMobileEx" } junit = { group = "junit", name = "junit", version.ref = "junit" } kchesslib = { module = "io.github.cvb941:kchesslib", version.ref = "kchesslib" } kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" } @@ -163,7 +162,6 @@ secp256k1-kmp-common = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp", v secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" } secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm", version.ref = "secp256k1KmpJniAndroid" } tarsosdsp = { group = "be.tarsos.dsp", name = "core", version.ref = "tarsosdsp" } -tor-android = { module = "info.guardianproject:tor-android", version.ref = "torAndroid" } unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version.ref = "unifiedpush" } vico-charts-compose = { group = "com.patrykandpatrick.vico", name = "compose", version.ref = "vico-charts-compose" } vico-charts-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico-charts-compose" }