Skip to content
minip2p
Esc
navigateopen⌘Jpreview
On this page

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.0

The 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

Known information Method
Peer ID only; rely on configured relay connect(&peer_id)
One direct PeerAddr, optionally raced against relays connect_addr(&peer_addr)
Peer ID and several transport Multiaddr candidates connect_with_addrs(peer_id, addrs)
use std::time::Duration;

use minip2p::NatEvent;

let connect_id = node.connect_addr(&target)?;
match node.wait_path(connect_id, Duration::from_secs(60))? {
    Some(path) => println!("connected via {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"),
        }
    }
}

wait_path returns Ok(Some(path)) when it consumes the matching NatEvent::PathEstablished. It returns Ok(None) in two cases:

  • the attempt failed; NatEvent::ConnectFailed stays queued for take_nat_events;
  • the deadline expired before either outcome; no ConnectFailed is 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.

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

wait_path returns when traffic can flow. Keep calling next_event, poll, or feature-focused waits afterward so DCUtR, reservation renewal, and connection events continue to progress.

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.

Last updated on September 7, 2026

Was this page helpful?