Commit Graph
11 Commits
Author SHA1 Message Date
Vitor PamplonaandClaude Opus 4.8 69b8ba9f23 fix: bound Arti bootstrap with a 60s timeout so a hostile network can't wedge Tor
A self-heal reset()/resetWithCleanState() (and the lifecycle serialization added
in the previous commit) can only recover Tor if initialize() actually returns.
ArtiNative.initialize() calls TorClient::create_bootstrapped, which on a hostile
network (unreachable guards, wiped consensus) retries internally for many
minutes. While it blocks it holds lifecycleMutex, so the watchdog's reset can
never run — Tor stays wedged at Connecting.

Wrap create_bootstrapped in a 60s tokio::time::timeout. On timeout the future is
dropped (tearing down the half-built client) and initialize() returns -4; the
JNI ABI is unchanged (still one String arg), so the checked-in CI host .so and
TorArtiNativeIntegrationTest keep working without a rebuild. TorService treats
-4 specially: drop the init flag and leave status Connecting (don't wipe+retry
inline under the lock, don't go Off) so TorManager's self-heal watchdog resets
and re-inits on its own cadence, and connectionFailure can still surface the
"use regular connection" dialog.

Rebuilt libarti_android.so for arm64-v8a + x86_64.

Verified on device: a no-network cold-start bootstrap timed out at exactly 60s
(previously hung 7+ min), released the lock, and on network restore the watchdog
re-init'd and Tor reached Active. Addresses #3225.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:41:09 -04:00
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
Claude 9a2adf091d chore(tor): bump Arti to v2.3.0
Wins: reduced GeoIP memory usage (moved off heap), CircuitClosed→NotConnected
error change (affects our handler error paths), DATA-cells-on-closed-streams
fix, and a flow-control sidechannel mitigation bug fix. Nothing here directly
addresses the stuck-Tor recovery work in the prior commits, but it's a clean
overdue bump while we're in this code.

Wrapper changes required by the bump:
- arti-client + tor-rtcompat: 0.41 → 0.42 to match the new crate versions
  shipped with arti-v2.3.0.
- arti-v2.3.0's tor-rtcompat no longer installs a rustls CryptoProvider
  implicitly (changelog: "if the application fails to install a rustls
  CryptoProvider, tor-rtcompat no longer installs one itself"). Add a direct
  `rustls = "0.23"` dep with the `ring` feature and `install_default()` it
  inside INIT_ONCE before runtime creation — otherwise create_bootstrapped
  panics on the first TLS handshake. Keeping `ring` (same as 2.2.0
  effectively used) rather than 2.3.0's new default `aws-lc-rs`, which is
  heavier on Android and has known build.rs pain on aarch64-linux-android.

Heads-up for the next bump: arti-v2.4.0 will explicitly wrap TorClient in
Arc rather than implicitly having Arc-like semantics. We already wrap
explicitly so the migration is a no-op aside from potential Arc<Arc<...>>
cleanup.

Rebuilds: libarti_android.so for arm64-v8a + x86_64.
2026-05-26 20:06:15 +00:00
Claude c3ddd4e7be fix(tor): audit fixes — first-bootstrap grace + tighten destroy() race
Audit of db378a1 surfaced three issues; this commit addresses them.

1) First-bootstrap self-heal storm (TorManager). On a fresh install with a
   slow network the legitimate first bootstrap takes 30–60s. The 45s
   stuck-Connecting watchdog used to fire resetWithCleanState, wiping an
   empty state dir and adding a full bootstrap cycle of delay for no gain.
   Now: track hasEverBootstrapped (flipped when status reaches Active);
   pre-first-bootstrap self-heals use the gentler reset (drop client only,
   keep state), post-first-bootstrap use resetWithCleanState. Wiping stale
   on-disk guards only matters once we know Arti can actually work.

2) Rust destroy() race (lib.rs). The accept loop in startSocksProxy has no
   .await between accept() returning and HANDLER_TASKS.push(h), so an
   abort() alone is racy — a new handler can be spawned and pushed AFTER
   our drain runs, which then holds an Arc<TorClient> past destroy() and
   keeps the state file lock alive. Now: after abort(), await the SOCKS
   JoinHandle with a 1s timeout so the listener fully terminates before
   we drain HANDLER_TASKS. No new handlers can be added once the listener
   is gone.

3) TOKIO_RUNTIME mutex held during block_on(sleep). The previous
   `if let Some(rt) = TOKIO_RUNTIME.lock().unwrap().as_ref()` kept the
   mutex held for the full sleep duration, blocking any other JNI caller
   that needs the runtime. Now: clone the runtime Handle and release the
   mutex immediately. Same fix applied to stopSocksProxy.

Rebuilds: libarti_android.so for arm64-v8a + x86_64.
2026-05-26 19:37:36 +00:00
Claude db378a105c feat(tor): self-heal — drop & rebuild Arti on network change and stuck Connecting
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).
2026-05-26 19:09:54 +00:00
Claude bc24434f1c refactor: remove duplicate android_logger from native Arti wrapper
All log messages go through send_log_to_java() → Kotlin ArtiLogCallback
→ Log.d("TorService"), which already writes to logcat. The android_logger
module was a second FFI call to __android_log_write that duplicated
every line.

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 17:32:38 +00:00
Claude 2429e695ce chore: bump Arti to 2.2.0 (arti-client/tor-rtcompat 0.41)
All features and APIs verified present in 0.41:
- tokio, rustls, compression, onion-service-client, static-sqlite
- TorClient::create_bootstrapped, from_directories, connect()

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 17:18:25 +00:00
Claude 53ae87aa9d chore: trim unnecessary Arti features to reduce binary size
- Set default-features = false on arti-client and tor-rtcompat
- Removed bridge-client (UI doesn't expose bridge config yet)
- Narrowed tokio features from "full" to only what the SOCKS proxy
  needs: rt-multi-thread, net, io-util, time, macros

Kept: tokio, rustls, compression, onion-service-client, static-sqlite
(all required for Amethyst's .onion relay support)

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 17:12:35 +00:00
Claude 35bf16b63e docs: add README for Arti Android build tools
Covers prerequisites, build commands, output verification, version
updates, architecture decisions, Cargo features, and troubleshooting.

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 17:03:47 +00:00
Claude 035c570902 chore: bump Arti version to 1.9.0 (arti-client/tor-rtcompat 0.38)
https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
2026-04-01 16:55:03 +00:00
Claude e50ae0fb1e 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
2026-04-01 16:37:44 +00:00