11 KiB
Phase 2: Mesh as Federation Transport
Context
Phase 1 built three independent communication silos: Tor-HTTP for federation, Meshcore LoRa for offline mesh, Nostr relays for discovery. They share no common abstraction — federation hardcodes Tor SOCKS5, mesh has its own serial listener, peer messaging uses a separate HTTP POST. Adding a new transport (LAN/mDNS) or changing routing logic means touching every caller.
Phase 2 unifies these under a NodeTransport trait with automatic transport selection per peer (Mesh > LAN > Tor), chunked LoRa messaging with Reed-Solomon FEC for payloads >160 bytes, CBOR delta compression for state sync, mDNS LAN discovery, and a "mesh only" off-grid mode.
The v2.0 roadmap (docs/roadmap-v2.0.md) also calls for a visual topology map and CRDT-based state sync — those are deferred to Phase 5 (Mesh Intelligence), but the transport abstraction here enables them.
Dependencies (Verified)
ciborium = "0.2.2" # CBOR serde (134M downloads, active)
reed-solomon-erasure = "6.0" # FEC (4M downloads, MIT)
mdns-sd = "0.18.2" # LAN peer discovery (1.86M, active)
Architecture
core/archipelago/src/transport/
├── mod.rs — NodeTransport trait, TransportKind, TransportRouter, PeerRegistry
├── tor.rs — TorTransport (wraps node_message.rs SOCKS5 logic)
├── mesh_transport.rs — MeshTransport (bridges MeshService cmd channel)
├── lan.rs — LanTransport (mdns-sd + direct HTTP)
├── chunking.rs — Split/reassemble with Reed-Solomon FEC
└── delta.rs — CBOR delta encoding for NodeStateSnapshot
Unified Peer Identity
DID is the canonical identifier. Transport-specific addresses are attributes:
pub struct PeerRecord {
pub did: String,
pub pubkey_hex: String,
pub name: Option<String>,
pub trust_level: TrustLevel,
// Transport capabilities
pub mesh_contact_id: Option<u32>,
pub lan_address: Option<SocketAddr>,
pub onion_address: Option<String>,
// Freshness
pub last_mesh: Option<String>,
pub last_lan: Option<String>,
pub last_tor: Option<String>,
}
Transport Routing
TransportRouter::send_to_peer(did, payload)
1. Look up PeerRecord by DID
2. Filter to transports where peer has address AND transport is available AND not stale
3. Sort by priority: Mesh(1) > LAN(2) > Tor(3)
4. If mesh_only_mode: only allow Mesh
5. Try each in order, fall back on failure
Chunking Protocol (for LoRa >160B)
Per-chunk structure (8 byte header + 124 byte payload = 132B, fits in 160B after encryption):
[0x01] [msg_id: u32 LE] [chunk_idx: u8] [total: u8] [is_parity: u8] [payload: 124B]
Reed-Solomon: 25% parity shards. 4 data chunks = 1 parity chunk. Any N-of-(N+parity) reconstructs. Max practical: ~10 chunks (~1.2KB) before LoRa airtime becomes unreasonable.
CBOR Delta Sync
Full JSON state: ~500-2000 bytes. CBOR delta when only CPU/mem changed: ~30-50 bytes. Fits single LoRa chunk.
pub struct StateDelta {
pub ts: String,
pub v: u8,
pub apps: Option<Vec<AppStatus>>, // Only changed apps
pub apps_rm: Option<Vec<String>>, // Removed app IDs
pub cpu: Option<f64>,
pub mem_u: Option<u64>,
// ... only changed fields, short names for wire size
}
Implementation Steps
Step 1: Add deps to Cargo.toml
File: core/archipelago/Cargo.toml
Add ciborium, reed-solomon-erasure, mdns-sd.
Step 2: Create transport/mod.rs — Core trait + PeerRegistry + TransportRouter
New file: core/archipelago/src/transport/mod.rs
NodeTransporttrait:kind(),is_available(),send(address, payload)TransportKindenum:Mesh = 1, Lan = 2, Tor = 3TransportMessage:{from_did, payload: Vec<u8>, message_type}PeerRegistry: unified peer store (DID -> PeerRecord), JSON persistence totransport-peers.jsonTransportRouter: holds Vec<Box>, routes by priority with fallback- Register
mod transport;incore/archipelago/src/lib.rsor main.rs
Step 3: Create transport/chunking.rs — Reed-Solomon FEC
New file: core/archipelago/src/transport/chunking.rs
encode_chunked(data: &[u8]) -> Vec<Vec<u8>>— split + FEC parity shardsChunkReassembler— state machine tracking pending messages, attempts reconstruction when enough chunks arrivedecode_chunked(chunks: &[Option<Vec<u8>>]) -> Result<Vec<u8>>— reconstruct from data+parity- Unit tests: roundtrip, missing chunks, corrupted chunks
Step 4: Create transport/delta.rs — CBOR delta sync
New file: core/archipelago/src/transport/delta.rs
compute_delta(prev, curr: &NodeStateSnapshot) -> StateDeltaapply_delta(base: &NodeStateSnapshot, delta: &StateDelta) -> NodeStateSnapshotencode_cbor(delta) -> Vec<u8>via ciboriumdecode_cbor(bytes) -> Result<StateDelta>- Unit tests: delta correctness, CBOR roundtrip, size comparison vs JSON
Step 5: Create transport/tor.rs — TorTransport
New file: core/archipelago/src/transport/tor.rs
- Wraps existing
node_message::send_to_peer()pattern (reqwest + SOCKS5) - Reuses:
node_message.rs:66send logic,node_message.rs:115health check is_available()checks SOCKS5 proxy reachable at 127.0.0.1:9050- Receives handled by existing HTTP handler in
api/handler.rs
Step 6: Create transport/mesh_transport.rs — MeshTransport
New file: core/archipelago/src/transport/mesh_transport.rs
- Holds reference to
Arc<RwLock<Option<MeshService>>>(same as RpcHandler) send()routes throughMeshService::send_message()for small payloads- For payloads >124B: chunk with
chunking::encode_chunked(), send each chunk viaMeshCommand::SendText - 200ms inter-chunk delay for LoRa airtime fairness
is_available()checks mesh device connected viaMeshService::status()
Step 7: Create transport/lan.rs — LanTransport
New file: core/archipelago/src/transport/lan.rs
- Uses
mdns-sd::ServiceDaemonfor discovery - Advertises:
_archipelago._tcp.local.with TXT records:did=..., pubkey=..., version=... - Browses for peers, updates PeerRegistry when found
send(): direct HTTP POST tohttp://{ip}:5678/archipelago/node-message(same endpoint as Tor, no proxy)- Non-blocking init — if Avahi not available, fail gracefully
Step 8: Add mesh_only_mode to MeshConfig
File: core/archipelago/src/mesh/mod.rs
- Add
pub mesh_only_mode: Option<bool>to MeshConfig (with#[serde(default)]) - TransportRouter reads this flag to restrict routing
Step 9: Wire TransportRouter into server.rs
File: core/archipelago/src/server.rs (~line 136, after mesh service init)
- Create PeerRegistry, load from disk
- Create TorTransport, MeshTransport, LanTransport
- Start LAN advertisement + discovery
- Create TransportRouter with all three + mesh_only flag
- Spawn background task bridging
MeshEvent::IdentityReceived-> PeerRegistry - Store router on RpcHandler
Step 10: Add transport RPC endpoints
New file: core/archipelago/src/api/rpc/transport.rs
Modify: core/archipelago/src/api/rpc/mod.rs (add dispatch routes)
Endpoints:
transport.status— available transports, mesh_only flag, peer counttransport.peers— unified peer list with per-peer transport capabilities + preferred transporttransport.send— send message via best transport (by DID)transport.set-mode— toggle mesh_only mode
Step 11: Add opt-in federation sync via transport
File: core/archipelago/src/federation.rs
- Add
sync_with_peer_via_transport()alongside existingsync_with_peer() - Uses CBOR delta encoding when transport is mesh (saves bandwidth)
- Falls back to full JSON over Tor for backward compat
- Background sync task checks for TransportRouter, uses it if available
Step 12: Frontend — transport store + UI indicators
New file: neode-ui/src/stores/transport.ts — Pinia store for transport status
Modify: neode-ui/src/views/Mesh.vue — "OFF-GRID" indicator when mesh_only mode
Modify: neode-ui/mock-backend.js — add transport.* mock RPC responses
Step 13: Mock backend transport data
File: neode-ui/mock-backend.js
Add transport.status, transport.peers, transport.send, transport.set-mode mock responses with realistic data showing mixed transport capabilities.
Files Summary
New (8)
core/archipelago/src/transport/mod.rscore/archipelago/src/transport/tor.rscore/archipelago/src/transport/mesh_transport.rscore/archipelago/src/transport/lan.rscore/archipelago/src/transport/chunking.rscore/archipelago/src/transport/delta.rscore/archipelago/src/api/rpc/transport.rsneode-ui/src/stores/transport.ts
Modified (8)
core/archipelago/Cargo.toml— add 3 depscore/archipelago/src/lib.rsor main —mod transport;core/archipelago/src/server.rs— init router, bridge eventscore/archipelago/src/api/rpc/mod.rs— dispatch + store routercore/archipelago/src/mesh/mod.rs—mesh_only_modeconfig fieldcore/archipelago/src/federation.rs—sync_with_peer_via_transport()neode-ui/src/views/Mesh.vue— off-grid indicatorneode-ui/mock-backend.js— transport.* mock data
Reused Existing Code
node_message.rs:66-112— Tor SOCKS5 send logic -> wrapped by TorTransportnode_message.rs:115-135— Tor health check -> TorTransport.is_available()mesh/mod.rs:209-282— MeshService::send_message() -> called by MeshTransportmesh/listener.rs:35-37— MeshCommand channel -> used for chunked sendsmesh/crypto.rs— X25519 ECDH + ChaCha20-Poly1305 -> reused as-isfederation.rs:36-49— FederatedNode struct -> PeerRecord wraps thisfederation.rs:179-191— update_node_state() -> adapted for CBOR deltas
Verification
-
Unit tests (run on dev server):
cargo test --all-features -- transport cargo test --all-features -- chunking cargo test --all-features -- delta -
Frontend type check:
cd neode-ui && npm run type-check -
Dev mode test:
cd neode-ui && npm start # Navigate to /dashboard/mesh — verify OFF-GRID toggle -
Deploy + integration test:
./scripts/deploy-to-target.sh --live # On .228: curl transport.status RPC # Verify mDNS advertises _archipelago._tcp.local. # If mesh device connected: send chunked message >160B to .198 # Verify federation sync uses CBOR delta when available
Implementation Order
Week 1: Steps 1-4 (deps, trait, chunking, delta — pure library code with unit tests) Week 2: Steps 5-7 (three transport implementations) Week 3: Steps 8-11 (integration: server wiring, RPC, federation migration) Week 4: Steps 12-13 (frontend UI, mock data, deploy + test)