Files
amethyst/tools/arti-build
Claude e39ea55fd6 test(tor): tier-3 integration — JVM host build of Arti, smoke + bootstrap tests
Closes the test gap below the tier-1 unit tests by running the real Arti
JNI shim end-to-end on JVM. Cheaper than an emulator + connectedAndroidTest,
and exercises the exact Rust + JNI code path the Android .so does.

Three tests in TorArtiNativeIntegrationTest:

1. `library loads and reports a version` — always-on smoke check. Loads
   libarti_android.so via System.loadLibrary and calls ArtiNative.getVersion.
   ~10ms. Catches build/link regressions (e.g. a stale .so after an ARTI
   bump, a missing JNI symbol export, a forgotten rebuild on this path).
   Skipped on non-Linux-x86_64 hosts with a clear message pointing at the
   build-arti-host.sh rebuild step.

2. `bootstraps and proxies an HTTPS request through Tor` — opt-in via
   -Pamethyst.arti.integration=true. ArtiNative.initialize → startSocksProxy
   → OkHttp-via-SOCKS → check.torproject.org/api/ip. Asserts "IsTor":true.
   Regression net for the rustls CryptoProvider install we added after the
   v2.3.0 bump and for the destroy/handler-abort fixes in the Rust shim.

3. `destroy then re-initialize releases the state file lock cleanly` — opt-in.
   The direct unit-test mirror of the self-heal path: bootstrap, destroy, hit
   the SAME data dir with initialize again, verify it succeeds without a
   "state file already locked" error and that traffic still flows.

Wiring:
- New tools/arti-build/build-arti-host.sh — companion to build-arti.sh.
  Cargo-builds the wrapper crate for the host target (x86_64-linux on most
  dev machines, but the script maps macOS / arm64-linux too) and copies to
  amethyst/src/test/native-libs/<host-tag>/libarti_android.so.
- amethyst/build.gradle.kts testOptions.unitTests.all configures
  -Djava.library.path so System.loadLibrary("arti_android") finds the
  checked-in host .so. Also forwards -Pamethyst.arti.integration so the
  opt-in gate works from a Gradle invocation.
- Checked-in src/test/native-libs/x86_64-linux/libarti_android.so for the
  most common dev/CI host (~6 MB).

Wrapper change to make the JVM path actually run:
- lib.rs: on #[cfg(not(target_os = "android"))], call
  builder.storage().permissions().dangerously_trust_everyone() so Arti's
  fs-mistrust check doesn't reject /tmp data dirs on hosts where parent
  directories have unusual UIDs (typical in containers). Android keeps its
  strict default — the app's private filesDir is already sandboxed by the OS.
  Compiled-out on Android, so the shipped Android .so is functionally
  unchanged.

Verified in this session:
- Smoke test passes without -P (3 tests, 1 ran, 2 skipped).
- Full unit test suite still passes.
- With -P the bootstrap tests get past Arti's permissions check; they hang
  on actual relay I/O in this container because outbound TCP egress is
  restricted to a CDN allow-list, not Tor relays. Tests succeed on hosts
  with unrestricted outbound — see the test kdoc.
2026-05-26 21:34:26 +00:00
..
2026-05-26 20:06:15 +00:00
2026-05-26 20:06:15 +00:00

Arti Android Build Tools

Custom-built Arti (Tor in Rust) native libraries for Amethyst Android. This replaces the Guardian Project's arti-mobile-ex AAR with a minimal JNI wrapper built directly from Arti source.

Why custom build?

Guardian Project AAR Custom build
Size ~140MB ~11MB
16KB pages No Yes (NDK 25+)
Stop/restart Broken (state file lock) Works (TorClient persists, only SOCKS proxy stops)
Version Behind Pinned to latest (currently 1.9.0)

Quick start

Pre-built .so files should be committed to amethyst/src/main/jniLibs/. You only need to rebuild if you want to verify binaries, update the Arti version, or modify the JNI wrapper.

Prerequisites

  1. Rust toolchain

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    
  2. Android targets

    rustup target add aarch64-linux-android x86_64-linux-android
    
  3. cargo-ndk

    cargo install cargo-ndk
    
  4. Android NDK 25+ (required for 16KB page size support)

    # Via Android Studio: SDK Manager → SDK Tools → NDK (Side by side)
    # Or via command line:
    sdkmanager "ndk;27.0.12077973"
    
    # Set environment variable
    export ANDROID_NDK_HOME="$HOME/Android/Sdk/ndk/27.0.12077973"
    

Building

cd tools/arti-build

# Build for all targets (arm64 + x86_64)
./build-arti.sh

# Build arm64 only (for release APKs)
./build-arti.sh --release

# Clean rebuild from scratch
./build-arti.sh --clean

The script will:

  1. Clone official Arti source from gitlab.torproject.org
  2. Check out the version pinned in ARTI_VERSION
  3. Copy the JNI wrapper into the source tree
  4. Compile with cargo-ndk for each target architecture
  5. Output .so files to amethyst/src/main/jniLibs/{arm64-v8a,x86_64}/
  6. Verify JNI symbols are exported correctly

Output

amethyst/src/main/jniLibs/
├── arm64-v8a/
│   └── libarti_android.so    (~5-6 MB)
└── x86_64/
    └── libarti_android.so    (~6-7 MB, emulator support)

Verifying 16KB page alignment

Google Play requires 16KB page-aligned native libraries. Verify with:

readelf -l amethyst/src/main/jniLibs/arm64-v8a/libarti_android.so | grep LOAD

The first LOAD segment alignment should be 0x4000 (16384 bytes).

Directory structure

tools/arti-build/
├── README.md           # This file
├── ARTI_VERSION        # Pinned Arti git tag (e.g., arti-v1.9.0)
├── Cargo.toml          # Rust dependencies and build profile
├── build-arti.sh       # Build script
├── src/
│   └── lib.rs          # JNI bridge (Rust → Kotlin)
└── .arti-source/       # [gitignored] Cloned Arti repository

Updating Arti version

  1. Check available versions:

    git ls-remote --tags https://gitlab.torproject.org/tpo/core/arti.git | grep 'arti-v' | tail -10
    
  2. Update the version file:

    echo "arti-v1.10.0" > ARTI_VERSION
    
  3. Update crate versions in Cargo.toml to match the new release. Check the crate versions at:

    https://gitlab.torproject.org/tpo/core/arti/-/raw/arti-v1.10.0/crates/arti-client/Cargo.toml
    
  4. Rebuild and test:

    ./build-arti.sh --clean
    

Architecture: JNI bridge

The Rust wrapper (src/lib.rs) exposes these JNI functions to Kotlin:

JNI function Kotlin Purpose
initialize(dataDir) ArtiNative.initialize() Create TorClient, bootstrap Tor network
startSocksProxy(port) ArtiNative.startSocksProxy() Bind SOCKS5 listener on localhost
stopSocksProxy() ArtiNative.stopSocksProxy() Abort listener, release port
getVersion() ArtiNative.getVersion() Return Arti version string
setLogCallback(cb) ArtiNative.setLogCallback() Register log callback

Key design decisions

  • TorClient is created once via initialize() and persists for the app's lifetime. Its state file lock is tied to the object's lifetime and released only on GC/process exit.
  • stopSocksProxy() only stops the TCP listener — it does NOT destroy the TorClient. This allows clean stop/start cycles without state file lock conflicts.
  • SOCKS5 is implemented in Rust using tokio::net::TcpListener, not delegated to Arti's built-in proxy. This gives us full control over the listener lifecycle.
  • Bidirectional forwarding uses tokio::io::copy with tokio::select! for efficiency.

Cargo.toml features

Default features are disabled (default-features = false) to minimize binary size.

Feature Purpose Why included
tokio Async runtime Required by our SOCKS proxy
rustls TLS via pure Rust No OpenSSL dependency, smaller binary
compression zstd/deflate relay traffic Reduces bandwidth on Tor circuits
onion-service-client Access .onion addresses Amethyst routes .onion relay connections through Tor
static-sqlite Bundled SQLite Android native code can't use system SQLite

Not included:

Feature Why excluded
native-tls Using rustls instead (smaller, no system dependency)
bridge-client Amethyst doesn't expose bridge configuration in UI yet. Add back if needed.
pt-client Pluggable transports — same reason as bridges
onion-service-service We only connect to .onion, we don't host them

Release profile

[profile.release]
opt-level = "z"       # Optimize for size
lto = true            # Link-time optimization
codegen-units = 1     # Single codegen unit (smaller binary)
strip = true          # Strip debug symbols
panic = "abort"       # No unwinding (smaller binary)

Troubleshooting

cargo-ndk not found

cargo install cargo-ndk

NDK not found

export ANDROID_NDK_HOME="$HOME/Android/Sdk/ndk/<version>"

Rust targets not installed

rustup target add aarch64-linux-android x86_64-linux-android

Build fails with dependency errors

Try a clean build:

./build-arti.sh --clean

JNI symbols missing after build

The build script verifies symbols automatically. If verification fails, check that src/lib.rs function names match the Kotlin package path:

Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_<methodName>