Two pieces that unblock the on-device cross-CGNAT test:
1. Rendezvous (new `rendezvous/` crate, tether-rendezvous, axum) — the only
server tether still needs on iroh: maps room → {EndpointId}, in-memory, TTL,
content-blind (room is a hash; never sees clipboard data). IrohTransport now
registers under cfg.server and bootstraps gossip from the returned peers, and
re-registers every 30s so late joiners can find it. The TETHER_IROH_BOOTSTRAP
env var is demoted to a test override. Verified: two engines self-discover via
a local rendezvous and sync text + a 200KB photo with zero env wiring.
2. iroh on Apple targets — the iroh-transport feature cross-compiles for
ios-arm64 / ios-sim / macOS once IPHONEOS_DEPLOYMENT_TARGET=26.0 is set (ring/
blake3 prebuilt objects target the newer SDK). build-apple.sh now exports the
deployment targets and takes a TETHER_FEATURES switch;
`TETHER_FEATURES=iroh-transport ./build-apple.sh` assembles + vendors the iroh
xcframework (4891 iroh symbols in the iOS .a). Swift bindings are unchanged —
the app builds on iroh with no Swift changes.
Remaining for the live test: deploy the rendezvous (+ optional self-hosted
iroh-relay) publicly on the homelab, then build TetherApp in Xcode and point its
server field at the rendezvous URL. Default ship build untouched throughout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
104 lines
3.8 KiB
Rust
104 lines
3.8 KiB
Rust
//! M4 integration test — the real `Engine` syncing a clipboard over iroh.
|
|
//! cargo run --features iroh-transport --example iroh_engine
|
|
//!
|
|
//! Two engines, same account → same room → same gossip topic. No SSE/RTC/LAN:
|
|
//! with the iroh-transport feature the engine's only transport is IrohTransport.
|
|
//! Proves the public API (Engine::new/start/send + MessageHandler) is unchanged
|
|
//! while the entire Mesh underneath is iroh — the payoff of the MeshEvent seam.
|
|
//!
|
|
//! Bootstrap is wired in-process via the TETHER_IROH_BOOTSTRAP env var (the v1
|
|
//! stopgap until the rendezvous supernode): start A, read its EndpointId, hand
|
|
//! it to B.
|
|
|
|
use std::sync::mpsc;
|
|
use std::time::Duration;
|
|
|
|
use tethercore::{Engine, Message, MessageHandler};
|
|
|
|
struct Noop;
|
|
impl MessageHandler for Noop {
|
|
fn on_message(&self, _m: Message) {}
|
|
fn on_status(&self, _c: bool) {}
|
|
fn on_file(&self, _n: String, _m: String, _d: Vec<u8>) {}
|
|
}
|
|
|
|
struct Collector {
|
|
text: mpsc::Sender<String>,
|
|
file: mpsc::Sender<(String, usize)>,
|
|
}
|
|
impl MessageHandler for Collector {
|
|
fn on_message(&self, m: Message) {
|
|
let _ = self.text.send(m.text);
|
|
}
|
|
fn on_status(&self, _c: bool) {}
|
|
fn on_file(&self, name: String, _mime: String, data: Vec<u8>) {
|
|
let _ = self.file.send((name, data.len()));
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let account = "pecord@gmail.com"; // same account → same room → same topic + key
|
|
let room = "iroh-engine-test";
|
|
// Zero-config bootstrap via the rendezvous (run tether-rendezvous first, or
|
|
// point at a deployed one). Both engines register under `room` and discover
|
|
// each other — no env var, no manual id wiring.
|
|
let server = std::env::var("TETHER_RENDEZVOUS")
|
|
.unwrap_or_else(|_| "http://localhost:8765".into());
|
|
eprintln!("rendezvous: {server}");
|
|
|
|
let a = Engine::new(
|
|
server.clone(), room.into(), "iroh-a".into(), "macos".into(), "A".into(), account.into(),
|
|
);
|
|
a.start(Box::new(Noop));
|
|
|
|
let (tx, rx) = mpsc::channel();
|
|
let (ftx, frx) = mpsc::channel();
|
|
let b = Engine::new(
|
|
server.clone(), room.into(), "iroh-b".into(), "ios".into(), "B".into(), account.into(),
|
|
);
|
|
b.start(Box::new(Collector { text: tx, file: ftx }));
|
|
|
|
eprintln!("waiting for gossip topic to form (via rendezvous)…");
|
|
std::thread::sleep(Duration::from_secs(8));
|
|
|
|
// 1) Text clipboard over gossip.
|
|
let payload = "clipboard synced over iroh 🎉";
|
|
eprintln!("A sending text: {payload:?}");
|
|
a.send(payload.to_string());
|
|
match rx.recv_timeout(Duration::from_secs(10)) {
|
|
Ok(t) if t == payload => {
|
|
println!("✅ text: B received {t:?} (E2E-decrypted, via gossip)");
|
|
}
|
|
Ok(t) => {
|
|
println!("⚠️ B got {t:?} (expected {payload:?})");
|
|
std::process::exit(1);
|
|
}
|
|
Err(_) => {
|
|
println!("❌ B received no text");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
// 2) A "photo"/blob over iroh-blobs (announced via gossip, fetched P2P).
|
|
let photo: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
|
|
eprintln!("A sending blob: photo.bin ({} bytes)", photo.len());
|
|
a.send_file("photo.bin".into(), "image/png".into(), photo.clone());
|
|
match frx.recv_timeout(Duration::from_secs(15)) {
|
|
Ok((name, len)) if len == photo.len() => {
|
|
println!("✅ blob: B received {name:?} ({len} bytes, E2E-decrypted, via iroh-blobs)");
|
|
}
|
|
Ok((name, len)) => {
|
|
println!("⚠️ B got {name:?} with {len} bytes (expected {})", photo.len());
|
|
std::process::exit(1);
|
|
}
|
|
Err(_) => {
|
|
println!("❌ B received no blob");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
println!("🎉 engine-over-iroh: text + photo synced, no SSE/RTC/LAN/relay");
|
|
a.stop();
|
|
b.stop();
|
|
}
|