mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
When Arti's in-memory TorClient gets into a broken state (bad guards from a previous network, dead circuits, expired consensus held in memory), nothing short of a process restart used to recover it: the JNI exposed initialize / startSocksProxy / stopSocksProxy but no way to drop the TorClient, and the Kotlin side gated initialize behind a one-shot AtomicBoolean. force-stop preserved the on-disk arti/state/, toggle-off-then-on only re-bound the SOCKS listener on the same broken client, and wiping app data was the only way out. Rust side - New JNI Java_..._ArtiNative_destroy: aborts the SOCKS listener task, aborts all in-flight per-connection handlers (each holds an Arc<TorClient> clone that would otherwise pin the state file lock), waits 500ms, drops the static ARTI_CLIENT. Next initialize() call creates a fresh client and re-bootstraps. - Track handler JoinHandles in HANDLER_TASKS so destroy can abort them; cull finished ones on each accept to keep the Vec bounded. Kotlin side - TorService.reset() / resetWithCleanState() — drop the native client, flip initialized=false. The second variant also wipes arti/state/ on disk to rebuild guard selection from scratch. - TorManager.resetEpoch StateFlow is now part of the status combine; bumping it re-fires the INTERNAL branch which calls service.start() and runs full Arti re-init. - onNetworkChange (wired from ConnectivityManager.networkId distinctUntilChanged) now calls service.reset() + clears the persisted bypass approval + bumps the epoch. Replaces the previous clearSessionBypass() which only touched the in-memory bypass half. - Self-heal watchdog: when status sits at Connecting for >45s (before the 60s connectionFailure dialog), calls resetWithCleanState. Rate-limited to one per 5 minutes so a permanently broken network doesn't loop us. onNetworkChange primes lastSelfHealAtMs so a slow legitimate post-network-change bootstrap doesn't get a second reset on top of itself. Rebuilds: libarti_android.so for arm64-v8a + x86_64 (NDK 27, 16KB-page aligned).
250 lines
7.6 KiB
Bash
Executable File
250 lines
7.6 KiB
Bash
Executable File
#!/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"
|
|
"Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_destroy"
|
|
)
|
|
|
|
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 "$@"
|