Files
tether/core/examples/iroh_p2p.rs
Patrick Ecord 6a96046a9c core: iroh spike milestone 2b — cross-NAT two-machine tester
examples/iroh_p2p.rs: `accept` prints this node's EndpointId and echoes any
stream; `connect <id>` resolves the peer by id via discovery (no addresses),
round-trips, and reports DIRECT (holepunch worked) vs RELAY (fallback) from
remote_info. This is the last unknown before M4 — M1/M2/M3 all ran same-host;
this proves real cross-NAT.

Same-host smoke test passes (DIRECT over loopback). Awaiting a genuine
two-network run (Mac on wifi ⇄ laptop on phone hotspot).

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

121 lines
4.8 KiB
Rust

//! Iroh spike, milestone 2b — cross-NAT holepunch on TWO machines / networks.
//! Machine A (e.g. Mac on home wifi):
//! cargo run --example iroh_p2p -- accept
//! → prints this node's EndpointId, then waits, echoing any stream.
//! Machine B (e.g. laptop on phone hotspot / a different network):
//! cargo run --example iroh_p2p -- connect <ENDPOINT_ID_FROM_A>
//! → resolves A by id (n0 discovery), connects, round-trips, and prints
//! whether the path is DIRECT (holepunch worked) or RELAY (fell back).
//!
//! This is the last unknown before committing to iroh (M4): the M1/M2/M3 spikes
//! all ran same-host. Here the two endpoints are on different NATs, so a DIRECT
//! path in `remote_info` proves real holepunch; RELAY proves the fallback floor.
//! Either way the round-trip must succeed — that's the cross-network sync we
//! verified on the hand-rolled stack, now on iroh.
use std::time::Duration;
use iroh::{
endpoint::{presets, Connection},
protocol::{AcceptError, ProtocolHandler, Router},
Endpoint, EndpointAddr, EndpointId,
};
const ALPN: &[u8] = b"tether/iroh-p2p/0";
const PAYLOAD: &[u8] = b"tether cross-NAT round-trip over iroh";
fn ae<E: std::fmt::Display>(e: E) -> anyhow::Error {
anyhow::anyhow!("{e}")
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mode = std::env::args().nth(1).unwrap_or_default();
match mode.as_str() {
"accept" => accept().await,
"connect" => {
let id = std::env::args()
.nth(2)
.ok_or_else(|| anyhow::anyhow!("usage: iroh_p2p connect <ENDPOINT_ID>"))?;
connect(&id).await
}
_ => {
eprintln!("usage:\n iroh_p2p accept\n iroh_p2p connect <ENDPOINT_ID>");
std::process::exit(2);
}
}
}
async fn accept() -> anyhow::Result<()> {
let endpoint = Endpoint::bind(presets::N0).await.map_err(ae)?;
let router = Router::builder(endpoint.clone())
.accept(ALPN, Echo)
.spawn();
endpoint.online().await;
println!("════════════════════════════════════════════════════════════");
println!(" accept side ready. On the OTHER machine/network, run:");
println!(" cargo run --example iroh_p2p -- connect {}", endpoint.id());
println!("════════════════════════════════════════════════════════════");
println!("(waiting for a connection — Ctrl-C to stop)");
std::future::pending::<()>().await; // run until killed
let _ = router;
Ok(())
}
async fn connect(id_str: &str) -> anyhow::Result<()> {
let target: EndpointId = id_str
.trim()
.parse()
.map_err(|_| anyhow::anyhow!("invalid EndpointId: {id_str:?}"))?;
let endpoint = Endpoint::bind(presets::N0).await.map_err(ae)?;
endpoint.online().await;
println!("[p2p] dialing {target} (resolving address via discovery)…");
// Address-less target → iroh's discovery (n0 DNS/pkarr) resolves it, then
// holepunches; falls back to relay if the NATs won't cooperate.
let conn = endpoint
.connect(EndpointAddr::new(target), 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)?;
anyhow::ensure!(echoed == PAYLOAD, "echo mismatch");
// Give holepunch a moment to upgrade past the initial path, then report.
tokio::time::sleep(Duration::from_secs(1)).await;
let path = match endpoint.remote_info(target).await {
Some(info) => format!("{info:?}"),
None => "(no remote_info)".to_string(),
};
let direct = path.contains("Ip(") && path.contains("Active");
println!(
"✅ cross-NAT round-trip OK ({} bytes). Path: {}\n remote_info: {}",
echoed.len(),
if direct { "DIRECT (holepunch worked)" } else { "RELAY (fallback floor)" },
path
);
conn.close(0u32.into(), b"done");
endpoint.close().await;
Ok(())
}
/// Echo the first stream back (stands in for emitting MeshEvents).
#[derive(Debug, Clone)]
struct Echo;
impl ProtocolHandler for Echo {
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
println!("[p2p] inbound connection from {}", connection.remote_id());
let (mut send, mut recv) = connection.accept_bi().await?;
let n = tokio::io::copy(&mut recv, &mut send).await?;
println!("[p2p] echoed {n} byte(s)");
send.finish()?;
connection.closed().await;
Ok(())
}
}