Building from source
This page walks through a full rebuild of Tetherand from a fresh clone, including every native library.
Toolchain prerequisites
| Tool | Version | Used for |
|---|---|---|
| Android NDK | r26 or newer | Cross-compiling Rust + C libs to arm64-android |
| Android SDK | API 36 platform | apksigner, zipalign, adb |
| Rust | 1.85+ | The workspace at relay/ |
aarch64-linux-android target | rustup target add aarch64-linux-android | Cross-compile output |
| Go | 1.22 or 1.23 (not 1.26 yet) | Conjure / Snowflake upstream binaries |
| cmake | 3.20+ | librtlsdr / libhackrf / libusb |
| Java | OpenJDK 21 | Gradle 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:
gradle assembleReleaseto produce the unsigned APK.zipalign -p 4for page-aligned native libraries.apksigner signwith 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
Bundle combinations
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
| Symptom | Cause | Fix |
|---|---|---|
ld.lld: error: unable to find library -lsqlite3 during Tor build | Arti's tor-dirmgr uses rusqlite and the NDK sysroot has no libsqlite3 | Already 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 error | Upstream 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 build | rtlsdr's CMakeLists hardcodes -lusb-1.0 and ignores LIBUSB_LIBRARIES | The 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.zoneCache | Upstream wlynxg/anet uses a Go internal symbol that moved in Go 1.26 | Downgrade 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.