Traverse NAT
Configure relay and AutoNAT, establish the first usable path, and observe direct upgrades or relay fallback.
This guide starts where direct listen and dial leaves off. It configures relays and AutoNAT, races direct and relayed paths, and tracks later upgrades or fallback. Relayed paths require a reachable Circuit Relay v2 server, which minip2p can host.
pre-1.0The nat feature adds an orchestrator that can race a direct QUIC or TCP dial against a Circuit Relay v2 path. DCUtR hole punching still needs QUIC.
Host the relay
The independent std-only relay-server feature hosts Circuit Relay v2 over QUIC, TCP, or both. The bundled operator example uses both:
cargo run -p minip2p-relay-server-example -- \
--key var/relay.ed25519 \
--announce /dns4/relay.example.com/udp/19876/quic-v1 \
--announce /dns4/relay.example.com/tcp/19876
Its default is the small .relay_server().bind_*() application path. Optional flags expose frozen resource/rate/control limits, and stdin pause/resume changes only new admissions. Explicit addresses are trusted operator input; otherwise selection prefers AutoNAT-confirmed direct addresses and then concrete listeners. Address changes affect future Identify responses only. There is no automatic relay discovery: distribute the printed peer address to clients explicitly. See the hosting guide.
Enable NAT traversal
cargo add minip2p-rs --features nat
Builder configuration determines the available behavior:
| Builder configuration | What it enables |
|---|---|
.relay(relay) |
Relay connects, reservations, and relay-assisted DCUtR |
.autonat_server(server) |
Reachability probes only, unless a relay is also configured |
.nat_config(config) |
Explicit timeouts, retries, relays, probes, and reservation policy |
.discovery() |
NAT coordination as part of discovery, with whatever relay/probe infrastructure is also configured |
use minip2p::{Endpoint, PeerAddr};
let relay: PeerAddr = std::env::var("MINIP2P_RELAY")?.parse()?;
let mut node = Endpoint::builder()
.relay(relay)
.bind_quic_dual_stack()?;
node.listen_all()?;
Calling .autonat_server(...) alone enables reachability probes, but it does not create a relay leg. Configure a relay for relayed connections, inbound reservations, and relay-assisted hole punching.
Choose a connect method
Endpoint::connect is the one entry. Pass a peer ID, one PeerAddr, or a set of addresses that all name the same peer:
| Known information | Call |
|---|---|
| Peer ID; known addresses and/or a configured relay | connect(&peer_id) |
One complete PeerAddr |
connect(&peer_addr) |
Several complete PeerAddrs for the same peer |
connect(addrs)? |
A Peer-ID target with neither known addresses nor a relay settles ConnectSettled { Failed(NoUsableRoute) } through one terminal event.
use std::time::Duration;
use minip2p::{EndpointEvent, NatEvent};
let connect_id = node.connect(&target)?;
match node.nat_wait_path(connect_id, Duration::from_secs(60))? {
Some(path) => println!("first usable path: {path:?}"),
None => {
let failed = node.take_nat_events().into_iter().find(|event| {
matches!(
event,
NatEvent::ConnectFailed { connect_id: id, .. } if *id == connect_id
)
});
match failed {
Some(NatEvent::ConnectFailed { error, .. }) => {
eprintln!("connect failed: {error:?}");
}
_ => eprintln!("no usable path before the deadline"),
}
}
}
nat_wait_path is a migration helper until #181. It returns Ok(Some(path)) when it consumes the matching NatEvent::PathEstablished — the first usable path, which may be a provisional Relayed circuit. The attempt’s terminal is EndpointEvent::ConnectSettled (connected after DCUtR settles, or immediately under force_relay). It returns Ok(None) in two cases:
- the attempt failed;
NatEvent::ConnectFailedstays queued fortake_nat_eventswhen the NAT leg failed (a Peer target with no route may settle only asConnectSettled); - the deadline expired before either outcome; no
ConnectFailedis synthesized.
Path selection and upgrade
The first usable path is explicit:
Path::DirectDialed: a direct candidate connected;Path::DirectPunched: a DCUtR hole punch connected;Path::Relayed { relay }: the protected relay circuit is usable.
That Relayed path is provisional until ConnectSettled. Applications that need the circuit immediately (echo, ping) can use it as soon as path(peer) is Relayed; the attempt is not finished until DCUtR settles or force_relay skips punching. When a relayed path upgrades later, the application receives NatEvent::PathUpgraded. If punch windows fail, it receives HolePunchFailed events and eventually FellBackToRelay; the relayed connection stays usable.
Reservations and reachability
A private listener needs a reservation before another peer can reach it through the relay. Watch for:
for event in node.take_nat_events() {
match event {
minip2p::NatEvent::RelayReserved { relay, .. } => {
println!("relay reservation ready: {relay}");
}
minip2p::NatEvent::ReachabilityChanged { new, .. } => {
println!("reachability={new:?}");
}
_ => {}
}
}
node.reachability() reports the current AutoNAT verdict. node.active_reservation() returns the currently held reservation, if any. AutoNAT servers are caller-supplied; minip2p does not discover them automatically.
Keep driving after the first path
nat_wait_path returns when traffic can flow. Keep calling focused waits, next_wake, next_event, or poll afterward so DCUtR, reservation renewal, and connection events continue to progress. Each call drives only its own endpoint, so blocking on one endpoint can delay others sharing the same thread.
Custom entropy without std
Most applications do not configure circuit entropy: Endpoint uses the operating system’s random source by default. When embedding minip2p-circuit with default features disabled, provide an EntropySource that never substitutes predictable bytes on failure. See the minip2p-circuit README for the trait contract and a custom-source example.
For a complete live demonstration, use the existing minip2p-peer example. It shows loopback, relay reservations, relay fallback, and RTT changes after a direct upgrade.