How to Configure Multiple Address Lookup Services in iroh
You configure multiple address lookup services in iroh by registering implementations of the AddressLookup trait or AddressLookupBuilder via Endpoint::builder().address_lookup(), which stores them in an AddressLookupServices registry that automatically merges resolution streams from all services when resolving remote EndpointIds.
When building peer-to-peer applications with the n0-computer/iroh framework, endpoints need a mechanism to discover how to reach remote peers knowing only their cryptographic EndpointId. The address lookup subsystem solves this by allowing you to publish local addressing information (relay URLs, direct sockets) to external discovery services and resolve others' addresses through multiple parallel channels.
Understanding the Core Architecture
The address lookup system in iroh is built around three primary components defined in iroh/src/address_lookup.rs.
The AddressLookup Trait
At the heart of the system is the AddressLookup trait (lines 33-50), which defines the minimal interface for any discovery service. Implementations must provide two capabilities: publishing local endpoint data via publish() and resolving remote endpoints via resolve(). The trait returns a boxed stream of results, allowing for asynchronous resolution.
AddressLookupServices Registry
Each Endpoint maintains an AddressLookupServices registry (lines 60-89 in iroh/src/address_lookup.rs) that stores a list of boxed AddressLookup instances. This registry handles the lifecycle of address publication and resolution. When you call Builder::address_lookup(), you are adding a service to this registry. The registry stores the last published EndpointData and forwards it to all registered services whenever addresses change.
AddrFilter for Address Control
Before data reaches any service, you can apply an AddrFilter to prune or reorder addresses. This user-supplied function filters the EndpointData before publication, useful for scenarios like "relay-only" mode where you want to hide direct IP addresses from external services.
Built-in Service Types
Iroh provides several production-ready and testing implementations:
MemoryLookup– In-process registry ideal for unit tests (located iniroh/src/memory.rs)DnsAddressLookup– Resolves peers via standard DNS using theiroh-dnscrate (located iniroh-dns/src/dns.rs)PkarrPublisher– Publishes to and resolves from pkarr relays via HTTP (located iniroh-dns/src/pkarr.rs)
Configuring Multiple Lookup Services
To configure multiple address lookup services in iroh, chain multiple .address_lookup() calls on the Endpoint builder. The system automatically merges results from all registered services using an AddressLookupStream.
Combining DNS and Pkarr Services
The most common production setup combines DNS-based resolution with pkarr publishing:
use iroh::{
Endpoint,
address_lookup::{self, AddrFilter},
endpoint::presets,
};
#[tokio::main]
async fn main() -> Result<(), iroh::Error> {
let ep = Endpoint::builder(presets::Minimal)
.addr_filter(AddrFilter::relay_only())
.address_lookup(address_lookup::PkarrPublisher::n0_dns())
.address_lookup(address_lookup::DnsAddressLookup::n0_dns())
.bind()
.await?;
Ok(())
}
This configuration publishes the local endpoint's relay URL to the public n0.computer pkarr service while simultaneously enabling DNS resolution for discovering other peers. Both services receive the same filtered address data.
Adding In-Memory Lookup for Testing
For integration tests or embedded scenarios, register a MemoryLookup alongside production services:
use iroh::{
Endpoint,
address_lookup::MemoryLookup,
endpoint::presets,
};
let memory = MemoryLookup::new();
let ep = Endpoint::builder(presets::Minimal)
.address_lookup(memory)
.bind()
.await?;
Implementing Custom Services
You can implement the AddressLookup trait for custom discovery mechanisms:
struct MyResolver;
impl iroh::address_lookup::AddressLookup for MyResolver {
fn resolve(&self, endpoint_id: iroh_base::EndpointId)
-> Option<BoxStream<Result<iroh::address_lookup::Item, iroh::address_lookup::Error>>> {
// Custom resolution logic returning a stream of results
None
}
}
let ep = Endpoint::builder(presets::Minimal).bind().await?;
ep.address_lookup().unwrap().add(MyResolver);
Configuring Address Filters
Apply filters to control what information gets published to all services. You can set this during building or dynamically after the endpoint starts:
let ep = Endpoint::builder(presets::Minimal).bind().await?;
let lookup = ep.address_lookup().expect("endpoint still open");
lookup.set_addr_filter(address_lookup::AddrFilter::relay_only());
This ensures only relay URLs are published, keeping direct socket addresses private from all lookup services.
How Merged Resolution Works
When Endpoint::connect needs to resolve a remote peer, AddressLookupServices::resolve creates an AddressLookupStream that merges streams from all registered services. According to the implementation in iroh/src/address_lookup.rs, the merge logic buffers errors and yields results as soon as any service returns them. A fast-failing service does not prevent the system from using results from slower services, as demonstrated in the test address_lookup_succeeds_after_other_resolver_errors. The stream only ends with AddressLookupFailed::NoResults if all services return empty results.
Summary
- Configure multiple services by chaining
.address_lookup()calls onEndpoint::builder() - The
AddressLookuptrait (iniroh/src/address_lookup.rs) defines the interface for custom discovery services, whileAddressLookupBuilderallows services to access endpoint configuration during construction AddressLookupServicesautomatically merges resolution streams from all registered services usingAddressLookupStream- Use
AddrFilterto control what endpoint data gets published to external services - Common combinations include
PkarrPublisherwithDnsAddressLookupfor production deployments - The merge algorithm ensures reliability by not letting a single failing service block successful resolutions from others
Frequently Asked Questions
How do I add a custom address lookup service to an iroh endpoint?
Implement the AddressLookup trait and register it using Endpoint::builder().address_lookup() before calling bind(). The trait requires implementing publish() for advertising your local endpoint and resolve() for querying remote endpoints. Alternatively, implement AddressLookupBuilder (lines 41-50) if your service needs to read the endpoint's configuration before becoming active. You can also add services after the endpoint is built by calling ep.address_lookup().unwrap().add(service).
What happens if one address lookup service fails but another succeeds?
The AddressLookupServices registry in iroh/src/address_lookup.rs merges streams from all services and buffers errors separately. Results are yielded as soon as any service returns them, so a fast failure does not hide later successes from slower services. Only if all services return no results does the resolution fail with AddressLookupFailed::NoResults.
Can I filter which addresses get published to lookup services?
Yes. Set an AddrFilter using Builder::addr_filter() or lookup.set_addr_filter() after the endpoint is built. This function filters the EndpointData before it reaches any registered service, allowing you to restrict published information to relay URLs only, direct addresses only, or apply custom logic.
Where are the built-in DNS and pkarr services implemented?
The DNS address lookup implementation resides in iroh-dns/src/dns.rs, while the pkarr publisher and resolver are in iroh-dns/src/pkarr.rs. These are re-exported through the main iroh crate's address lookup module. The memory lookup implementation for testing is in iroh/src/memory.rs.
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 →