core+rendezvous: zero-config bootstrap + iroh on iOS
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>
This commit is contained in:
+17
-5
@@ -435,11 +435,23 @@ multi-modal payloads (text inline, audio/photo/data via `iroh-blobs`). Receiving
|
|||||||
peers fetch P2P by hash and emit `MeshEvent::File`. Replaces the
|
peers fetch P2P by hash and emit `MeshEvent::File`. Replaces the
|
||||||
M/C/E frame protocol. Verified: a 200 KB photo syncs A→B
|
M/C/E frame protocol. Verified: a 200 KB photo syncs A→B
|
||||||
E2E-decrypted alongside text (`iroh_engine` example).
|
E2E-decrypted alongside text (`iroh_engine` example).
|
||||||
- [ ] Remaining: rendezvous for zero-config bootstrap (today via
|
- [x] **Rendezvous for zero-config bootstrap** — `rendezvous/`
|
||||||
`TETHER_IROH_BOOTSTRAP` env stopgap); cross-compile the feature
|
(tether-rendezvous, axum): `room → {EndpointId}`, metadata-only,
|
||||||
for iOS + build the xcframework with it; make webrtc/mdns/reqwest
|
content-blind. IrohTransport registers under `cfg.server` and
|
||||||
optional so the iroh/wasm build is lean; A/B vs RTC+LAN; flip the
|
bootstraps gossip from the returned peers (env var now just a
|
||||||
feature on by default.
|
test override). Verified: two engines self-discover via a local
|
||||||
|
rendezvous, sync text + photo, zero env wiring.
|
||||||
|
- [x] **iOS/Apple cross-compile + xcframework** — `iroh-transport`
|
||||||
|
builds for ios-arm64 / ios-sim / macOS (needs
|
||||||
|
`IPHONEOS_DEPLOYMENT_TARGET=26.0`, now in `build-apple.sh` along
|
||||||
|
with a `TETHER_FEATURES` switch). `TETHER_FEATURES=iroh-transport
|
||||||
|
./build-apple.sh` assembles + vendors the iroh xcframework
|
||||||
|
(4,891 iroh symbols in the iOS staticlib); Swift bindings
|
||||||
|
unchanged, so the app builds with no Swift changes.
|
||||||
|
- [ ] Remaining: deploy the rendezvous + a self-hosted iroh-relay
|
||||||
|
publicly (homelab) for the on-device cross-CGNAT test; make
|
||||||
|
webrtc/mdns/reqwest optional for a lean iroh/wasm build; A/B vs
|
||||||
|
RTC+LAN; flip the feature on by default.
|
||||||
- [ ] Spike **Automerge** as the Sync tier; re-express the clipboard as a tiny
|
- [ ] Spike **Automerge** as the Sync tier; re-express the clipboard as a tiny
|
||||||
app over replicated state to validate the App API.
|
app over replicated state to validate the App API.
|
||||||
- [ ] Durable device keypairs + authenticated handshake (Noise/`snow`, or
|
- [ ] Durable device keypairs + authenticated handshake (Noise/`snow`, or
|
||||||
|
|||||||
+17
-3
@@ -13,11 +13,25 @@ IOS=aarch64-apple-ios
|
|||||||
SIM=aarch64-apple-ios-sim
|
SIM=aarch64-apple-ios-sim
|
||||||
MAC=aarch64-apple-darwin
|
MAC=aarch64-apple-darwin
|
||||||
|
|
||||||
|
# Deployment targets — must match the apps (iOS 26). Without this the linker
|
||||||
|
# defaults to iOS 10 and the cdylib fails to link against ring/blake3's prebuilt
|
||||||
|
# assembly objects (built for the newer SDK).
|
||||||
|
export IPHONEOS_DEPLOYMENT_TARGET=26.0
|
||||||
|
export MACOSX_DEPLOYMENT_TARGET=26.0
|
||||||
|
|
||||||
|
# Optional Cargo features, e.g. TETHER_FEATURES=iroh-transport to build the
|
||||||
|
# iroh-backed core. Empty → the default (legacy SSE/RTC/LAN) build. Plain string
|
||||||
|
# (not an array) so macOS bash 3.2 + `set -u` doesn't choke when it's empty.
|
||||||
|
FEATURES="${TETHER_FEATURES:-}"
|
||||||
|
FEAT_ARGS=""
|
||||||
|
[ -n "$FEATURES" ] && FEAT_ARGS="--features $FEATURES" && echo "▸ features: $FEATURES"
|
||||||
|
|
||||||
echo "▸ building static libs ($IOS, $SIM, $MAC)…"
|
echo "▸ building static libs ($IOS, $SIM, $MAC)…"
|
||||||
rustup target add "$IOS" "$SIM" "$MAC" >/dev/null 2>&1 || true
|
rustup target add "$IOS" "$SIM" "$MAC" >/dev/null 2>&1 || true
|
||||||
cargo build --release --target "$IOS" --lib
|
# shellcheck disable=SC2086 # intentional word-split of FEAT_ARGS
|
||||||
cargo build --release --target "$SIM" --lib
|
cargo build --release --target "$IOS" --lib $FEAT_ARGS
|
||||||
cargo build --release --target "$MAC" --lib
|
cargo build --release --target "$SIM" --lib $FEAT_ARGS
|
||||||
|
cargo build --release --target "$MAC" --lib $FEAT_ARGS
|
||||||
|
|
||||||
echo "▸ generating Swift bindings…"
|
echo "▸ generating Swift bindings…"
|
||||||
cargo run --release --bin uniffi-bindgen -- \
|
cargo run --release --bin uniffi-bindgen -- \
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
//! it to B.
|
//! it to B.
|
||||||
|
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::Duration;
|
||||||
|
|
||||||
use tethercore::{Engine, Message, MessageHandler};
|
use tethercore::{Engine, Message, MessageHandler};
|
||||||
|
|
||||||
@@ -39,40 +39,26 @@ impl MessageHandler for Collector {
|
|||||||
fn main() {
|
fn main() {
|
||||||
let account = "pecord@gmail.com"; // same account → same room → same topic + key
|
let account = "pecord@gmail.com"; // same account → same room → same topic + key
|
||||||
let room = "iroh-engine-test";
|
let room = "iroh-engine-test";
|
||||||
let server = "unused"; // iroh ignores cfg.server; it uses n0 infra
|
// 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}");
|
||||||
|
|
||||||
// Opener (no bootstrap yet).
|
|
||||||
let a = Engine::new(
|
let a = Engine::new(
|
||||||
server.into(), room.into(), "iroh-a".into(), "macos".into(), "A".into(), account.into(),
|
server.clone(), room.into(), "iroh-a".into(), "macos".into(), "A".into(), account.into(),
|
||||||
);
|
);
|
||||||
a.start(Box::new(Noop));
|
a.start(Box::new(Noop));
|
||||||
|
|
||||||
// Wait for A's iroh endpoint to come online, then bootstrap B off it.
|
|
||||||
let a_id = {
|
|
||||||
let deadline = Instant::now() + Duration::from_secs(20);
|
|
||||||
loop {
|
|
||||||
if let Some(id) = a.iroh_id() {
|
|
||||||
break id;
|
|
||||||
}
|
|
||||||
if Instant::now() > deadline {
|
|
||||||
eprintln!("❌ A never came online");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
std::thread::sleep(Duration::from_millis(200));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
eprintln!("A online, id={a_id}");
|
|
||||||
std::env::set_var("TETHER_IROH_BOOTSTRAP", &a_id);
|
|
||||||
|
|
||||||
// Joiner (bootstraps to A via the env var its IrohTransport reads at start).
|
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
let (ftx, frx) = mpsc::channel();
|
let (ftx, frx) = mpsc::channel();
|
||||||
let b = Engine::new(
|
let b = Engine::new(
|
||||||
server.into(), room.into(), "iroh-b".into(), "ios".into(), "B".into(), account.into(),
|
server.clone(), room.into(), "iroh-b".into(), "ios".into(), "B".into(), account.into(),
|
||||||
);
|
);
|
||||||
b.start(Box::new(Collector { text: tx, file: ftx }));
|
b.start(Box::new(Collector { text: tx, file: ftx }));
|
||||||
|
|
||||||
eprintln!("waiting for gossip topic to form…");
|
eprintln!("waiting for gossip topic to form (via rendezvous)…");
|
||||||
std::thread::sleep(Duration::from_secs(8));
|
std::thread::sleep(Duration::from_secs(8));
|
||||||
|
|
||||||
// 1) Text clipboard over gossip.
|
// 1) Text clipboard over gossip.
|
||||||
|
|||||||
+49
-2
@@ -77,7 +77,8 @@ fn topic_for(room: &str) -> TopicId {
|
|||||||
TopicId::from_bytes(id)
|
TopicId::from_bytes(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bootstrap_ids() -> Vec<EndpointId> {
|
/// Env-var bootstrap (test/override path): comma-separated EndpointIds.
|
||||||
|
fn env_bootstrap_ids() -> Vec<EndpointId> {
|
||||||
std::env::var("TETHER_IROH_BOOTSTRAP")
|
std::env::var("TETHER_IROH_BOOTSTRAP")
|
||||||
.ok()
|
.ok()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -89,6 +90,32 @@ fn bootstrap_ids() -> Vec<EndpointId> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register this node with the rendezvous (cfg.server) under our room and get
|
||||||
|
/// back the current peers to bootstrap the gossip topic. Zero-config: the room
|
||||||
|
/// is a hash, content never touches the rendezvous. No-op if server isn't a URL.
|
||||||
|
async fn rendezvous_register(server: &str, room: &str, my_id: &str) -> Vec<EndpointId> {
|
||||||
|
if !(server.starts_with("http://") || server.starts_with("https://")) {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let url = format!("{}/rendezvous/register", server.trim_end_matches('/'));
|
||||||
|
let body = serde_json::json!({ "room": room, "id": my_id });
|
||||||
|
let resp = match reqwest::Client::new().post(&url).json(&body).send().await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return Vec::new(),
|
||||||
|
};
|
||||||
|
let Ok(v) = resp.json::<serde_json::Value>().await else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
v["peers"]
|
||||||
|
.as_array()
|
||||||
|
.map(|a| {
|
||||||
|
a.iter()
|
||||||
|
.filter_map(|p| p.as_str()?.parse::<EndpointId>().ok())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
impl IrohTransport {
|
impl IrohTransport {
|
||||||
/// Fetch a blob announced over gossip from its provider and surface it.
|
/// Fetch a blob announced over gossip from its provider and surface it.
|
||||||
async fn fetch_blob(blobs: Blobs, events: EventTx, ann: BlobAnnounce) {
|
async fn fetch_blob(blobs: Blobs, events: EventTx, ann: BlobAnnounce) {
|
||||||
@@ -154,8 +181,28 @@ impl Transport for IrohTransport {
|
|||||||
my_id: my_id.clone(),
|
my_id: my_id.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bootstrap: env override (tests) + the rendezvous (zero-config).
|
||||||
|
let mut boot = env_bootstrap_ids();
|
||||||
|
boot.extend(rendezvous_register(&this.cfg.server, &this.cfg.room, &my_id).await);
|
||||||
|
eprintln!("tethercore[iroh]: bootstrapping with {} peer(s)", boot.len());
|
||||||
|
|
||||||
|
// Keep re-registering so late-joining devices can bootstrap off us
|
||||||
|
// (rendezvous TTL is 90s; refresh well under it).
|
||||||
|
{
|
||||||
|
let server = this.cfg.server.clone();
|
||||||
|
let room = this.cfg.room.clone();
|
||||||
|
let id = my_id.clone();
|
||||||
|
let running = running.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while running.load(Ordering::SeqCst) {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
||||||
|
let _ = rendezvous_register(&server, &room, &id).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let topic = topic_for(&this.cfg.room);
|
let topic = topic_for(&this.cfg.room);
|
||||||
let sub = match gossip.subscribe_and_join(topic, bootstrap_ids()).await {
|
let sub = match gossip.subscribe_and_join(topic, boot).await {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("tethercore[iroh]: topic join failed: {e}");
|
eprintln!("tethercore[iroh]: topic join failed: {e}");
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+540
@@ -0,0 +1,540 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-waker"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum"
|
||||||
|
version = "0.7.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"axum-core",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"itoa",
|
||||||
|
"matchit",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustversion",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-core"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustversion",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "form_urlencoded"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"itoa",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body-util"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httparse"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpdate"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
|
"itoa",
|
||||||
|
"pin-project-lite",
|
||||||
|
"smallvec",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-util"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"hyper",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.186"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "percent-encoding"
|
||||||
|
version = "2.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.150"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_path_to_error"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.118"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sync_wrapper"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tether-rendezvous"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.52.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-layer"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-service"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing"
|
||||||
|
version = "0.1.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-core"
|
||||||
|
version = "0.1.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "tether-rendezvous"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Tiny bootstrap rendezvous for tether's iroh gossip: room → {EndpointId}. Metadata-only, content never touches it. The one thing the supernode does for zero-config discovery."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "tether-rendezvous"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = "0.7"
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "time", "signal"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# tether-rendezvous
|
||||||
|
|
||||||
|
The one piece of server tether still needs once it's on iroh: a **bootstrap
|
||||||
|
rendezvous**. To join the gossip topic a device needs to reach ≥1 peer already
|
||||||
|
in it; this hands it one. After that, everything is peer-to-peer.
|
||||||
|
|
||||||
|
It is deliberately tiny and **content-blind**:
|
||||||
|
- maps `room → {EndpointId: last_seen}` in memory, TTL 90s
|
||||||
|
- `room` is already `sha256(account)[..4]`, so the account is never exposed
|
||||||
|
- it only ever sees iroh public keys + timestamps — **never clipboard content**
|
||||||
|
(that's E2E between peers)
|
||||||
|
|
||||||
|
This is "supernode #1, the boring half." The other half is a self-hosted
|
||||||
|
`iroh-relay` (for holepunch-assist / relay fallback). Neither replaces the old
|
||||||
|
Go server's role in *content* — there is none anymore.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /rendezvous/register {"room":"<hex>","id":"<endpoint-id>"}
|
||||||
|
→ {"peers":["<other-id>", ...]} # current members minus the caller
|
||||||
|
GET /rendezvous/peers?room=<hex> → {"peers":[...]}
|
||||||
|
GET /healthz → "ok"
|
||||||
|
```
|
||||||
|
|
||||||
|
Clients register on a short interval (well under the 90s TTL) so the member list
|
||||||
|
reflects who's actually online.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --release # binds 0.0.0.0:8765
|
||||||
|
PORT=9000 cargo run --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy (homelab)
|
||||||
|
|
||||||
|
It needs to be **publicly reachable** for cross-network (cellular/CGNAT) devices
|
||||||
|
to bootstrap — same requirement as the iroh-relay. On the homelab:
|
||||||
|
|
||||||
|
1. Run it on a CT/VM (systemd unit below).
|
||||||
|
2. Front it with Caddy for TLS, e.g. `rendezvous.pecord.io`:
|
||||||
|
|
||||||
|
```
|
||||||
|
rendezvous.pecord.io {
|
||||||
|
reverse_proxy 127.0.0.1:8765
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(Plain HTTP reverse-proxy is fine — unlike the iroh-relay, there's no
|
||||||
|
UDP/QUIC path here, just small JSON over HTTPS.)
|
||||||
|
|
||||||
|
3. Point the tether clients' server field at `https://rendezvous.pecord.io`.
|
||||||
|
|
||||||
|
### systemd unit
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=tether rendezvous
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/opt/tether/tether-rendezvous
|
||||||
|
Environment=PORT=8765
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security note
|
||||||
|
|
||||||
|
Registration is unauthenticated — anyone who knows a `room` (a hash) can register
|
||||||
|
an id or enumerate members. That's metadata only and content stays E2E, but a
|
||||||
|
production deployment should add a token/auth gate and rate-limiting (the
|
||||||
|
`MAX_PER_ROOM` cap is only a flood blunt). Tracked as a follow-up.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
//! tether-rendezvous — the only thing the server still does once tether is on
|
||||||
|
//! iroh: hand a joining device one current peer so it can bootstrap into the
|
||||||
|
//! gossip topic. After that, everything is peer-to-peer.
|
||||||
|
//!
|
||||||
|
//! It maps `room → {EndpointId: last_seen}`, in memory, with a short TTL. The
|
||||||
|
//! room is already sha256(account)[..4], so the account is never exposed; and it
|
||||||
|
//! only ever sees iroh public keys + timestamps — **never content** (that's E2E
|
||||||
|
//! between peers). This is "supernode #1, the boring part"; the iroh-relay is
|
||||||
|
//! the other part.
|
||||||
|
//!
|
||||||
|
//! Run: tether-rendezvous # binds 0.0.0.0:8765
|
||||||
|
//! PORT=9000 tether-rendezvous
|
||||||
|
//! Behind Caddy at e.g. rendezvous.pecord.io. Clients point cfg.server at it.
|
||||||
|
//!
|
||||||
|
//! API:
|
||||||
|
//! POST /rendezvous/register {"room": "...", "id": "<endpoint-id>"}
|
||||||
|
//! → {"peers": ["<other-id>", ...]} (current members minus the caller)
|
||||||
|
//! GET /rendezvous/peers?room=... → {"peers": [...]}
|
||||||
|
//! GET /healthz → "ok"
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::{Query, State},
|
||||||
|
routing::{get, post},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Members are forgotten this long after their last register — so a member list
|
||||||
|
/// reflects who's actually online (clients re-register on a shorter interval).
|
||||||
|
const TTL: Duration = Duration::from_secs(90);
|
||||||
|
/// Cap members per room to blunt a junk-registration flood.
|
||||||
|
const MAX_PER_ROOM: usize = 64;
|
||||||
|
|
||||||
|
type Rooms = Arc<Mutex<HashMap<String, HashMap<String, Instant>>>>;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Register {
|
||||||
|
room: String,
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct PeersQuery {
|
||||||
|
room: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Peers {
|
||||||
|
peers: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn live_peers(rooms: &Rooms, room: &str, exclude: Option<&str>) -> Vec<String> {
|
||||||
|
let now = Instant::now();
|
||||||
|
let mut map = rooms.lock().unwrap();
|
||||||
|
let members = map.entry(room.to_string()).or_default();
|
||||||
|
members.retain(|_, seen| now.duration_since(*seen) < TTL);
|
||||||
|
members
|
||||||
|
.keys()
|
||||||
|
.filter(|id| Some(id.as_str()) != exclude)
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn register(State(rooms): State<Rooms>, Json(r): Json<Register>) -> Json<Peers> {
|
||||||
|
if !r.room.is_empty() && !r.id.is_empty() {
|
||||||
|
let now = Instant::now();
|
||||||
|
let mut map = rooms.lock().unwrap();
|
||||||
|
let members = map.entry(r.room.clone()).or_default();
|
||||||
|
members.retain(|_, seen| now.duration_since(*seen) < TTL);
|
||||||
|
if members.len() < MAX_PER_ROOM || members.contains_key(&r.id) {
|
||||||
|
members.insert(r.id.clone(), now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Json(Peers { peers: live_peers(&rooms, &r.room, Some(&r.id)) })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn peers(State(rooms): State<Rooms>, Query(q): Query<PeersQuery>) -> Json<Peers> {
|
||||||
|
Json(Peers { peers: live_peers(&rooms, &q.room, None) })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let port: u16 = std::env::var("PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(8765);
|
||||||
|
let rooms: Rooms = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/rendezvous/register", post(register))
|
||||||
|
.route("/rendezvous/peers", get(peers))
|
||||||
|
.route("/healthz", get(|| async { "ok" }))
|
||||||
|
.with_state(rooms);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await.unwrap();
|
||||||
|
println!("tether-rendezvous listening on 0.0.0.0:{port}");
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.with_graceful_shutdown(async {
|
||||||
|
let _ = tokio::signal::ctrl_c().await;
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user