16 KiB
FIPS Android VPN App — Architecture Plan
Goal
Build an Android application (.apk) that runs the Rust FIPS daemon as a VPN service, enabling any app on the device — browsers, curl, etc. — to resolve <npub>.fips domains and route traffic through the FIPS mesh network. The user types http://npub1abc...xyz.fips in Chrome and it just works.
Strategy
- Compile fips as a shared library — build the Rust
fipscrate as acdylibtargetingaarch64-linux-android(andx86_64-linux-androidfor emulators), exposing a C-ABI FFI layer - Android VpnService integration — use Android's
VpnServiceAPI to create a TUN interface that the OS routes all traffic through, replacing the Linux-specifictuncrate usage - DNS interception — configure the VPN to use the fips DNS resolver for
.fipsdomains, so the Android system resolver forwards queries to the in-process fips DNS responder - Kotlin wrapper app — minimal Android app with a single-activity UI for start/stop, status display, and configuration
How It Works (User Perspective)
User opens FIPS app → taps "Connect" → VPN icon appears in status bar
User opens Chrome → types "http://npub1abc...xyz.fips" → page loads
Architecture Overview
graph TD
subgraph "Android Device"
subgraph "FIPS App Process"
UI["Kotlin UI<br/>MainActivity"]
SVC["FipsVpnService<br/>extends VpnService"]
FFI["JNI/FFI Bridge<br/>libfips_android.so"]
subgraph "Rust Core"
NODE["Node<br/>mesh routing engine"]
DNS["DNS Responder<br/>port 5354"]
TUN_ADAPTER["VpnTunAdapter<br/>reads/writes VPN fd"]
UDP["UdpTransport"]
TCP["TcpTransport"]
TOR["TorTransport"]
end
end
BROWSER["Chrome / any app"]
OS_VPN["Android OS<br/>VPN routing table"]
end
subgraph "FIPS Mesh Network"
PEER1["Peer Node A"]
PEER2["Peer Node B"]
end
UI -->|"start/stop"| SVC
SVC -->|"passes VPN fd"| FFI
FFI -->|"JNI calls"| NODE
NODE --- DNS
NODE --- TUN_ADAPTER
NODE --- UDP
NODE --- TCP
BROWSER -->|"DNS query: npub1...fips"| OS_VPN
BROWSER -->|"HTTP traffic"| OS_VPN
OS_VPN -->|"all traffic"| TUN_ADAPTER
TUN_ADAPTER -->|"IPv6 packets"| NODE
UDP -->|"mesh traffic"| PEER1
TCP -->|"mesh traffic"| PEER2
Data Flow: Browser to Mesh
sequenceDiagram
participant Browser
participant AndroidOS as Android OS
participant VPN as VpnService TUN fd
participant TunAdapter as VpnTunAdapter
participant DNS as FIPS DNS Responder
participant Node as FIPS Node
participant Mesh as FIPS Mesh
Browser->>AndroidOS: DNS query npub1abc.fips
AndroidOS->>VPN: Route DNS to VPN DNS server
VPN->>TunAdapter: UDP packet to 10.0.0.1:53
TunAdapter->>DNS: Forward .fips query
DNS->>DNS: Resolve npub to fd00::... IPv6
DNS-->>TunAdapter: AAAA response fd00::abc
TunAdapter-->>VPN: DNS response packet
VPN-->>AndroidOS: fd00::abc
AndroidOS-->>Browser: fd00::abc
Browser->>AndroidOS: TCP SYN to fd00::abc:80
AndroidOS->>VPN: Route through VPN
VPN->>TunAdapter: IPv6 packet
TunAdapter->>Node: Inject into mesh routing
Node->>Mesh: Encrypted mesh packet
Mesh-->>Node: Response
Node-->>TunAdapter: IPv6 response packet
TunAdapter-->>VPN: Write to TUN fd
VPN-->>AndroidOS: Route back
AndroidOS-->>Browser: HTTP response
Key Components
1. Rust FFI Library (libfips_android.so)
The fips crate is compiled as a shared library with a thin C-ABI wrapper. This is the bridge between Kotlin and Rust.
New file: fips/src/android/mod.rs
// Exposed functions (C-ABI via #[no_mangle] extern "C"):
//
// fips_android_start(config_json, vpn_fd) -> handle
// fips_android_stop(handle)
// fips_android_status(handle) -> status_json
// fips_android_get_dns_addr() -> "10.0.0.1:53"
The critical difference from the Linux daemon: instead of creating a TUN device via the tun crate, the Android build receives a file descriptor from the VpnService and reads/writes raw IP packets on it directly.
Cargo.toml changes:
[lib]
crate-type = ["cdylib", "rlib"]
[target.'cfg(target_os = "android")'.dependencies]
jni = "0.21"
android_logger = "0.14"
2. VPN TUN Adapter (fips/src/upper/tun_android.rs)
Replaces the Linux/macOS TunDevice on Android. Instead of calling tun::create(), it wraps a raw file descriptor received from VpnService.Builder.establish().
Key differences from Linux TUN:
| Aspect | Linux (tun.rs) |
Android (tun_android.rs) |
|---|---|---|
| Device creation | tun::create() with CAP_NET_ADMIN |
VpnService.Builder.establish() in Kotlin, fd passed to Rust |
| Interface config | rtnetlink / ifconfig |
VpnService.Builder methods in Kotlin |
| Address assignment | ip -6 addr add via rtnetlink |
.addAddress() in VpnService.Builder |
| Route setup | ip -6 route add via rtnetlink |
.addRoute() in VpnService.Builder |
| DNS config | External resolv.conf / systemd-resolved | .addDnsServer() in VpnService.Builder |
| Read/write | tun::Device read/write |
read(fd) / write(fd) via libc |
3. Android VPN Service (FipsVpnService.kt)
The Android foreground service that:
- Calls
VpnService.Builderto configure the TUN interface - Passes the resulting file descriptor to the Rust core via JNI
- Manages the fips daemon lifecycle (start/stop)
- Shows a persistent notification (required for foreground services)
class FipsVpnService : VpnService() {
// VPN configuration:
// - addAddress("fd00::1", 8) // FIPS IPv6 prefix
// - addRoute("::", 0) // Route all IPv6 through VPN
// - addDnsServer("10.0.0.1") // In-VPN DNS server
// - setMtu(1280) // Match FIPS TUN MTU
// - establish() // Returns ParcelFileDescriptor
//
// Then: NativeLib.fipsStart(config, fd.detachFd())
}
4. DNS Resolution Strategy
The FIPS daemon already includes a DNS responder (port 5354) that resolves <npub>.fips → IPv6. On Android, we need to make this accessible to the system resolver:
Option A — VPN-internal DNS (recommended):
- Bind the DNS responder to a virtual IP inside the VPN tunnel (e.g.,
10.0.0.1:53) - Configure
VpnService.Builder.addDnsServer("10.0.0.1") - Android routes all DNS queries to this address
- The fips DNS responder handles
.fipsqueries directly - Non-
.fipsqueries are forwarded to an upstream resolver (e.g.,1.1.1.1)
Option B — Split DNS:
- Only route
.fipsqueries to the internal resolver - Requires Android 10+ per-domain DNS routing (limited API support)
Option A is simpler and more reliable. The DNS responder already exists in fips/src/upper/dns.rs; it just needs a small enhancement to forward non-.fips queries upstream.
5. Android App Structure
fips-android/
├── app/
│ ├── src/main/
│ │ ├── java/com/fips/android/
│ │ │ ├── MainActivity.kt # UI: connect/disconnect, status
│ │ │ ├── FipsVpnService.kt # VpnService implementation
│ │ │ ├── NativeLib.kt # JNI bindings to libfips_android.so
│ │ │ └── FipsConfig.kt # Config management
│ │ ├── jniLibs/
│ │ │ ├── arm64-v8a/
│ │ │ │ └── libfips_android.so # Rust-compiled shared library
│ │ │ └── x86_64/
│ │ │ └── libfips_android.so # For emulator testing
│ │ ├── res/
│ │ │ ├── layout/activity_main.xml
│ │ │ └── values/strings.xml
│ │ └── AndroidManifest.xml
│ └── build.gradle.kts
├── build.gradle.kts
├── settings.gradle.kts
└── gradle/
AndroidManifest.xml permissions:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<service
android:name=".FipsVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:foregroundServiceType="specialUse">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
Platform Compatibility Considerations
What Works As-Is on Android
| Component | Status | Notes |
|---|---|---|
Node mesh routing |
✅ Works | Pure Rust, no platform deps |
Identity / crypto |
✅ Works | secp256k1, chacha20poly1305 — all portable |
TreeState / BloomState |
✅ Works | Pure data structures |
UdpTransport |
✅ Works | tokio UDP sockets work on Android |
TcpTransport |
✅ Works | tokio TCP sockets work on Android |
TorTransport |
⚠️ Needs work | Requires bundling a Tor binary or using Orbot |
DNS Responder |
✅ Works | UDP socket-based, portable |
Config / YAML parsing |
✅ Works | serde_yaml is portable |
What Needs Android-Specific Implementation
| Component | Current | Android Replacement |
|---|---|---|
| TUN device creation | tun crate + rtnetlink |
VpnService fd from Kotlin |
| TUN interface config | rtnetlink / ifconfig | VpnService.Builder in Kotlin |
| Signal handling | SIGTERM / Ctrl+C | Service lifecycle callbacks |
| Logging | tracing-subscriber to stderr | android_logger to logcat |
| Config file location | /etc/fips/fips.yaml | App internal storage |
EthernetTransport |
Raw sockets (CAP_NET_RAW) | ❌ Not available on Android |
BLE Transport |
BlueZ/D-Bus via bluer | ❌ Needs Android BLE API (future) |
What to Exclude from Android Build
fips-gatewaybinary (requires nftables, Linux-only)fipstopTUI binary (terminal UI, not useful on Android)fipsctlCLI binary (replaced by in-app controls)EthernetTransport(requires raw socket access)BLE Transport(requires BlueZ, Linux-only; Android BLE would be a separate implementation)rustables/ nftables dependencyratatuiTUI dependency
Build System
Cross-Compilation Setup
# Install Android NDK targets
rustup target add aarch64-linux-android x86_64-linux-android
# Install cargo-ndk for simplified Android builds
cargo install cargo-ndk
# Build the shared library
cargo ndk -t arm64-v8a -t x86_64 -o app/src/main/jniLibs build --release
Cargo Feature Flags
[features]
default = ["tui", "ble"]
android = [] # New: excludes TUI, BLE, gateway; enables android TUN adapter
tui = ["dep:ratatui"]
ble = ["dep:bluer"]
gateway = ["dep:rustables"]
The android feature flag gates:
tun_android.rsinstead oftun.rsfor TUN device handlingandroid_loggerinstead oftracing-subscriberstderr output- Exclusion of platform-specific Linux dependencies (bluer, rtnetlink, rustables)
Conditional Compilation in Existing Code
The existing codebase already uses #[cfg(target_os = "linux")] and #[cfg(unix)] guards extensively. Android is target_os = "android" but IS unix, so:
#[cfg(unix)]— matches Android (good for most things)#[cfg(target_os = "linux")]— does NOT match Android (good, excludes BLE/rtnetlink)- New guard needed:
#[cfg(target_os = "android")]for Android-specific TUN adapter
The tun crate dependency (currently under [target.'cfg(unix)'.dependencies]) needs to be excluded on Android since we use the VpnService fd directly:
[target.'cfg(all(unix, not(target_os = "android")))'.dependencies]
tun = { version = "0.8.5", features = ["async"] }
Implementation Phases
Phase 1: Rust Library for Android
- Add
androidfeature flag toCargo.toml - Create
fips/src/upper/tun_android.rs— TUN adapter that wraps a raw fd - Create
fips/src/android/mod.rs— C-ABI FFI entry points - Add
#[cfg(target_os = "android")]guards to swap TUN implementations - Verify cross-compilation with
cargo ndkbuilds cleanly
Phase 2: Minimal Android App Shell
- Create
fips-android/Gradle project with Kotlin - Implement
FipsVpnServicewith VpnService.Builder configuration - Implement
NativeLib.ktJNI bindings - Implement
MainActivitywith connect/disconnect button - Wire up: button → VpnService → JNI → Rust fips start
Phase 3: DNS Resolution
- Enhance DNS responder to forward non-
.fipsqueries upstream - Configure VPN DNS to point to in-tunnel DNS server
- Test:
nslookup npub1...fipsresolves to fd00::... address - Test:
nslookup google.comstill resolves normally
Phase 4: End-to-End Testing
- Start fips node on a Linux machine with TUN enabled
- Start fips Android app on device/emulator
- Configure both to peer with each other (UDP transport)
- Verify: Android browser can load
http://npub1...fipsserved by Linux node - Verify: mesh routing works through intermediate hops
Phase 5: Polish and APK
- Add proper Android notification for foreground service
- Add configuration UI (peer addresses, identity management)
- Add connection status display (peer count, tree state)
- Build signed release APK
- Test on physical Android device
Key Technical Decisions
Why VpnService (not root/iptables)?
- No root required — VpnService is a standard Android API available to all apps
- Works on all Android devices — no custom ROM or rooting needed
- OS-level integration — Android shows VPN status in notification bar, handles reconnection
- DNS interception built-in — VpnService.Builder.addDnsServer() handles DNS routing
Why shared library (not separate process)?
- Single process — simpler lifecycle management, no IPC needed
- Direct fd passing — VpnService fd can be passed directly to Rust via JNI
- Lower latency — no serialization overhead for packet I/O
- Standard pattern — this is how WireGuard, Tailscale, and other VPN apps work on Android
Why not use the tun crate on Android?
- The
tuncrate callsioctl(TUNSETIFF)which requiresCAP_NET_ADMIN— not available to unprivileged Android apps - Android's
VpnServiceis the only way to create a TUN interface without root - The VpnService provides a raw fd that behaves identically to a TUN fd — we just need to read/write on it
Dependencies Summary
New Rust Dependencies (Android only)
| Crate | Purpose |
|---|---|
jni |
JNI bindings for Kotlin ↔ Rust calls |
android_logger |
Route tracing output to Android logcat |
Android/Gradle Dependencies
| Dependency | Purpose |
|---|---|
| Android SDK 34+ | Target API level |
| Android NDK r26+ | Rust cross-compilation toolchain |
| Kotlin 2.0+ | App language |
| Gradle 8.x | Build system |
| cargo-ndk | Simplified Rust → Android cross-compilation |
Risk Assessment
| Risk | Mitigation |
|---|---|
| Android kills VPN service in background | Use foreground service with persistent notification; Android protects VPN services from aggressive battery optimization |
| DNS forwarding for non-.fips domains adds latency | Use fast upstream resolver (1.1.1.1); cache aggressively; only intercept when VPN is active |
| Large APK size from Rust binary | Strip symbols, use LTO, target only arm64-v8a (covers 99%+ of modern devices) |
| tokio runtime on Android | Well-tested; WireGuard-rs, Tailscale use tokio on Android successfully |
| VPN permission prompt scares users | Clear explanation in UI before requesting; this is standard for all VPN apps |