How iroh's Address Lookup Service Connects Peers by Public Key
TLDR: iroh enables peer-to-peer connections using only cryptographic public keys as stable identifiers, resolving current network locations through pluggable lookup services that map EndpointIds (public keys) to reachable IP addresses and relay URLs.
The iroh networking stack from n0-computer treats public keys as the primary peer identity. When an application needs to establish a connection, the iroh address lookup service connects peers by public key through a distributed resolution process that translates static cryptographic identifiers into dynamic network locations. This architecture eliminates the need for out-of-band IP address exchange while maintaining strong authentication guarantees.
The 5-Step Resolution Flow
When connecting to a remote peer using only its public key, iroh executes a multi-phase resolution process that bridges the gap between cryptographic identity and network reachability.
Step 1: Publishing Address Information
When an endpoint starts, it creates an EndpointData structure containing its relay URL and any direct IP addresses. It then calls AddressLookup::publish on every configured lookup service (such as pkarr, DNS, or memory backends). Each service stores this data keyed by the endpoint’s public key, creating a distributed directory of peer locations.
According to the source code in iroh/src/address_lookup.rs (lines 33-41), the publish method takes the endpoint's EndpointId (derived from its secret key) and registers the current network coordinates.
Step 2: Resolving the Remote Public Key
When a caller invokes Endpoint::connect(..., alpn) with an EndpointId (or an EndpointAddr containing only the ID), the method triggers self.inner.resolve_remote(endpoint_addr). This async call queries the Remote Map to look up the address for the given public key.
This resolution chain is implemented in iroh/src/endpoint.rs (lines 27-34) and delegates to the low-level networking layer in iroh/src/socket.rs (lines 1320-1335).
Step 3: Querying Services in Parallel
The AddressLookupServices::resolve method in iroh/src/address_lookup.rs (lines 54-66) creates a resolution stream for each registered lookup service. The AddressLookupStream merges these streams and yields the first successful Item (containing EndpointInfo) while continuing to listen to slower services.
Errors from individual services are buffered and logged via debug!("address lookup error…"). Only if all services fail does the stream emit an AddressLookupFailed::NoResults error, as implemented in the poll_next method (lines 26-48).
Step 4: Selecting the Best Path
The returned Item converts to an EndpointAddr via Item::to_endpoint_addr() (located in iroh/src/address_lookup.rs, lines 24-27). This address contains either a relay URL, direct IP sockets, or both. The endpoint’s path selector—by default a biased-RTT selector—picks the optimal path, preferring direct addresses over relayed connections.
Step 5: Dialing the Remote Peer
With the concrete MappedAddr obtained from resolve_remote, the endpoint builds a QUIC client configuration and calls the underlying NOQ transport to open a connection. As implemented in iroh/src/endpoint.rs (lines 87-95), the connection succeeds as soon as a reachable address is found; if the direct address works, the relay is never used.
Why Public Keys Are Sufficient for Peer Discovery
All address lookup services store location data under the peer’s public key (EndpointId). Because the public key is globally unique and cryptographically unforgeable—it serves as the authentication credential for the TLS handshake—any peer retrieving the address record can verify that the discovered addresses belong to the intended remote. The lookup services function as a trustless directory where the identifier itself provides the security guarantee.
Configuring Address Lookup Services
iroh ships with multiple implementations of the AddressLookup trait. You can combine them to create a robust resolution strategy:
use iroh::{
address_lookup::{AddrFilter, PkarrPublisher},
endpoint::{presets, Builder},
Endpoint,
};
// Build an endpoint that publishes via public pkarr server
let ep = Builder::new(presets::Minimal)
.address_lookup(PkarrPublisher::n0_dns())
.address_lookup(address_lookup::DnsAddressLookup::n0_dns())
.bind()
.await?;
// Connect using only the remote public key
let remote_id = /* the other peer's public key */ ;
let conn = ep.connect(remote_id, b"my-alpn").await?;
For testing or isolated networks, use the in-memory lookup:
use iroh::address_lookup::memory::MemoryLookup;
let mem = MemoryLookup::new();
mem.add(remote_id, EndpointAddr::new(remote_id).with_relay_url("https://my.relay".parse()?));
let ep = Builder::empty()
.address_lookup(mem)
.bind()
.await?;
Key Implementation Details
Pluggable Lookup Back-ends
The AddressLookup trait in iroh/src/address_lookup.rs defines the interface for custom resolution strategies. The codebase includes MemoryLookup for local testing, PkarrResolver for DHT-based lookups, and DnsAddressLookup for DNS-based resolution using PKARR packets stored in DNS records.
Concurrent Resolution Strategy
Multiple lookup services execute simultaneously in separate async streams. The AddressLookupStream uses a biased merge strategy that returns the first successful result, reducing connection latency while maintaining fallback options.
Error Handling and Fallbacks
Individual service failures do not abort the resolution process. The system aggregates errors and only returns NoResults when every configured service fails to provide an address for the given public key.
Summary
- iroh uses EndpointId (public keys) as canonical peer identifiers, storing address records in distributed lookup services.
- The resolution process runs through five phases: publishing local addresses, resolving remote keys, parallel service queries, path selection, and QUIC connection establishment.
- Multiple lookup services run concurrently in
iroh/src/address_lookup.rs, with the first successful result establishing the connection. - Public keys provide both identification and authentication, ensuring that resolved addresses cryptographically belong to the intended peer.
- The system is configurable via the
AddressLookuptrait, supporting memory, DNS, and pkarr backends.
Frequently Asked Questions
What happens if all address lookup services fail to resolve a public key?
The AddressLookupStream buffers errors from individual services and only emits AddressLookupFailed::NoResults if every configured service returns no results. This error propagates through resolve_remote in iroh/src/socket.rs and causes the Endpoint::connect call to fail, requiring the application to retry or verify the remote public key.
Can I use custom address lookup services beyond the built-in DNS and pkarr options?
Yes. You can implement the AddressLookup trait for your own backend by defining the publish and resolve methods. Inject your implementation into the endpoint builder using .address_lookup(my_custom_service), and iroh will include it in the parallel resolution process alongside other services.
How does iroh handle address changes when a peer moves between networks?
When a peer's network interfaces change, the endpoint updates its EndpointData and calls AddressLookup::publish again to propagate new addresses to all configured lookup services. Peers resolving the public key receive the updated location information on their next connection attempt, allowing the system to handle NAT changes and mobile network transitions transparently.
Is the public key used for both identification and encryption during the connection?
Yes. The EndpointId (public key) serves as the stable identifier for lookup purposes, but also acts as the cryptographic identity for the TLS handshake during the QUIC connection establishment. This ensures that the resolved address actually belongs to the peer holding the corresponding private key, preventing man-in-the-middle attacks during the connection process.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →