# How iroh Peer Discovery Works: Architecture and Implementation

> Learn how iroh handles peer discovery using a dual-layer architecture with pluggable address-lookup services and QUIC Address Discovery (QAD). Understand iroh's unique approach to finding peers.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: architecture
- Published: 2026-07-04

---

**iroh discovers peers through a dual-layer architecture that combines pluggable address-lookup services for publishing and resolving endpoint addresses, and QUIC Address Discovery (QAD) for learning public IPs when only relay URLs are known.**

iroh is a Rust-based peer-to-peer networking stack that uses public keys as stable endpoint identifiers. When an endpoint needs to establish a connection to a remote peer identified only by its `EndpointId`, the iroh peer discovery system resolves this identifier into usable network addresses (IP:port combinations or relay URLs). The implementation separates concerns between service-based address publication and protocol-level NAT traversal.

## The Dual-Layer Discovery Architecture

iroh separates peer discovery into two complementary layers that work automatically when using the standard `Endpoint` builder.

| Layer | Function | Primary Source Files |
|-------|----------|---------------------|
| **Address-lookup services** | Publish the endpoint's own addressing information and resolve remote `EndpointId`s to `EndpointInfo` on demand. The default `N0` preset uses DNS/Pkarr, but the system supports custom implementations via the `AddressLookup` trait. | [`iroh/src/address_lookup.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup.rs) |
| **QUIC Address Discovery (QAD)** | When direct IP addresses are unknown, send tiny QUIC probes to the home relay to learn the endpoint's public-facing IP and port. This works even behind restrictive NATs. | [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) (lines 1041-1055), `iroh/src/net_report/` |

Both layers are wired together by the `Endpoint::builder()`, which accepts presets like `presets::N0` (enables both layers) or `presets::Minimal` (relay-only).

## Address Lookup Services

The address lookup layer provides the **publish-subscribe** mechanism that maps static `EndpointId`s (public keys) to dynamic network addresses.

### Publishing Endpoint Addresses

Every iroh endpoint creates an `EndpointData` structure containing its `EndpointId`, a list of transport addresses (IP+port or relay URL), and optional user data. When the set of addresses changes—such as after a successful STUN probe or relay assignment—the endpoint calls `address_lookup.publish(&data)` to advertise the update.

In [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) (lines 2100-2105), the `Socket` actor invokes this publication whenever the endpoint's addressing state changes:

```rust
// Simplified from socket.rs
if let Some(ref lookup) = self.address_lookup {
    lookup.publish(&endpoint_data)?;
}

```

### Resolving Remote Addresses

When the application calls `Endpoint::connect(remote_id, alpn)`, iroh queries the configured `AddressLookupServices` for the remote's current addresses. The system returns a merged stream (`AddressLookupStream`) that yields the first usable result while continuing to listen to slower services.

The resolution logic in [`iroh/src/address_lookup.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup.rs) (lines 540-570) implements a **race** between lookup providers:

```rust
// Conceptual flow from address_lookup.rs
self.sock.address_lookup().resolve(remote_id)
    .into_stream() // AddressLookupStream
    .next()        // First usable EndpointInfo
    .await

```

This ensures that if DNS resolution is slow but a local cache (or mDNS) responds quickly, the connection attempt proceeds immediately.

### Service Implementations

The `AddressLookup` trait is implemented by several providers:

- **PkarrPublisher / DnsAddressLookup**: The default `N0` preset configuration. Publishes signed PKARR packets to a relay (`iroh-dns` crate) and resolves via DNS TXT records.
- **MemoryLookup**: An in-process hash map useful for unit tests and local development.
- **Custom transports**: Any type implementing `AddressLookup` can be registered via `builder.address_lookup(my_lookup)`.

## QUIC Address Discovery (QAD)

When an endpoint advertises only a relay URL (common behind NAT), iroh uses **QUIC Address Discovery** to learn its public IP address dynamically.

### Enabling QAD

QAD is configured in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) (lines 1041-1055) when the endpoint has IP transports (UDP sockets). The builder creates a `net_report::QuicConfig` containing the endpoint handle, TLS client configuration, and IPv4/IPv6 probe flags:

```rust
// From socket.rs
let quic_config = has_ip_transports.then(|| net_report::QuicConfig {
    ep: self.endpoint.clone(),
    client_config: tls_config,
    ipv4: true,
    ipv6: self.enable_ipv6,
});
let net_opts = net_opts.quic_config(quic_config);

```

If the endpoint is relay-only (`has_ip_transports` is false), the code skips QAD to avoid unnecessary timeouts.

### The Probe Flow

The `net_report::Client` spawns background probe tasks ([`iroh/src/net_report/probes.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report/probes.rs)) that open short-lived QUIC connections to the **relay's QAD endpoint** ([`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs)). The relay observes the source IP address of the incoming QUIC packet and returns it in an `ObservedAddressFrame`:

1. Endpoint sends QUIC probe to relay's QAD server
2. Relay records the observed source address
3. Relay writes `ObservedAddressFrame` back to the client
4. Client receives the frame and extracts the public IP:port

### Applying Discovered Addresses

When a probe succeeds, the discovered address is stored in the endpoint's `DirectAddrUpdateState` (see [`socket.rs`](https://github.com/n0-computer/iroh/blob/main/socket.rs) lines 1069-1080). This state then triggers a new publication via the address lookup services, making the direct IP immediately available for subsequent connections without needing a relay.

## End-to-End Discovery Flow

The complete peer discovery process follows this sequence:

1. **Publish**: Endpoint A publishes its address set (relay URL + any direct IPs) via `AddressLookupServices::publish` (lines 516-527 in [`address_lookup.rs`](https://github.com/n0-computer/iroh/blob/main/address_lookup.rs)).
2. **Resolve**: When Endpoint B calls `connect(endpoint_a_id)`, it queries `AddressLookupServices::resolve`, merging results from all configured providers (lines 540-566).
3. **Relay Fallback**: If Endpoint A's DNS record contains only a relay URL, Endpoint B connects via the relay.
4. **QAD Trigger**: Endpoint A's `net_report::Client` sends QUIC probes to its home relay's QAD endpoint ([`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs)).
5. **Address Learning**: The relay returns Endpoint A's observed public IP in a QUIC frame.
6. **Update**: Endpoint A publishes the newly discovered direct IP via its address lookup service.
7. **Direct Connection**: Endpoint B resolves the updated address set and establishes a direct QUIC connection, upgrading from the relay.

## Implementation Examples

### Basic Usage with the N0 Preset

The `presets::N0` configuration enables both DNS/Pkarr lookup and QAD automatically:

```rust
use iroh::{Endpoint, endpoint::presets};
use iroh::protocol::{AcceptError, ProtocolHandler, Router};

const ALPN: &[u8] = b"my-app/1";

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // N0 preset includes PkarrPublisher and QAD configuration
    let endpoint = Endpoint::bind(presets::N0).await?;
    
    // Start accepting connections and publishing addresses
    endpoint.online().await;
    
    let addr = endpoint.addr();
    println!("Node address: {}", addr);
    
    // Connect from another endpoint
    let other = Endpoint::bind(presets::N0).await?;
    let conn = other.connect(addr, ALPN).await?;
    
    Ok(())
}

```

### Custom Memory Lookup for Testing

For testing environments, replace the DNS infrastructure with an in-memory lookup:

```rust
use iroh::{Endpoint, address_lookup::{MemoryLookup, AddressLookupBuilder}, endpoint::presets};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut builder = Endpoint::builder(presets::Minimal);
    let mem_lookup = MemoryLookup::default();
    builder.address_lookup(mem_lookup.clone());
    
    let ep = builder.bind().await?;
    
    // Manually inject a peer's address
    let peer_id = /* some EndpointId */;
    let peer_addr = iroh_base::EndpointAddr::from_parts(
        peer_id,
        [iroh_base::TransportAddr::Ip("203.0.113.42:4000".parse().unwrap())],
    );
    let data = iroh::address_lookup::EndpointData::from_iter([peer_addr.transport()]);
    mem_lookup.publish(&data);
    
    // Now connect using the injected address
    let conn = ep.connect(peer_id, b"test/alpn").await?;
    Ok(())
}

```

### Manual QAD Configuration

To enable QUIC Address Discovery without the full N0 DNS infrastructure:

```rust
use iroh::{Endpoint, net_report::{self, Options}, endpoint::presets};
use iroh::tls::ClientConfig;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let tls_cfg = ClientConfig::default(); // Load proper certificates in production
    
    let qad = net_report::QuicConfig {
        ep: Endpoint::builder(presets::Minimal).bind().await?.inner,
        client_config: tls_cfg,
        ipv4: true,
        ipv6: false,
    };
    
    let net_opts = Options::default()
        .quic_config(Some(qad))
        .net_report_config(net_report::Config::default());
    
    let endpoint = Endpoint::builder(presets::Minimal)
        .net_report(net_opts)
        .bind()
        .await?;
        
    Ok(())
}

```

This corresponds to the internal configuration logic found in [`socket.rs`](https://github.com/n0-computer/iroh/blob/main/socket.rs) (lines 1041-1055).

## Key Source Files

| File | Purpose |
|------|---------|
| [`iroh/src/address_lookup.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup.rs) | Defines `AddressLookup` trait, `AddressLookupServices` registry, and the merging `AddressLookupStream`. |
| [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) | Builds `net_report::Client`, conditionally creates `QuicConfig` for QAD (lines 1041-1055), and handles publishing at lines 2100-2105. |
| `iroh/src/net_report/` | Implements background probe tasks and the client-side QAD logic. |
| [`iroh/src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint/quic.rs) | Contains QUIC transport flags like `send_observed_address_reports` (lines 453-470). |
| [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs) | Runs the server-side QAD endpoint that echoes observed addresses back to clients. |
| [`iroh-dns/src/endpoint_info.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-dns/src/endpoint_info.rs) | Handles signed PKARR packets for the DNS-based discovery service. |

## Summary

- **iroh peer discovery** operates through two layers: pluggable address lookup services and QUIC Address Discovery (QAD).
- **Address lookup services** publish `EndpointData` (containing relay URLs and direct IPs) and resolve remote `EndpointId`s via the `AddressLookup` trait, with the `N0` preset providing DNS/Pkarr as the default.
- **QUIC Address Discovery** enables NAT traversal by sending probes to the home relay's QAD endpoint ([`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs)), learning the public IP from the relay's observed address frame.
- The **merge stream** architecture in [`address_lookup.rs`](https://github.com/n0-computer/iroh/blob/main/address_lookup.rs) ensures the fastest lookup provider wins, while slower services continue in the background.
- **QAD is conditional**: it only activates when the endpoint has IP transports, avoiding timeouts in relay-only deployments.

## Frequently Asked Questions

### What is the difference between address lookup and QUIC address discovery in iroh?

Address lookup services resolve an `EndpointId` to network addresses (IP or relay URL) using external infrastructure like DNS or local memory maps. QUIC Address Discovery (QAD) is a protocol-level mechanism that learns the endpoint's own public IP address by probing the relay, enabling direct connections even when the endpoint only knows its private IP. Lookup services answer "where is the remote peer," while QAD answers "what is my public address."

### How does iroh handle peer discovery when both endpoints are behind NAT?

When both endpoints are behind NAT, they initially publish only their relay URLs via the address lookup service. Upon connection, the endpoints use QUIC Address Discovery to learn their respective public IPs through probes to the relay. Once each endpoint discovers and publishes its public address, iroh attempts hole punching; if successful, the connection upgrades from relayed to direct. If hole punching fails, traffic continues through the relay.

### Can I use iroh peer discovery without the N0 DNS infrastructure?

Yes. The `AddressLookup` trait is fully pluggable. You can implement custom discovery using mDNS for local networks, a private HTTP service, or the built-in `MemoryLookup` for testing. Configure custom lookup services via `Endpoint::builder().address_lookup(my_service)`. However, without DNS/Pkarr, you must provide an alternative mechanism to distribute endpoint addresses out-of-band.

### Where is the observed address reported back to the endpoint in the source code?

The observed address is reported in [`iroh-relay/src/quic.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/quic.rs), where the relay server writes an `ObservedAddressFrame` to the QUIC stream. The client receives this frame in `iroh/src/net_report/` (specifically within the probe handling logic), extracts the address, and forwards it to the `DirectAddrUpdateState` in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) (lines 1069-1080), which then triggers a new publication via the configured address lookup services.