# How iroh Establishes Direct P2P QUIC Connections Between Endpoints

> iroh establishes direct P2P QUIC connections with NAT traversal, prioritizing direct paths and falling back to relays when required. Discover how iroh ensures seamless P2P communication.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: how-to-guide
- Published: 2026-06-22

---

**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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs) module contains the `trigger_holepunching` function (lines 504–523 in [`iroh/src/socket/remote_map/remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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:

1. **Caller supplies direct addresses** via `EndpointAddr`, optionally including a relay URL for fallback.
2. `Endpoint::connect` creates a `Connecting` future that first attempts direct addresses using the noq transport.
3. If the QUIC handshake succeeds, the connection immediately becomes usable as a `Connection` object.
4. If direct attempts fail (NAT blocks or firewall rules), iroh falls back to the relay connection (still via QUIC).
5. While the relay connection handles traffic, the endpoint's **hole-punch loop** runs in [`remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs).
6. Upon successful NAT traversal, a new direct `Path` is inserted into the path list, the relay dependency is removed for that peer, and subsequent streams flow directly.

### Example: Direct Connection with Known Addresses

```rust
// 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

```rust
// 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

```rust
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 `EndpointAddr` before 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_holepunching` in [`remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs) managing 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`](https://github.com/n0-computer/iroh/blob/main/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.