Files
fips_playground/plans/fips-android-vpn-app.md
T
2026-05-02 18:08:59 -04:00

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

  1. Compile fips as a shared library — build the Rust fips crate as a cdylib targeting aarch64-linux-android (and x86_64-linux-android for emulators), exposing a C-ABI FFI layer
  2. Android VpnService integration — use Android's VpnService API to create a TUN interface that the OS routes all traffic through, replacing the Linux-specific tun crate usage
  3. DNS interception — configure the VPN to use the fips DNS resolver for .fips domains, so the Android system resolver forwards queries to the in-process fips DNS responder
  4. 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.Builder to 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 .fips queries directly
  • Non-.fips queries are forwarded to an upstream resolver (e.g., 1.1.1.1)

Option B — Split DNS:

  • Only route .fips queries 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-gateway binary (requires nftables, Linux-only)
  • fipstop TUI binary (terminal UI, not useful on Android)
  • fipsctl CLI 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 dependency
  • ratatui TUI 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.rs instead of tun.rs for TUN device handling
  • android_logger instead of tracing-subscriber stderr 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 android feature flag to Cargo.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 ndk builds cleanly

Phase 2: Minimal Android App Shell

  • Create fips-android/ Gradle project with Kotlin
  • Implement FipsVpnService with VpnService.Builder configuration
  • Implement NativeLib.kt JNI bindings
  • Implement MainActivity with connect/disconnect button
  • Wire up: button → VpnService → JNI → Rust fips start

Phase 3: DNS Resolution

  • Enhance DNS responder to forward non-.fips queries upstream
  • Configure VPN DNS to point to in-tunnel DNS server
  • Test: nslookup npub1...fips resolves to fd00::... address
  • Test: nslookup google.com still 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...fips served 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 tun crate calls ioctl(TUNSETIFF) which requires CAP_NET_ADMIN — not available to unprivileged Android apps
  • Android's VpnService is 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