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>
This commit is contained in:
@@ -313,8 +313,15 @@ and crypto and getting it subtly wrong.
|
||||
`connect()` ≈ the dial path; `connection.remote_id()` is a durable
|
||||
per-device public key — identity *is* the transport (no separate
|
||||
keypair/Noise layer needed, cf. §3).
|
||||
- [ ] Milestone 2 — cross-network holepunch via a relay (two machines);
|
||||
stand up the homelab as a self-hosted iroh relay (supernode #1).
|
||||
- [x] **Milestone 2 — n0's hosted relay carries us.**
|
||||
`examples/iroh_relay.rs`: connect side with address lookup OFF + a
|
||||
relay-only `EndpointAddr` → the connection *had* to bootstrap through
|
||||
n0's relay (`use1-1.relay.n0.iroh.link`). `remote_info` then showed a
|
||||
DCUtR upgrade to a direct path (relay + direct both Active) — relay
|
||||
bootstrap *and* holepunch upgrade, on one machine.
|
||||
- [ ] Milestone 2b — true cross-NAT between two machines on different
|
||||
networks; then stand up the homelab as a self-hosted iroh relay
|
||||
(supernode #1) and repeat against it instead of n0's.
|
||||
- [ ] Milestone 3 — discovery: map account/room → `EndpointId`
|
||||
(`iroh-gossip` topic, or DNS/pkarr), replacing the SSE presence bus.
|
||||
- [ ] Milestone 4 — wrap as `IrohTransport` behind the `Transport`/
|
||||
|
||||
101
core/examples/iroh_relay.rs
Normal file
101
core/examples/iroh_relay.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
//! 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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user