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>
102 lines
3.8 KiB
Rust
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(())
|
|
}
|
|
}
|