# How Direct Address Discovery Works in iroh Without a Relay Server

> Learn how iroh enables direct peer-to-peer UDP QUIC connections. Discover direct address discovery without relay servers using PKARR or DNS for fast, efficient communication.

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

---

**Direct address discovery in iroh allows two peers to connect over UDP/QUIC by publishing local IP addresses to an address-lookup service—such as PKARR/DNS—so a remote peer can resolve them and open a direct path without using a relay.**

Direct address discovery in iroh enables peer-to-peer connectivity even when no relay server is configured. By leveraging the address-lookup subsystem, an endpoint can distribute its own IP addresses and rely on the remote peer to resolve them. This article explains the exact flow according to the `n0-computer/iroh` source code, from publishing local socket addresses to establishing a direct QUIC connection.

## What an iroh Endpoint Needs to Connect

According to the `Endpoint` implementation in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs), an endpoint requires three pieces of information to open a QUIC connection:

- The **EndpointId** (the peer’s public key).
- A **RelayUrl** or one or more **direct addresses** (`IP:port`).
- The ALPN string for the application protocol.

When a relay is not used, the second requirement is satisfied through the **Address-Lookup subsystem**.

## How iroh Publishes Direct Addresses

The publishing flow is handled inside the address-lookup logic defined in [`iroh/src/address_lookup.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup.rs). When an endpoint starts, it collects its local socket addresses, optionally filters out relay URLs, and pushes the remaining direct IPs to a lookup service.

### Gathering Local Socket Addresses

When an endpoint starts, it gathers the socket addresses it is listening on via `socket::remote_map::RemoteMap::local_direct_addrs`. These addresses are represented as `TransportAddr::Ip` items and form the set of direct IPs that other peers may use to reach it.

### Filtering Out Relay URLs

Before publication, the endpoint builder can attach an **AddrFilter**. For example, calling `AddrFilter::direct_only()` ensures only direct IP addresses are sent to the lookup service, omitting any relay URL. The filter is applied inside `AddressLookupServices::publish`; see the `addr_filter` handling at lines 18–22 of [`iroh/src/address_lookup.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup.rs).

### Publishing to an Address-Lookup Service

The endpoint calls `AddressLookupServices::publish(&data)`, where `data` contains the collected `TransportAddr::Ip` entries. The most common implementation is `PkarrPublisher`, which writes a signed PKARR record containing the `EndpointInfo` to a public DNS zone. As implemented in [`iroh/src/address_lookup/pkarr.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup/pkarr.rs) inside `PkarrPublisher::update_endpoint_data`, this publishing is fire-and-forget; a background task sends the record.

## How Remote Peers Resolve Direct Addresses

To discover a peer without a relay, the remote endpoint calls `AddressLookupServices::resolve(endpoint_id)`. All configured lookup services are queried in parallel, and each returns a stream of `Item`s.

The first `Item` that contains a direct `TransportAddr::Ip` is used for the connection attempt. If a resolver returns an error, the stream continues. This logic is handled in `AddressLookupStream::poll_next` at lines 27–38 of [`iroh/src/address_lookup.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup.rs).

## Establishing a Direct QUIC Path

Once the remote’s IP address is known, the socket actor creates a **direct transport** using `socket::transports::four_tuple::FourTuple::Ip`. If necessary, iroh performs hole-punching (STUN-style NAT traversal). Once the first direct-to-direct packet succeeds, the connection is promoted to a direct path.

The state of having ever been direct is tracked by `RemoteStateActor::has_been_direct` at line 1250 of [`iroh/src/socket/remote_map/remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map/remote_state.rs). The transport selector in [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs) chooses the direct IP transport when one is available.

## Running iroh in No-Relay Mode

If the endpoint is configured without a relay—for example, using `RelayMode::None` or a custom `AddrFilter` that only publishes IPs—the lookup service still publishes the direct addresses, and the remote resolves them exactly as above. The connection proceeds entirely over UDP/QUIC without ever contacting a relay. The builder integration in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) exposes `Builder::address_lookup` and `addr_filter` for this configuration. You can see a concrete example in [`iroh/examples/connect.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/connect.rs), which builds an endpoint with `addr_filter(AddrFilter::direct_only())` and no `RelayUrl`.

## Code Examples

### Publish Only Direct Addresses

```rust
use iroh::{
    address_lookup::{AddrFilter, PkarrPublisher},
    endpoint::{Builder, presets},
    Endpoint,
};
use iroh_base::TransportAddr;

// Build an endpoint that only publishes IP addresses.
let ep = Builder::new(presets::Minimal)
    .addr_filter(AddrFilter::direct_only())           // filter out any relay URL
    .address_lookup(PkarrPublisher::n0_dns())        // publish to the public PKARR/DNS service
    .bind()
    .await?;                                          // the endpoint now publishes its IPs

```

### Connect Using Only the EndpointId

```rust
let remote_id = /* the peer's public key */;
let conn = ep.connect(remote_id, b"my-alpn").await?;   // address lookup resolves the IP

```

### Custom In-Process Lookup for Testing

```rust
use iroh::address_lookup::MemoryLookup;

let mem = MemoryLookup::new();                         // stores EndpointInfo in-process
ep.address_lookup().unwrap().add(mem.clone());        // add it to the endpoint

// In another task register a direct address for the remote:
mem.insert(
    remote_id,
    EndpointInfo::from_parts(
        remote_id,
        EndpointData::from_iter([TransportAddr::Ip("203.0.113.7:45678".parse().unwrap())]),
    ),
);

```

## Summary

- Direct address discovery in iroh relies on the **Address-Lookup subsystem** to publish and resolve `TransportAddr::Ip` entries.
- An endpoint gathers local addresses, optionally applies an **`AddrFilter`**, and publishes them via services like **`PkarrPublisher`**.
- Remote peers call **`AddressLookupServices::resolve`** to retrieve direct IPs and initiate a QUIC connection.
- The socket actor uses **`FourTuple::Ip`** for direct transport and tracks success via **`has_been_direct`** in `RemoteStateActor`.
- Integration tests in **[`iroh/tests/patchbay.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay.rs)** verify that a direct path can be established with no relay involved.

## Frequently Asked Questions

### Can two iroh peers connect without any relay server?

Yes. When configured without a relay—using `RelayMode::None` or `AddrFilter::direct_only()`—the endpoint publishes its direct IP addresses via the address-lookup subsystem. The remote peer resolves these addresses through services like PKARR/DNS or an in-memory map, then opens a direct QUIC connection without ever contacting a relay.

### What lookup services can publish direct addresses?

iroh exposes the `AddressLookup` trait in [`iroh/src/address_lookup.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup.rs), allowing any implementation to be plugged into the endpoint builder. The codebase includes `PkarrPublisher` for public DNS-based discovery and `MemoryLookup` for in-process testing. Custom implementations can be added by implementing the trait and registering them with `Builder::address_lookup`.

### How does iroh perform NAT traversal for direct connections?

After resolving a direct `TransportAddr::Ip`, the socket actor creates a direct transport using `FourTuple::Ip`. The stack then attempts hole-punching using STUN-style NAT traversal. Once a direct packet successfully reaches the peer, the connection is promoted to a direct path, and the `has_been_direct` flag is set in `RemoteStateActor` to record that a direct path was achieved.

### Which source files control direct address discovery?

The core logic lives in [`iroh/src/address_lookup.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup.rs) (publishing, resolving, and stream merging), [`iroh/src/address_lookup/pkarr.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup/pkarr.rs) (signed DNS records), [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) (builder configuration), and [`iroh/src/socket/remote_map/remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map/remote_state.rs) (tracking direct-path state). Integration tests in [`iroh/tests/patchbay.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay.rs) verify end-to-end direct connectivity without a relay.