An Android app that runs a FIPS mesh node as a Rust library
via UniFFI, routing IPv6 traffic through a VpnService TUN. Built on the
fips/ submodule, which implements the core mesh protocol (spanning tree,
Noise encryption, multi-transport: UDP/TCP/Tor/Bluetooth, IPv6 shim).
Prototype. End-to-end mesh routing has been verified on real hardware.
Working
- IPv6 mesh routing over an Android
VpnServiceTUN (verified: laptopping6and TCP to an Android handset'sfd00::/8address via a relay) .fipsname resolution forbionicgetaddrinfoclients — e.g. Termuxping6 zephyrus.fips,curl http://<npub>.fips:port/,dig @10.1.1.1- Application traffic from system apps that use the bionic resolver
- Start / stop VPN from the UI; persistent Nostr identity in
filesDir
Known limitations
- Browsers don't work yet. Chromium-based browsers (Brave, Chrome,
Vanadium) suppress AAAA queries on ULA-only networks, so they cannot
resolve
.fipsnames even with Secure DNS disabled. This is not a bug in this code — it's an upstream Chromium Happy Eyeballs heuristic. Planned fix: NAT46 — synthesize IPv4 addresses from a10.2.0.0/16pool and rewrite packets both ways in the TUN adapter. Not yet implemented. - Stop/start VPN cycle is flaky — sometimes requires an app restart.
- TCP over mesh is experimental; UDP is the primary tested transport.
A first-class feature: peers registered in the node config (by npub and
optional alias) are resolvable as .fips hostnames from any app on the
device that uses the system resolver.
alias.fips— e.g.zephyrus.fips,fips-test-node.fips<npub>.fips— the full bech32 public key as a hostname- Only
AAAArecords are served;.fipsis IPv6-only by design - Non-
.fipsqueries are refused (RCODE=REFUSED) at our resolver so the system falls through to the next DNS server (8.8.8.8) — we do not proxy the public internet - Implemented in
fips-mobile/src/dns_intercept.rs. DNS packets are pulled out of the TUN reader thread (UDP/53 to the virtual10.1.1.1address) rather than bound to a port — no privileged port binding needed.
The easiest path is Nix:
nix develop # provides Rust, cargo-ndk, NDK 26.1, JDK 17, Gradle, just, adbManual install, if you prefer:
- Rust stable toolchain +
cargo-ndk - Android NDK 26.1
- JDK 17, Android SDK, Gradle
just,adb
All commands are driven by just:
just build # cross-compile fips-mobile, generate Kotlin bindings, assemble debug APK
just device # build, install, launch on connected device
just test # autotest: start node, wait for peers, dump state, exit
just debug # build, install, launch, tail filtered logcat
just status # show last debug dump from logcat
just install # build + install, no launch
just clean # wipe Gradle + Cargo + jniLibs + generated bindingsjust build runs three steps in order:
cargo ndk -t arm64-v8a build -p fips-mobile --release- Run the local
uniffi-bindgenagainst the compiled.soto generate Kotlin stubs intoandroid/app/src/main/java - Copy
libfips_mobile.sointoandroid/app/src/main/jniLibs/arm64-v8a ./gradlew assembleDebug
Four layers:
fips/(git submodule) — core mesh protocol. Spanning tree coordination, Noise-encrypted sessions, multi-transport (UDP / TCP / Tor / Bluetooth), IPv6 shim over the mesh address space.fips-mobile/— Rust crate that exposesFipsMobileNodevia UniFFI. Owns a tokio runtime, spawns the node'srun_rx_loop, manages TUN reader/writer threads, and runs the.fipsDNS interceptor.uniffi-bindgen/— local fork ofuniffi-bindgenthat generates Kotlin bindings from the compiled.so.android/— Kotlin / Jetpack Compose app.FipsTunServicemanages theVpnServicelifecycle;FipsViewModelpolls node state over the UniFFI control channel;StatusScreenis the dashboard (peers, transports, own npub + IPv6, start/stop).
- The TUN file descriptor is established by
FipsTunServicebefore the Rust node starts — the node does not create the VPN interface. Ownership of the fd transfers into Rust via UniFFI and is closed onstop_tun(). - The app is self-excluded from its own VPN via
addDisallowedApplication(packageName)so the node's transport sockets use the real network. This removes the need toprotect()individual sockets. - DNS interception happens inside the TUN reader thread rather than on a listening socket — Android won't let an unprivileged app bind port 53.
- Identity is a Nostr keypair (nsec) persisted to
filesDir. The device's FIPS IPv6 address is derived deterministically from the public key, so it's stable across restarts.
android/ Kotlin / Compose app
fips/ core protocol (git submodule)
fips-mobile/ Rust UniFFI wrapper (TUN + DNS)
uniffi-bindgen/ Kotlin codegen fork
justfile build / run / test recipes
flake.nix Nix dev shell
Cargo.toml workspace root
- Core protocol:
fips/README.md - Design documents:
fips/docs/design/(transport layer, mesh operation, spanning tree, wire formats, IPv6 adapter, DNS, session layer, configuration)
Most of the code in this repo — the fips-mobile/ Rust wrapper, the
android/ Kotlin / Compose app, and the build glue in justfile — was
generated by Claude Opus 4.6 (Anthropic) under human direction, with
minimal human code review. The core fips/ submodule is upstream
work and is not AI-generated.
Treat this project as an experimental prototype: it builds and runs, end-to-end mesh routing over TUN has been verified on real hardware, but the code has not had line-by-line human audit. If you depend on it, read it first. Bugs, odd patterns, and over-engineering are to be expected. PRs and issue reports welcome.