Tetherand

Architecture

Tetherand splits into three independently shippable subsystems:

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.

Process layout

The Android side runs a single process with several 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`)

Tether data flow

┌──────── 5364C13D (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 5364C13D'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.

Privacy chain data flow

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/:

CrateHopWhat it does
relay/wgWireGuardHopBoringTun WireGuard userspace, JNI'd
relay/torTorHoparti-client 0.27, JNI'd
relay/nymNymHopnym-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.

Threat detection data flow

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.

On-device file layout

/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).

Subsystem boundaries

The licensing converges towards GPL-3.0-or-later 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 GPL-3.0-or-later once the threat module is included. See Licensing for the full per-milestone map.