Add Supa account and search changes
This commit is contained in:
+43
-14
@@ -7,6 +7,7 @@ import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.weblibre.flutter_tor.generated.IPtProxyController
|
||||
import eu.weblibre.flutter_tor.generated.TorApi
|
||||
import eu.weblibre.flutter_tor.generated.TorConfiguration
|
||||
@@ -29,22 +30,25 @@ class FlutterTorPlugin : FlutterPlugin, TorApi {
|
||||
}
|
||||
|
||||
private var context: Context? = null
|
||||
private var binaryMessenger: io.flutter.plugin.common.BinaryMessenger? = null
|
||||
private var torService: TorService? = null
|
||||
private var serviceConnection: ServiceConnection? = null
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
// Service connection state
|
||||
@Volatile
|
||||
private var serviceConnected = CompletableDeferred<Unit>()
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
Log.d(TAG, "onAttachedToEngine")
|
||||
context = flutterPluginBinding.applicationContext
|
||||
binaryMessenger = flutterPluginBinding.binaryMessenger
|
||||
|
||||
// Setup Pigeon API
|
||||
TorApi.setUp(flutterPluginBinding.binaryMessenger, this)
|
||||
|
||||
// Bind to TorService
|
||||
bindTorService(flutterPluginBinding)
|
||||
// Reconnect to an already-running TorService without creating a new idle instance.
|
||||
bindTorService(createIfNeeded = false)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
@@ -60,40 +64,62 @@ class FlutterTorPlugin : FlutterPlugin, TorApi {
|
||||
scope.cancel()
|
||||
|
||||
context = null
|
||||
binaryMessenger = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to TorService
|
||||
* Bind to TorService. Idempotent — used both for initial bind and rebind on disconnect.
|
||||
*/
|
||||
private fun bindTorService(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
private fun bindTorService(createIfNeeded: Boolean) {
|
||||
val ctx = context ?: return
|
||||
val messenger = binaryMessenger ?: return
|
||||
if (serviceConnection != null) return
|
||||
|
||||
val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
Log.d(TAG, "TorService connected")
|
||||
val binder = service as? TorService.LocalBinder
|
||||
torService = binder?.getService()
|
||||
torService?.initialize(flutterPluginBinding.binaryMessenger)
|
||||
torService?.initialize(messenger)
|
||||
|
||||
// Signal that service is connected
|
||||
serviceConnected.complete(Unit)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
Log.w(TAG, "TorService disconnected")
|
||||
Log.w(TAG, "TorService disconnected — will rebind")
|
||||
torService = null
|
||||
|
||||
// Reset connection deferred for potential reconnection
|
||||
serviceConnected = CompletableDeferred()
|
||||
// Drop the stale ServiceConnection reference so bindTorService() reattempts.
|
||||
serviceConnection = null
|
||||
scope.launch {
|
||||
delay(500)
|
||||
bindTorService(createIfNeeded = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serviceConnection = connection
|
||||
|
||||
val intent = Intent(ctx, TorService::class.java)
|
||||
// Only bind to service, don't start it yet
|
||||
// Service will be started when startTor() is called
|
||||
ctx.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
val intent = Intent(ctx, TorService::class.java).apply {
|
||||
if (createIfNeeded) {
|
||||
action = TorService.ACTION_START
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (createIfNeeded) {
|
||||
ContextCompat.startForegroundService(ctx, intent)
|
||||
}
|
||||
|
||||
val flags = if (createIfNeeded) Context.BIND_AUTO_CREATE else 0
|
||||
if (!ctx.bindService(intent, connection, flags)) {
|
||||
Log.w(TAG, "bindService returned false (createIfNeeded=$createIfNeeded)")
|
||||
serviceConnection = null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "bindService failed", e)
|
||||
serviceConnection = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,9 +138,12 @@ class FlutterTorPlugin : FlutterPlugin, TorApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for service to be connected
|
||||
* Wait for service to be connected. Triggers a rebind if needed.
|
||||
*/
|
||||
private suspend fun waitForService(): TorService {
|
||||
if (serviceConnection == null) {
|
||||
bindTorService(createIfNeeded = true)
|
||||
}
|
||||
return withTimeoutOrNull(SERVICE_CONNECTION_TIMEOUT_MS) {
|
||||
serviceConnected.await()
|
||||
torService
|
||||
|
||||
+32
-40
@@ -30,6 +30,9 @@ class PluggableTransportManager private constructor(private val context: Context
|
||||
private val SNOWFLAKE_AMP_FRONTS = listOf("www.google.com")
|
||||
private const val SNOWFLAKE_ICE_SERVERS = "stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478"
|
||||
|
||||
private const val PORT_READY_TIMEOUT_MS = 10_000L
|
||||
private const val PORT_READY_POLL_MS = 100L
|
||||
|
||||
@Volatile
|
||||
private var instance: PluggableTransportManager? = null
|
||||
|
||||
@@ -102,60 +105,25 @@ class PluggableTransportManager private constructor(private val context: Context
|
||||
try {
|
||||
when (type) {
|
||||
TransportType.OBFS4 -> {
|
||||
val transportName = IPtProxy.Obfs4
|
||||
controller.start(transportName, null) // null = no proxy
|
||||
activeTransports.add(transportName)
|
||||
val port = controller.port(transportName)
|
||||
if (port > 0) {
|
||||
ports[transportName] = port.toInt()
|
||||
Log.d(TAG, "$transportName started on port $port")
|
||||
}
|
||||
startAndAwait(IPtProxy.Obfs4)?.let { ports[IPtProxy.Obfs4] = it }
|
||||
}
|
||||
|
||||
TransportType.SNOWFLAKE -> {
|
||||
val transportName = IPtProxy.Snowflake
|
||||
configureSnowflake(useAmp = false)
|
||||
controller.start(transportName, null)
|
||||
activeTransports.add(transportName)
|
||||
val port = controller.port(transportName)
|
||||
if (port > 0) {
|
||||
ports[transportName] = port.toInt()
|
||||
Log.d(TAG, "$transportName started on port $port")
|
||||
}
|
||||
startAndAwait(IPtProxy.Snowflake)?.let { ports[IPtProxy.Snowflake] = it }
|
||||
}
|
||||
|
||||
TransportType.SNOWFLAKE_AMP -> {
|
||||
val transportName = IPtProxy.Snowflake
|
||||
configureSnowflake(useAmp = true)
|
||||
controller.start(transportName, null)
|
||||
activeTransports.add(transportName)
|
||||
val port = controller.port(transportName)
|
||||
if (port > 0) {
|
||||
ports[transportName] = port.toInt()
|
||||
Log.d(TAG, "$transportName (AMP) started on port $port")
|
||||
}
|
||||
startAndAwait(IPtProxy.Snowflake)?.let { ports[IPtProxy.Snowflake] = it }
|
||||
}
|
||||
|
||||
TransportType.MEEK, TransportType.MEEK_AZURE -> {
|
||||
val transportName = IPtProxy.MeekLite
|
||||
controller.start(transportName, null)
|
||||
activeTransports.add(transportName)
|
||||
val port = controller.port(transportName)
|
||||
if (port > 0) {
|
||||
ports[transportName] = port.toInt()
|
||||
Log.d(TAG, "$transportName started on port $port")
|
||||
}
|
||||
startAndAwait(IPtProxy.MeekLite)?.let { ports[IPtProxy.MeekLite] = it }
|
||||
}
|
||||
|
||||
TransportType.WEBTUNNEL -> {
|
||||
val transportName = IPtProxy.Webtunnel
|
||||
controller.start(transportName, null)
|
||||
activeTransports.add(transportName)
|
||||
val port = controller.port(transportName)
|
||||
if (port > 0) {
|
||||
ports[transportName] = port.toInt()
|
||||
Log.d(TAG, "$transportName started on port $port")
|
||||
}
|
||||
startAndAwait(IPtProxy.Webtunnel)?.let { ports[IPtProxy.Webtunnel] = it }
|
||||
}
|
||||
|
||||
TransportType.NONE, TransportType.CUSTOM -> {
|
||||
@@ -169,6 +137,30 @@ class PluggableTransportManager private constructor(private val context: Context
|
||||
return ports
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a transport and poll until controller.port(name) returns a usable value.
|
||||
* IPtProxy assigns the port asynchronously after `start()`; without this poll, we
|
||||
* may read 0 and silently skip the ClientTransportPlugin line in torrc, causing
|
||||
* tor to bootstrap with `UseBridges 1` but no transport — bootstrap stalls.
|
||||
*/
|
||||
private fun startAndAwait(transportName: String): Int? {
|
||||
controller.start(transportName, null) // null = no proxy
|
||||
activeTransports.add(transportName)
|
||||
|
||||
val deadline = System.currentTimeMillis() + PORT_READY_TIMEOUT_MS
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
val port = controller.port(transportName)
|
||||
if (port > 0) {
|
||||
Log.d(TAG, "$transportName started on port $port")
|
||||
return port.toInt()
|
||||
}
|
||||
Thread.sleep(PORT_READY_POLL_MS)
|
||||
}
|
||||
|
||||
Log.e(TAG, "$transportName failed to bind a port within ${PORT_READY_TIMEOUT_MS}ms")
|
||||
throw IllegalStateException("Pluggable transport $transportName did not become ready in time")
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Snowflake-specific settings
|
||||
*/
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package eu.weblibre.flutter_tor
|
||||
|
||||
import java.net.ServerSocket
|
||||
|
||||
/**
|
||||
* Manages random port allocation for Tor and pluggable transports
|
||||
*/
|
||||
object PortManager {
|
||||
|
||||
/**
|
||||
* Find an available random port by binding to port 0
|
||||
* @return Available port number
|
||||
*/
|
||||
fun findAvailablePort(): Int {
|
||||
return ServerSocket(0).use { socket ->
|
||||
socket.localPort
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific port is available
|
||||
* @param port Port to check
|
||||
* @return true if port is available
|
||||
*/
|
||||
fun isPortAvailable(port: Int): Boolean {
|
||||
return try {
|
||||
ServerSocket(port).use { true }
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,39 +5,36 @@ import IPtProxy.IPtProxy
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Generates Tor configuration (torrc) based on user settings
|
||||
* Generates Tor configuration (torrc) based on user settings.
|
||||
*
|
||||
* Notes on what we deliberately do NOT set here:
|
||||
* - DataDirectory, ControlSocket, CookieAuthentication, RunAsDaemon, CacheDirectory,
|
||||
* SyslogIdentityTag — upstream org.torproject.jni.TorService passes these on the
|
||||
* `tor` command line. Setting them here causes duplicate-option warnings or contradicts
|
||||
* the values upstream needs (e.g. RunAsDaemon must be 0 in-process).
|
||||
* - SocksPort / HTTPTunnelPort — upstream rewrites defaults-torrc on every start with
|
||||
* `SOCKSPort 9050|auto` and `HTTPTunnelPort 8118|auto`. tor REJECTS mixing
|
||||
* `SocksPort 0` with any other SocksPort line ("Invalid SocksPort configuration"),
|
||||
* so we cannot override these from torrc. Instead we just consume what upstream
|
||||
* binds — read it from the static `TorService.socksPort` field (or via
|
||||
* `getInfo net/listeners/socks`).
|
||||
* - DNSPort / TransPort default to 0 in tor; no need to set them.
|
||||
*/
|
||||
class TorConfig(private val config: TorConfiguration) {
|
||||
|
||||
/**
|
||||
* Generate torrc file content
|
||||
* @param socksPort SOCKS proxy port
|
||||
* @param dataDir Tor data directory
|
||||
* @param geoipFile GeoIP file (optional, for country selection)
|
||||
* @param geoip6File GeoIP6 file (optional, for IPv6 country selection)
|
||||
* @param transportPorts Map of transport name to port (from PluggableTransportManager)
|
||||
* @return torrc content as string
|
||||
*
|
||||
* Note: ControlPort is NOT set in torrc. The tor-android library automatically
|
||||
* uses ControlSocket (Unix domain socket) which is more secure than TCP ControlPort.
|
||||
* See SECURITY_CONTROL_PORT.md for details.
|
||||
*/
|
||||
fun generateTorrc(
|
||||
socksPort: Int,
|
||||
dataDir: File,
|
||||
geoipFile: File?,
|
||||
geoip6File: File?,
|
||||
transportPorts: Map<String, Int>
|
||||
): String = buildString {
|
||||
// Core Tor settings
|
||||
append("# Generated torrc for flutter_tor\n")
|
||||
append("SocksPort 127.0.0.1:$socksPort\n")
|
||||
// ControlPort is NOT set - tor-android uses ControlSocket (Unix domain socket)
|
||||
// This is more secure as it uses file permissions instead of TCP authentication
|
||||
append("DataDirectory ${dataDir.absolutePath}\n")
|
||||
|
||||
// Start with networking disabled. Re-enabled via control port
|
||||
// (setConf DisableNetwork 0) AFTER our event listener is wired up so we
|
||||
// don't miss bootstrap events.
|
||||
append("DisableNetwork 1\n")
|
||||
append("\n")
|
||||
|
||||
// GeoIP files for country-based node selection
|
||||
if (geoipFile != null && geoipFile.exists()) {
|
||||
append("GeoIPFile ${geoipFile.absolutePath}\n")
|
||||
}
|
||||
@@ -46,34 +43,27 @@ class TorConfig(private val config: TorConfiguration) {
|
||||
}
|
||||
append("\n")
|
||||
|
||||
// Entry node countries
|
||||
config.entryNodeCountries?.let { countries ->
|
||||
if (countries.isNotBlank()) {
|
||||
val formatted = formatCountries(countries)
|
||||
append("EntryNodes $formatted\n")
|
||||
append("EntryNodes ${formatCountries(countries)}\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Exit node countries
|
||||
config.exitNodeCountries?.let { countries ->
|
||||
if (countries.isNotBlank()) {
|
||||
val formatted = formatCountries(countries)
|
||||
append("ExitNodes $formatted\n")
|
||||
append("ExitNodes ${formatCountries(countries)}\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Strict nodes (only use specified countries)
|
||||
if (config.strictNodes == true) {
|
||||
append("StrictNodes 1\n")
|
||||
}
|
||||
append("\n")
|
||||
|
||||
// Pluggable transport configuration
|
||||
val transport = TransportType.fromPigeon(config.transport)
|
||||
when (transport) {
|
||||
TransportType.OBFS4 -> {
|
||||
transportPorts[IPtProxy.Obfs4]?.let { port ->
|
||||
// Validate port is valid (like Orbot does)
|
||||
if (port > 0) {
|
||||
append("ClientTransportPlugin ${IPtProxy.Obfs4} socks5 127.0.0.1:$port\n")
|
||||
}
|
||||
@@ -101,17 +91,12 @@ class TorConfig(private val config: TorConfiguration) {
|
||||
}
|
||||
}
|
||||
TransportType.CUSTOM -> {
|
||||
// Custom bridges - transport plugin defined in bridge line
|
||||
// We'll try to detect and configure based on bridge lines
|
||||
configureCustomTransports(transportPorts)
|
||||
}
|
||||
TransportType.NONE -> {
|
||||
// Direct connection, no pluggable transports
|
||||
}
|
||||
TransportType.NONE -> {}
|
||||
}
|
||||
append("\n")
|
||||
|
||||
// Bridge configuration
|
||||
if (transport != TransportType.NONE) {
|
||||
val normalizedBridges = BridgeParser.normalize(config.bridgeLines)
|
||||
if (normalizedBridges.isNotEmpty()) {
|
||||
@@ -123,9 +108,6 @@ class TorConfig(private val config: TorConfiguration) {
|
||||
}
|
||||
}
|
||||
|
||||
// Additional Tor settings (matching Orbot's configuration)
|
||||
append("# Additional settings\n")
|
||||
append("RunAsDaemon 1\n")
|
||||
append("AvoidDiskWrites 1\n")
|
||||
append("SafeSocks 0\n")
|
||||
append("TestSocks 0\n")
|
||||
@@ -133,14 +115,6 @@ class TorConfig(private val config: TorConfiguration) {
|
||||
append("AutomapHostsOnResolve 1\n")
|
||||
append("DormantClientTimeout 10 minutes\n")
|
||||
append("DormantCanceledByStartup 1\n")
|
||||
|
||||
// CRITICAL: Set DisableNetwork 1 to prevent bootstrap before event listener is ready
|
||||
// This will be changed to 0 via control port AFTER we set up event listeners
|
||||
// This matches Orbot's approach and ensures we receive all bootstrap events
|
||||
append("DisableNetwork 1\n")
|
||||
|
||||
append("Log notice stdout\n") // Log to stdout for capture
|
||||
append("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,15 +129,11 @@ class TorConfig(private val config: TorConfiguration) {
|
||||
.split(",")
|
||||
.map { it.trim().uppercase() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.filter { it.length == 2 } // ISO 3166-1 alpha-2 codes
|
||||
.filter { it.length == 2 }
|
||||
|
||||
return codes.joinToString(",") { "{$it}" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure custom transports based on bridge lines
|
||||
* Detects transport type from bridge lines and configures accordingly
|
||||
*/
|
||||
private fun StringBuilder.configureCustomTransports(transportPorts: Map<String, Int>) {
|
||||
val bridgeTransports = config.bridgeLines
|
||||
.mapNotNull { BridgeParser.extractTransportType(it) }
|
||||
@@ -195,11 +165,6 @@ class TorConfig(private val config: TorConfiguration) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write torrc to file
|
||||
* @param torrcFile File to write to
|
||||
* @param content torrc content
|
||||
*/
|
||||
fun writeTorrc(torrcFile: File, content: String) {
|
||||
torrcFile.parentFile?.mkdirs()
|
||||
torrcFile.writeText(content)
|
||||
|
||||
+268
-141
@@ -1,11 +1,14 @@
|
||||
package eu.weblibre.flutter_tor
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import eu.weblibre.flutter_tor.generated.TorConfiguration
|
||||
import eu.weblibre.flutter_tor.generated.TorStatus
|
||||
import kotlinx.coroutines.*
|
||||
@@ -27,12 +30,18 @@ class TorManager(
|
||||
) {
|
||||
companion object {
|
||||
const val TAG = "TorManager"
|
||||
private const val STATUS_OFF_TIMEOUT_MS = 10_000L
|
||||
}
|
||||
|
||||
private val dataDir = File(context.filesDir, "tor_data")
|
||||
// GeoIP files live alongside other install assets. tor-android owns its own
|
||||
// DataDirectory/CacheDirectory under getDir("TorService"), so we don't define one.
|
||||
private val installDir = File(context.filesDir, "tor_install")
|
||||
private var torServiceConnection: ServiceConnection? = null
|
||||
|
||||
@Volatile
|
||||
private var controlConnection: TorControlConnection? = null
|
||||
|
||||
@Volatile
|
||||
private var torService: TorService? = null
|
||||
|
||||
val pluggableTransportManager = PluggableTransportManager.getInstance(context)
|
||||
@@ -45,20 +54,67 @@ class TorManager(
|
||||
|
||||
var socksPort: Int = -1
|
||||
private set
|
||||
// Note: No controlPort - tor-android uses ControlSocket (Unix domain socket) instead
|
||||
// This is more secure than TCP ControlPort as it uses file permissions for access control
|
||||
|
||||
@Volatile
|
||||
private var isRunning = false
|
||||
@Volatile
|
||||
private var bootstrapProgress = 0
|
||||
|
||||
// Lock for synchronizing status updates
|
||||
private val statusLock = Any()
|
||||
|
||||
// Latch awaited by stop() — completed when upstream broadcasts STATUS_OFF.
|
||||
@Volatile
|
||||
private var stopSignal: CompletableDeferred<Unit>? = null
|
||||
|
||||
// Background bootstrap-progress poller — fallback for when NOTICE/STATUS_CLIENT
|
||||
// events are silently dropped by jtorctl/tor (observed intermittently in the wild).
|
||||
private var bootstrapPollJob: Job? = null
|
||||
|
||||
// Mirror of upstream TorService's status — its own static `currentStatus` is package-private.
|
||||
@Volatile
|
||||
private var lastUpstreamStatus: String = TorService.STATUS_OFF
|
||||
|
||||
private val statusReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||
val status = intent?.getStringExtra(TorService.EXTRA_STATUS) ?: return
|
||||
Log.d(TAG, "TorService broadcast: $status")
|
||||
lastUpstreamStatus = status
|
||||
if (status == TorService.STATUS_OFF) {
|
||||
stopSignal?.complete(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val errorReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||
val msg = intent?.getStringExtra(Intent.EXTRA_TEXT) ?: "unknown"
|
||||
Log.e(TAG, "TorService error broadcast: $msg")
|
||||
logHandler.error("Tor service error: $msg")
|
||||
}
|
||||
}
|
||||
|
||||
private var receiversRegistered = false
|
||||
|
||||
private fun registerReceivers() {
|
||||
if (receiversRegistered) return
|
||||
val lbm = LocalBroadcastManager.getInstance(context)
|
||||
lbm.registerReceiver(statusReceiver, IntentFilter(TorService.ACTION_STATUS))
|
||||
lbm.registerReceiver(errorReceiver, IntentFilter(TorService.ACTION_ERROR))
|
||||
receiversRegistered = true
|
||||
}
|
||||
|
||||
private fun unregisterReceivers() {
|
||||
if (!receiversRegistered) return
|
||||
val lbm = LocalBroadcastManager.getInstance(context)
|
||||
try { lbm.unregisterReceiver(statusReceiver) } catch (_: Exception) {}
|
||||
try { lbm.unregisterReceiver(errorReceiver) } catch (_: Exception) {}
|
||||
receiversRegistered = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Tor with the given configuration
|
||||
* @param config Tor configuration from Flutter
|
||||
* @return SOCKS port
|
||||
* @return SOCKS port (discovered from tor via `getInfo net/listeners/socks`)
|
||||
*/
|
||||
suspend fun start(config: TorConfiguration): Int = withContext(Dispatchers.IO) {
|
||||
if (isRunning) {
|
||||
@@ -69,66 +125,41 @@ class TorManager(
|
||||
try {
|
||||
logHandler.notice("Starting Tor...")
|
||||
|
||||
// Create directories
|
||||
dataDir.mkdirs()
|
||||
installDir.mkdirs()
|
||||
lastUpstreamStatus = TorService.STATUS_STARTING
|
||||
registerReceivers()
|
||||
|
||||
// Allocate random SOCKS port
|
||||
// Note: We don't allocate a control port - tor-android uses ControlSocket instead
|
||||
socksPort = PortManager.findAvailablePort()
|
||||
|
||||
Log.d(TAG, "Allocated SOCKS port: $socksPort")
|
||||
Log.d(TAG, "Control connection will use ControlSocket (Unix domain socket)")
|
||||
logHandler.notice("SOCKS port: $socksPort")
|
||||
|
||||
// Start pluggable transports if needed
|
||||
val transport = TransportType.fromPigeon(config.transport)
|
||||
val transportPorts = if (transport != TransportType.NONE && transport != TransportType.CUSTOM) {
|
||||
logHandler.notice("Starting pluggable transport: $transport")
|
||||
pluggableTransportManager.startTransport(transport)
|
||||
} else if (transport == TransportType.CUSTOM) {
|
||||
// For custom, we need to detect and start appropriate transports
|
||||
startCustomTransports(config.bridgeLines)
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
// Generate torrc
|
||||
val geoipFile = geoIpManager.getGeoIpFile(installDir)
|
||||
val geoip6File = geoIpManager.getGeoIp6File(installDir)
|
||||
|
||||
val torConfig = TorConfig(config)
|
||||
val torrcContent = torConfig.generateTorrc(
|
||||
socksPort = socksPort,
|
||||
// controlPort removed - tor-android uses ControlSocket (Unix domain socket) for security
|
||||
dataDir = dataDir,
|
||||
geoipFile = geoipFile,
|
||||
geoip6File = geoip6File,
|
||||
transportPorts = transportPorts
|
||||
)
|
||||
|
||||
// Write torrc to the correct location (like Orbot does)
|
||||
// CRITICAL: Must use TorService.getTorrc() so TorService can find it!
|
||||
// Tor reads torrc from the location upstream TorService passes via -f.
|
||||
val torrcFile = TorService.getTorrc(context)
|
||||
torConfig.writeTorrc(torrcFile, torrcContent)
|
||||
|
||||
Log.d(TAG, "Generated torrc at ${torrcFile.absolutePath}:\n$torrcContent")
|
||||
|
||||
// Write defaults torrc (required by tor-android)
|
||||
// Set DisableNetwork 1 initially like Orbot does, will be enabled via control port
|
||||
// Also disable DNSPort and TransPort (matching Orbot)
|
||||
val defaultsTorrcFile = TorService.getDefaultsTorrc(context)
|
||||
defaultsTorrcFile.writeText("""
|
||||
DNSPort 0
|
||||
TransPort 0
|
||||
DisableNetwork 1
|
||||
""".trimIndent())
|
||||
// Note: do NOT write defaults-torrc here. Upstream's setDefaultProxyPorts()
|
||||
// truncates and rewrites it on every start with `SOCKSPort/HTTPTunnelPort auto`.
|
||||
// We neutralise those listeners with `SOCKSPort 0` / `HTTPTunnelPort 0` in
|
||||
// the regular torrc instead (see TorConfig.kt).
|
||||
|
||||
// Start TorService
|
||||
// Note: torrcFile is now written to the correct location via TorService.getTorrc()
|
||||
// so TorService will automatically find and use it
|
||||
// Note: isRunning will be set to true in setupControlConnection() before network is enabled
|
||||
// This ensures status is consistent when bootstrap events start arriving
|
||||
startTorService()
|
||||
|
||||
socksPort
|
||||
@@ -136,13 +167,11 @@ class TorManager(
|
||||
Log.e(TAG, "Failed to start Tor", e)
|
||||
logHandler.error("Failed to start Tor: ${e.message}")
|
||||
cleanup()
|
||||
unregisterReceivers()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start custom transports based on bridge lines
|
||||
*/
|
||||
private fun startCustomTransports(bridgeLines: List<String>): Map<String, Int> {
|
||||
val transports = bridgeLines
|
||||
.mapNotNull { BridgeParser.extractTransportType(it) }
|
||||
@@ -168,8 +197,7 @@ class TorManager(
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the native TorService and bind to it
|
||||
* TorService will automatically use the torrc written to TorService.getTorrc(context)
|
||||
* Start upstream tor-android TorService and bind to it.
|
||||
*/
|
||||
private suspend fun startTorService() = suspendCancellableCoroutine<Unit> { continuation ->
|
||||
val connection = object : ServiceConnection {
|
||||
@@ -178,28 +206,48 @@ class TorManager(
|
||||
val binder = service as? TorService.LocalBinder
|
||||
torService = binder?.service
|
||||
|
||||
// Wait for control connection to be available
|
||||
// Wait for control connection to be available AND for TorService's
|
||||
// own controlPortThread to have finished its setup (auth + addRawEventListener
|
||||
// + setEvents). TorService.torControlConnection becomes non-null immediately
|
||||
// after `new TorControlConnection(...)`, but TorService then calls
|
||||
// setEvents([EVENT_STATUS_CLIENT]) — if we race and call our setEvents first,
|
||||
// TorService overwrites it and we stop receiving NOTICE/BW/CIRC events.
|
||||
// TorService.socksPort is set AFTER its setEvents call, so polling for it
|
||||
// gives us a reliable "TorService is done initializing the control port" signal.
|
||||
scope.launch {
|
||||
var conn: TorControlConnection? = null
|
||||
var attempts = 0
|
||||
while (conn == null && attempts < 60) { // 30 seconds timeout
|
||||
while ((conn == null || TorService.socksPort == -1) && attempts < 60) {
|
||||
delay(500)
|
||||
conn = torService?.torControlConnection
|
||||
attempts++
|
||||
// Bail early if upstream gave up (typically due to a torrc
|
||||
// parse error caught by `tor --verify-config`).
|
||||
if (lastUpstreamStatus == TorService.STATUS_OFF ||
|
||||
lastUpstreamStatus == TorService.STATUS_STOPPING) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (conn != null) {
|
||||
// Wait an additional second before setting up event listener
|
||||
// This matches Orbot's behavior and ensures Tor is fully initialized
|
||||
delay(1000)
|
||||
|
||||
if (conn != null && TorService.socksPort != -1) {
|
||||
controlConnection = conn
|
||||
setupControlConnection(conn)
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(Unit)
|
||||
try {
|
||||
setupControlConnection(conn)
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (continuation.isActive) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val error = Exception("Failed to get control connection after 30 seconds")
|
||||
val error = Exception(
|
||||
"Failed to get fully-initialized control connection " +
|
||||
"(conn=${conn != null}, torServiceSocksPort=${TorService.socksPort}, " +
|
||||
"upstreamStatus=$lastUpstreamStatus). " +
|
||||
"If upstreamStatus is STOPPING/OFF, tor likely rejected the torrc — check logcat for TorService."
|
||||
)
|
||||
if (continuation.isActive) {
|
||||
continuation.resumeWithException(error)
|
||||
}
|
||||
@@ -220,7 +268,6 @@ class TorManager(
|
||||
val intent = Intent(context, org.torproject.jni.TorService::class.java)
|
||||
|
||||
try {
|
||||
// Start the service first (like Orbot does) before binding
|
||||
context.startService(intent)
|
||||
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
} catch (e: Exception) {
|
||||
@@ -231,26 +278,20 @@ class TorManager(
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup control connection and event listeners
|
||||
* This follows Orbot's approach: query ports first, then set up events, then enable network
|
||||
* Setup control connection and event listeners.
|
||||
* Order: discover real SOCKS port → register listener → subscribe events → enable network.
|
||||
*/
|
||||
private fun setupControlConnection(conn: TorControlConnection) {
|
||||
try {
|
||||
// Query control connection to verify SOCKS port (like Orbot's initControlConnection)
|
||||
// This also properly initializes the control connection for event delivery
|
||||
try {
|
||||
conn.getInfo("net/listeners/socks")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Could not query SOCKS port from control connection", e)
|
||||
}
|
||||
|
||||
logHandler.notice("Connected to Tor control port")
|
||||
|
||||
// Create and store event listener instance (prevents garbage collection)
|
||||
// Wire up event listener BEFORE enabling network so we don't miss bootstrap
|
||||
// notices. Created as a field to keep a strong reference (avoids GC).
|
||||
torEventListener = TorEventListener()
|
||||
conn.addRawEventListener(torEventListener)
|
||||
|
||||
// Subscribe to events (matching Orbot's event subscriptions)
|
||||
// EVENT_STATUS_CLIENT must be included — upstream TorService relies on it
|
||||
// internally to detect circuit-established and broadcast STATUS_ON.
|
||||
conn.setEvents(listOf(
|
||||
TorControlCommands.EVENT_OR_CONN_STATUS,
|
||||
TorControlCommands.EVENT_CIRCUIT_STATUS,
|
||||
@@ -259,26 +300,53 @@ class TorManager(
|
||||
TorControlCommands.EVENT_ERR_MSG,
|
||||
TorControlCommands.EVENT_BANDWIDTH_USED,
|
||||
TorControlCommands.EVENT_NEW_DESC,
|
||||
TorControlCommands.EVENT_ADDRMAP
|
||||
TorControlCommands.EVENT_ADDRMAP,
|
||||
TorControlCommands.EVENT_STATUS_CLIENT
|
||||
))
|
||||
|
||||
Log.d(TAG, "Control connection setup complete, enabling network")
|
||||
// Replace upstream's default SOCKS/HTTP listeners with our own. Upstream's
|
||||
// defaults-torrc binds `SOCKSPort 9050|auto` and `HTTPTunnelPort 8118|auto`;
|
||||
// a well-known port lets other apps on the device probe / use our SOCKS proxy,
|
||||
// so we want a random one. We can't disable these in torrc (tor rejects mixing
|
||||
// `SocksPort 0` with other SocksPort lines), so SETCONF here:
|
||||
// - SETCONF replaces the listener list (closes prev, opens new).
|
||||
// - `auto` is a bare-port token; SocksPort defaults to binding 127.0.0.1.
|
||||
// - SETCONF is recorded immediately, but listeners only physically bind
|
||||
// once DisableNetwork=0 (see GoLog: "DisableNetwork is set. ... Shutting
|
||||
// down all existing connections.") — so we read the port back AFTER
|
||||
// enabling network below.
|
||||
try {
|
||||
conn.setConf("HTTPTunnelPort", "0")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Could not disable HTTPTunnelPort", e)
|
||||
}
|
||||
conn.setConf("SocksPort", "auto")
|
||||
|
||||
// Set isRunning=true BEFORE enabling network so status is consistent
|
||||
// when bootstrap events start arriving
|
||||
// when bootstrap events start arriving.
|
||||
synchronized(statusLock) {
|
||||
isRunning = true
|
||||
}
|
||||
logHandler.notice("Tor started successfully")
|
||||
sendStatusUpdate()
|
||||
|
||||
// Enable network now that configuration is complete (like Orbot does)
|
||||
// This will trigger bootstrap events to start
|
||||
Log.d(TAG, "Control connection setup complete, enabling network")
|
||||
conn.setConf("DisableNetwork", "0")
|
||||
|
||||
// Now that listeners are physically bound, discover the random port tor
|
||||
// chose. Poll briefly — binding takes a few hundred ms after enabling net.
|
||||
socksPort = awaitSocksListener(conn)
|
||||
if (socksPort <= 0) {
|
||||
throw IllegalStateException("Failed to discover SOCKS listener after enabling network")
|
||||
}
|
||||
Log.d(TAG, "Bound random SOCKS port: $socksPort")
|
||||
logHandler.notice("SOCKS port: $socksPort")
|
||||
sendStatusUpdate()
|
||||
|
||||
startBootstrapPoller(conn)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to setup control connection", e)
|
||||
logHandler.error("Control connection error: ${e.message}")
|
||||
// Reset isRunning on failure
|
||||
synchronized(statusLock) {
|
||||
isRunning = false
|
||||
}
|
||||
@@ -287,6 +355,74 @@ class TorManager(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll `getInfo("status/bootstrap-phase")` until progress reaches 100, the
|
||||
* connection is gone, or tor stops. This is a fallback for the intermittent
|
||||
* issue where NOTICE / STATUS_CLIENT events are silently not delivered to
|
||||
* our raw event listener even though tor accepted SETEVENTS for them.
|
||||
*
|
||||
* Reply format: `NOTICE BOOTSTRAP PROGRESS=85 TAG=loading_descriptors SUMMARY="..."`
|
||||
*/
|
||||
private fun startBootstrapPoller(conn: TorControlConnection) {
|
||||
bootstrapPollJob?.cancel()
|
||||
bootstrapPollJob = scope.launch {
|
||||
try {
|
||||
while (isActive && isRunning && controlConnection === conn) {
|
||||
val info = try {
|
||||
conn.getInfo("status/bootstrap-phase")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "bootstrap-phase poll failed", e)
|
||||
null
|
||||
}
|
||||
if (info != null) {
|
||||
val progress = Regex("""PROGRESS=(\d+)""").find(info)
|
||||
?.groupValues?.get(1)?.toIntOrNull() ?: -1
|
||||
if (progress >= 0) {
|
||||
val changed = synchronized(statusLock) {
|
||||
if (progress > bootstrapProgress) {
|
||||
bootstrapProgress = progress
|
||||
true
|
||||
} else false
|
||||
}
|
||||
if (changed) {
|
||||
sendStatusUpdate()
|
||||
if (progress == 100) {
|
||||
logHandler.notice("Tor is ready!")
|
||||
break
|
||||
}
|
||||
} else if (progress == 100) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(750)
|
||||
}
|
||||
} finally {
|
||||
Log.d(TAG, "Bootstrap poller exiting")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun awaitSocksListener(conn: TorControlConnection): Int {
|
||||
val deadline = System.currentTimeMillis() + 5_000L
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
val port = parseSocksPort(conn.getInfo("net/listeners/socks"))
|
||||
if (port > 0) return port
|
||||
Thread.sleep(100)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `net/listeners/socks` reply, e.g. `"127.0.0.1:43251"` or
|
||||
* `"127.0.0.1:43251" "127.0.0.1:9050"`. Returns the first numeric port, or -1.
|
||||
*/
|
||||
private fun parseSocksPort(reply: String?): Int {
|
||||
if (reply.isNullOrBlank()) return -1
|
||||
val match = Regex("""127\.0\.0\.1:(\d+)""").find(reply) ?: return -1
|
||||
return match.groupValues[1].toIntOrNull() ?: -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop Tor and cleanup
|
||||
*/
|
||||
@@ -294,25 +430,32 @@ class TorManager(
|
||||
Log.d(TAG, "Stopping Tor")
|
||||
logHandler.notice("Stopping Tor...")
|
||||
|
||||
val signal = CompletableDeferred<Unit>().also { stopSignal = it }
|
||||
|
||||
try {
|
||||
// DON'T call shutdownTor() here - let the service's onDestroy() handle it
|
||||
// Otherwise we get a broken pipe error when service tries to shutdown again
|
||||
// DON'T call shutdownTor() here - let upstream's onDestroy() handle it,
|
||||
// otherwise we get a broken pipe error.
|
||||
cleanup()
|
||||
|
||||
// Give the native service time to fully stop before potential restart
|
||||
// This is CRITICAL to prevent binding to a stale service instance
|
||||
delay(2000)
|
||||
// Wait for upstream to broadcast STATUS_OFF (which it does after releasing
|
||||
// its static runLock). Without this, the next start() can hang on runLock.lock().
|
||||
val arrived = withTimeoutOrNull(STATUS_OFF_TIMEOUT_MS) { signal.await(); true } ?: false
|
||||
if (!arrived) {
|
||||
Log.w(TAG, "Did not receive STATUS_OFF within ${STATUS_OFF_TIMEOUT_MS}ms — proceeding anyway")
|
||||
}
|
||||
|
||||
logHandler.notice("Tor stopped")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error stopping Tor", e)
|
||||
cleanup()
|
||||
delay(2000)
|
||||
} finally {
|
||||
stopSignal = null
|
||||
unregisterReceivers()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
* Cleanup resources. Safe to call from any thread.
|
||||
*/
|
||||
private fun cleanup() {
|
||||
synchronized(statusLock) {
|
||||
@@ -321,47 +464,34 @@ class TorManager(
|
||||
socksPort = -1
|
||||
}
|
||||
|
||||
try {
|
||||
// Unsubscribe from all events before removing listener
|
||||
if (controlConnection != null) {
|
||||
try {
|
||||
controlConnection?.setEvents(emptyList())
|
||||
} catch (e: Exception) {
|
||||
// Connection may already be closed
|
||||
}
|
||||
}
|
||||
bootstrapPollJob?.cancel()
|
||||
bootstrapPollJob = null
|
||||
|
||||
// Remove event listener
|
||||
if (torEventListener != null && controlConnection != null) {
|
||||
try {
|
||||
controlConnection?.removeRawEventListener(torEventListener)
|
||||
} catch (e: Exception) {
|
||||
// Connection may already be closed
|
||||
}
|
||||
val conn = controlConnection
|
||||
val listener = torEventListener
|
||||
if (conn != null) {
|
||||
try { conn.setEvents(emptyList()) } catch (_: Exception) {}
|
||||
if (listener != null) {
|
||||
try { conn.removeRawEventListener(listener) } catch (_: Exception) {}
|
||||
}
|
||||
torEventListener = null
|
||||
controlConnection = null
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error closing control connection", e)
|
||||
}
|
||||
torEventListener = null
|
||||
controlConnection = null
|
||||
|
||||
// Service cleanup
|
||||
runBlocking(Dispatchers.Main) {
|
||||
try {
|
||||
torServiceConnection?.let {
|
||||
context.unbindService(it)
|
||||
}
|
||||
torServiceConnection = null
|
||||
// unbindService / stopService are safe from any thread; no need to hop to Main
|
||||
// (the previous runBlocking(Dispatchers.Main) wrapper risked deadlocking with
|
||||
// the IO-dispatcher caller chain in FlutterTorPlugin.stopTor).
|
||||
try {
|
||||
torServiceConnection?.let { context.unbindService(it) }
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error unbinding TorService", e)
|
||||
}
|
||||
torServiceConnection = null
|
||||
|
||||
// Small delay to ensure unbind completes before stopping service
|
||||
delay(100)
|
||||
|
||||
// Stop the native TorService to ensure clean restart
|
||||
val intent = Intent(context, org.torproject.jni.TorService::class.java)
|
||||
context.stopService(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error unbinding TorService", e)
|
||||
}
|
||||
try {
|
||||
context.stopService(Intent(context, org.torproject.jni.TorService::class.java))
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error stopping TorService", e)
|
||||
}
|
||||
|
||||
torService = null
|
||||
@@ -384,9 +514,6 @@ class TorManager(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current Tor status
|
||||
*/
|
||||
fun getStatus(): TorStatus {
|
||||
synchronized(statusLock) {
|
||||
val status = TorStatus(
|
||||
@@ -403,9 +530,6 @@ class TorManager(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send status update to Flutter
|
||||
*/
|
||||
private fun sendStatusUpdate() {
|
||||
logHandler.sendStatusChange(getStatus())
|
||||
}
|
||||
@@ -415,41 +539,44 @@ class TorManager(
|
||||
*/
|
||||
private inner class TorEventListener : RawEventListener {
|
||||
override fun onEvent(eventType: String, eventData: String) {
|
||||
// Handle bootstrap progress (comes in NOTICE events)
|
||||
if (eventData.contains("Bootstrapped")) {
|
||||
val progress = extractBootstrapProgress(eventData)
|
||||
if (progress >= 0) {
|
||||
synchronized(statusLock) {
|
||||
bootstrapProgress = progress
|
||||
}
|
||||
sendStatusUpdate()
|
||||
Log.d(TAG, "ReceivedData: $eventType: $eventData")
|
||||
|
||||
// Bootstrap progress can arrive in two formats:
|
||||
// - EVENT_NOTICE_MSG: "Bootstrapped 85% (loading_descriptors): ..."
|
||||
// - EVENT_STATUS_CLIENT: "NOTICE BOOTSTRAP PROGRESS=85 TAG=... SUMMARY=..."
|
||||
val progress = extractBootstrapProgress(eventData)
|
||||
if (progress >= 0) {
|
||||
val changed = synchronized(statusLock) {
|
||||
if (progress > bootstrapProgress) {
|
||||
bootstrapProgress = progress
|
||||
true
|
||||
} else false
|
||||
}
|
||||
if (changed) {
|
||||
sendStatusUpdate()
|
||||
if (progress == 100) {
|
||||
logHandler.notice("Tor is ready!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forward to log handler
|
||||
logHandler.handleTorEvent(eventType, eventData)
|
||||
}
|
||||
|
||||
private fun extractBootstrapProgress(eventData: String): Int {
|
||||
// Extract from format like "Bootstrapped 85% (loading_descriptors): ..."
|
||||
val regex = "Bootstrapped\\s+(\\d+)%".toRegex()
|
||||
return regex.find(eventData)?.groupValues?.get(1)?.toIntOrNull() ?: -1
|
||||
Regex("""Bootstrapped\s+(\d+)%""").find(eventData)?.let {
|
||||
return it.groupValues[1].toIntOrNull() ?: -1
|
||||
}
|
||||
Regex("""BOOTSTRAP\s+PROGRESS=(\d+)""").find(eventData)?.let {
|
||||
return it.groupValues[1].toIntOrNull() ?: -1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup when manager is destroyed
|
||||
*/
|
||||
fun destroy() {
|
||||
cleanup()
|
||||
unregisterReceivers()
|
||||
scope.cancel()
|
||||
runBlocking {
|
||||
if (isRunning) {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-19
@@ -55,6 +55,11 @@ class TorService : Service() {
|
||||
Log.d(TAG, "onStartCommand: ${intent?.action}")
|
||||
|
||||
when (intent?.action) {
|
||||
ACTION_START -> {
|
||||
// startForegroundService() must promote the service promptly, before
|
||||
// the later binder call reaches startTor().
|
||||
startForeground(NOTIFICATION_ID, createNotification("Tor is connecting..."))
|
||||
}
|
||||
ACTION_STOP -> {
|
||||
scope.launch {
|
||||
stopTor()
|
||||
@@ -66,7 +71,8 @@ class TorService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
// Tor needs explicit user start; don't auto-restart with a null intent.
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,10 +98,7 @@ class TorService : Service() {
|
||||
suspend fun startTor(config: TorConfiguration): Int {
|
||||
Log.d(TAG, "Starting Tor...")
|
||||
|
||||
// Start foreground service with notification
|
||||
// This keeps the service alive even when the app is backgrounded
|
||||
startForeground(NOTIFICATION_ID, createNotification("Tor is connecting..."))
|
||||
Log.d(TAG, "Started foreground service")
|
||||
|
||||
val manager = torManager ?: throw IllegalStateException("Service not initialized")
|
||||
|
||||
@@ -105,9 +108,8 @@ class TorService : Service() {
|
||||
return socksPort
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start Tor", e)
|
||||
updateNotification("Failed to start Tor")
|
||||
// Stop foreground on failure
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -120,9 +122,8 @@ class TorService : Service() {
|
||||
|
||||
torManager?.stop()
|
||||
|
||||
// Stop foreground service and remove notification
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
Log.d(TAG, "Stopped foreground service")
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,17 +200,13 @@ class TorService : Service() {
|
||||
super.onDestroy()
|
||||
Log.d(TAG, "Service destroyed")
|
||||
|
||||
// Stop pluggable transports synchronously to release Go references.
|
||||
// Previously this was scope.launch { torManager?.destroy() } followed by
|
||||
// scope.cancel(), which meant the cleanup coroutine was immediately cancelled
|
||||
// and never ran — causing "trackGoRef called with Java refnum" crashes when
|
||||
// TorService was recreated and tried to create a new IPtProxy.Controller.
|
||||
//
|
||||
// Note: We only stop transports here. The PluggableTransportManager singleton
|
||||
// and its Controller persist across service restarts by design.
|
||||
// Full TorManager.destroy() is not called because it would deadlock
|
||||
// (cleanup() uses runBlocking(Dispatchers.Main) while onDestroy runs on Main).
|
||||
torManager?.pluggableTransportManager?.stopAll()
|
||||
// Tear down TorManager synchronously so receivers, transports, and the
|
||||
// upstream tor-android service do not survive wrapper service teardown.
|
||||
try {
|
||||
torManager?.destroy()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error destroying TorManager", e)
|
||||
}
|
||||
|
||||
torManager = null
|
||||
logHandler = null
|
||||
|
||||
Reference in New Issue
Block a user