How iroh Establishes Direct P2P QUIC Connections Between Endpoints
iroh establishes direct peer-to-peer QUIC connections by attempting direct socket addresses first, falling back to relay connections when necessary, and continuously running the QUIC NAT Traversal Extension to punch holes through NATs and migrate traffic to direct paths automatically.
The iroh networking stack from n0-computer/iroh provides robust peer-to-peer connectivity by combining intelligent address selection, the noq QUIC transport, and active NAT traversal. When you invoke Endpoint::connect, the system attempts to establish direct P2P QUIC connections through a coordinated three-layer architecture that prioritizes low-latency paths while seamlessly handling complex network topologies.
Address Selection with EndpointAddr
Every connection attempt begins with an EndpointAddr, a structure that encapsulates the remote peer's EndpointId, an optional RelayUrl, and a list of direct socket addresses (IPv4/IPv6). When you call Endpoint::connect or Endpoint::connect_with_opts, iroh extracts these direct addresses and attempts them before considering relay fallback.
In iroh/src/endpoint.rs (lines 4100–4150), the connect_with_opts method maps the logical address to physical socket addresses via mapped_addr.private_socket_addr(). This address list represents the candidate direct paths to the remote peer.
Prioritizing Direct Paths
The connection logic explicitly prefers direct addresses over relay URLs. If the caller provides known public IPs—obtained from iroh DNS servers or previous address discovery—the endpoint immediately attempts a QUIC handshake to these locations. Only when direct attempts fail or timeout does the system escalate to the relay URL specified in the EndpointAddr.
QUIC Transport and Connection Establishment
For the actual packet handling, iroh uses the noq library, a Rust QUIC implementation defined in iroh/src/endpoint/quic.rs. Once address selection completes, the chosen direct address is passed to noq_endpoint().connect_with(...) in iroh/src/endpoint.rs (lines 4130–4150).
This call creates a client QUIC connection with the remote peer's public key baked into the TLS server name via tls::name::encode. This cryptographic binding ensures that the connection authenticates directly to the expected endpoint identity, preventing man-in-the-middle attacks even during the initial handshake.
NAT Traversal and Hole-Punching
When the remote peer resides behind a NAT, iroh employs the QUIC NAT Traversal Extension (draft-seemann-quic-nat-traversal). This protocol allows peers to exchange observed address reports and validation tokens configured via QuicTransportConfig::max_remote_nat_traversal_addresses.
The remote_state.rs module contains the trigger_holepunching function (lines 504–523 in iroh/src/socket/remote_map/remote_state.rs), which periodically initiates hole-punch attempts. Successful hole-punching creates a new direct path that is immediately advertised to the peer, enabling subsequent traffic to bypass the relay entirely.
Background Hole-Punch Loop
While a connection operates over a relay path, a dedicated actor runs continuously in the background. This actor monitors network conditions and attempts to establish direct connectivity through coordinated port prediction and validation token exchange. Once the NAT traversal succeeds, the new direct path becomes available for immediate use without disrupting existing streams.
Path Management and Selection
The remote_map module maintains a dynamic list of candidate paths for each peer, including relay, direct, and hole-punched options. After validating a direct path, the PathSelector—referenced in iroh/src/endpoint.rs (lines 16–22)—promotes it to the selected path status.
The selection logic prefers direct, low-latency paths over relayed routes. Once a direct path achieves validation, all new streams automatically migrate to it, though existing relay connections remain active until the direct path proves stable.
Connection Flow in Practice
The complete lifecycle of establishing direct P2P QUIC connections follows this sequence:
- Caller supplies direct addresses via
EndpointAddr, optionally including a relay URL for fallback. Endpoint::connectcreates aConnectingfuture that first attempts direct addresses using the noq transport.- If the QUIC handshake succeeds, the connection immediately becomes usable as a
Connectionobject. - If direct attempts fail (NAT blocks or firewall rules), iroh falls back to the relay connection (still via QUIC).
- While the relay connection handles traffic, the endpoint's hole-punch loop runs in
remote_state.rs. - Upon successful NAT traversal, a new direct
Pathis inserted into the path list, the relay dependency is removed for that peer, and subsequent streams flow directly.
Example: Direct Connection with Known Addresses
// Connect using known direct IPs (no relay)
let ep = iroh::Endpoint::builder(presets::N0)
.alpns(vec![b"my-alpn".to_vec()])
.bind()
.await?;
// Assume we already know the remote's public key and its public IPv4 address.
let remote_id = iroh_base::EndpointId::from_verifying_key(...);
let remote_addr = iroh_base::EndpointAddr::from_parts(
remote_id,
vec![iroh_base::TransportAddr::Ip("203.0.113.42:4200".parse().unwrap())],
);
let conn = ep.connect(remote_addr, b"my-alpn").await?; // Direct QUIC handshake
Example: Relay Fallback with Automatic Hole-Punching
// Add an external address that will be advertised for NAT traversal.
// This is useful when you are behind a NAT and want peers to hole-punch.
ep.add_external_addr("198.51.100.23:4210".parse().unwrap()).await;
// Connect via relay, then let built-in hole-punching establish a direct path.
let remote = iroh_base::EndpointAddr::from_parts(
remote_id,
vec![iroh_base::TransportAddr::Relay("https://use1-1.relay.n0.iroh.link/".parse().unwrap())],
);
let conn = ep.connect(remote, b"my-alpn").await?; // Starts on relay
// Background hole-punch runs; after a few seconds `conn.paths()` will contain a direct IP path.
Example: Inspecting Active Paths
use iroh::endpoint::PathId;
let paths = conn.paths(); // PathList snapshot
for (id, info) in paths.iter() {
println!("Path {}: {:?} (is_direct = {})", id, info, info.is_direct());
}
Summary
- iroh prioritizes direct P2P QUIC connections by attempting socket addresses from
EndpointAddrbefore using relays. - The noq library handles QUIC transport, embedding the remote's public key into the TLS handshake for authentication.
- NAT traversal operates via the QUIC NAT Traversal Extension, with
trigger_holepunchinginremote_state.rsmanaging the hole-punch lifecycle. - The PathSelector automatically migrates traffic from relay paths to validated direct paths with lower latency.
- Connection attempts fallback gracefully from direct to relay, then upgrade to direct once hole-punching succeeds, ensuring robust connectivity across network conditions.
Frequently Asked Questions
What happens if direct connection attempts fail?
If direct addresses provided in the EndpointAddr are unreachable, iroh automatically falls back to the relay URL specified in the same address structure. While the relay connection maintains traffic flow, the background hole-punching actor continues attempting NAT traversal. Once successful, traffic migrates to the new direct path without requiring reconnection.
How does iroh handle NAT traversal without manual configuration?
iroh implements the QUIC NAT Traversal Extension (draft-seemann-quic-nat-traversal) automatically. Peers exchange observed address reports and validation tokens through the relay connection. The remote_state.rs actor schedules periodic hole-punch attempts using trigger_holepunching, requiring no manual port forwarding or network configuration from users.
Can I force iroh to use only direct connections?
Yes. When constructing the EndpointAddr, omit the RelayUrl and provide only direct socket addresses in the address list. In this configuration, Endpoint::connect will attempt only the specified direct addresses and fail if they are unreachable, never falling back to a relay. Ensure the remote peer has properly configured external address advertisement via add_external_addr if they are behind a NAT.
Which QUIC implementation does iroh use?
According to the iroh source code, the project uses the noq library for QUIC transport. This implementation handles the cryptographic handshake, stream multiplexing, and path migration required for direct P2P communication, including the TLS server name encoding that binds connections to specific endpoint identities.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →