feat: replace arti-mobile-ex with custom-built Arti native library
The Guardian Project's arti-mobile-ex AAR has three problems: 1. No 16KB page-aligned binaries (required for Google Play) 2. ArtiProxy's stop()+start() causes state file lock conflicts (lock is tied to TorClient object lifetime, released only on GC) 3. ~140MB AAR size Replace with a custom JNI bridge built from Arti source, following BitChat's proven approach: Build tooling (tools/arti-build/): - build-arti.sh: Clones official Arti, compiles with cargo-ndk for ARM64 + x86_64, NDK 25+ for 16KB page alignment - Cargo.toml: Minimal deps with size-optimized release profile - src/lib.rs: Custom SOCKS5 proxy with proper lifecycle: - initialize() creates TorClient once (holds state lock forever) - startSocksProxy() binds port and accepts connections - stopSocksProxy() aborts listener only (TorClient stays alive) This cleanly separates "stop routing traffic" from "destroy client" Kotlin side: - ArtiNative.kt: JNI declarations + ArtiLogCallback interface - TorService.kt: Uses ArtiNative directly, start() initializes + starts proxy, stop() only stops proxy (no lock issues) - TorManager.kt: Restored stop() calls for OFF/EXTERNAL modes since our native stop is now safe Removed: arti-mobile-ex dependency from build.gradle and version catalog Native libraries must be built separately: cd tools/arti-build && ./build-arti.sh https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
This commit is contained in:
@@ -372,8 +372,6 @@ dependencies {
|
|||||||
// Kotlin serialization for the times where we need the Json tree and performance is not that important.
|
// Kotlin serialization for the times where we need the Json tree and performance is not that important.
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
implementation libs.arti.mobile.ex
|
|
||||||
|
|
||||||
testImplementation libs.junit
|
testImplementation libs.junit
|
||||||
testImplementation libs.mockk
|
testImplementation libs.mockk
|
||||||
testImplementation libs.kotlinx.coroutines.test
|
testImplementation libs.kotlinx.coroutines.test
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.tor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JNI bridge to the custom-built Arti native library (libarti_android.so).
|
||||||
|
*
|
||||||
|
* The native TorClient is created once via [initialize] and persists for the
|
||||||
|
* app's lifetime — its state file lock is never released until the process exits.
|
||||||
|
*
|
||||||
|
* The SOCKS proxy can be started and stopped independently via [startSocksProxy]
|
||||||
|
* and [stopSocksProxy] without affecting the TorClient.
|
||||||
|
*/
|
||||||
|
object ArtiNative {
|
||||||
|
init {
|
||||||
|
System.loadLibrary("arti_android")
|
||||||
|
}
|
||||||
|
|
||||||
|
external fun getVersion(): String
|
||||||
|
|
||||||
|
external fun setLogCallback(callback: ArtiLogCallback)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the Arti runtime and bootstrap the Tor client.
|
||||||
|
* @param dataDir Path to the app's private data directory for Arti state/cache.
|
||||||
|
* @return 0 on success, negative on error.
|
||||||
|
*/
|
||||||
|
external fun initialize(dataDir: String): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the SOCKS5 proxy on the given port.
|
||||||
|
* Can be called multiple times — stops any existing listener first.
|
||||||
|
* @return 0 on success, negative on error.
|
||||||
|
*/
|
||||||
|
external fun startSocksProxy(port: Int): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the SOCKS5 proxy listener and release the port.
|
||||||
|
* The TorClient stays alive — no state file lock issues.
|
||||||
|
* @return 0 on success.
|
||||||
|
*/
|
||||||
|
external fun stopSocksProxy(): Int
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback interface for Arti log messages from the native layer.
|
||||||
|
*/
|
||||||
|
fun interface ArtiLogCallback {
|
||||||
|
fun onLogLine(line: String)
|
||||||
|
}
|
||||||
@@ -63,10 +63,12 @@ class TorManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
TorType.OFF -> {
|
TorType.OFF -> {
|
||||||
|
service.stop()
|
||||||
emit(TorServiceStatus.Off)
|
emit(TorServiceStatus.Off)
|
||||||
}
|
}
|
||||||
|
|
||||||
TorType.EXTERNAL -> {
|
TorType.EXTERNAL -> {
|
||||||
|
service.stop()
|
||||||
if (externalSocksPort > 0) {
|
if (externalSocksPort > 0) {
|
||||||
emit(TorServiceStatus.Active(externalSocksPort))
|
emit(TorServiceStatus.Active(externalSocksPort))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -22,95 +22,100 @@ package com.vitorpamplona.amethyst.ui.tor
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import info.guardianproject.arti.ArtiLogListener
|
|
||||||
import info.guardianproject.arti.ArtiProxy
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
private const val DEFAULT_SOCKS_PORT = 19050
|
private const val DEFAULT_SOCKS_PORT = 19050
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages a single ArtiProxy instance for the app's lifetime.
|
* Manages the Arti Tor client via custom JNI bindings.
|
||||||
*
|
*
|
||||||
* Arti's state file lock is tied to the TorClient object's lifetime —
|
* The native TorClient is initialized once and persists for the app's
|
||||||
* it is only released when the object is garbage collected, not when
|
* lifetime — its state file lock is never released until the process exits.
|
||||||
* stop() is called. Calling stop()+start() on ArtiProxy creates a new
|
* The SOCKS proxy can be started/stopped independently without affecting
|
||||||
* internal TorClient that conflicts with the old lock.
|
* the TorClient or its file locks.
|
||||||
*
|
|
||||||
* Therefore, ArtiProxy is created and started once. It runs for the
|
|
||||||
* entire process lifetime. When the user turns Tor "off", TorManager
|
|
||||||
* simply stops emitting Active status — no traffic is routed through
|
|
||||||
* the proxy, but the proxy itself stays alive. This is safe because
|
|
||||||
* an idle Arti uses negligible resources and maintains no circuits
|
|
||||||
* when no SOCKS connections are made.
|
|
||||||
*/
|
*/
|
||||||
class TorService(
|
class TorService(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
) {
|
) {
|
||||||
private val socksPort = DEFAULT_SOCKS_PORT
|
private val socksPort = DEFAULT_SOCKS_PORT
|
||||||
private val bootstrapped = AtomicBoolean(false)
|
private val initialized = AtomicBoolean(false)
|
||||||
private val started = AtomicBoolean(false)
|
private val proxyRunning = AtomicBoolean(false)
|
||||||
|
|
||||||
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 val logListener =
|
init {
|
||||||
ArtiLogListener { logLine ->
|
ArtiNative.setLogCallback(
|
||||||
val text = logLine ?: return@ArtiLogListener
|
ArtiLogCallback { text ->
|
||||||
Log.d("TorService") { "Arti: $text" }
|
Log.d("TorService") { "Arti: $text" }
|
||||||
|
|
||||||
when {
|
when {
|
||||||
text.contains("Sufficiently bootstrapped", ignoreCase = true) ||
|
text.contains("Sufficiently bootstrapped", ignoreCase = true) -> {
|
||||||
text.contains("is usable", ignoreCase = true) -> {
|
|
||||||
if (bootstrapped.compareAndSet(false, true)) {
|
|
||||||
_status.value = TorServiceStatus.Active(socksPort)
|
_status.value = TorServiceStatus.Active(socksPort)
|
||||||
Log.d("TorService") { "Arti bootstrapped on port $socksPort" }
|
Log.d("TorService") { "Arti SOCKS proxy active on port $socksPort" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
text.contains(
|
/**
|
||||||
"Another process has the lock",
|
* Initialize the TorClient (once) and start the SOCKS proxy.
|
||||||
ignoreCase = true,
|
*/
|
||||||
) -> {
|
|
||||||
Log.e("TorService") { "Arti state file lock conflict" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val artiProxy: ArtiProxy =
|
|
||||||
ArtiProxy
|
|
||||||
.Builder(context.applicationContext)
|
|
||||||
.setSocksPort(socksPort)
|
|
||||||
.setDnsPort(socksPort + 1)
|
|
||||||
.setLogListener(logListener)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
suspend fun start() {
|
suspend fun start() {
|
||||||
if (started.get()) {
|
if (proxyRunning.get()) {
|
||||||
// Already started — just emit current state
|
if (_status.value is TorServiceStatus.Active) return
|
||||||
if (bootstrapped.get()) {
|
_status.value = TorServiceStatus.Connecting
|
||||||
_status.value = TorServiceStatus.Active(socksPort)
|
|
||||||
} else {
|
|
||||||
_status.value = TorServiceStatus.Connecting
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_status.value = TorServiceStatus.Connecting
|
_status.value = TorServiceStatus.Connecting
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
// Initialize TorClient once — this bootstraps the Tor network
|
||||||
artiProxy.start()
|
if (initialized.compareAndSet(false, true)) {
|
||||||
started.set(true)
|
val dataDir = File(context.filesDir, "arti").absolutePath
|
||||||
Log.d("TorService") { "Arti started on port $socksPort" }
|
Log.d("TorService") { "Initializing Arti with data dir: $dataDir" }
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("TorService") { "Failed to start Arti: ${e.message}" }
|
val initResult = ArtiNative.initialize(dataDir)
|
||||||
_status.value = TorServiceStatus.Off
|
if (initResult != 0) {
|
||||||
|
Log.e("TorService") { "Failed to initialize Arti: error $initResult" }
|
||||||
|
initialized.set(false)
|
||||||
|
_status.value = TorServiceStatus.Off
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start the SOCKS proxy (can be called multiple times safely)
|
||||||
|
val proxyResult = ArtiNative.startSocksProxy(socksPort)
|
||||||
|
if (proxyResult != 0) {
|
||||||
|
Log.e("TorService") { "Failed to start SOCKS proxy: error $proxyResult" }
|
||||||
|
_status.value = TorServiceStatus.Off
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyRunning.set(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the SOCKS proxy and release the port.
|
||||||
|
* The TorClient stays alive — no file lock issues on restart.
|
||||||
|
*/
|
||||||
|
suspend fun stop() {
|
||||||
|
if (!proxyRunning.compareAndSet(true, false)) return
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ArtiNative.stopSocksProxy()
|
||||||
|
Log.d("TorService") { "SOCKS proxy stopped" }
|
||||||
|
}
|
||||||
|
|
||||||
|
_status.value = TorServiceStatus.Off
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ fragmentKtx = "1.8.9"
|
|||||||
gms = "4.4.4"
|
gms = "4.4.4"
|
||||||
jacksonModuleKotlin = "2.21.2"
|
jacksonModuleKotlin = "2.21.2"
|
||||||
javaKeyring = "1.0.4"
|
javaKeyring = "1.0.4"
|
||||||
artiMobileEx = "1.2.3"
|
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kchesslib = "1.0.5"
|
kchesslib = "1.0.5"
|
||||||
kotlin = "2.3.20"
|
kotlin = "2.3.20"
|
||||||
@@ -140,7 +139,6 @@ google-mlkit-language-id = { group = "com.google.mlkit", name = "language-id", v
|
|||||||
google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" }
|
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" }
|
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" }
|
java-keyring = { group = "com.github.javakeyring", name = "java-keyring", version.ref = "javaKeyring" }
|
||||||
arti-mobile-ex = { module = "info.guardianproject:arti-mobile-ex", version.ref = "artiMobileEx" }
|
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
kchesslib = { module = "io.github.cvb941:kchesslib", version.ref = "kchesslib" }
|
kchesslib = { module = "io.github.cvb941:kchesslib", version.ref = "kchesslib" }
|
||||||
kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
|
kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.arti-source/
|
||||||
|
target/
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
arti-v1.2.3
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
[package]
|
||||||
|
name = "arti-android"
|
||||||
|
version = "1.2.3"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
arti-client = { version = "0.25", features = [
|
||||||
|
"tokio",
|
||||||
|
"rustls",
|
||||||
|
"compression",
|
||||||
|
"bridge-client",
|
||||||
|
"onion-service-client",
|
||||||
|
"static-sqlite",
|
||||||
|
] }
|
||||||
|
tor-rtcompat = { version = "0.25", features = ["tokio", "rustls"] }
|
||||||
|
jni = "0.21"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
anyhow = "1"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z"
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
panic = "abort"
|
||||||
Executable
+248
@@ -0,0 +1,248 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Build Arti native libraries for Android from source.
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - Rust toolchain: rustup, cargo
|
||||||
|
# - Android targets: rustup target add aarch64-linux-android x86_64-linux-android
|
||||||
|
# - cargo-ndk: cargo install cargo-ndk
|
||||||
|
# - Android NDK 25+ (for 16KB page size support)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./build-arti.sh # Build for all targets (arm64 + x86_64)
|
||||||
|
# ./build-arti.sh --release # Build arm64 only (for release)
|
||||||
|
# ./build-arti.sh --clean # Clean and rebuild
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
ARTI_SOURCE_DIR="$SCRIPT_DIR/.arti-source"
|
||||||
|
ARTI_VERSION=$(cat "$SCRIPT_DIR/ARTI_VERSION" | tr -d '[:space:]')
|
||||||
|
OUTPUT_DIR="$PROJECT_ROOT/amethyst/src/main/jniLibs"
|
||||||
|
LIB_NAME="libarti_android.so"
|
||||||
|
MIN_SDK_VERSION=26
|
||||||
|
|
||||||
|
# Default targets
|
||||||
|
TARGETS=("aarch64-linux-android" "x86_64-linux-android")
|
||||||
|
RELEASE_ONLY=false
|
||||||
|
CLEAN=false
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
for arg in "$@"; do
|
||||||
|
case $arg in
|
||||||
|
--release) RELEASE_ONLY=true; TARGETS=("aarch64-linux-android") ;;
|
||||||
|
--clean) CLEAN=true ;;
|
||||||
|
--help) echo "Usage: $0 [--release] [--clean] [--help]"; exit 0 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
print_header() { echo -e "\n${BLUE}=== $1 ===${NC}"; }
|
||||||
|
print_success() { echo -e "${GREEN}✓ $1${NC}"; }
|
||||||
|
print_error() { echo -e "${RED}✗ $1${NC}"; }
|
||||||
|
print_info() { echo -e "${YELLOW}→ $1${NC}"; }
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Prerequisites
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
check_prerequisites() {
|
||||||
|
print_header "Checking prerequisites"
|
||||||
|
|
||||||
|
command -v git >/dev/null 2>&1 || { print_error "git not found"; exit 1; }
|
||||||
|
command -v rustup >/dev/null 2>&1 || { print_error "rustup not found"; exit 1; }
|
||||||
|
command -v cargo >/dev/null 2>&1 || { print_error "cargo not found"; exit 1; }
|
||||||
|
command -v cargo-ndk >/dev/null 2>&1 || { print_error "cargo-ndk not found. Install: cargo install cargo-ndk"; exit 1; }
|
||||||
|
|
||||||
|
if [ -z "${ANDROID_NDK_HOME:-}" ]; then
|
||||||
|
# Try common locations
|
||||||
|
for candidate in \
|
||||||
|
"$HOME/Android/Sdk/ndk/"*/ \
|
||||||
|
"$HOME/Library/Android/sdk/ndk/"*/ \
|
||||||
|
"/usr/local/lib/android/sdk/ndk/"*/; do
|
||||||
|
if [ -d "$candidate" ]; then
|
||||||
|
export ANDROID_NDK_HOME="${candidate%/}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${ANDROID_NDK_HOME:-}" ]; then
|
||||||
|
print_error "ANDROID_NDK_HOME not set and NDK not found in common locations"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "NDK: $ANDROID_NDK_HOME"
|
||||||
|
|
||||||
|
for target in "${TARGETS[@]}"; do
|
||||||
|
if ! rustup target list --installed | grep -q "$target"; then
|
||||||
|
print_info "Adding Rust target: $target"
|
||||||
|
rustup target add "$target"
|
||||||
|
fi
|
||||||
|
print_success "Target: $target"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Source Management
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
clone_or_update_arti() {
|
||||||
|
print_header "Setting up Arti source ($ARTI_VERSION)"
|
||||||
|
|
||||||
|
if [ "$CLEAN" = true ] && [ -d "$ARTI_SOURCE_DIR" ]; then
|
||||||
|
print_info "Cleaning existing source"
|
||||||
|
rm -rf "$ARTI_SOURCE_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$ARTI_SOURCE_DIR" ]; then
|
||||||
|
print_info "Cloning Arti repository..."
|
||||||
|
git clone --depth 1 --branch "$ARTI_VERSION" \
|
||||||
|
https://gitlab.torproject.org/tpo/core/arti.git \
|
||||||
|
"$ARTI_SOURCE_DIR"
|
||||||
|
else
|
||||||
|
print_info "Updating existing clone to $ARTI_VERSION"
|
||||||
|
cd "$ARTI_SOURCE_DIR"
|
||||||
|
git fetch --depth 1 origin tag "$ARTI_VERSION"
|
||||||
|
git checkout "$ARTI_VERSION"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "Arti source ready at $ARTI_SOURCE_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Wrapper Setup
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
setup_wrapper() {
|
||||||
|
print_header "Setting up JNI wrapper"
|
||||||
|
|
||||||
|
local wrapper_dir="$ARTI_SOURCE_DIR/arti-android-wrapper"
|
||||||
|
mkdir -p "$wrapper_dir/src"
|
||||||
|
|
||||||
|
cp "$SCRIPT_DIR/Cargo.toml" "$wrapper_dir/Cargo.toml"
|
||||||
|
cp "$SCRIPT_DIR/src/lib.rs" "$wrapper_dir/src/lib.rs"
|
||||||
|
|
||||||
|
# Patch Cargo.toml to use local arti-client from the source tree
|
||||||
|
# instead of pulling from crates.io
|
||||||
|
cd "$wrapper_dir"
|
||||||
|
|
||||||
|
# Add path overrides for the local arti source
|
||||||
|
cat >> Cargo.toml << 'PATCH'
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
arti-client = { path = "../crates/arti-client" }
|
||||||
|
tor-rtcompat = { path = "../crates/tor-rtcompat" }
|
||||||
|
PATCH
|
||||||
|
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
print_success "JNI wrapper configured"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Build
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
build_for_target() {
|
||||||
|
local target="$1"
|
||||||
|
print_header "Building for $target"
|
||||||
|
|
||||||
|
local arch_dir
|
||||||
|
case "$target" in
|
||||||
|
aarch64-linux-android) arch_dir="arm64-v8a" ;;
|
||||||
|
x86_64-linux-android) arch_dir="x86_64" ;;
|
||||||
|
armv7-linux-androideabi) arch_dir="armeabi-v7a" ;;
|
||||||
|
i686-linux-android) arch_dir="x86" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local out_dir="$OUTPUT_DIR/$arch_dir"
|
||||||
|
mkdir -p "$out_dir"
|
||||||
|
|
||||||
|
cargo ndk \
|
||||||
|
-t "$target" \
|
||||||
|
--platform "$MIN_SDK_VERSION" \
|
||||||
|
-o "$OUTPUT_DIR" \
|
||||||
|
build --release \
|
||||||
|
--manifest-path "$ARTI_SOURCE_DIR/arti-android-wrapper/Cargo.toml"
|
||||||
|
|
||||||
|
if [ -f "$out_dir/$LIB_NAME" ]; then
|
||||||
|
local size=$(du -h "$out_dir/$LIB_NAME" | cut -f1)
|
||||||
|
print_success "Built $arch_dir/$LIB_NAME ($size)"
|
||||||
|
else
|
||||||
|
print_error "Build failed — $out_dir/$LIB_NAME not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Verification
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
verify_jni_symbols() {
|
||||||
|
print_header "Verifying JNI symbols"
|
||||||
|
|
||||||
|
local expected_symbols=(
|
||||||
|
"Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_getVersion"
|
||||||
|
"Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_setLogCallback"
|
||||||
|
"Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_initialize"
|
||||||
|
"Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_startSocksProxy"
|
||||||
|
"Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_stopSocksProxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
for arch_dir in "$OUTPUT_DIR"/*/; do
|
||||||
|
local lib="$arch_dir$LIB_NAME"
|
||||||
|
[ -f "$lib" ] || continue
|
||||||
|
|
||||||
|
local arch=$(basename "$arch_dir")
|
||||||
|
local missing=0
|
||||||
|
|
||||||
|
for sym in "${expected_symbols[@]}"; do
|
||||||
|
if ! nm -D "$lib" 2>/dev/null | grep -q "$sym"; then
|
||||||
|
print_error "$arch: Missing symbol $sym"
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$missing" -eq 0 ]; then
|
||||||
|
print_success "$arch: All JNI symbols present"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
main() {
|
||||||
|
echo -e "${BLUE}Arti Android Build — version $ARTI_VERSION${NC}"
|
||||||
|
|
||||||
|
check_prerequisites
|
||||||
|
clone_or_update_arti
|
||||||
|
setup_wrapper
|
||||||
|
|
||||||
|
for target in "${TARGETS[@]}"; do
|
||||||
|
build_for_target "$target"
|
||||||
|
done
|
||||||
|
|
||||||
|
verify_jni_symbols
|
||||||
|
|
||||||
|
print_header "Build complete"
|
||||||
|
echo ""
|
||||||
|
echo "Libraries written to: $OUTPUT_DIR"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Verify 16KB page alignment: readelf -l <lib> | grep LOAD"
|
||||||
|
echo " 2. Build the app: ./gradlew :amethyst:assembleDebug"
|
||||||
|
echo " 3. Test on device"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
use jni::JNIEnv;
|
||||||
|
use jni::objects::{JClass, JString, JObject, GlobalRef};
|
||||||
|
use jni::sys::{jint, jstring};
|
||||||
|
use jni::JavaVM;
|
||||||
|
|
||||||
|
use arti_client::TorClient;
|
||||||
|
use arti_client::config::TorClientConfigBuilder;
|
||||||
|
use tor_rtcompat::PreferredRuntime;
|
||||||
|
|
||||||
|
use std::sync::{Arc, Mutex, Once};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Global State
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
static ARTI_CLIENT: Mutex<Option<Arc<TorClient<PreferredRuntime>>>> = Mutex::new(None);
|
||||||
|
static TOKIO_RUNTIME: Mutex<Option<tokio::runtime::Runtime>> = Mutex::new(None);
|
||||||
|
static JAVA_VM: Mutex<Option<JavaVM>> = Mutex::new(None);
|
||||||
|
static LOG_CALLBACK: Mutex<Option<GlobalRef>> = Mutex::new(None);
|
||||||
|
static SOCKS_TASK: Mutex<Option<tokio::task::JoinHandle<()>>> = Mutex::new(None);
|
||||||
|
static INIT_ONCE: Once = Once::new();
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Logging
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
fn send_log_to_java(message: String) {
|
||||||
|
let vm_opt = JAVA_VM.lock().unwrap();
|
||||||
|
let callback_opt = LOG_CALLBACK.lock().unwrap();
|
||||||
|
|
||||||
|
if let (Some(vm), Some(callback)) = (vm_opt.as_ref(), callback_opt.as_ref()) {
|
||||||
|
if let Ok(mut env) = vm.attach_current_thread() {
|
||||||
|
if let Ok(jmessage) = env.new_string(&message) {
|
||||||
|
let _ = env.call_method(
|
||||||
|
callback.as_obj(),
|
||||||
|
"onLogLine",
|
||||||
|
"(Ljava/lang/String;)V",
|
||||||
|
&[(&jmessage).into()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! log_info {
|
||||||
|
($($arg:tt)*) => {{
|
||||||
|
let msg = format!($($arg)*);
|
||||||
|
android_logger::log(&format!("Arti: {}", msg));
|
||||||
|
send_log_to_java(msg);
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! log_error {
|
||||||
|
($($arg:tt)*) => {{
|
||||||
|
let msg = format!("ERROR: {}", format!($($arg)*));
|
||||||
|
android_logger::log(&format!("Arti: {}", msg));
|
||||||
|
send_log_to_java(msg);
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// JNI Functions — package: com.vitorpamplona.amethyst.ui.tor
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_getVersion(
|
||||||
|
env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
) -> jstring {
|
||||||
|
if JAVA_VM.lock().unwrap().is_none() {
|
||||||
|
if let Ok(vm) = env.get_java_vm() {
|
||||||
|
*JAVA_VM.lock().unwrap() = Some(vm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = format!("Arti {} (custom build with rustls)", env!("CARGO_PKG_VERSION"));
|
||||||
|
let output = env.new_string(version).expect("Couldn't create java string!");
|
||||||
|
output.into_raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_setLogCallback(
|
||||||
|
env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
callback: JObject,
|
||||||
|
) {
|
||||||
|
if JAVA_VM.lock().unwrap().is_none() {
|
||||||
|
if let Ok(vm) = env.get_java_vm() {
|
||||||
|
*JAVA_VM.lock().unwrap() = Some(vm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(global_ref) = env.new_global_ref(callback) {
|
||||||
|
*LOG_CALLBACK.lock().unwrap() = Some(global_ref);
|
||||||
|
log_info!("Log callback registered");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize Arti runtime and bootstrap the TorClient.
|
||||||
|
/// The TorClient is created once and reused for the app's lifetime.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_initialize(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
data_dir: JString,
|
||||||
|
) -> jint {
|
||||||
|
if JAVA_VM.lock().unwrap().is_none() {
|
||||||
|
if let Ok(vm) = env.get_java_vm() {
|
||||||
|
*JAVA_VM.lock().unwrap() = Some(vm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already initialized — skip
|
||||||
|
if ARTI_CLIENT.lock().unwrap().is_some() {
|
||||||
|
log_info!("Arti already initialized, reusing existing client");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data_dir_str: String = match env.get_string(&data_dir) {
|
||||||
|
Ok(s) => s.into(),
|
||||||
|
Err(e) => {
|
||||||
|
log_error!("Failed to convert data_dir: {:?}", e);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
log_info!("Initializing Arti with data directory: {}", data_dir_str);
|
||||||
|
|
||||||
|
INIT_ONCE.call_once(|| {
|
||||||
|
match tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(rt) => {
|
||||||
|
log_info!("Tokio runtime created successfully");
|
||||||
|
*TOKIO_RUNTIME.lock().unwrap() = Some(rt);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_error!("Failed to create Tokio runtime: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let runtime_guard = TOKIO_RUNTIME.lock().unwrap();
|
||||||
|
let runtime = match runtime_guard.as_ref() {
|
||||||
|
Some(rt) => rt,
|
||||||
|
None => {
|
||||||
|
log_error!("Tokio runtime not initialized");
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let data_path = PathBuf::from(data_dir_str);
|
||||||
|
let cache_dir = data_path.join("cache");
|
||||||
|
let state_dir = data_path.join("state");
|
||||||
|
|
||||||
|
std::fs::create_dir_all(&cache_dir).ok();
|
||||||
|
std::fs::create_dir_all(&state_dir).ok();
|
||||||
|
|
||||||
|
let result: Result<()> = runtime.block_on(async {
|
||||||
|
log_info!("Creating Arti client...");
|
||||||
|
|
||||||
|
let config = TorClientConfigBuilder::from_directories(state_dir, cache_dir)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let client = TorClient::create_bootstrapped(config).await?;
|
||||||
|
|
||||||
|
log_info!("Arti client created and bootstrapped");
|
||||||
|
|
||||||
|
*ARTI_CLIENT.lock().unwrap() = Some(Arc::new(client));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
log_info!("Arti initialized successfully");
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_error!("Failed to initialize Arti: {:?}", e);
|
||||||
|
-3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the SOCKS5 proxy on the specified port.
|
||||||
|
/// Can be called multiple times — stops any existing listener first.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_startSocksProxy(
|
||||||
|
_env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
port: jint,
|
||||||
|
) -> jint {
|
||||||
|
log_info!("Starting SOCKS proxy on port {}", port);
|
||||||
|
|
||||||
|
// Stop any existing SOCKS server first
|
||||||
|
if let Some(handle) = SOCKS_TASK.lock().unwrap().take() {
|
||||||
|
log_info!("Aborting previous SOCKS server task");
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
let client_guard = ARTI_CLIENT.lock().unwrap();
|
||||||
|
let client = match client_guard.as_ref() {
|
||||||
|
Some(c) => Arc::clone(c),
|
||||||
|
None => {
|
||||||
|
log_error!("Arti client not initialized — call initialize() first");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
drop(client_guard);
|
||||||
|
|
||||||
|
let runtime_guard = TOKIO_RUNTIME.lock().unwrap();
|
||||||
|
let runtime = match runtime_guard.as_ref() {
|
||||||
|
Some(rt) => rt,
|
||||||
|
None => {
|
||||||
|
log_error!("Tokio runtime not initialized");
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let addr = format!("127.0.0.1:{}", port);
|
||||||
|
|
||||||
|
let bind_result = runtime.block_on(async {
|
||||||
|
tokio::net::TcpListener::bind(&addr).await
|
||||||
|
});
|
||||||
|
|
||||||
|
let listener = match bind_result {
|
||||||
|
Ok(l) => {
|
||||||
|
log_info!("SOCKS proxy bound to {}", addr);
|
||||||
|
l
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_error!("Failed to bind SOCKS proxy to {}: {:?}", addr, e);
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let handle = runtime.spawn(async move {
|
||||||
|
log_info!("Sufficiently bootstrapped; system SOCKS now functional");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match listener.accept().await {
|
||||||
|
Ok((stream, _peer_addr)) => {
|
||||||
|
let client_clone = Arc::clone(&client);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = handle_socks_connection(stream, client_clone).await {
|
||||||
|
log_error!("SOCKS connection error: {:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_error!("Failed to accept SOCKS connection: {:?}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
*SOCKS_TASK.lock().unwrap() = Some(handle);
|
||||||
|
log_info!("SOCKS proxy started on port {}", port);
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a single SOCKS5 connection through Tor.
|
||||||
|
async fn handle_socks_connection(
|
||||||
|
mut stream: tokio::net::TcpStream,
|
||||||
|
client: Arc<TorClient<PreferredRuntime>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
|
let mut buf = [0u8; 512];
|
||||||
|
|
||||||
|
// SOCKS5 handshake: read version + methods
|
||||||
|
let n = stream.read(&mut buf).await?;
|
||||||
|
if n < 2 {
|
||||||
|
return Err(anyhow::anyhow!("Invalid SOCKS handshake"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// No auth required
|
||||||
|
stream.write_all(&[0x05, 0x00]).await?;
|
||||||
|
|
||||||
|
// Read request
|
||||||
|
let n = stream.read(&mut buf).await?;
|
||||||
|
if n < 10 {
|
||||||
|
return Err(anyhow::anyhow!("Invalid SOCKS request"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = buf[0];
|
||||||
|
let cmd = buf[1];
|
||||||
|
let atyp = buf[3];
|
||||||
|
|
||||||
|
if version != 0x05 {
|
||||||
|
return Err(anyhow::anyhow!("Unsupported SOCKS version: {}", version));
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd != 0x01 {
|
||||||
|
stream.write_all(&[0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||||
|
return Err(anyhow::anyhow!("Unsupported SOCKS command: {}", cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (target_host, target_port) = match atyp {
|
||||||
|
0x01 => {
|
||||||
|
let ip = format!("{}.{}.{}.{}", buf[4], buf[5], buf[6], buf[7]);
|
||||||
|
let port = u16::from_be_bytes([buf[8], buf[9]]);
|
||||||
|
(ip, port)
|
||||||
|
}
|
||||||
|
0x03 => {
|
||||||
|
let len = buf[4] as usize;
|
||||||
|
if n < 5 + len + 2 {
|
||||||
|
return Err(anyhow::anyhow!("Invalid domain name length"));
|
||||||
|
}
|
||||||
|
let domain = String::from_utf8_lossy(&buf[5..5 + len]).to_string();
|
||||||
|
let port = u16::from_be_bytes([buf[5 + len], buf[5 + len + 1]]);
|
||||||
|
(domain, port)
|
||||||
|
}
|
||||||
|
0x04 => {
|
||||||
|
if n < 22 {
|
||||||
|
stream.write_all(&[0x05, 0x01, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||||
|
return Err(anyhow::anyhow!("Truncated IPv6 request"));
|
||||||
|
}
|
||||||
|
let ip = format!(
|
||||||
|
"{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}",
|
||||||
|
buf[4], buf[5], buf[6], buf[7], buf[8], buf[9], buf[10], buf[11],
|
||||||
|
buf[12], buf[13], buf[14], buf[15], buf[16], buf[17], buf[18], buf[19]
|
||||||
|
);
|
||||||
|
let port = u16::from_be_bytes([buf[20], buf[21]]);
|
||||||
|
(ip, port)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
stream.write_all(&[0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||||
|
return Err(anyhow::anyhow!("Unsupported address type: {}", atyp));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let tor_stream = match client.connect((target_host.as_str(), target_port)).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
log_error!("Failed to connect through Tor to {}:{}: {:?}", target_host, target_port, e);
|
||||||
|
stream.write_all(&[0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// SOCKS5 success
|
||||||
|
stream.write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?;
|
||||||
|
|
||||||
|
// Bidirectional forwarding
|
||||||
|
let (mut client_read, mut client_write) = stream.split();
|
||||||
|
let (mut tor_read, mut tor_write) = tor_stream.split();
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
r = tokio::io::copy(&mut client_read, &mut tor_write) => {
|
||||||
|
if let Err(ref e) = r { log_error!("Client->Tor error: {:?}", e); }
|
||||||
|
}
|
||||||
|
r = tokio::io::copy(&mut tor_read, &mut client_write) => {
|
||||||
|
if let Err(ref e) = r { log_error!("Tor->Client error: {:?}", e); }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the SOCKS proxy listener. The TorClient stays alive.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_stopSocksProxy(
|
||||||
|
_env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
) -> jint {
|
||||||
|
log_info!("Stopping SOCKS proxy...");
|
||||||
|
|
||||||
|
if let Some(handle) = SOCKS_TASK.lock().unwrap().take() {
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(rt) = TOKIO_RUNTIME.lock().unwrap().as_ref() {
|
||||||
|
rt.block_on(async {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: TorClient is NOT destroyed — it persists for reuse.
|
||||||
|
|
||||||
|
log_info!("SOCKS proxy stopped");
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Android Logger
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
mod android_logger {
|
||||||
|
use std::ffi::CString;
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
type c_int = i32;
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
type c_char = i8;
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
fn __android_log_write(prio: c_int, tag: *const c_char, text: *const c_char) -> c_int;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ANDROID_LOG_INFO: c_int = 4;
|
||||||
|
|
||||||
|
pub fn log(message: &str) {
|
||||||
|
unsafe {
|
||||||
|
let tag = CString::new("ArtiNative").unwrap();
|
||||||
|
let text = CString::new(message).unwrap();
|
||||||
|
__android_log_write(ANDROID_LOG_INFO, tag.as_ptr() as *const c_char, text.as_ptr() as *const c_char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user