# How to Establish Direct Peer-to-Peer Connections Without Relay Servers in Iroh

> Learn how to establish direct peer to peer connections in Iroh by disabling relay servers. Force UDP hole punching and bypass fallback for faster, more efficient P2P.

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

---

**To establish direct peer-to-peer connections without relay servers in iroh, configure the `Endpoint` with `RelayMode::Disabled` to force pure UDP hole-punching and bypass the relay fallback layer.**

The iroh networking stack provides a robust QUIC-based peer-to-peer communication layer that defaults to relay-assisted NAT traversal. For scenarios requiring guaranteed direct connections—whether for privacy, latency, or network isolation—you can disable relay servers entirely and rely solely on UDP hole-punching between peers.

## Architecture Overview

Iroh's networking layer is built on QUIC (via the `noq` library) with a custom relay protocol for fallback. By default, an endpoint attempts to **hole-punch** a direct UDP path and only falls back to public relays when that path fails (e.g., due to symmetric NATs or strict firewalls).

Key components in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) govern this behavior:

- **`Endpoint`** – The main API providing `builder`, `bind`, `connect`, and `accept` methods for connection management
- **`RelayMode`** – Configuration enum at line 1912 that controls relay usage, including the `Disabled` variant
- **PathSelector** – Internal logic that prefers direct IP paths over relay paths when both are available

When `RelayMode::Disabled` is set, the endpoint does not contact relay servers for home-relay lookups, use HTTPS-based relay protocols, or advertise relay URLs in its `EndpointAddr`.

## How Direct Hole-Punching Works

Direct connections rely on the underlying QUIC implementation (`noq`) sending packets over UDP sockets. The process follows these steps:

1. The endpoint binds local UDP sockets via `Endpoint::bind` (creating a random socket by default)
2. Peers exchange their public UDP addresses through out-of-band signaling (e.g., manual configuration or application-specific discovery)
3. The `noq` library performs STUN-style NAT traversal probing to establish a direct path
4. The `PathSelector` chooses the direct IP route since no relay candidates exist when relays are disabled

If the remote addresses are reachable across the network (both peers share the same NAT or have public IPs), the connection succeeds. Otherwise, `connect` returns a `ConnectError::Noq` error.

## Implementation Guide

To force pure direct connections, you must disable relays during endpoint construction on both sides of the connection.

### Listener Configuration

Create a listener that accepts direct connections without relay advertisement:

```rust
use iroh::{Endpoint, RelayMode, endpoint::presets, TransportAddr};
use tracing::info;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Build an endpoint that will *not* use any relay servers.
    let endpoint = Endpoint::builder(presets::N0)
        .relay_mode(RelayMode::Disabled)   // <‑‑ crucial line
        .bind()
        .await?;

    // Wait until the endpoint is online (i.e. sockets are bound).
    endpoint.online().await;

    // Print our public key – peers need this to address us.
    println!("My endpoint id: {}", endpoint.id());

    // Accept a single incoming connection on a custom ALPN.
    let alpn = b"my-app/0";
    let (conn, _) = endpoint.accept(alpn).await?;
    info!("direct connection accepted");

    // Echo whatever the peer sends.
    let (mut send, mut recv) = conn.open_bi().await?;
    tokio::io::copy(&mut recv, &mut send).await?;
    Ok(())
}

```

Source: [`iroh/examples/listen.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/listen.rs)

### Connector Configuration

Connect to a peer using only their direct UDP addresses:

```rust
use std::{net::SocketAddr, sync::Arc};
use clap::Parser;
use iroh::{
    Endpoint, EndpointAddr, RelayMode, SecretKey, TransportAddr, endpoint::presets,
};
use tracing::info;

#[derive(Debug, Parser)]
struct Cli {
    /// Remote endpoint public key
    #[clap(long)]
    endpoint_id: iroh::EndpointId,
    /// Remote UDP addresses (space‑separated)
    #[clap(long, value_delimiter = ' ', num_args = 1..)]
    addrs: Vec<SocketAddr>,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let args = Cli::parse();

    // Create a local endpoint; again we disable relays.
    let endpoint = Endpoint::builder(presets::N0)
        .relay_mode(RelayMode::Disabled)   // <‑‑ crucial line
        .bind()
        .await?;

    // Build the address that the remote advertised (only UDP transports).
    let transports = args
        .addrs
        .into_iter()
        .map(TransportAddr::Ip)
        .collect::<Vec<_>>();
    let remote = EndpointAddr::from_parts(args.endpoint_id, transports);

    // Try to open a direct QUIC connection.
    let alpn = b"my-app/0";
    let conn = endpoint.connect(remote, alpn).await?;
    info!("direct connection established");

    // Send a greeting.
    let (mut send, mut recv) = conn.open_bi().await?;
    send.write_all(b"hello from client").await?;
    send.finish().await?;

    // Receive the echo.
    let mut buf = Vec::new();
    recv.read_to_end(&mut buf).await?;
    println!("peer replied: {}", String::from_utf8_lossy(&buf));

    endpoint.close().await;
    Ok(())
}

```

Source: [`iroh/examples/connect.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/connect.rs)

### Verification Steps

To verify a pure-direct connection:

1. Run the listener and note the printed endpoint ID and bound UDP port
2. Execute the connector with the listener's endpoint ID and UDP address
3. Confirm successful `connect` completion without relay timeout delays

The connection will succeed only if the UDP ports are mutually reachable; otherwise, it fails immediately with `ConnectError::Noq` rather than falling back to a relay.

## Testing and Validation

The iroh repository includes comprehensive NAT testing in [`iroh/tests/patchbay/nat.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/nat.rs). The `nat_none_x_none` test case specifically validates direct peer-to-peer connections without relay involvement, simulating environments where both endpoints have public IP addresses or exist on the same local network.

For manual verification, ensure your firewall rules allow inbound UDP on the bound ports. The `add_external_addr` method at line 939 in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) handles the advertisement of external UDP sockets that enable direct traversal.

## When to Use Direct-Only Mode

**Appropriate scenarios** for `RelayMode::Disabled` include:

- **Corporate LANs or VPNs** where you own the network infrastructure and can guarantee UDP reachability
- **Privacy-sensitive environments** where relay traffic is undesirable
- **Low-latency requirements** where relay hop introduces unacceptable delay
- **Testing and debugging** where you must guarantee traffic never leaves the local network

**Critical caveats**:

- If any NAT or firewall blocks inbound UDP on the advertised ports, the connection fails outright with no fallback
- Symmetric NAT configurations cannot be traversed without relay assistance; these scenarios require `RelayMode::Default` or a custom relay map
- Both peers must use `RelayMode::Disabled` to ensure neither attempts relay discovery

## Summary

- **Disable relays** by setting `.relay_mode(RelayMode::Disabled)` on the `Endpoint::builder` to force direct connections
- **Direct connections** rely on UDP hole-punching via the `noq` QUIC implementation without third-party relay assistance
- **Failure mode** changes from relay fallback to immediate `ConnectError::Noq` when the direct path is unreachable
- **Configuration** requires both peers to disable relays and exchange `EndpointAddr` information containing direct UDP transport addresses
- **Validation** is available through the `patchbay` test suite, specifically the `nat_none_x_none` scenario

## Frequently Asked Questions

### What happens if a direct connection fails when RelayMode::Disabled is set?

The `connect` call returns a `ConnectError::Noq` error immediately. Unlike the default configuration, which falls back to a relay server after attempting hole-punching, disabled relay mode provides no fallback mechanism. You must handle this error by either retrying with different addresses or reconfiguring the endpoint with relay support enabled.

### Does disabling relays affect the security of the connection?

No. The security model remains identical because iroh uses QUIC's built-in TLS 1.3 encryption for all connections regardless of transport path. Disabling relays only removes the relay server from the network path; the cryptographic handshake and certificate validation between peers function identically whether traversing directly or through a relay.

### Can I use direct connections across the public internet?

Yes, but only if both peers have public IP addresses or can traverse their respective NATs via hole-punching. If either peer sits behind a symmetric NAT or strict firewall that blocks unsolicited UDP packets, the direct connection will fail. For reliable public internet connectivity, retain the default relay configuration as a fallback.

### How do I verify that no relay is being used for an active connection?

Monitor the connection's `ConnectionInfo` metadata, which reports the selected network path. When operating in direct mode, the path will show a direct IP route rather than a relay URL. Additionally, network traffic analysis will show UDP packets flowing directly between peer endpoints on their bound ports, with no HTTPS traffic to relay servers.