How Direct Address Discovery Works in iroh Without a Relay Server

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, 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. 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.

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 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 Items.

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.

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. The transport selector in 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 exposes Builder::address_lookup and addr_filter for this configuration. You can see a concrete example in iroh/examples/connect.rs, which builds an endpoint with addr_filter(AddrFilter::direct_only()) and no RelayUrl.

Code Examples

Publish Only Direct Addresses

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

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

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 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, 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 (publishing, resolving, and stream merging), iroh/src/address_lookup/pkarr.rs (signed DNS records), iroh/src/endpoint.rs (builder configuration), and iroh/src/socket/remote_map/remote_state.rs (tracking direct-path state). Integration tests in iroh/tests/patchbay.rs verify end-to-end direct connectivity without a relay.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →