How to Establish a Peer-to-Peer QUIC Connection with Iroh

Iroh establishes direct peer-to-peer QUIC connections by registering with a relay server, discovering external addresses via QUIC Address Discovery (QAD), and performing authenticated handshakes over UDP while maintaining a relay fallback path.

The n0-computer/iroh library provides a high-level API for building direct peer-to-peer connections over QUIC, abstracting complex NAT traversal, hole punching, and path management. This guide explains the internal machinery that enables direct QUIC connectivity between peers, even when both lie behind firewalls or NATs.

Overview of the Iroh Connection Flow

Iroh does not simply open a UDP socket. Instead, it orchestrates a multi-step process that combines relay-assisted discovery with direct QUIC handshakes. The flow involves six distinct phases: endpoint initialization, relay registration, address discovery, direct handshake, path management, and stream multiplexing.

According to the source code in iroh/src/lib.rs, the Endpoint type serves as the primary interface, while lower-level modules handle the transport specifics. The implementation relies on the noq QUIC stack for crypto handshakes and stream management.

Step-by-Step Connection Establishment

1. Endpoint Creation and QUIC Configuration

The process begins with Endpoint::builder(...).bind(), which creates a UDP socket, generates a secret key for the node, and constructs a QuicTransportConfig. This configuration is stored in the endpoint and passed to the underlying noq QUIC stack.

In iroh/src/endpoint.rs (lines 58–87), the builder assembles transport settings, while iroh/src/endpoint/quic.rs (lines 90–120) defines the default transport parameters tuned for hole punching. The iroh/src/lib.rs file exposes the high-level API that applications use to initiate this process.

2. Home-Relay Registration

Once the endpoint exists, it contacts the closest Relay server using an HTTP/1.1 connection upgraded to a custom binary protocol. The endpoint registers its EndpointId (derived from its public key), allowing the relay to forward encrypted datagrams to the node.

The relay actor maintains this persistent connection in iroh/src/socket/transports/relay/actor.rs (lines 64–84). The client-side implementation of the relay protocol resides in iroh-relay/src/client.rs, handling the protocol upgrade and registration handshake.

3. QUIC Address Discovery (QAD)

Both peers utilize the relay for QUIC Address Discovery. The relay server runs a small QUIC endpoint defined in iroh-relay/src/quic.rs (lines 1–15) that publishes each node's observed external IP and port inside QUIC-encrypted frames.

The endpoint triggers QAD probes via the network reporting module in iroh/src/net_report/probes.rs (lines 28–33). This data allows each peer to learn the direct UDP socket address of the remote peer before attempting a connection.

4. Direct QUIC Handshake and Authentication

Armed with the remote's external address, the endpoint sends a QUIC Initial packet directly to the peer, bypassing the relay. The noq stack performs a TLS 1.3 handshake where peers authenticate each other using their public keys—no X.509 certificates are required.

The connection logic is invoked through Endpoint::connect in iroh/src/lib.rs (lines 64–72). Types such as Connection, OpenBi, and RecvStream are re-exported in iroh/src/endpoint/quic.rs (lines 13–41), providing the interface for direct QUIC communication.

5. Path Selection and Relay Fallback

The endpoint maintains both a direct path and a relay path simultaneously. If the direct QUIC connection drops (for example, due to NAT mapping expiration), the endpoint instantly falls back to the relay without breaking higher-level streams.

The socket::remote_map module tracks the state of each path. In iroh/src/socket/remote_map/remote_state.rs (lines 1245–1252), the system marks connections as "direct" when a path is established. Metrics exposing these states are defined in iroh/src/socket/metrics.rs (lines 49–65), including counters for paths_direct.

Working with QUIC Streams

Once the direct QUIC connection is established, the API offers bidirectional (open_bi) and unidirectional (open_uni) streams. Streams are cheap to create and multiplex over the same QUIC connection, allowing many simultaneous messages.

The iroh/examples/listen.rs file demonstrates accepting incoming connections and reading from streams, while iroh/examples/screening-connection.rs shows opening unidirectional streams after establishing direct connectivity.

Complete Code Examples

The following examples demonstrate the high-level API that automatically triggers the direct-QUIC flow:

// Create a default endpoint that can talk to relays and perform QAD.
let ep = iroh::Endpoint::bind(iroh::endpoint::presets::N0).await?;

// Connect to a remote peer (EndpointId is a public key) using an ALPN label.
let conn = ep.connect(remote_endpoint_id, b"my-alpn").await?;

// Open a bidirectional stream, send a message and receive the reply.
let (mut send, mut recv) = conn.open_bi().await?;
send.write_all(b"hello").await?;
send.finish().await?;
let reply = recv.read_to_end(1024).await?;
println!("got: {}", String::from_utf8_lossy(&reply));
// Server side: accept incoming connections and read from a stream.
let ep = iroh::Endpoint::builder(iroh::endpoint::presets::N0)
    .alpns(vec![b"my-alpn".to_vec()])
    .bind()
    .await?;

let incoming = ep.accept().await?.await?;          // a `Connection`
let (mut send, mut recv) = incoming.accept_bi().await?;
let data = recv.read_to_end(1024).await?;
println!("peer sent: {}", String::from_utf8_lossy(&data));
send.write_all(b"world").await?;
send.finish().await?;

These snippets hide the complexity of NAT traversal, QUIC configuration, and path management behind simple bind and connect calls.

Summary

  • Use Endpoint::bind with presets to initialize the QUIC stack, UDP socket, and secret key generation.
  • Register with a home relay automatically to ensure reachability behind NATs and firewalls.
  • Rely on QUIC Address Discovery (QAD) to learn peer external addresses before attempting direct connections.
  • Authenticate via TLS 1.3 using public keys (EndpointId) instead of X.509 certificates.
  • Maintain parallel paths with automatic fallback from direct UDP to relay if connectivity changes.

Frequently Asked Questions

What is QUIC Address Discovery (QAD) in Iroh?

QUIC Address Discovery is a protocol where the relay server publishes observed external IP and port information inside QUIC-encrypted frames. Peers query this service via the relay to learn each other's direct UDP addresses before initiating a connection, enabling successful hole punching through NATs.

How does Iroh handle NAT traversal without manual configuration?

Iroh combines relay-assisted connectivity with automatic hole punching. The endpoint registers with a relay server for initial reachability, discovers its external address via QAD, and attempts direct UDP connections. If the direct path fails, it falls back to the relay transparently, all without requiring manual port forwarding or STUN server configuration.

What is the role of the relay server in Iroh connections?

The relay server serves two critical functions: it provides a fallback path for encrypted traffic when direct UDP connectivity is impossible, and it hosts the QUIC Address Discovery endpoint that helps peers find each other's public addresses. The relay never sees plaintext data, as all traffic remains encrypted end-to-end.

How does Iroh authenticate peers without X.509 certificates?

Iroh uses the noq QUIC stack to perform a TLS 1.3 handshake where authentication is based on the peers' public keys (EndpointId) rather than traditional X.509 certificates. This eliminates certificate management overhead while providing cryptographic verification of peer identity.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →