Configuring DNS and Pkarr Address Lookup Services in iroh
Iroh discovers peers by translating EndpointId into EndpointAddr through pluggable address lookup services that support both DNS TXT records and Pkarr signed packets, configured via the Endpoint builder API.
The iroh networking library enables decentralized peer discovery by publishing endpoint data to multiple resolution mechanisms simultaneously. By configuring both traditional DNS resolution and modern Pkarr (Public Key Addressable Resource Records) relays, applications can ensure connectivity across diverse network topologies. This guide examines the builder patterns, trait implementations, and configuration options found in the iroh source code for setting up robust address lookup services.
How Address Lookup Services Work
Iroh abstracts peer discovery through the AddressLookup trait, which bridges endpoint identifiers with actual network addresses. The system separates publishing (advertising your endpoint's addresses) from resolving (discovering a remote peer's addresses), allowing each to scale independently.
The Endpoint Builder Pattern
Configuration begins with Endpoint::builder(presets::Minimal), which returns a mutable Builder struct. The Builder::address_lookup method accepts any type implementing AddressLookupBuilder, including types that already implement AddressLookup through a blanket implementation (lines 91-98 of iroh/src/address_lookup.rs).
use iroh::{Endpoint, SecretKey, endpoint::presets};
use iroh::address_lookup::PkarrPublisher;
let secret = SecretKey::generate();
let endpoint = Endpoint::builder(presets::Minimal)
.address_lookup(PkarrPublisher::n0_dns())
.bind()
.await?;
Publishing and Resolving Endpoints
When local transport addresses change, the Endpoint invokes AddressLookupServices::publish, distributing EndpointData to all configured services. For resolution, calling AddressLookupServices::resolve(endpoint_id) returns a BoxStream of Item structs containing EndpointInfo and provenance metadata. The AddressLookupStream (lines 540-566 of address_lookup.rs) merges results from all services, yielding the first available address while slower services continue in the background.
Configuring DNS Resolution
The DNS implementation resides in iroh-dns/src/dns.rs and provides the DnsResolver type for querying _iroh.<z-base-32-pubkey>.<origin> TXT records. By default, the resolver reads system configuration from /etc/resolv.conf (or Android JNI bridges) and falls back to Google DNS if initialization fails (lines 30-40 of dns.rs).
Custom Nameservers and Transport Protocols
The DnsResolver::builder API supports explicit nameserver configuration via DnsResolver::with_nameserver(addr), which bypasses system settings and targets a specific UDP server (lines 50-55). Transport security is controlled through the DnsProtocol enum, supporting UDP, TCP, TLS, and HTTPS variants (lines 38-65).
use iroh::address_lookup::DnsAddressLookup;
use iroh_dns::DnsResolver;
use std::net::SocketAddr;
let resolver = DnsResolver::with_nameserver(
"10.0.0.53:53".parse::<SocketAddr>()?
);
let dns_lookup = DnsAddressLookup::new(resolver);
TLS Verification
For TLS or HTTPS transports, customize the TLS verification via Builder::tls_client_config, accepting a rustls::ClientConfig (lines 14-18 of dns.rs). This enables enterprise deployments with private certificate authorities or mutual TLS authentication.
Implementing Pkarr Publishing
Pkarr provides censorship-resistant address publishing through cryptographically signed DNS packets stored on public relays. The implementation in iroh-dns/src/pkarr.rs defines PkarrPublisher, which signs endpoint data using the node's secret key and uploads it to a relay via HTTP/TLS/DoH.
Signed Packets and Relay Configuration
Each published packet contains a 32-byte public key, 64-byte signature, 8-byte timestamp, and the serialized DNS packet (see SignedPacket::from_txt_strings, lines 41-89). The Timestamp::now implementation guarantees strictly monotonic values even during system clock adjustments (lines 25-62).
use iroh::address_lookup::PkarrPublisher;
let publisher = PkarrPublisher::n0_dns();
The n0_dns() helper configures the publisher to use the public relay at https://pkarr.n0.com/ with appropriate TLS settings. For verification, SignedPacket::from_bytes validates signatures and parses DNS packets (lines 90-114).
Combining DNS and Pkarr for Robust Discovery
Production deployments typically enable both services to maximize discoverability. The AddressLookupStream merges results from both mechanisms, ensuring that a fast DNS response immediately establishes connectivity while the Pkarr lookup continues asynchronously.
use iroh::address_lookup::{AddrFilter, DnsAddressLookup};
let endpoint = Endpoint::builder(presets::Minimal)
.addr_filter(AddrFilter::relay_only())
.address_lookup(PkarrPublisher::n0_dns())
.address_lookup(DnsAddressLookup::n0_dns())
.bind()
.await?;
The AddrFilter::relay_only() filter (defined in address_lookup.rs) strips direct IP addresses before publishing, exposing only the relay URL for enhanced privacy. DnsAddressLookup::n0_dns() targets the public zone dns.iroh.link. (constants defined in dns.rs lines 44-48).
Creating Custom Lookup Services
You can extend iroh's discovery by implementing the AddressLookup and AddressLookupBuilder traits. The AddressLookupServices registry maintains service state in an Arc<RwLock>, supporting runtime modification via add, clear, and set_addr_filter methods.
The Resolver trait (used by DnsResolver) can also be implemented for custom resolution logic, as demonstrated in the custom_resolver test in iroh-dns/src/dns.rs (lines 735-784). This pattern enables integration with private DNS infrastructure or alternative discovery protocols.
Runtime Behavior and Error Handling
Publishing operates fire-and-forget; services spawn internal tasks for async network operations (default implementation in AddressLookup::publish, lines 33-41 of address_lookup.rs).
Resolving aggregates streams from all services. The AddressLookupStream buffers per-service errors and only returns AddressLookupFailed::NoResults after all services have failed (lines 548-555). If no services are configured, it immediately returns NoServiceConfigured. This design prevents temporary network failures from blocking successful resolution via alternative methods, as verified by regression tests in the source (lines 913-951).
Resolving Remote Peers
To resolve a specific EndpointId:
let remote_id = /* known EndpointId */;
let endpoint_addr = endpoint
.address_lookup()
.expect("lookup configured")
.resolve(remote_id)
.await?
.next()
.await
.ok_or("no address found")??
.to_endpoint_addr();
Summary
- Address lookup services translate
EndpointIdtoEndpointAddrthrough pluggable traits defined iniroh/src/address_lookup.rs. - DNS resolution uses
DnsResolverwith configurable nameservers, transport protocols, and TLS settings located iniroh-dns/src/dns.rs. - Pkarr publishing signs endpoint data and publishes to relays via
PkarrPublisheriniroh-dns/src/pkarr.rs, using monotonic timestamps for replay protection. - Combined configuration uses the
Endpointbuilder to register multiple services, withAddressLookupStreammerging results for optimal connectivity. - Custom implementations can replace default resolvers by implementing the
ResolverorAddressLookuptraits, with thread-safe runtime registration.
Frequently Asked Questions
How do I configure a private DNS resolver instead of using public DNS?
Create a DnsResolver using DnsResolver::with_nameserver(socket_addr) to specify your internal DNS server IP and port, then wrap it in DnsAddressLookup::new(resolver). This bypasses the default system resolver configuration and directs all queries to your specified endpoint.
What is the difference between PkarrPublisher and PkarrResolver?
PkarrPublisher signs and uploads your local endpoint's addresses to a Pkarr relay using your secret key, while PkarrResolver queries relays to retrieve signed packets for remote peers. The publisher handles the "advertising" side, whereas the resolver handles the "discovery" side of the peer connection flow.
Why would I use AddrFilter::relay_only() when configuring my endpoint?
This filter removes direct IP addresses from the published endpoint data, exposing only the relay URL. According to the implementation in iroh/src/address_lookup.rs, this prevents direct connections to your node, forcing all traffic through configured relays for enhanced privacy and NAT traversal reliability.
Can I add or remove address lookup services after the endpoint has started?
Yes. The AddressLookupServices registry uses an Arc<RwLock> internally, providing methods like add, clear, and set_addr_filter that modify the active service set at runtime. However, these changes only affect future publish and resolve operations; existing connections remain unaffected.
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 →