first
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
fips/
|
||||
37
deploy/raspberry-pi/fips.yaml
Normal file
37
deploy/raspberry-pi/fips.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
# Pi Zero 2W — leaf endpoint configuration
|
||||
node:
|
||||
identity:
|
||||
persistent: true
|
||||
leaf_only: true
|
||||
cache:
|
||||
coord_size: 1000
|
||||
identity_size: 500
|
||||
limits:
|
||||
max_peers: 8
|
||||
max_connections: 16
|
||||
max_pending_inbound: 50
|
||||
|
||||
tun:
|
||||
enabled: true
|
||||
name: fips0
|
||||
mtu: 1280
|
||||
|
||||
dns:
|
||||
enabled: true
|
||||
bind_addr: "127.0.0.1"
|
||||
port: 5354
|
||||
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:2121"
|
||||
tcp:
|
||||
bind_addr: "0.0.0.0:8443"
|
||||
|
||||
peers: []
|
||||
# Add your upstream peer:
|
||||
# - npub: "npub1..."
|
||||
# alias: "upstream"
|
||||
# addresses:
|
||||
# - transport: udp
|
||||
# addr: "x.x.x.x:2121"
|
||||
# connect_policy: auto_connect
|
||||
134
fips_debug.md
Normal file
134
fips_debug.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# FIPS Debug Notes: `.fips` HTTP reachability incident
|
||||
|
||||
## Summary
|
||||
|
||||
We investigated why this URL failed locally but worked from the remote host:
|
||||
|
||||
- `http://npub1crpldvy49ef8z34wlacwujnfudy4nd7k96aqdx5wgn6ckztz7z8q9t59ud.fips/index.html`
|
||||
|
||||
Outcome:
|
||||
|
||||
1. Initial local failure was due to a stale `fd00::/8` route pointing to an unreachable next-hop.
|
||||
2. After route recovery, TCP connected but larger HTTP responses stalled.
|
||||
3. Root cause for the stall was path/MTU behavior (large payload transfer blackholing/retransmission).
|
||||
4. Setting local UDP transport MTU to `1280` resolved the issue; full page download succeeded.
|
||||
|
||||
---
|
||||
|
||||
## What we observed
|
||||
|
||||
### 1) DNS was correct
|
||||
|
||||
- `.fips` name resolved to the expected IPv6 address.
|
||||
- Name resolution was **not** the issue.
|
||||
|
||||
### 2) Endpoints were alive
|
||||
|
||||
- On remote host (`ubuntu@laantungir.net`), the same URL returned `HTTP/1.1 200 OK` with full content.
|
||||
- Nginx was listening and serving properly.
|
||||
|
||||
### 3) Local routing was initially broken
|
||||
|
||||
- Local route had `fd00::/8` via a link-local next-hop that was unreachable.
|
||||
- Neighbor state for that gateway showed `FAILED`.
|
||||
- This produced local `No route to host` errors.
|
||||
|
||||
### 4) After local route fix, only large responses failed
|
||||
|
||||
- Local curl connected to remote `:80` and sent GET.
|
||||
- Small responses worked:
|
||||
- direct IPv6 host request without vhost Host header got `410` small body,
|
||||
- ranged request got `206` with a small payload.
|
||||
- Full `index.html` (10KB+) timed out with `0 bytes received` locally.
|
||||
- Remote socket stats indicated retransmissions and congestion-window collapse, consistent with MTU/path fragmentation loss behavior.
|
||||
|
||||
### 5) Mitigation validated
|
||||
|
||||
- Local config changed:
|
||||
|
||||
```yaml
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: 0.0.0.0:2121
|
||||
mtu: 1280
|
||||
```
|
||||
|
||||
- Restarted local `fips.service`.
|
||||
- Retested URL: `HTTP 200`, full `10156` bytes downloaded.
|
||||
|
||||
---
|
||||
|
||||
## Lessons learned
|
||||
|
||||
1. **Treat `.fips` failures as a pipeline problem**
|
||||
- Validate in this order: DNS → route → reachability → app response size behavior.
|
||||
|
||||
2. **“Connect succeeds” does not prove path health**
|
||||
- A successful TCP handshake can still hide payload delivery failures.
|
||||
- Always test both tiny and realistic/full payload responses.
|
||||
|
||||
3. **Stale `fd00::/8` routes can silently break TUN startup**
|
||||
- We saw TUN init warnings when route insertion conflicted with existing state.
|
||||
- Route hygiene matters before/after service restarts.
|
||||
|
||||
4. **MTU defaults are not always safe on every real path**
|
||||
- Especially over mixed internet routes and overlays.
|
||||
- `1280` is often the practical “known-good” baseline for IPv6 paths.
|
||||
|
||||
5. **Use control-plane telemetry early**
|
||||
- `fipsctl show status|peers|sessions|routing` quickly distinguishes control-plane up/down from data-plane degradation.
|
||||
|
||||
6. **Host-header/vhost behavior can mislead debugging**
|
||||
- Explicit `Host:` header to the same IPv6 returned 200 remotely.
|
||||
- Direct IP without host header returned a different vhost response (410).
|
||||
- This is expected and should be factored into tests.
|
||||
|
||||
---
|
||||
|
||||
## Recommended runbook for future incidents
|
||||
|
||||
1. **Reproduce quickly**
|
||||
- `curl -v --max-time 10 http://<npub>.fips/index.html`
|
||||
|
||||
2. **Check resolution and route**
|
||||
- `getent ahosts <npub>.fips`
|
||||
- `ip -6 route get <resolved_ipv6>`
|
||||
- `ip -6 neigh show`
|
||||
|
||||
3. **Check FIPS health**
|
||||
- `sudo /usr/local/bin/fipsctl show status`
|
||||
- `sudo /usr/local/bin/fipsctl show peers`
|
||||
- `sudo /usr/local/bin/fipsctl show sessions`
|
||||
|
||||
4. **Differentiate small vs large payload behavior**
|
||||
- Small: direct IP / tiny endpoint / range request
|
||||
- Large: full page/object transfer
|
||||
|
||||
5. **If large payloads fail but small succeed, test MTU mitigation**
|
||||
- Set `transports.udp.mtu: 1280` locally
|
||||
- Restart `fips.service`
|
||||
- Retest full payload
|
||||
|
||||
6. **Persist fix and monitor**
|
||||
- Keep 1280 unless measured path supports higher values reliably.
|
||||
- Track retransmission symptoms and transfer success over time.
|
||||
|
||||
---
|
||||
|
||||
## Config change made during this debug
|
||||
|
||||
Local `/etc/fips/fips.yaml` was updated to include:
|
||||
|
||||
```yaml
|
||||
transports:
|
||||
udp:
|
||||
mtu: 1280
|
||||
```
|
||||
|
||||
A backup of the prior local file was created before modification.
|
||||
|
||||
---
|
||||
|
||||
## Final status
|
||||
|
||||
The target `.fips` web page is reachable from this local node after lowering local UDP MTU to `1280` and restarting `fips.service`.
|
||||
11
fips_playground.code-workspace
Normal file
11
fips_playground.code-workspace
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../tab_5/Tab5NostrClient"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
396
plans/fips-android-vpn-app.md
Normal file
396
plans/fips-android-vpn-app.md
Normal file
@@ -0,0 +1,396 @@
|
||||
# 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
|
||||
|
||||
```mermaid
|
||||
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
|
||||
|
||||
```mermaid
|
||||
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`
|
||||
|
||||
```rust
|
||||
// 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:**
|
||||
```toml
|
||||
[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)
|
||||
|
||||
```kotlin
|
||||
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`](fips/src/upper/dns.rs:1); 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:**
|
||||
```xml
|
||||
<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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```toml
|
||||
[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:
|
||||
|
||||
```toml
|
||||
[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 |
|
||||
386
plans/fips-esp32-minimal-port.md
Normal file
386
plans/fips-esp32-minimal-port.md
Normal file
@@ -0,0 +1,386 @@
|
||||
# FIPS Minimal C Port — Architecture Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Create a new `c_fips` repository containing a minimal FIPS mesh protocol implementation in C, built on top of `nostr_core_lib` for crypto primitives. Develop and test on Linux first, then target ESP32.
|
||||
|
||||
The end result: a C program that can join a FIPS mesh network, authenticate with peers, and send/receive end-to-end encrypted data — wire-compatible with the Rust FIPS implementation.
|
||||
|
||||
## Strategy
|
||||
|
||||
1. **New repo `c_fips`** — standalone project, pulls in `nostr_core_lib` as a dependency
|
||||
2. **Extend `nostr_core_lib`** — add Poly1305 MAC to complete ChaCha20-Poly1305 AEAD (the one missing crypto primitive)
|
||||
3. **Linux first** — develop and test everything on Linux using standard sockets, validate interop against the Rust FIPS node
|
||||
4. **ESP32 second** — once Linux interop works, add ESP-IDF platform layer and build for ESP32
|
||||
|
||||
## What nostr_core_lib Already Provides
|
||||
|
||||
| Primitive | nostr_core_lib Function | FIPS Usage |
|
||||
|-----------|------------------------|------------|
|
||||
| secp256k1 keygen | `nostr_ec_public_key_from_private_key()` | Identity generation |
|
||||
| secp256k1 ECDH | `ecdh_shared_secret()` | Noise DH operations |
|
||||
| secp256k1 Schnorr sign/verify | `nostr_schnorr_sign()` | Tree declaration signing |
|
||||
| SHA-256 (streaming) | `nostr_sha256_init/update/final()` | Noise handshake hash, NodeAddr derivation, Bloom hashing |
|
||||
| HMAC-SHA256 | `nostr_hmac_sha256()` | Noise key derivation |
|
||||
| HKDF extract+expand | `nostr_hkdf_extract()`, `nostr_hkdf_expand()` | Noise MixKey operation |
|
||||
| ChaCha20 stream cipher | `chacha20_encrypt()` | Part of AEAD construction |
|
||||
| Bech32 encode/decode | `nostr_key_to_bech32()`, `nostr_decode_npub/nsec()` | npub/nsec display |
|
||||
| Hex encode/decode | `nostr_bytes_to_hex()`, `nostr_hex_to_bytes()` | Debug output |
|
||||
| Platform CSPRNG | `nostr_platform_random()` | Ephemeral key generation, nonces |
|
||||
| cJSON | `cJSON.c` | Config parsing (optional) |
|
||||
|
||||
## What We Need to Add to nostr_core_lib
|
||||
|
||||
### Poly1305 MAC + ChaCha20-Poly1305 AEAD
|
||||
|
||||
`nostr_core_lib` has ChaCha20 but **not** Poly1305. FIPS needs ChaCha20-Poly1305 AEAD (RFC 8439) for all Noise encryption.
|
||||
|
||||
**Approach:** Add a pure-C Poly1305 implementation to `nostr_core_lib/nostr_core/crypto/`, then build the AEAD construction on top. This keeps everything self-contained and portable (no external dependency needed on Linux; on ESP32, could optionally use mbedtls hardware acceleration).
|
||||
|
||||
New files in nostr_core_lib:
|
||||
```
|
||||
nostr_core/crypto/nostr_poly1305.c — Poly1305 MAC (RFC 8439 Section 2.5)
|
||||
nostr_core/crypto/nostr_chacha20poly1305.c — AEAD construction (RFC 8439 Section 2.8)
|
||||
```
|
||||
|
||||
New API:
|
||||
```c
|
||||
// Poly1305 one-shot MAC
|
||||
int nostr_poly1305_mac(const unsigned char *key, const unsigned char *msg,
|
||||
size_t msg_len, unsigned char *tag);
|
||||
|
||||
// ChaCha20-Poly1305 AEAD encrypt
|
||||
int nostr_chacha20poly1305_encrypt(const unsigned char key[32],
|
||||
const unsigned char nonce[12],
|
||||
const unsigned char *aad, size_t aad_len,
|
||||
const unsigned char *plaintext, size_t pt_len,
|
||||
unsigned char *ciphertext,
|
||||
unsigned char tag[16]);
|
||||
|
||||
// ChaCha20-Poly1305 AEAD decrypt
|
||||
int nostr_chacha20poly1305_decrypt(const unsigned char key[32],
|
||||
const unsigned char nonce[12],
|
||||
const unsigned char *aad, size_t aad_len,
|
||||
const unsigned char *ciphertext, size_t ct_len,
|
||||
const unsigned char tag[16],
|
||||
unsigned char *plaintext);
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Module Dependency Map
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "nostr_core_lib - existing"
|
||||
SECP["secp256k1<br/>keygen, ECDH, sign/verify"]
|
||||
SHA["SHA-256<br/>streaming + single-shot"]
|
||||
HMAC["HMAC-SHA256"]
|
||||
HKDF["HKDF extract+expand"]
|
||||
CHACHA["ChaCha20 stream cipher"]
|
||||
BECH["Bech32 npub/nsec"]
|
||||
PLATFORM["Platform abstraction<br/>CSPRNG"]
|
||||
end
|
||||
|
||||
subgraph "nostr_core_lib - new"
|
||||
POLY["Poly1305 MAC"]
|
||||
AEAD["ChaCha20-Poly1305 AEAD"]
|
||||
end
|
||||
|
||||
subgraph "c_fips - Noise Protocol"
|
||||
NOISE_IK["Noise IK Handshake<br/>link-layer auth"]
|
||||
NOISE_XK["Noise XK Handshake<br/>e2e sessions"]
|
||||
NOISE_SESSION["Noise Transport Session<br/>encrypt/decrypt + replay"]
|
||||
REPLAY["Replay Window<br/>2048-bit sliding bitmap"]
|
||||
end
|
||||
|
||||
subgraph "c_fips - Wire Protocol"
|
||||
WIRE["FMP Wire Format<br/>4-byte prefix parser"]
|
||||
LINK_MSG["Link Messages<br/>TreeAnnounce, FilterAnnounce, etc."]
|
||||
SESSION_MSG["Session Messages<br/>SessionSetup, SessionAck, DataPacket"]
|
||||
end
|
||||
|
||||
subgraph "c_fips - Routing"
|
||||
IDENTITY["FIPS Identity<br/>NodeAddr from pubkey"]
|
||||
TREE["Spanning Tree<br/>coordinates, parent selection"]
|
||||
BLOOM["Bloom Filter<br/>1KB bit array, k=5"]
|
||||
COORD["Coordinate Cache<br/>fixed-size array"]
|
||||
end
|
||||
|
||||
subgraph "c_fips - Node"
|
||||
NODE["FIPS Node<br/>main event loop"]
|
||||
PEER["Peer Table<br/>fixed-capacity"]
|
||||
UDP["UDP Transport<br/>POSIX sockets / lwIP"]
|
||||
end
|
||||
|
||||
AEAD --> CHACHA
|
||||
AEAD --> POLY
|
||||
NOISE_IK --> SECP
|
||||
NOISE_IK --> SHA
|
||||
NOISE_IK --> HKDF
|
||||
NOISE_IK --> AEAD
|
||||
NOISE_XK --> SECP
|
||||
NOISE_XK --> SHA
|
||||
NOISE_XK --> HKDF
|
||||
NOISE_XK --> AEAD
|
||||
NOISE_SESSION --> AEAD
|
||||
NOISE_SESSION --> REPLAY
|
||||
IDENTITY --> SECP
|
||||
IDENTITY --> SHA
|
||||
IDENTITY --> BECH
|
||||
BLOOM --> SHA
|
||||
NODE --> PEER
|
||||
NODE --> TREE
|
||||
NODE --> BLOOM
|
||||
NODE --> COORD
|
||||
NODE --> UDP
|
||||
PEER --> NOISE_SESSION
|
||||
PEER --> NOISE_IK
|
||||
NODE --> WIRE
|
||||
NODE --> LINK_MSG
|
||||
NODE --> SESSION_MSG
|
||||
UDP --> PLATFORM
|
||||
```
|
||||
|
||||
### Memory Budget (Leaf Node, 3 Peers)
|
||||
|
||||
| Component | RAM | Notes |
|
||||
|-----------|-----|-------|
|
||||
| Identity | ~96 B | Secret key + pubkey + node_addr |
|
||||
| Tree State | ~512 B | Declaration + parent + coordinates |
|
||||
| Per-Peer x3 (Noise + Bloom + Tree + MMP) | ~7 KB | ~2.3 KB each |
|
||||
| Coordinate Cache (32 entries) | ~5 KB | Fixed array, LRU eviction |
|
||||
| Packet Buffers (2x 1500B) | ~3 KB | RX + TX |
|
||||
| Handshake scratch | ~512 B | Temporary during handshake |
|
||||
| Stack + misc | ~4 KB | |
|
||||
| **Total RAM** | **~22 KB** | Fits easily in ESP32 520 KB SRAM |
|
||||
| **Code (flash)** | **~80-120 KB** | |
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
c_fips/
|
||||
├── README.md
|
||||
├── Makefile # Linux build
|
||||
├── CMakeLists.txt # ESP-IDF component build
|
||||
│
|
||||
├── nostr_core_lib/ # Git submodule or vendored copy
|
||||
│ └── ... # With Poly1305/AEAD additions
|
||||
│
|
||||
├── include/
|
||||
│ └── fips/
|
||||
│ ├── fips.h # Top-level public API
|
||||
│ ├── fips_types.h # Common types: NodeAddr, etc.
|
||||
│ ├── fips_identity.h # Identity generation/management
|
||||
│ ├── fips_noise.h # Noise protocol API
|
||||
│ ├── fips_wire.h # Wire format constants and parsers
|
||||
│ ├── fips_protocol.h # Protocol message types
|
||||
│ ├── fips_bloom.h # Bloom filter
|
||||
│ ├── fips_tree.h # Spanning tree
|
||||
│ ├── fips_node.h # Node state and event loop
|
||||
│ └── fips_platform.h # Platform abstraction
|
||||
│
|
||||
├── src/
|
||||
│ ├── identity/
|
||||
│ │ └── fips_identity.c # NodeAddr derivation, key management
|
||||
│ ├── noise/
|
||||
│ │ ├── fips_noise_symmetric.c # SymmetricState: MixHash, MixKey
|
||||
│ │ ├── fips_noise_ik.c # Noise IK handshake (link layer)
|
||||
│ │ ├── fips_noise_xk.c # Noise XK handshake (session layer)
|
||||
│ │ ├── fips_noise_session.c # Transport encrypt/decrypt
|
||||
│ │ └── fips_noise_replay.c # 2048-bit sliding window
|
||||
│ ├── wire/
|
||||
│ │ └── fips_wire.c # FMP wire format parse/build
|
||||
│ ├── protocol/
|
||||
│ │ ├── fips_link_msg.c # Link-layer message encode/decode
|
||||
│ │ ├── fips_session_msg.c # Session-layer message encode/decode
|
||||
│ │ ├── fips_tree_msg.c # TreeAnnounce encode/decode
|
||||
│ │ ├── fips_filter_msg.c # FilterAnnounce encode/decode
|
||||
│ │ └── fips_discovery_msg.c # LookupRequest/Response
|
||||
│ ├── routing/
|
||||
│ │ ├── fips_bloom.c # Bloom filter operations
|
||||
│ │ ├── fips_tree_coord.c # Coordinate math, distance
|
||||
│ │ ├── fips_tree_state.c # Parent selection, declarations
|
||||
│ │ └── fips_coord_cache.c # Fixed-size coordinate cache
|
||||
│ ├── node/
|
||||
│ │ ├── fips_node.c # Node state + event loop
|
||||
│ │ ├── fips_peer.c # Peer table management
|
||||
│ │ ├── fips_node_handshake.c # Handshake handling
|
||||
│ │ ├── fips_node_tree.c # TreeAnnounce logic
|
||||
│ │ ├── fips_node_bloom.c # FilterAnnounce logic
|
||||
│ │ ├── fips_node_session.c # E2E session management
|
||||
│ │ └── fips_node_data.c # Application data send/receive
|
||||
│ ├── transport/
|
||||
│ │ ├── fips_udp_linux.c # Linux: POSIX UDP sockets
|
||||
│ │ └── fips_udp_esp32.c # ESP32: lwIP sockets
|
||||
│ └── platform/
|
||||
│ ├── fips_platform_linux.c # Linux: time, logging
|
||||
│ └── fips_platform_esp32.c # ESP32: time, logging
|
||||
│
|
||||
├── test/
|
||||
│ ├── test_aead.c # ChaCha20-Poly1305 test vectors (RFC 8439)
|
||||
│ ├── test_noise_ik.c # Noise IK handshake unit tests
|
||||
│ ├── test_noise_xk.c # Noise XK handshake unit tests
|
||||
│ ├── test_wire.c # Wire format round-trip tests
|
||||
│ ├── test_bloom.c # Bloom filter tests
|
||||
│ ├── test_tree.c # Tree coordinate/distance tests
|
||||
│ ├── test_identity.c # NodeAddr derivation tests
|
||||
│ └── test_interop.c # Connect to Rust FIPS node over UDP
|
||||
│
|
||||
├── examples/
|
||||
│ ├── linux_leaf_node.c # Complete Linux leaf node example
|
||||
│ └── esp32_leaf_node/ # ESP-IDF project
|
||||
│ ├── CMakeLists.txt
|
||||
│ └── main/
|
||||
│ ├── CMakeLists.txt
|
||||
│ └── app_main.c
|
||||
│
|
||||
└── tools/
|
||||
└── gen_test_vectors.sh # Script to generate test vectors from Rust FIPS
|
||||
```
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Crypto Foundation (Linux)
|
||||
|
||||
Add Poly1305 + AEAD to nostr_core_lib, then build the Noise protocol.
|
||||
|
||||
- [ ] Add `nostr_poly1305.c` to nostr_core_lib (RFC 8439 Section 2.5)
|
||||
- [ ] Add `nostr_chacha20poly1305.c` to nostr_core_lib (RFC 8439 Section 2.8)
|
||||
- [ ] Write AEAD test vectors from RFC 8439 Appendix A
|
||||
- [ ] Implement `fips_noise_symmetric.c` — SymmetricState (MixHash, MixKey, EncryptAndHash, DecryptAndHash)
|
||||
- [ ] Implement `fips_noise_ik.c` — Noise IK initiator and responder
|
||||
- [ ] Implement `fips_noise_session.c` — transport encrypt/decrypt with counter
|
||||
- [ ] Implement `fips_noise_replay.c` — 2048-bit sliding window
|
||||
- [ ] Write Noise IK unit tests (handshake completes, keys match, encrypt/decrypt works)
|
||||
- [ ] Generate test vectors from Rust FIPS node for cross-validation
|
||||
|
||||
### Phase 2: Wire Protocol + Identity (Linux)
|
||||
|
||||
Port the binary wire format and identity system.
|
||||
|
||||
- [ ] Implement `fips_identity.c` — NodeAddr derivation from pubkey (SHA-256 + fd prefix)
|
||||
- [ ] Implement `fips_wire.c` — common prefix, established frame header, handshake msg1/msg2
|
||||
- [ ] Implement `fips_tree_msg.c` — TreeAnnounce encode/decode
|
||||
- [ ] Implement `fips_filter_msg.c` — FilterAnnounce encode/decode
|
||||
- [ ] Implement `fips_link_msg.c` — SessionDatagram, Heartbeat, Disconnect
|
||||
- [ ] Implement `fips_session_msg.c` — SessionSetup, SessionAck, DataPacket
|
||||
- [ ] Implement `fips_discovery_msg.c` — LookupRequest, LookupResponse
|
||||
- [ ] Write wire format round-trip tests
|
||||
- [ ] Validate against captured packets from Rust FIPS node
|
||||
|
||||
### Phase 3: Routing Data Structures (Linux)
|
||||
|
||||
- [ ] Implement `fips_bloom.c` — 1KB Bloom filter with k=5 double hashing
|
||||
- [ ] Implement `fips_tree_coord.c` — TreeCoordinate, distance calculation, LCA
|
||||
- [ ] Implement `fips_tree_state.c` — parent selection, declaration creation/signing
|
||||
- [ ] Implement `fips_coord_cache.c` — fixed-size (32 entry) LRU cache
|
||||
- [ ] Write unit tests for each
|
||||
|
||||
### Phase 4: Node Core + Linux Interop (Linux)
|
||||
|
||||
Build the node, connect to a Rust FIPS node.
|
||||
|
||||
- [ ] Implement `fips_udp_linux.c` — POSIX UDP socket transport
|
||||
- [ ] Implement `fips_platform_linux.c` — monotonic time, logging, random
|
||||
- [ ] Implement `fips_peer.c` — fixed-capacity peer table (4 slots)
|
||||
- [ ] Implement `fips_node.c` — node state, poll-based event loop
|
||||
- [ ] Implement `fips_node_handshake.c` — Noise IK handshake handling
|
||||
- [ ] Implement `fips_node_tree.c` — TreeAnnounce send/receive
|
||||
- [ ] Implement `fips_node_bloom.c` — FilterAnnounce send/receive
|
||||
- [ ] Implement `fips_node_session.c` — Noise XK e2e session setup
|
||||
- [ ] Implement `fips_node_data.c` — application data send/receive
|
||||
- [ ] **Milestone: C leaf node connects to Rust FIPS node, completes handshake**
|
||||
- [ ] **Milestone: C node joins spanning tree, exchanges bloom filters**
|
||||
- [ ] **Milestone: C node sends/receives encrypted data to remote Rust node**
|
||||
|
||||
### Phase 5: ESP32 Port
|
||||
|
||||
- [ ] Implement `fips_udp_esp32.c` — lwIP UDP sockets
|
||||
- [ ] Implement `fips_platform_esp32.c` — ESP-IDF time, logging
|
||||
- [ ] Create ESP-IDF CMakeLists.txt (component registration)
|
||||
- [ ] Create `esp32_leaf_node` example project (WiFi init + FIPS node)
|
||||
- [ ] Test ESP32 connecting to Rust FIPS node over WiFi
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### No Dynamic Allocation in Protocol Core
|
||||
|
||||
All state is either:
|
||||
- **Statically sized** (peer table = 4 slots, coord cache = 32 entries, bloom = 1024 bytes)
|
||||
- **Caller-provided buffers** (all encode/decode functions take output buffer + size)
|
||||
- **Stack-allocated scratch** (handshake temporary state)
|
||||
|
||||
This makes the code predictable for embedded and eliminates memory leak concerns.
|
||||
|
||||
### Poll-Based Event Loop (No Async Runtime)
|
||||
|
||||
Replace Tokio with a simple cooperative loop:
|
||||
|
||||
```c
|
||||
void fips_node_run(fips_node_t *node) {
|
||||
while (node->running) {
|
||||
// 1. Poll UDP for incoming packets (non-blocking)
|
||||
fips_node_poll_rx(node);
|
||||
|
||||
// 2. Process pending handshakes
|
||||
fips_node_process_handshakes(node);
|
||||
|
||||
// 3. Periodic tick (1s): tree announce, filter update, keepalive
|
||||
uint64_t now = fips_time_ms();
|
||||
if (now - node->last_tick_ms >= 1000) {
|
||||
fips_node_tick(node);
|
||||
node->last_tick_ms = now;
|
||||
}
|
||||
|
||||
// 4. Process application TX queue
|
||||
fips_node_poll_tx(node);
|
||||
|
||||
// 5. Brief sleep (Linux: usleep; ESP32: vTaskDelay)
|
||||
fips_platform_sleep_ms(10);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Platform Abstraction
|
||||
|
||||
Same pattern as nostr_core_lib — a header with platform-specific `.c` files:
|
||||
|
||||
```c
|
||||
// fips_platform.h
|
||||
uint64_t fips_time_ms(void); // Monotonic milliseconds
|
||||
void fips_platform_sleep_ms(uint32_t ms);
|
||||
void fips_log(int level, const char *fmt, ...);
|
||||
```
|
||||
|
||||
### Interop Testing Strategy
|
||||
|
||||
The Rust FIPS node is the reference. Testing approach:
|
||||
|
||||
1. **Generate test vectors** from Rust node (keypair → expected NodeAddr, handshake bytes, etc.)
|
||||
2. **Packet capture** — run Rust nodes, capture UDP traffic, replay against C parser
|
||||
3. **Live interop** — C leaf node connects to Rust node, validates full protocol flow
|
||||
4. **Docker test** — Rust FIPS node in Docker, C node on host, automated test
|
||||
|
||||
## Crypto Primitive Mapping
|
||||
|
||||
| FIPS Rust Code | Rust Crate | C Function (nostr_core_lib) |
|
||||
|----------------|------------|----------------------------|
|
||||
| `Sha256::new()` / `update()` / `finalize()` | `sha2` | `nostr_sha256_init/update/final()` |
|
||||
| `Hkdf::<Sha256>::new(salt, ikm)` / `expand()` | `hkdf` | `nostr_hkdf_extract()` + `nostr_hkdf_expand()` |
|
||||
| `ChaCha20Poly1305::encrypt(nonce, payload)` | `chacha20poly1305` | `nostr_chacha20poly1305_encrypt()` **NEW** |
|
||||
| `shared_secret_point(pub, sec)` | `secp256k1` | `ecdh_shared_secret()` |
|
||||
| `Keypair::new(secp, &secret_key)` | `secp256k1` | `nostr_ec_public_key_from_private_key()` |
|
||||
| `rand::thread_rng().gen()` | `rand` | `nostr_platform_random()` |
|
||||
| `bech32::encode("npub", ...)` | `bech32` | `nostr_key_to_bech32()` |
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
The FIPS design docs in `docs/design/` are strong enough to implement from:
|
||||
|
||||
- **Wire formats**: [`fips-wire-formats.md`](../docs/design/fips-wire-formats.md) — byte-level spec with field tables and SVG diagrams
|
||||
- **Mesh layer**: [`fips-mesh-layer.md`](../docs/design/fips-mesh-layer.md) — peer auth, link encryption, forwarding
|
||||
- **Session layer**: [`fips-session-layer.md`](../docs/design/fips-session-layer.md) — e2e encryption, Noise XK
|
||||
- **Mesh operation**: [`fips-mesh-operation.md`](../docs/design/fips-mesh-operation.md) — routing decisions, discovery, error recovery
|
||||
- **Spanning tree**: [`fips-spanning-tree.md`](../docs/design/fips-spanning-tree.md) — algorithms, parent selection
|
||||
- **Bloom filters**: [`fips-bloom-filters.md`](../docs/design/fips-bloom-filters.md) — parameters, propagation rules
|
||||
539
plans/fips-esp32-noise-bridge.md
Normal file
539
plans/fips-esp32-noise-bridge.md
Normal file
@@ -0,0 +1,539 @@
|
||||
# FIPS ESP32 Noise Bridge — Architecture Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Enable ESP32 devices to be first-class addressable endpoints on the FIPS mesh
|
||||
— each with its own npub and true end-to-end Noise XK encryption — without
|
||||
running the full FIPS mesh protocol stack. A **bridge daemon** on a Linux host
|
||||
handles all mesh protocol complexity; the ESP32 only implements the Noise XK
|
||||
session cryptography.
|
||||
|
||||
The end result: an ESP32 light switch with npub `npub1kitchen...` is directly
|
||||
reachable from anywhere on the FIPS mesh. Traffic is end-to-end encrypted
|
||||
between the remote peer and the ESP32. The bridge daemon never sees plaintext.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The full `c_fips` port (spanning tree, bloom filters, wire format, routing,
|
||||
Noise IK link layer, Noise XK session layer) is complex to implement on
|
||||
ESP32 — roughly 20 C source files and 5,000–8,000 lines of code. Meanwhile,
|
||||
the ESP32 already has all the cryptographic primitives needed for Noise XK:
|
||||
|
||||
- secp256k1 keygen, ECDH, sign/verify
|
||||
- SHA-256 (streaming)
|
||||
- HMAC-SHA256 / HKDF
|
||||
- ChaCha20-Poly1305 AEAD
|
||||
|
||||
These are the same primitives used by NIP-44. The insight: if the ESP32 can
|
||||
do the session-layer crypto, a bridge daemon can handle everything else.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "ESP32 Device"
|
||||
APP["Application<br/>e.g. light controller"]
|
||||
NOISE_XK["Noise XK<br/>Responder + Session<br/>encrypt/decrypt"]
|
||||
TRANSPORT_E["WiFi UDP Client"]
|
||||
end
|
||||
|
||||
subgraph "Linux Host"
|
||||
TRANSPORT_B["WiFi UDP Server"]
|
||||
BRIDGE["Bridge Daemon<br/>fips-bridge"]
|
||||
FIPSD["fipsd<br/>Full mesh node"]
|
||||
CONTROL["Control Socket"]
|
||||
TUN["fips0 TUN"]
|
||||
end
|
||||
|
||||
subgraph "FIPS Mesh"
|
||||
REMOTE["Remote Peer<br/>e.g. office laptop"]
|
||||
end
|
||||
|
||||
APP <--> NOISE_XK
|
||||
NOISE_XK <--> TRANSPORT_E
|
||||
TRANSPORT_E <-->|"WiFi UDP<br/>Noise handshake bytes<br/>+ encrypted data"| TRANSPORT_B
|
||||
TRANSPORT_B <--> BRIDGE
|
||||
BRIDGE <-->|"leaf_dependent registration<br/>+ identity cache priming"| CONTROL
|
||||
BRIDGE <-->|"SessionDatagram relay<br/>via fips0 TUN"| TUN
|
||||
TUN <--> FIPSD
|
||||
FIPSD <--> REMOTE
|
||||
```
|
||||
|
||||
### Component Responsibilities
|
||||
|
||||
**ESP32 (Noise XK endpoint):**
|
||||
- Holds its own nsec/npub keypair
|
||||
- Implements Noise XK responder (process msg1, generate msg2, process msg3)
|
||||
- Implements Noise XK initiator (for ESP32-initiated sessions)
|
||||
- Encrypts/decrypts data with session keys
|
||||
- Runs application logic (GPIO, sensors, etc.)
|
||||
|
||||
**Bridge daemon (`fips-bridge`, new Linux binary):**
|
||||
- Manages ESP32 device registry (npub → WiFi address mapping)
|
||||
- Registers each ESP32 as a leaf dependent in fipsd (bloom filter advertising)
|
||||
- Primes fipsd identity cache with ESP32 npub → NodeAddr → IPv6 mappings
|
||||
- Intercepts mesh traffic for ESP32 addresses on fips0
|
||||
- Relays Noise XK handshake messages between fipsd and ESP32
|
||||
- Relays encrypted session data (opaque ciphertext) between fipsd and ESP32
|
||||
- Handles all FIPS wire format encoding/decoding
|
||||
- Never possesses ESP32 private keys; never sees plaintext data
|
||||
|
||||
**fipsd (existing, with minor modifications):**
|
||||
- Advertises ESP32 node_addrs in bloom filters via leaf_dependents
|
||||
- Routes mesh traffic toward ESP32 destinations
|
||||
- Needs modification: treat leaf dependent destinations as local delivery
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Inbound: Remote Peer → ESP32
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant REMOTE as Remote Peer
|
||||
participant MESH as FIPS Mesh
|
||||
participant FIPSD as fipsd
|
||||
participant BRIDGE as Bridge Daemon
|
||||
participant ESP as ESP32
|
||||
|
||||
Note over REMOTE: Wants to reach npub1kitchen
|
||||
|
||||
rect rgb(230, 245, 255)
|
||||
Note right of REMOTE: Session Handshake
|
||||
REMOTE->>MESH: SessionSetup (XK msg1)<br/>dest: node_addr of npub1kitchen
|
||||
MESH->>FIPSD: Bloom-guided routing to home node
|
||||
FIPSD->>FIPSD: dest is leaf_dependent → local delivery
|
||||
FIPSD->>BRIDGE: SessionDatagram on fips0
|
||||
BRIDGE->>BRIDGE: Extract XK msg1 payload
|
||||
BRIDGE->>ESP: WiFi UDP: msg1 bytes
|
||||
ESP->>ESP: Process msg1 (ECDH, MixKey, etc.)
|
||||
ESP->>ESP: Generate msg2
|
||||
ESP->>BRIDGE: WiFi UDP: msg2 bytes
|
||||
BRIDGE->>FIPSD: SessionDatagram (SessionAck) on fips0
|
||||
FIPSD->>MESH: Route back to remote
|
||||
MESH->>REMOTE: SessionAck (XK msg2)
|
||||
REMOTE->>MESH: SessionMsg3 (XK msg3)
|
||||
MESH->>FIPSD->>BRIDGE->>ESP: msg3 bytes
|
||||
ESP->>ESP: Process msg3 → session established
|
||||
end
|
||||
|
||||
rect rgb(230, 255, 230)
|
||||
Note right of REMOTE: Encrypted Data
|
||||
REMOTE->>MESH: Encrypted datagram (session key)
|
||||
MESH->>FIPSD: Route to home
|
||||
FIPSD->>BRIDGE: Ciphertext on fips0
|
||||
BRIDGE->>ESP: WiFi UDP: ciphertext
|
||||
ESP->>ESP: Decrypt with recv_key
|
||||
ESP->>ESP: Execute command (light ON)
|
||||
ESP->>ESP: Encrypt response with send_key
|
||||
ESP->>BRIDGE: WiFi UDP: ciphertext
|
||||
BRIDGE->>FIPSD: On fips0
|
||||
FIPSD->>MESH: Route to remote
|
||||
MESH->>REMOTE: Encrypted response
|
||||
end
|
||||
```
|
||||
|
||||
### Outbound: ESP32 → Remote Peer
|
||||
|
||||
The ESP32 can also initiate sessions (e.g., push sensor data). The bridge
|
||||
daemon handles the routing; the ESP32 runs the Noise XK initiator side.
|
||||
|
||||
## Changes Required to fipsd
|
||||
|
||||
### 1. Leaf Dependent Local Delivery
|
||||
|
||||
Currently, [`handle_session_datagram()`](../src/node/handlers/forwarding.rs:61)
|
||||
only delivers locally when `dest_addr == self.node_addr()`. Packets for leaf
|
||||
dependents fall through to `find_next_hop()`, which has no route (the
|
||||
destination isn't a peer, and bloom filters won't match due to split-horizon).
|
||||
|
||||
**Change:** Add a leaf dependent check to the local delivery path:
|
||||
|
||||
```rust
|
||||
// Local delivery: self OR leaf dependent
|
||||
if datagram.dest_addr == *self.node_addr()
|
||||
|| self.bloom_state.leaf_dependents().contains(&datagram.dest_addr)
|
||||
{
|
||||
self.handle_session_payload(...).await;
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
This delivers the SessionDatagram to the session layer, which passes it to
|
||||
the IPv6 adapter, which writes it to fips0 — where the bridge daemon picks
|
||||
it up.
|
||||
|
||||
### 2. Control Socket: Leaf Dependent Management
|
||||
|
||||
Add control socket commands for the bridge daemon to register/unregister
|
||||
ESP32 devices:
|
||||
|
||||
```json
|
||||
{"command": "add_leaf_dependent", "params": {"npub": "npub1kitchen..."}}
|
||||
{"command": "remove_leaf_dependent", "params": {"npub": "npub1kitchen..."}}
|
||||
{"command": "list_leaf_dependents"}
|
||||
```
|
||||
|
||||
These commands call the existing [`add_leaf_dependent()`](../src/bloom/state.rs:85)
|
||||
and [`remove_leaf_dependent()`](../src/bloom/state.rs:90) methods, plus prime
|
||||
the identity cache with the npub → NodeAddr → IPv6 mapping.
|
||||
|
||||
### 3. Session Layer: Proxy Mode for Leaf Dependents
|
||||
|
||||
The session layer currently expects to complete Noise XK handshakes using the
|
||||
local node's keypair. For leaf dependent destinations, the session layer needs
|
||||
to **pass through** the SessionSetup/SessionAck/SessionMsg3 payloads without
|
||||
processing them — the ESP32 handles the crypto.
|
||||
|
||||
**Approach:** When a SessionSetup arrives for a leaf dependent address, instead
|
||||
of creating a local HandshakeState, fipsd delivers the raw SessionDatagram to
|
||||
fips0. The bridge daemon extracts the handshake payload, relays it to the
|
||||
ESP32, receives the response, and injects the reply SessionDatagram back into
|
||||
fips0. fipsd forwards it into the mesh.
|
||||
|
||||
This requires the session layer to recognize leaf dependent addresses and skip
|
||||
local handshake processing, instead treating the traffic as pass-through to
|
||||
the TUN interface.
|
||||
|
||||
## Bridge Daemon Design
|
||||
|
||||
### Configuration
|
||||
|
||||
```yaml
|
||||
# /etc/fips/bridge.yaml
|
||||
bridge:
|
||||
listen: "0.0.0.0:2122" # WiFi UDP listener for ESP32 devices
|
||||
fipsd_control: "/tmp/fips-control.sock" # fipsd control socket path
|
||||
|
||||
devices:
|
||||
- npub: "npub1kitchen..."
|
||||
alias: "kitchen-light"
|
||||
address: "192.168.1.50:2122" # ESP32 WiFi address
|
||||
transport: udp
|
||||
|
||||
- npub: "npub1bedroom..."
|
||||
alias: "bedroom-light"
|
||||
address: "192.168.1.51:2122"
|
||||
transport: udp
|
||||
```
|
||||
|
||||
### Startup Sequence
|
||||
|
||||
1. Parse config, load device registry
|
||||
2. Connect to fipsd control socket
|
||||
3. For each device:
|
||||
a. Compute NodeAddr from npub (SHA-256 truncation)
|
||||
b. Register as leaf dependent via control socket
|
||||
c. Prime identity cache via control socket
|
||||
4. Open WiFi UDP listener for ESP32 connections
|
||||
5. Open raw socket or TUN tap on fips0 for intercepting leaf dependent traffic
|
||||
6. Enter event loop: relay packets between fips0 and ESP32 devices
|
||||
|
||||
### Packet Relay Logic
|
||||
|
||||
The bridge daemon operates as a bidirectional relay:
|
||||
|
||||
**fips0 → ESP32:**
|
||||
1. Read IPv6 packet from fips0 destined for a leaf dependent address
|
||||
2. Look up destination in device registry → find ESP32 WiFi address
|
||||
3. Extract the FSP payload (strip IPv6 header, reverse the IPv6 adapter
|
||||
compression)
|
||||
4. Forward FSP payload to ESP32 via WiFi UDP
|
||||
|
||||
**ESP32 → fips0:**
|
||||
1. Receive FSP payload from ESP32 via WiFi UDP
|
||||
2. Look up source ESP32 in device registry → find its npub/NodeAddr
|
||||
3. Wrap in IPv6 header (apply IPv6 adapter compression)
|
||||
4. Write to fips0 TUN interface
|
||||
|
||||
The bridge handles the IPv6 ↔ FSP translation that the IPv6 adapter normally
|
||||
does, but for leaf dependent addresses instead of the local node.
|
||||
|
||||
### Alternative: Direct SessionDatagram Relay
|
||||
|
||||
Instead of going through the IPv6 adapter on fips0, the bridge could
|
||||
interact at the SessionDatagram level via a new fipsd control socket API:
|
||||
|
||||
```json
|
||||
{"command": "send_session_datagram", "params": {"src": "npub1kitchen...", "dest": "npub1remote...", "payload": "<base64>"}}
|
||||
```
|
||||
|
||||
And receive inbound datagrams via a subscription:
|
||||
|
||||
```json
|
||||
{"command": "subscribe_leaf_datagrams"}
|
||||
// fipsd streams: {"event": "session_datagram", "src": "npub1remote...", "dest": "npub1kitchen...", "payload": "<base64>"}
|
||||
```
|
||||
|
||||
This avoids the IPv6 adapter entirely and operates at the raw FSP level.
|
||||
It's cleaner but requires more fipsd changes.
|
||||
|
||||
## ESP32 Firmware
|
||||
|
||||
### What to Implement
|
||||
|
||||
The ESP32 needs only the Noise XK state machine and session transport:
|
||||
|
||||
```
|
||||
esp32_fips_endpoint/
|
||||
├── noise_symmetric.c — SymmetricState (MixHash, MixKey, EncryptAndHash, DecryptAndHash)
|
||||
├── noise_symmetric.h
|
||||
├── noise_xk.c — XK responder + initiator
|
||||
├── noise_xk.h
|
||||
├── noise_session.c — Transport encrypt/decrypt with counter
|
||||
├── noise_session.h
|
||||
├── noise_replay.c — Sliding window replay protection (2048-bit)
|
||||
├── noise_replay.h
|
||||
├── fips_identity.c — NodeAddr derivation from pubkey
|
||||
├── fips_identity.h
|
||||
├── bridge_transport.c — WiFi UDP communication with bridge
|
||||
├── bridge_transport.h
|
||||
├── bridge_protocol.c — Simple framing for bridge ↔ ESP32 messages
|
||||
├── bridge_protocol.h
|
||||
└── app_main.c — Application entry point
|
||||
```
|
||||
|
||||
### Crypto Primitive Mapping
|
||||
|
||||
All primitives come from the existing `nostr_core_lib` (already on ESP32):
|
||||
|
||||
| Noise Operation | Primitive | nostr_core_lib Function |
|
||||
|----------------|-----------|------------------------|
|
||||
| DH | secp256k1 ECDH | `ecdh_shared_secret()` |
|
||||
| Keygen | secp256k1 | `nostr_ec_public_key_from_private_key()` |
|
||||
| Hash | SHA-256 | `nostr_sha256_init/update/final()` |
|
||||
| KDF | HKDF-SHA256 | `nostr_hkdf_extract()` + `nostr_hkdf_expand()` |
|
||||
| AEAD | ChaCha20-Poly1305 | `nostr_chacha20poly1305_encrypt/decrypt()` |
|
||||
| Random | CSPRNG | `nostr_platform_random()` |
|
||||
|
||||
### Noise XK Responder Implementation
|
||||
|
||||
The responder processes the three-message handshake:
|
||||
|
||||
```
|
||||
Protocol: Noise_XK_secp256k1_ChaChaPoly_SHA256
|
||||
|
||||
Pre-message: ← s (responder's static pubkey known to initiator)
|
||||
|
||||
msg1 (→ e, es):
|
||||
- Receive initiator ephemeral e_init (33 bytes compressed secp256k1)
|
||||
- MixHash(e_init)
|
||||
- es = ECDH(s_local, e_init) ← uses responder's static nsec
|
||||
- MixKey(es)
|
||||
|
||||
msg2 (← e, ee):
|
||||
- Generate ephemeral keypair e_resp
|
||||
- MixHash(e_resp.pub)
|
||||
- ee = ECDH(e_resp, e_init)
|
||||
- MixKey(ee)
|
||||
- EncryptAndHash(epoch) ← 8-byte random startup epoch
|
||||
- Output: e_resp.pub (33) + encrypted_epoch (24) = 57 bytes
|
||||
|
||||
msg3 (→ s, se):
|
||||
- DecryptAndHash(encrypted_static) → initiator's pubkey
|
||||
- se = ECDH(e_resp, s_init)
|
||||
- MixKey(se)
|
||||
- DecryptAndHash(encrypted_epoch) → initiator's epoch
|
||||
- Split() → (send_cipher, recv_cipher)
|
||||
```
|
||||
|
||||
**Message sizes** (from [`noise/mod.rs`](../src/noise/mod.rs:76)):
|
||||
- XK msg1: 33 bytes (ephemeral only)
|
||||
- XK msg2: 33 + 24 = 57 bytes (ephemeral + encrypted epoch)
|
||||
- XK msg3: 49 + 24 = 73 bytes (encrypted static + encrypted epoch)
|
||||
|
||||
**secp256k1 parity normalization** (from
|
||||
[session layer spec](../docs/design/fips-session-layer.md:259)): The
|
||||
pre-message hash uses even parity (`0x02` prefix), and ECDH hashes only the
|
||||
x-coordinate. This must match the Rust implementation exactly.
|
||||
|
||||
### Memory Budget
|
||||
|
||||
| Component | RAM | Notes |
|
||||
|-----------|-----|-------|
|
||||
| Noise XK handshake state | ~256 B | Temporary during handshake |
|
||||
| Session keys (per session) | ~128 B | send_cipher + recv_cipher + counters |
|
||||
| Replay window (per session) | ~256 B | 2048-bit bitmap |
|
||||
| Max concurrent sessions | 2-4 | ~512 B–1 KB total |
|
||||
| WiFi UDP buffers | ~3 KB | RX + TX |
|
||||
| Application state | ~512 B | GPIO, sensor data |
|
||||
| **Total** | **~4-5 KB** | vs ~22 KB for full c_fips |
|
||||
|
||||
### Bridge ↔ ESP32 Protocol
|
||||
|
||||
Simple length-prefixed binary framing over WiFi UDP:
|
||||
|
||||
```
|
||||
┌──────────┬──────────┬──────────┬─────────────────┐
|
||||
│ msg_type │ session │ payload │ payload │
|
||||
│ (1 byte) │ id (4B) │ len (2B) │ (variable) │
|
||||
└──────────┴──────────┴──────────┴─────────────────┘
|
||||
```
|
||||
|
||||
Message types:
|
||||
|
||||
| Type | Direction | Description |
|
||||
|------|-----------|-------------|
|
||||
| 0x01 | Bridge → ESP | Handshake msg1 (SessionSetup payload) |
|
||||
| 0x02 | ESP → Bridge | Handshake msg2 (SessionAck payload) |
|
||||
| 0x03 | Bridge → ESP | Handshake msg3 (SessionMsg3 payload) |
|
||||
| 0x04 | ESP → Bridge | Handshake msg2 for ESP-initiated session |
|
||||
| 0x10 | Bridge → ESP | Encrypted data (FSP ciphertext) |
|
||||
| 0x11 | ESP → Bridge | Encrypted data (FSP ciphertext) |
|
||||
| 0x20 | ESP → Bridge | Initiate session to remote npub |
|
||||
| 0xF0 | Bridge → ESP | Device registration ack |
|
||||
| 0xF1 | ESP → Bridge | Device heartbeat / keepalive |
|
||||
| 0xFF | Either | Error |
|
||||
|
||||
The session_id field allows multiplexing multiple concurrent sessions
|
||||
(e.g., two different remote peers talking to the same ESP32).
|
||||
|
||||
## Security Properties
|
||||
|
||||
| Property | Status | Mechanism |
|
||||
|----------|--------|-----------|
|
||||
| End-to-end encryption | ✅ | Noise XK between ESP32 and remote peer |
|
||||
| Forward secrecy | ✅ | Ephemeral DH in Noise XK handshake |
|
||||
| Mutual authentication | ✅ | Both parties prove nsec ownership |
|
||||
| Replay protection | ✅ | Counter-based sliding window |
|
||||
| Bridge sees plaintext | ❌ Never | Bridge relays opaque ciphertext |
|
||||
| Bridge has ESP32 nsec | ❌ Never | ESP32 holds its own keys |
|
||||
| WiFi eavesdropper | Sees ciphertext only | Noise XK encrypts from ESP32 |
|
||||
|
||||
### Trust Model
|
||||
|
||||
The bridge daemon is trusted to:
|
||||
- Faithfully relay packets (availability, not confidentiality)
|
||||
- Not drop or delay packets maliciously
|
||||
- Correctly register leaf dependents with fipsd
|
||||
|
||||
The bridge daemon is NOT trusted with:
|
||||
- ESP32 private keys
|
||||
- Plaintext application data
|
||||
- Session key material
|
||||
|
||||
This is the same trust model as any FIPS transit node — it can observe
|
||||
metadata (timing, packet sizes, which sessions are active) but cannot read
|
||||
content.
|
||||
|
||||
### WiFi Segment Security
|
||||
|
||||
Traffic between ESP32 and bridge over WiFi is already Noise XK encrypted
|
||||
(the session keys are established between ESP32 and the remote peer, and
|
||||
the bridge just relays ciphertext). A WiFi eavesdropper sees encrypted
|
||||
blobs. WPA2/WPA3 on the WiFi network provides an additional layer but is
|
||||
not required for confidentiality.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: fipsd Modifications
|
||||
|
||||
Changes to the Rust fipsd codebase:
|
||||
|
||||
- [ ] Add leaf dependent check to `handle_session_datagram()` local delivery path
|
||||
- [ ] Add control socket commands: `add_leaf_dependent`, `remove_leaf_dependent`, `list_leaf_dependents`
|
||||
- [ ] Add control socket command to prime identity cache for an npub
|
||||
- [ ] Add session layer pass-through for leaf dependent addresses (skip local Noise XK, deliver raw SessionDatagram to fips0)
|
||||
- [ ] Test: verify SessionDatagram for leaf dependent arrives on fips0
|
||||
|
||||
### Phase 2: Bridge Daemon (Linux)
|
||||
|
||||
New Rust binary `fips-bridge`:
|
||||
|
||||
- [ ] YAML config parser (device registry)
|
||||
- [ ] Control socket client (connect to fipsd, register leaf dependents)
|
||||
- [ ] Identity computation (npub → NodeAddr → IPv6 address)
|
||||
- [ ] fips0 TUN packet interception for leaf dependent addresses
|
||||
- [ ] WiFi UDP server for ESP32 communication
|
||||
- [ ] Bridge protocol framing (msg_type + session_id + payload)
|
||||
- [ ] Bidirectional relay: fips0 ↔ WiFi UDP
|
||||
- [ ] Session tracking (map session_id to remote NodeAddr)
|
||||
- [ ] Device keepalive / liveness detection
|
||||
- [ ] Test: bridge relays handshake bytes between fipsd and a test client
|
||||
|
||||
### Phase 3: ESP32 Noise XK Endpoint
|
||||
|
||||
C firmware for ESP32:
|
||||
|
||||
- [ ] Port Noise SymmetricState from Rust to C (MixHash, MixKey, EncryptAndHash, DecryptAndHash)
|
||||
- [ ] Implement Noise XK responder (process msg1, generate msg2, process msg3)
|
||||
- [ ] Implement Noise XK initiator (generate msg1, process msg2, generate msg3)
|
||||
- [ ] Implement session transport encrypt/decrypt with counter
|
||||
- [ ] Implement replay window (2048-bit sliding bitmap)
|
||||
- [ ] Implement NodeAddr derivation (SHA-256 of pubkey, truncate to 16 bytes)
|
||||
- [ ] Implement bridge protocol framing
|
||||
- [ ] Implement WiFi UDP transport to bridge
|
||||
- [ ] Write test vectors validated against Rust Noise implementation
|
||||
- [ ] Test: ESP32 completes Noise XK handshake with Rust FIPS node via bridge
|
||||
|
||||
### Phase 4: Integration Testing
|
||||
|
||||
- [ ] End-to-end test: remote FIPS node → mesh → fipsd → bridge → ESP32
|
||||
- [ ] Verify bridge never sees plaintext (instrument bridge, check logs)
|
||||
- [ ] Test session rekey (if supported)
|
||||
- [ ] Test ESP32 reboot recovery (epoch mismatch detection)
|
||||
- [ ] Test multiple concurrent sessions to same ESP32
|
||||
- [ ] Test multiple ESP32 devices behind one bridge
|
||||
- [ ] Latency measurement: added overhead of bridge relay
|
||||
|
||||
### Phase 5: Example Application
|
||||
|
||||
- [ ] ESP32 light switch example (GPIO control via FIPS mesh)
|
||||
- [ ] ESP32 sensor example (push temperature readings to mesh)
|
||||
- [ ] Documentation: setup guide for bridge + ESP32
|
||||
|
||||
## Comparison With Alternatives
|
||||
|
||||
| Approach | ESP32 Code | Own npub | E2E Encrypted | Forward Secrecy | Mesh Addressable | Bridge Trust |
|
||||
|----------|-----------|----------|---------------|-----------------|-----------------|--------------|
|
||||
| LAN Gateway (existing) | None | ❌ | ❌ (gateway sees plaintext) | ❌ | ❌ (outbound only) | Full trust |
|
||||
| Full c_fips port | ~8,000 LOC | ✅ | ✅ | ✅ | ✅ | N/A |
|
||||
| NIP-44 bridge | ~200 LOC | ✅ (app-level) | ✅ (NIP-44) | ❌ | ❌ (via bridge npub) | Relay only |
|
||||
| **Noise XK bridge** | **~1,200 LOC** | **✅** | **✅ (Noise XK)** | **✅** | **✅** | **Relay only** |
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### Why Noise XK Instead of NIP-44
|
||||
|
||||
Both use the same crypto primitives (secp256k1 ECDH + ChaCha20-Poly1305).
|
||||
The difference:
|
||||
|
||||
- **NIP-44**: Static-static ECDH. No forward secrecy. Simpler (no state
|
||||
machine). But the remote peer needs a NIP-44-aware application — standard
|
||||
FIPS nodes don't speak NIP-44.
|
||||
- **Noise XK**: Ephemeral-static DH. Forward secrecy. The ESP32 speaks the
|
||||
same session protocol as every other FIPS node — full interoperability
|
||||
with unmodified remote peers.
|
||||
|
||||
Noise XK means the remote peer doesn't need to know or care that the
|
||||
destination is an ESP32 behind a bridge. It's a standard FIPS session.
|
||||
|
||||
### Why Bridge Instead of Full Port
|
||||
|
||||
The full c_fips port requires implementing: Noise IK (link layer), Noise XK
|
||||
(session layer), spanning tree, bloom filters, wire format, routing, peer
|
||||
management, and an event loop. The bridge approach requires only Noise XK
|
||||
on the ESP32 — everything else runs on Linux where it's already implemented
|
||||
and tested.
|
||||
|
||||
### Why fips0 TUN Instead of Custom API
|
||||
|
||||
Using fips0 TUN for the bridge ↔ fipsd interface means:
|
||||
- No new IPC protocol to design
|
||||
- Standard IPv6 packet handling
|
||||
- Works with existing fipsd IPv6 adapter
|
||||
- Bridge can be developed and tested independently
|
||||
|
||||
The tradeoff is IPv6 header overhead on the local path, but this is
|
||||
negligible for IoT command/response traffic.
|
||||
|
||||
## References
|
||||
|
||||
- [fips-session-layer.md](../docs/design/fips-session-layer.md) — Noise XK handshake, session lifecycle
|
||||
- [fips-mesh-layer.md](../docs/design/fips-mesh-layer.md) — Link encryption, peer authentication
|
||||
- [fips-gateway.md](../docs/design/fips-gateway.md) — Existing LAN gateway (outbound only)
|
||||
- [fips-ipv6-adapter.md](../docs/design/fips-ipv6-adapter.md) — IPv6 adapter, identity cache, TUN
|
||||
- [fips-bloom-filters.md](../docs/design/fips-bloom-filters.md) — Bloom filter parameters
|
||||
- [fips-configuration.md](../docs/design/fips-configuration.md) — YAML config reference
|
||||
- [fips-wire-formats.md](../docs/design/fips-wire-formats.md) — Wire format reference
|
||||
- [Noise Protocol Framework](https://noiseprotocol.org/) — Noise XK pattern specification
|
||||
- [fips-esp32-minimal-port.md](fips-esp32-minimal-port.md) — Original full c_fips port plan (superseded by this approach for constrained devices)
|
||||
374
plans/fips-esp32-tunnel-proxy.md
Normal file
374
plans/fips-esp32-tunnel-proxy.md
Normal file
@@ -0,0 +1,374 @@
|
||||
# FIPS ESP32 Internet Tunnel Proxy — Architecture Plan (Option 2)
|
||||
|
||||
## Goal
|
||||
|
||||
Enable an ESP32 device on **any WiFi network** to reach FIPS mesh destinations by **npub** without running the full FIPS mesh stack locally.
|
||||
|
||||
This option introduces a public **Tunnel Proxy** service:
|
||||
|
||||
- ESP32 connects to proxy over an internet-safe authenticated tunnel transport.
|
||||
- Proxy joins/runs on FIPS mesh as a full node.
|
||||
- ESP32 keeps end-to-end session cryptography (Noise XK to remote FIPS destination).
|
||||
- Proxy handles mesh routing, peer management, tree/bloom/discovery internals.
|
||||
|
||||
## Why This Exists
|
||||
|
||||
The full embedded FIPS port is powerful but heavy (link layer, mesh routing, discovery, bloom filters, tree state, wire framing, etc.).
|
||||
|
||||
A plain internet gateway is simpler, but it degrades the core FIPS property (routing/addressing by npub identity at the endpoint layer).
|
||||
|
||||
This tunnel-proxy design targets a middle path:
|
||||
|
||||
- Keep ESP32 lightweight enough for constrained firmware.
|
||||
- Preserve destination addressing by npub.
|
||||
- Keep session-layer crypto endpoint-bound (ESP32 ↔ remote destination).
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Replacing native full-node FIPS implementations.
|
||||
- Guaranteeing relay-free NAT traversal between two arbitrary private endpoints.
|
||||
- Eliminating all trust in proxy operators (proxy still affects availability/metadata).
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "ESP32 Device (Any WiFi)"
|
||||
APP["Nostr App / Publisher"]
|
||||
XK["Noise XK Session Engine\n(init/responder)"]
|
||||
TP["Tunnel Transport Client\n(TLS/Noise-IK stream)"]
|
||||
TPC["Tunnel Proxy Client Protocol\n(frame mux)"]
|
||||
end
|
||||
|
||||
subgraph "Public Internet"
|
||||
NET["Internet"]
|
||||
end
|
||||
|
||||
subgraph "Proxy Host (Public)"
|
||||
TPS["Tunnel Proxy Server\n(auth + session map)"]
|
||||
BR["Leaf Session Broker\nXK frame relay"]
|
||||
FIPSD["fipsd full node\n(tree+bloom+routing)"]
|
||||
CTL["Control API to fipsd"]
|
||||
TUN["fips0 TUN / session ingress-egress"]
|
||||
end
|
||||
|
||||
subgraph "FIPS Mesh"
|
||||
REMOTE["Remote FIPS Destination\n(e.g., Nostr relay node)"]
|
||||
end
|
||||
|
||||
APP <--> XK
|
||||
XK <--> TPC
|
||||
TPC <--> TP
|
||||
TP <--> NET
|
||||
NET <--> TPS
|
||||
TPS <--> BR
|
||||
BR <--> CTL
|
||||
BR <--> TUN
|
||||
TUN <--> FIPSD
|
||||
FIPSD <--> REMOTE
|
||||
```
|
||||
|
||||
## Core Design Choice
|
||||
|
||||
### Split responsibilities
|
||||
|
||||
**ESP32 keeps:**
|
||||
|
||||
- Device identity key material (nsec/npub).
|
||||
- Noise XK session handshake + transport encryption/decryption.
|
||||
- Application framing (Nostr over WS-like stream abstraction).
|
||||
|
||||
**Proxy keeps:**
|
||||
|
||||
- FIPS mesh connectivity (peer transport, IK links, routing, tree/bloom/discovery).
|
||||
- Mapping device identity to routable leaf representation.
|
||||
- Reliable relay of opaque session datagrams between mesh and device tunnel.
|
||||
|
||||
## Trust Model
|
||||
|
||||
### Proxy is trusted for
|
||||
|
||||
- Availability (may drop, delay, or reorder packets).
|
||||
- Correct registration and routing relay behavior.
|
||||
- Access control enforcement for admitted devices.
|
||||
|
||||
### Proxy is NOT trusted for
|
||||
|
||||
- Device private key custody.
|
||||
- Session key custody.
|
||||
- Decrypting application payload that is Noise XK-protected end-to-end.
|
||||
|
||||
### Metadata exposure
|
||||
|
||||
Proxy can observe timing, volume, target identity metadata, and liveness.
|
||||
|
||||
## Identity and Addressing Model
|
||||
|
||||
Each ESP32 has its own FIPS identity (npub/node_addr). The proxy maintains a **device registry**:
|
||||
|
||||
- `device_npub -> node_addr -> active_tunnel_connection`
|
||||
- `device_npub -> auth policy`
|
||||
- optional aliases/tags for operations
|
||||
|
||||
The proxy registers device identities as managed dependents in the local FIPS node state so mesh traffic can be routed to those addresses.
|
||||
|
||||
## Tunnel Transport Options
|
||||
|
||||
## Option A (recommended first): TLS + mutual device authentication
|
||||
|
||||
- ESP32 opens outbound TCP/TLS to public proxy endpoint.
|
||||
- Device authenticates with challenge signed by device nsec (or static client cert mapped to npub).
|
||||
- Easy NAT/firewall compatibility.
|
||||
|
||||
## Option B (future): Noise IK tunnel
|
||||
|
||||
- Lower overhead and cryptographic consistency with FIPS stack.
|
||||
- More custom implementation and operational tooling required.
|
||||
|
||||
### Requirement regardless of option
|
||||
|
||||
Tunnel transport itself must provide anti-injection and replay-resilient framing for control messages.
|
||||
|
||||
## Tunnel Control Protocol (TCP over TLS/Noise)
|
||||
|
||||
Length-prefixed binary messages, multiplexed per logical FIPS session.
|
||||
|
||||
Frame header proposal:
|
||||
|
||||
```text
|
||||
+---------+-----------+------------+-------------+
|
||||
| type(1) | flags(1) | stream_id | len (4 BE) |
|
||||
| | | (4 bytes) | |
|
||||
+---------+-----------+------------+-------------+
|
||||
| payload (len bytes)
|
||||
+---------------------------------------------------+
|
||||
```
|
||||
|
||||
### Message types
|
||||
|
||||
- `0x01 AUTH_HELLO` (device -> proxy)
|
||||
- `0x02 AUTH_CHALLENGE` (proxy -> device)
|
||||
- `0x03 AUTH_PROOF` (device -> proxy)
|
||||
- `0x04 AUTH_OK` / `0x05 AUTH_FAIL`
|
||||
- `0x10 OPEN_SESSION` (device requests remote npub + service/port)
|
||||
- `0x11 SESSION_OPENED` / `0x12 SESSION_OPEN_FAIL`
|
||||
- `0x20 XK_MSG1` (initiator msg1 payload)
|
||||
- `0x21 XK_MSG2`
|
||||
- `0x22 XK_MSG3`
|
||||
- `0x30 DATA_CIPHERTEXT`
|
||||
- `0x40 CLOSE_SESSION`
|
||||
- `0x50 HEARTBEAT`
|
||||
- `0xFF ERROR`
|
||||
|
||||
`stream_id` is tunnel-local multiplexing key for concurrent destination sessions.
|
||||
|
||||
## Session/Data Path
|
||||
|
||||
### Outbound (ESP32 initiates to remote npub)
|
||||
|
||||
1. ESP32 authenticates tunnel.
|
||||
2. ESP32 sends `OPEN_SESSION(remote_npub, service_port)`.
|
||||
3. Proxy resolves/registers route context in fipsd.
|
||||
4. ESP32 sends XK msg1 via `XK_MSG1(stream_id)`.
|
||||
5. Proxy wraps as FIPS SessionSetup toward remote node_addr.
|
||||
6. Mesh delivers to remote destination.
|
||||
7. Remote returns msg2/msg3 path via mesh; proxy forwards raw handshake bytes.
|
||||
8. ESP32 and remote complete XK and derive session keys.
|
||||
9. ESP32 sends encrypted application ciphertext frames; proxy relays opaquely.
|
||||
|
||||
### Inbound (remote initiates to ESP32)
|
||||
|
||||
1. Remote sends SessionSetup to device node_addr.
|
||||
2. fipsd local-delivers to proxy-managed dependent handler.
|
||||
3. Proxy forwards raw msg1 to active ESP32 tunnel.
|
||||
4. ESP32 generates msg2; proxy relays back through mesh.
|
||||
5. msg3 relayed to ESP32; session established.
|
||||
6. Ongoing ciphertext data relay in both directions.
|
||||
|
||||
## Required fipsd/Proxy Integration Surface
|
||||
|
||||
Two viable server-side integration styles:
|
||||
|
||||
## Style 1: TUN interception (lower daemon changes)
|
||||
|
||||
- Proxy reads/writes on `fips0` for managed dependent addresses.
|
||||
- Performs IPv6 adapter translation where needed.
|
||||
- Faster prototype but more packet-shim complexity.
|
||||
|
||||
## Style 2: SessionDatagram control API (preferred long-term)
|
||||
|
||||
Expose explicit APIs/events from fipsd:
|
||||
|
||||
- `register_managed_leaf(npub)` / `unregister_managed_leaf(npub)`
|
||||
- `send_session_datagram(src,dst,payload)`
|
||||
- `subscribe_managed_leaf_datagrams()`
|
||||
|
||||
This avoids tunnel-proxy dependence on TUN packet plumbing and keeps relay at protocol-native layer.
|
||||
|
||||
## Security Requirements
|
||||
|
||||
- Device authentication bound to npub identity.
|
||||
- Per-device ACLs (allowed destinations, rate limits, max sessions).
|
||||
- Replay protection for tunnel control plane (nonce/window or strict monotonic sequence per connection).
|
||||
- Idle/session timeout and forced re-auth.
|
||||
- Structured audit logs (auth, open/close, errors, throttles).
|
||||
|
||||
## Failure Handling
|
||||
|
||||
### Device disconnect
|
||||
|
||||
- Mark device offline.
|
||||
- Keep managed leaf registration with short grace period (configurable).
|
||||
- Reject inbound session opens until re-authenticated.
|
||||
|
||||
### Proxy restart
|
||||
|
||||
- Rebuild device registry from persisted config/state.
|
||||
- Re-register managed leaves in fipsd.
|
||||
- Active sessions re-established lazily.
|
||||
|
||||
### Mesh path failures
|
||||
|
||||
- Surface CoordsRequired/PathBroken/MtuExceeded upstream as control events.
|
||||
- ESP32 may retry setup using same stream_id or a new stream_id.
|
||||
|
||||
## Performance Targets (initial)
|
||||
|
||||
- Tunnel auth handshake: < 1.5 s on typical WiFi.
|
||||
- Session open (XK) median: < 2.5 s internet-to-mesh.
|
||||
- Added proxy relay overhead: < 15 ms median per direction (excluding mesh path latency).
|
||||
- Concurrent sessions per device: 2 initially (expand later).
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
## Phase 0 — Protocol/spec freeze
|
||||
|
||||
- Finalize tunnel frame format and auth method.
|
||||
- Decide Style 1 (TUN) vs Style 2 (SessionDatagram API) for first implementation.
|
||||
- Define compatibility/version negotiation field in `AUTH_HELLO`.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `docs/design/fips-esp32-tunnel-proxy-spec.md` (wire-level spec)
|
||||
- test vectors for auth and framing
|
||||
|
||||
## Phase 1 — Proxy MVP (single device, outbound only)
|
||||
|
||||
- Public listener with TLS auth challenge.
|
||||
- Static registry for one device npub.
|
||||
- `OPEN_SESSION` + outbound XK relay only.
|
||||
- Relay encrypted data frames.
|
||||
|
||||
Validation:
|
||||
|
||||
- ESP32 publishes kind1 to FIPS-hosted relay by remote npub.
|
||||
|
||||
## Phase 2 — Inbound initiation and multi-device
|
||||
|
||||
- Inbound SessionSetup forwarding to correct device tunnel.
|
||||
- Multi-device concurrent tunnels.
|
||||
- Session map eviction and collision handling.
|
||||
|
||||
Validation:
|
||||
|
||||
- Remote node initiates to ESP32 identity successfully.
|
||||
|
||||
## Phase 3 — Robustness and controls
|
||||
|
||||
- ACL, rate-limits, heartbeat, reconnect backoff.
|
||||
- Observability metrics and trace IDs.
|
||||
- Integration tests under packet loss/reorder.
|
||||
|
||||
Validation:
|
||||
|
||||
- 24h soak test with intermittent WiFi loss.
|
||||
|
||||
## Phase 4 — Hardening and rollout
|
||||
|
||||
- Key rotation procedures.
|
||||
- Proxy HA strategy (active/passive or sticky shard by device npub).
|
||||
- Operational docs and incident runbooks.
|
||||
|
||||
## ESP32 Firmware Delta vs Current Projects
|
||||
|
||||
Relative to existing internet nostr client:
|
||||
|
||||
- Add tunnel client transport module.
|
||||
- Add auth challenge signer using existing nsec.
|
||||
- Keep/use Noise XK session module from bridge plan.
|
||||
- Keep Nostr message construction/signing path unchanged.
|
||||
|
||||
Relative to current full leaf port effort:
|
||||
|
||||
- Remove need for on-device tree/bloom/discovery/link IK stack.
|
||||
- Keep only session crypto + app protocol + tunnel mux.
|
||||
|
||||
## Suggested Repository Layout (new components)
|
||||
|
||||
```text
|
||||
proxy/
|
||||
fips-tunnel-proxy/
|
||||
src/
|
||||
auth.rs
|
||||
frame.rs
|
||||
session_broker.rs
|
||||
fips_integration.rs
|
||||
main.rs
|
||||
|
||||
esp32/
|
||||
tunnel_client/
|
||||
tunnel_transport.c
|
||||
tunnel_transport.h
|
||||
tunnel_proto.c
|
||||
tunnel_proto.h
|
||||
tunnel_auth.c
|
||||
tunnel_auth.h
|
||||
```
|
||||
|
||||
## Test Plan
|
||||
|
||||
### Unit
|
||||
|
||||
- Frame parser fuzz tests.
|
||||
- Auth challenge/response verification tests.
|
||||
- Session map state machine tests.
|
||||
|
||||
### Integration
|
||||
|
||||
- ESP32 simulator/client -> proxy -> test fipsd node -> relay service.
|
||||
- Inbound and outbound XK handshake relay.
|
||||
- Packet loss/reorder simulation on tunnel and mesh links.
|
||||
|
||||
### End-to-end
|
||||
|
||||
- Real ESP32 from multiple WiFi networks (home, hotspot, enterprise guest).
|
||||
- Publish + readback nostr events over FIPS path by destination npub.
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
1. Keep current full-leaf client as reference implementation.
|
||||
2. Implement tunnel-proxy MVP and switch one test device.
|
||||
3. Compare reliability/latency and code size.
|
||||
4. Decide whether to deprecate full on-device mesh stack for production devices.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- **Proxy centralization risk** -> support multiple proxies and device failover list.
|
||||
- **Tunnel protocol complexity creep** -> strict scope; avoid embedding mesh semantics in tunnel layer.
|
||||
- **Auth/key misuse** -> use explicit key separation labels and domain-separated signatures.
|
||||
- **Operational single point of failure** -> health checks, watchdog restart, persistent registry snapshots.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- ESP32 can publish to FIPS-hosted nostr relay from arbitrary internet WiFi.
|
||||
- Destination addressed by npub at session setup interface.
|
||||
- ESP32 firmware complexity materially reduced versus full mesh port.
|
||||
- Proxy never requires device private keys and cannot decrypt end-to-end session payload.
|
||||
|
||||
## References
|
||||
|
||||
- [fips-session-layer.md](../fips/docs/design/fips-session-layer.md)
|
||||
- [fips-mesh-layer.md](../fips/docs/design/fips-mesh-layer.md)
|
||||
- [fips-gateway.md](../fips/docs/design/fips-gateway.md)
|
||||
- [fips-esp32-noise-bridge.md](fips-esp32-noise-bridge.md)
|
||||
- [fips-esp32-minimal-port.md](fips-esp32-minimal-port.md)
|
||||
312
plans/fips-raspberry-pi-zero-2w.md
Normal file
312
plans/fips-raspberry-pi-zero-2w.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# FIPS on Raspberry Pi Zero 2W — Deployment Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Deploy the Rust FIPS mesh daemon on a Raspberry Pi Zero 2W as a personal mesh endpoint. The Pi joins the FIPS network, gets its own npub identity, and lets applications on the device (or forwarded from the LAN) reach other FIPS nodes by npub address.
|
||||
|
||||
## Hardware Profile
|
||||
|
||||
| Spec | Pi Zero 2W | FIPS Requirement |
|
||||
|------|-----------|-----------------|
|
||||
| SoC | BCM2710A1 — quad-core Cortex-A53 @ 1 GHz | Rust binary, Noise crypto |
|
||||
| Architecture | ARMv8-A (aarch64) | First-class target in FIPS |
|
||||
| RAM | 512 MB | ~10-30 MB typical for FIPS |
|
||||
| WiFi | 802.11 b/g/n (2.4 GHz) | UDP/TCP transports |
|
||||
| Bluetooth | BT 4.2 / BLE | BLE L2CAP transport (with BlueZ) |
|
||||
| Storage | microSD | ~5-10 MB for binaries + config |
|
||||
| GPIO | 40-pin header | Not needed for FIPS |
|
||||
| USB | 1x micro-USB OTG | USB Ethernet adapter for wired transport |
|
||||
|
||||
**Verdict: Fully capable.** The Pi Zero 2W has the same Cortex-A53 cores found in many OpenWrt routers already running FIPS, with 4-16x more RAM than typical router targets.
|
||||
|
||||
## Strategy
|
||||
|
||||
1. **Cross-compile** from an x86_64 dev machine targeting `aarch64-unknown-linux-gnu` (Raspberry Pi OS) or `aarch64-unknown-linux-musl` (static binary)
|
||||
2. **Deploy via systemd tarball** — the simplest packaging path for Raspberry Pi OS
|
||||
3. **Configure as leaf-only** — minimize resource usage by delegating routing to an upstream peer
|
||||
4. **Optionally enable BLE** — the onboard Bluetooth can bridge nearby BLE mesh devices
|
||||
|
||||
## OS Choice
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Pi Zero 2W"] --> B{"Which OS?"}
|
||||
B -->|Simplest| C["Raspberry Pi OS 64-bit<br/>Debian bookworm based"]
|
||||
B -->|Router-style| D["OpenWrt<br/>bcm27xx/bcm2710 target"]
|
||||
|
||||
C --> E["Deploy via .deb or systemd tarball"]
|
||||
D --> F["Deploy via .ipk package"]
|
||||
|
||||
C --> G["Best for: personal endpoint,<br/>BLE bridge, development"]
|
||||
D --> H["Best for: headless relay,<br/>network appliance"]
|
||||
```
|
||||
|
||||
**Recommended: Raspberry Pi OS 64-bit** — it's the path of least resistance. Debian-based, so the existing FIPS `.deb` packaging or systemd tarball works directly. BlueZ is available from apt for BLE support.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Pi Zero 2W"
|
||||
subgraph "FIPS Daemon"
|
||||
NODE["fips<br/>mesh node"]
|
||||
TUN["fips0 TUN<br/>fd00::/8 IPv6"]
|
||||
DNS["DNS responder<br/>127.0.0.1:5354"]
|
||||
end
|
||||
|
||||
subgraph "Transports"
|
||||
WIFI_UDP["UDP via WiFi<br/>0.0.0.0:2121"]
|
||||
WIFI_TCP["TCP via WiFi<br/>0.0.0.0:8443"]
|
||||
BLE_T["BLE L2CAP<br/>optional"]
|
||||
end
|
||||
|
||||
subgraph "Applications"
|
||||
SSH["SSH to npub.fips"]
|
||||
PING["ping6 npub.fips"]
|
||||
APP["Any IPv6 app"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph "FIPS Mesh"
|
||||
UPSTREAM["Upstream Peer<br/>handles routing"]
|
||||
OTHER["Other Mesh Nodes"]
|
||||
end
|
||||
|
||||
APP --> TUN
|
||||
SSH --> TUN
|
||||
PING --> TUN
|
||||
TUN --> NODE
|
||||
NODE --> WIFI_UDP
|
||||
NODE --> WIFI_TCP
|
||||
NODE --> BLE_T
|
||||
WIFI_UDP --> UPSTREAM
|
||||
WIFI_TCP --> UPSTREAM
|
||||
UPSTREAM --> OTHER
|
||||
DNS --> NODE
|
||||
```
|
||||
|
||||
## Build & Deploy Steps
|
||||
|
||||
### Phase 1: Cross-Compile
|
||||
|
||||
Build on an x86_64 host targeting the Pi's architecture.
|
||||
|
||||
**Option A — Dynamic linking (Raspberry Pi OS):**
|
||||
```bash
|
||||
# Install cross-compilation toolchain
|
||||
sudo apt install gcc-aarch64-linux-gnu
|
||||
|
||||
# Add Rust target
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
|
||||
# Build FIPS
|
||||
cd fips
|
||||
cargo build --release --target aarch64-unknown-linux-gnu --features gateway
|
||||
```
|
||||
|
||||
**Option B — Static binary via cargo-zigbuild (any Linux):**
|
||||
```bash
|
||||
cargo install cargo-zigbuild
|
||||
cargo zigbuild --release --target aarch64-unknown-linux-musl --features gateway
|
||||
```
|
||||
|
||||
**Option C — Use the existing tarball script:**
|
||||
```bash
|
||||
./packaging/systemd/build-tarball.sh --target aarch64-unknown-linux-gnu
|
||||
# Output: deploy/fips-<version>-linux-aarch64.tar.gz
|
||||
```
|
||||
|
||||
**Option D — Build a .deb package:**
|
||||
```bash
|
||||
cargo install cargo-deb
|
||||
./packaging/debian/build-deb.sh --target aarch64-unknown-linux-gnu
|
||||
# Output: deploy/fips_<version>_arm64.deb
|
||||
```
|
||||
|
||||
### Phase 2: Pi OS Setup
|
||||
|
||||
On the Pi Zero 2W running Raspberry Pi OS 64-bit:
|
||||
|
||||
```bash
|
||||
# Update system
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# Install TUN support (usually already present)
|
||||
sudo modprobe tun
|
||||
|
||||
# For BLE transport (optional):
|
||||
sudo apt install bluez libdbus-1-dev
|
||||
|
||||
# Create fips user and directories
|
||||
sudo useradd -r -s /usr/sbin/nologin fips
|
||||
sudo mkdir -p /etc/fips /run/fips
|
||||
sudo chown fips:fips /run/fips
|
||||
```
|
||||
|
||||
### Phase 3: Install FIPS
|
||||
|
||||
**From tarball:**
|
||||
```bash
|
||||
# Copy tarball to Pi
|
||||
scp deploy/fips-*-linux-aarch64.tar.gz pi@raspberrypi:
|
||||
|
||||
# On the Pi:
|
||||
sudo tar -xzf fips-*-linux-aarch64.tar.gz -C /
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable fips
|
||||
```
|
||||
|
||||
**From .deb:**
|
||||
```bash
|
||||
scp deploy/fips_*_arm64.deb pi@raspberrypi:
|
||||
sudo dpkg -i fips_*_arm64.deb
|
||||
```
|
||||
|
||||
### Phase 4: Configure
|
||||
|
||||
Create `/etc/fips/fips.yaml`:
|
||||
|
||||
```yaml
|
||||
# Pi Zero 2W — leaf endpoint configuration
|
||||
node:
|
||||
identity:
|
||||
persistent: true # Keep same npub across restarts
|
||||
leaf_only: true # Delegate routing to upstream peer
|
||||
cache:
|
||||
coord_size: 1000 # Reduced from default 50,000
|
||||
identity_size: 500 # Reduced from default 10,000
|
||||
limits:
|
||||
max_peers: 8 # Reduced from default 128
|
||||
max_connections: 16 # Reduced from default 256
|
||||
|
||||
tun:
|
||||
enabled: true
|
||||
name: fips0
|
||||
mtu: 1280
|
||||
|
||||
dns:
|
||||
enabled: true
|
||||
bind_addr: "127.0.0.1"
|
||||
port: 5354
|
||||
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:2121"
|
||||
tcp:
|
||||
bind_addr: "0.0.0.0:8443"
|
||||
# Uncomment for BLE mesh bridging:
|
||||
# ble:
|
||||
# adapter: "hci0"
|
||||
# mtu: 2048
|
||||
# advertise: true
|
||||
# scan: true
|
||||
# auto_connect: true
|
||||
# accept_connections: true
|
||||
|
||||
peers:
|
||||
- npub: "npub1..." # Your upstream peer's npub
|
||||
alias: "upstream"
|
||||
addresses:
|
||||
- transport: udp
|
||||
addr: "x.x.x.x:2121" # Upstream peer's address
|
||||
connect_policy: auto_connect
|
||||
```
|
||||
|
||||
### Phase 5: Start & Verify
|
||||
|
||||
```bash
|
||||
# Start the daemon
|
||||
sudo systemctl start fips
|
||||
|
||||
# Check status
|
||||
sudo systemctl status fips
|
||||
journalctl -u fips -f
|
||||
|
||||
# Verify mesh connectivity
|
||||
fipsctl show_node
|
||||
fipsctl show_peers
|
||||
|
||||
# Test connectivity to another node
|
||||
ping6 npub1<other-node>.fips
|
||||
```
|
||||
|
||||
## Resource Tuning for 512 MB RAM
|
||||
|
||||
The default FIPS configuration targets servers/desktops with ample RAM. For the Pi Zero 2W, these config overrides keep memory usage well under 50 MB:
|
||||
|
||||
| Parameter | Default | Pi Recommended | Why |
|
||||
|-----------|---------|---------------|-----|
|
||||
| `node.cache.coord_size` | 50,000 | 1,000 | Small mesh, few destinations |
|
||||
| `node.cache.identity_size` | 10,000 | 500 | Few unique peers |
|
||||
| `node.limits.max_peers` | 128 | 8 | Leaf node, 1-3 peers typical |
|
||||
| `node.limits.max_connections` | 256 | 16 | Proportional to max_peers |
|
||||
| `node.limits.max_pending_inbound` | 1,000 | 50 | Leaf gets few inbound |
|
||||
| `node.leaf_only` | false | true | Delegate routing overhead |
|
||||
|
||||
## Transport Options on Pi Zero 2W
|
||||
|
||||
### WiFi (UDP + TCP) — Primary
|
||||
The onboard 2.4 GHz WiFi connects to your home network and reaches internet-connected FIPS peers. This is the default and requires no extra hardware.
|
||||
|
||||
### BLE — Local Mesh Bridge
|
||||
The Pi's Bluetooth 4.2 supports BLE L2CAP, which FIPS uses for short-range mesh links. Build with `--features ble` and install BlueZ. The Pi could bridge BLE-only devices (like ESP32 nodes) into the wider mesh.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
ESP1["ESP32<br/>BLE only"] -->|"BLE L2CAP"| PI["Pi Zero 2W<br/>BLE + WiFi"]
|
||||
ESP2["ESP32<br/>BLE only"] -->|"BLE L2CAP"| PI
|
||||
PI -->|"UDP over WiFi"| MESH["Internet Mesh<br/>Peers"]
|
||||
```
|
||||
|
||||
### USB Ethernet — Wired Option
|
||||
A USB Ethernet adapter on the OTG port enables the raw Ethernet transport (EtherType 0x2121) for direct L2 mesh links to nearby nodes without IP overhead.
|
||||
|
||||
### Tor — Anonymous Overlay
|
||||
Install the `tor` package and configure the Tor transport for anonymous mesh participation. Adds latency (200ms-2s RTT) but works.
|
||||
|
||||
## Leaf-Only Mode Status
|
||||
|
||||
The `node.leaf_only` config flag exists in the codebase and is wired through:
|
||||
- Config parsing: `node.leaf_only` in `fips.yaml`
|
||||
- Node constructor: `Node::leaf_only()`
|
||||
- Bloom filter support: upstream peer includes leaf identity via `add_leaf_dependent()`
|
||||
- Status reporting: visible in `fipsctl show_node` and `fipstop` dashboard
|
||||
|
||||
The design docs mark it as "under development" — the config flag and data structures are in place, but the full behavioral changes (suppressing tree participation, simplified routing) may not all be active yet. Even without leaf-only mode fully enabled, a standard FIPS node with 2-3 peers runs comfortably on the Pi Zero 2W's hardware.
|
||||
|
||||
## Monitoring
|
||||
|
||||
The Pi can run `fipstop` (the TUI dashboard) over SSH for live monitoring:
|
||||
|
||||
```bash
|
||||
ssh pi@raspberrypi
|
||||
fipstop
|
||||
```
|
||||
|
||||
Or use `fipsctl` for scriptable status checks:
|
||||
|
||||
```bash
|
||||
fipsctl show_node # Node identity and state
|
||||
fipsctl show_peers # Connected peers
|
||||
fipsctl show_links # Active transport links
|
||||
fipsctl show_routing # Spanning tree and bloom filter state
|
||||
fipsctl show_sessions # End-to-end encrypted sessions
|
||||
```
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|-----------|
|
||||
| Compilation on Pi is slow | Dev friction | Cross-compile from x86_64 host |
|
||||
| 512 MB RAM under heavy transit load | OOM if forwarding for many nodes | Use `leaf_only: true` + reduced cache sizes |
|
||||
| WiFi-only networking (no Ethernet) | Single transport, no redundancy | Add USB Ethernet adapter for second transport |
|
||||
| BLE range limited (~10m) | Short-range only | Use as bridge, not primary transport |
|
||||
| `leaf_only` mode not fully implemented | May still participate in routing | Acceptable — Pi has enough resources for light routing |
|
||||
| SD card wear from logging | Card failure over time | Use `journald` rate limiting, consider tmpfs for `/run/fips` |
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **Headless auto-provisioning**: Script that generates identity on first boot and registers with a known upstream peer
|
||||
- **BLE bridge mode**: Dedicated configuration for bridging BLE-only devices into the mesh
|
||||
- **Watchdog integration**: Use systemd watchdog to auto-restart on hang
|
||||
- **Read-only root filesystem**: Protect SD card, write only to `/etc/fips/` and `/run/fips/`
|
||||
Reference in New Issue
Block a user