-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
Tetherand splits into three independently shippable subsystems:
- Tether (milestones M1βM2) β the reverse-tether to a laptop.
- Privacy Chain (M3βM6) β composable VPN hops.
- Threat Detection (M7, M9, M10) β on-device monitors and hardened-mode lockdown.
Each subsystem works on its own. You can use the Threat tab without ever enabling the privacy chain; you can use the privacy chain without tethering to a laptop; you can use the tether with no chain and no monitoring.
The Android side runs a single process with three foreground services:
dev.tetherand.app (single process)
ββ MainActivity (Compose UI, 4 tabs)
ββ TetherandService (VpnService β reverse tether)
ββ TetherandChainService (VpnService β privacy chain)
ββ ThreatDetectionService (specialUse FGS β monitors)
ββ ClipboardScrubberService (specialUse FGS β prompt-injection)
ββ DecoyListenerService (specialUse FGS β Hardened Mode honeypot)
ββ HardenedTileService (QuickSettings tile)
ββ AoaAccessoryService (USB-accessory receiver)
Only one VpnService can be active at a time β Android enforces this. The user picks which via the bottom-nav tabs.
The host (Mac/Linux) side runs tetherand as a CLI:
tetherand-cli
ββ adb reverse (USB-ADB transport)
ββ tetherand-relay-core (userspace TCP/UDP/ICMP stack,
β forked from Genymobile/gnirehtet)
ββ tetherand-transport-{bt,aoa,tcp} (other transports)
ββ ratatui dashboard (`tetherand tui`)
βββββββββ Seeker (dev.tetherand.app APK) βββββββββ
β TetherandService (VpnService) β
β β TUN (10.0.0.2) β
β βΌ β
β PersistentRelayTunnel ββββΊ LocalSocket β
β "tetherand" β
βββββββββββββββββββββββββββββββββββ¬ββββββββββββββββ
β adb reverse
βΌ
βββββββββ Mac (bin/tetherand) ββββββββββββββββββββ
β tetherand-relay-core β
β β userspace TCP/UDP/ICMP β
β βΌ β
β Real internet (host Wi-Fi/Ethernet) β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
The Seeker's VPN TUN receives every packet the device wants to send.
The persistent relay tunnel hands it over adb reverse to a TCP
socket on the laptop, where the relay-core (forked from Gnirehtet,
Apache-2.0) maintains a userspace TCP/UDP/ICMP stack and forwards
through the host's normal network egress.
Bluetooth-RFCOMM, USB-AOA, and LAN-TCP transports plug in at the
LocalSocket boundary instead of adb reverse. The data shape is the
same β raw IP packets delimited by the IPv4 header length field β
so the relay-core doesn't change.
The chain orchestrator (dev.tetherand.app.chain.ChainOrchestrator)
defines a generic Hop interface:
interface Hop {
val id: String
val displayName: String
val caps: HopCaps
suspend fun start(input: Channel<ByteArray>): Channel<ByteArray>
suspend fun stop()
}Each hop consumes IP packets from its input channel and emits processed IP packets on its output channel. The orchestrator wires them in sequence, so the user-visible chain is just a list of hops:
TUN β WireGuardHop β MullvadHop β NymHop β TorHop β Internet
Each hop also exposes its own capability flags (supportsPQ,
supportsMultihop, supportsAntiCensorship), which the UI uses to
build the chain-status badge.
The native crypto and protocol logic for each hop lives in a Rust
crate at relay/:
| Crate | Hop | What it does |
|---|---|---|
relay/wg |
WireGuardHop | BoringTun WireGuard userspace, JNI'd |
relay/tor |
TorHop | arti-client 0.27, JNI'd |
relay/nym |
NymHop | nym-sdk gated by with-sdk feature, JNI'd |
relay/pt-bridge |
(auxiliary) | obfs4 / meek / webtunnel binary, spawned by Arti's PT manager |
The chain orchestrator never sees the underlying crypto; it only sees IP packets in and IP packets out.
The threat-detection foreground service runs five collectors concurrently:
CellInfoSource ββββ
LocationSource ββββ€
WifiScanner ββββΌββββΊ Heuristic orchestrator ββββΊ ThreatDb (Room)
BluetoothScan ββββ€ β
AppAudit ββββ βΌ
ThreatScreen
Each collector emits observations into a coroutine flow. The
heuristic orchestrator zips observations across collectors when a
heuristic needs cross-source data (for example, "TAC change without
motion" needs cell observations and accelerometer observations on the
same timeline). Verdicts get written to a Room-backed ThreatDb
which the Threat tab observes via Compose collectAsState.
The per-location baseline lives in the same database, keyed by a six-character geohash. Baseline writes happen automatically as observations arrive; baseline reads happen when a heuristic needs to know "what's normal for this geohash". No network calls.
/data/data/dev.tetherand.app/
ββ files/
β ββ arti/ # Arti's cache + state directory
β ββ nym/ # Nym SDK state
β ββ databases/
β ββ threats.db # Room-backed alert feed + baseline
β ββ ...
ββ cache/
β ββ arti/ # arti cache subdir
β ββ pts/ # extracted PT binaries (chmod +x)
ββ shared_prefs/
ββ tetherand-hardened.xml # EncryptedSharedPreferences
ββ tetherand-nym.xml
ββ tetherand-tor-bridges.xml
ββ tetherand-voiceprint.xml
ββ ...
The encrypted SharedPreferences files use the AndroidX security library: AES256-SIV for key encryption, AES256-GCM for value encryption, keys held in the AndroidKeyStore (hardware-backed where available).
The licensing converges towards GPLv3 from the moment M7a links in
(NetMonster reflection and the AIMSICD port). M1 and M2's tether code
is Apache-2.0 in isolation, but the shipped APK ends up GPLv3 once
the threat module is included. The NOTICE files under each
subdirectory document this.
Use
Features
Build
Project