Tetherand

Building from source

This page walks through a full rebuild of Tetherand from a fresh clone, including every native library.

Toolchain prerequisites

ToolVersionUsed for
Android NDKr26 or newerCross-compiling Rust + C libs to arm64-android
Android SDKAPI 36 platformapksigner, zipalign, adb
Rust1.85+The workspace at relay/
aarch64-linux-android targetrustup target add aarch64-linux-androidCross-compile output
Go1.22 or 1.23 (not 1.26 yet)Conjure / Snowflake upstream binaries
cmake3.20+librtlsdr / libhackrf / libusb
JavaOpenJDK 21Gradle wrapper expects 21+

Set NDK_HOME (or ANDROID_NDK_HOME) to the NDK root before any native build:

export NDK_HOME=$HOME/Library/Android/sdk/ndk/26.3.11579264

(macOS path shown; Linux path is similar under ~/Android/Sdk/ndk.)

Quick path — debug APK with all native libs

make native-all   # cross-compiles wg, tor, nym, pt-bridge, librtlsdr stack
make apk          # repackages the APK + emits hash sidecars
make install      # adb install + pre-grants VPN consent
make smoke-device # UiAutomator walk

The full bundle APK lands in bin/tetherand.apk at around 59 MB. Hash sidecars (tetherand.apk.sha256 and tetherand.apk.sha3-256) and a master bin/SHASUMS.txt are emitted by make hashes, which make apk runs automatically.

Individual native libraries

make native-wg       # libtetherand_wg.so (WireGuard via BoringTun)
make native-tor      # libtetherand_tor.so (Arti)
make native-nym      # libtetherand_nym.so (NymVPN JNI surface)
make native-pt       # libtetherand_pt.so (obfs4/meek/webtunnel) + libconjure_client.so
make native-rtlsdr   # librtlsdr.so + libhackrf.so + libusb1.0.so

Each script lives under scripts/build-*-android.sh. They all apply --remap-path-prefix so that build-time absolute paths from your workstation don't end up in the shipped .rodata panic strings.

Signed release APK

make release-signed

This runs:

  1. gradle assembleRelease to produce the unsigned APK.
  2. zipalign -p 4 for page-aligned native libraries.
  3. apksigner sign with SHA256-RSA2048 against the Android v2 signing scheme.

The script gates the signing step against an out-of-repo allow-list of approved certificate DNs (~/.tetherand-signing-allow). The stock CN=Android Debug, O=Android, C=US debug subject is always allowed without an entry. To use a production keystore:

keytool -genkeypair -alias tetherand-prod -keystore tetherand-prod.jks \
  -keyalg RSA -keysize 4096 -validity 36500 \
  -dname "CN=Tetherand, O=pq-cybarg, C=US"

echo "CN=Tetherand, O=pq-cybarg, C=US" >> ~/.tetherand-signing-allow

scripts/bundle-combinations.sh generates every subset of the six native-library groups (wg, tor, nym, pt, pts, sdr) as its own zip with paired SHA-256 + SHA3-256 sidecars under dist/bundles/. Details at Bundle combinations.

Known build snags

SymptomCauseFix
ld.lld: error: unable to find library -lsqlite3 during Tor buildArti's tor-dirmgr uses rusqlite and the NDK sysroot has no libsqlite3Already pinned in relay/tor/Cargo.toml via rusqlite = { features = ["bundled"] }; if you're patching it, keep the bundled feature.
nym-noise fails with a type-inference errorUpstream nym-noise 1.20.4 has a closure parameter that needs annotation under rustc 1.83+TETHERAND_NYM_SDK=0 make native-nym (default; ships the JNI surface only). The with-sdk feature is opt-in.
ld.lld: error: unable to find library -lusb-1.0 during rtlsdr buildrtlsdr's CMakeLists hardcodes -lusb-1.0 and ignores LIBUSB_LIBRARIESThe script in scripts/build-rtlsdr-android.sh already passes -L$WORK/libusb/build via CMAKE_SHARED_LINKER_FLAGS.
Snowflake build fails with link: github.com/wlynxg/anet: invalid reference to net.zoneCacheUpstream wlynxg/anet uses a Go internal symbol that moved in Go 1.26Downgrade Go to 1.22 or 1.23 for the snowflake build; or wait for upstream to fix. v0.1 skips snowflake.

Test suite

make test       # all Rust workspace unit tests
cd android && ./gradlew :app:testDebugUnitTest   # Kotlin unit tests

The Rust workspace ships unit tests for the codec, the bridge parser, the obfs4 cert decoder, the perplexity rule, the prompt-injection regex, and the provenance checker. The Kotlin unit tests cover the geohash6 implementation.

Integration tests against the real Tor network live behind #[ignore] — run explicitly:

cd relay && cargo test -p tetherand-tor --test live_probe -- --ignored

The probe dials check.torproject.org:443 and an .onion service. Requires internet egress to the Tor network from the test runner.