Files
tether/core/examples/iroh_relay.rs
Patrick Ecord 1788bf7a37 core: iroh spike milestone 2 — prove n0's hosted relay carries us
examples/iroh_relay.rs forces the relay path on one machine: the connect side
turns address lookup OFF and dials a relay-only EndpointAddr (id + home relay
URL, no direct IPs), so the connection can only bootstrap through n0's hosted
relay. Verified round-trip via use1-1.relay.n0.iroh.link; remote_info then shows
a DCUtR upgrade to a direct path (relay + direct both Active) — so this proves
both the relay bootstrap and the holepunch upgrade in one run.

Remaining for cross-network confidence (milestone 2b): two machines on different
networks, then repeat against a self-hosted homelab relay (supernode #1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 00:01:45 -05:00

102 lines
3.8 KiB
Rust

//! Iroh spike, milestone 2 — prove n0's HOSTED RELAY carries our traffic.
//! cargo run --example iroh_relay
//!
//! Milestone 1 (iroh_spike.rs) connected two endpoints on loopback — that could
//! have gone direct. This forces the relay path so we actually exercise n0's
//! hosted infra (the reliability floor that makes cross-network work when
//! holepunch fails):
//! - the connecting endpoint has address lookup turned OFF, so it CANNOT
//! discover the server's direct socket addresses, and
//! - it is handed a RELAY-ONLY EndpointAddr (id + home relay URL, no IPs).
//! With no other path available, a successful round-trip can only have traversed
//! n0's relay. `remote_info` is printed to show the path actually in use.
//!
//! Honest scope: this proves the *relay carries data*, on one machine. It does
//! NOT prove cross-NAT holepunch+upgrade — that needs two machines on different
//! networks (milestone 2b, and the homelab-as-relay step).
use iroh::{
endpoint::{presets, Connection},
protocol::{AcceptError, ProtocolHandler, Router},
Endpoint, EndpointAddr,
};
const ALPN: &[u8] = b"tether/iroh-relay/0";
const PAYLOAD: &[u8] = b"tether clipboard, carried over n0's hosted relay";
fn ae<E: std::fmt::Display>(e: E) -> anyhow::Error {
anyhow::anyhow!("{e}")
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// ── accept side: full N0 (relay + address publishing) ────────────────────
let server = Endpoint::bind(presets::N0).await.map_err(ae)?;
let server_id = server.id();
let router = Router::builder(server).accept(ALPN, Echo).spawn();
router.endpoint().online().await;
let relay = router
.endpoint()
.addr()
.relay_urls()
.next()
.cloned()
.ok_or_else(|| anyhow::anyhow!("accept side has no home relay assigned"))?;
println!("[relay] accept side online — id={server_id}\n home relay = {relay}");
// ── connect side: n0 relay kept, address lookup OFF, relay-only target ────
let client = Endpoint::builder(presets::N0)
.clear_address_lookup()
.bind()
.await
.map_err(ae)?;
client.online().await;
let relay_only = EndpointAddr::new(server_id).with_relay_url(relay);
println!(
"[relay] connect side id={} — dialing RELAY-ONLY (no direct addrs, lookup off)",
client.id()
);
let conn = client.connect(relay_only, ALPN).await.map_err(ae)?;
let (mut send, mut recv) = conn.open_bi().await.map_err(ae)?;
send.write_all(PAYLOAD).await.map_err(ae)?;
send.finish().map_err(ae)?;
let echoed = recv.read_to_end(64 * 1024).await.map_err(ae)?;
// Show the path actually negotiated (relay vs any holepunched direct upgrade).
if let Some(info) = client.remote_info(server_id).await {
println!("[relay] remote_info after round-trip: {info:?}");
}
conn.close(0u32.into(), b"done");
client.close().await;
router.shutdown().await.map_err(ae)?;
anyhow::ensure!(
echoed == PAYLOAD,
"echo mismatch: {:?}",
String::from_utf8_lossy(&echoed)
);
println!(
"✅ n0 hosted-relay round-trip: {} bytes (address lookup off + relay-only addr → \
the relay had to carry it)",
echoed.len()
);
Ok(())
}
/// Echo the first stream's bytes back (stands in for emitting MeshEvents).
#[derive(Debug, Clone)]
struct Echo;
impl ProtocolHandler for Echo {
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
let (mut send, mut recv) = connection.accept_bi().await?;
let n = tokio::io::copy(&mut recv, &mut send).await?;
println!("[relay] echoed {n} byte(s) back to {}", connection.remote_id());
send.finish()?;
connection.closed().await;
Ok(())
}
}