Setting up Pkarr and DNS Address Lookup in Iroh: A Complete Guide

You configure Pkarr and DNS address lookup in Iroh by registering implementations of the AddressLookup trait with the Endpoint builder, allowing the endpoint to publish its transport addresses to both DNS TXT records and Pkarr relays while resolving remote peers through merged streams from both services.

Setting up Pkarr and DNS address lookup in Iroh enables automatic peer discovery by translating EndpointId values into reachable network addresses. The iroh crate provides built-in services that implement the AddressLookup trait, letting you publish endpoint data to DNS zones and Pkarr relays while resolving remote peers through parallel redundant lookups. This architecture ensures that peers can discover each other even if one lookup mechanism fails.

How Address Lookup Works in Iroh

Iroh discovers peers by translating an EndpointId into an EndpointAddr through pluggable address-lookup services. According to the iroh source code, these services integrate into an Endpoint via the builder API found in iroh/src/endpoint.rs.

The architecture follows four distinct phases:

  1. Endpoint Builder – Endpoint::builder(presets::Minimal) creates a mutable Builder instance.
  2. Service Registration – Builder::address_lookup accepts any type implementing AddressLookupBuilder. Types that already implement AddressLookup receive a blanket implementation automatically (see lines 91-98 of iroh/src/address_lookup.rs).
  3. Publishing – When local transport addresses change, the Endpoint calls AddressLookupServices::publish, distributing EndpointData to each configured service.
  4. Resolving – Remote resolution calls AddressLookupServices::resolve(endpoint_id), returning a BoxStream of Item structs containing EndpointInfo and provenance strings. The AddressLookupStream merges streams from all services, yielding results as they arrive (lines 540-566).

The AddressLookupServices registry lives in iroh/src/address_lookup.rs and uses Arc<RwLock> for thread-safe access, supporting runtime modification via add, clear, and set_addr_filter methods.

Configuring DNS Address Lookup

The DNS address lookup service resolves _iroh.<z-base-32-pubkey>.<origin> TXT records through the DnsResolver type defined in iroh-dns/src/dns.rs.

Default Resolver Behavior

By default, DnsResolver reads the system's /etc/resolv.conf (or Android JNI bridge) and falls back to Google DNS if that fails (see HickoryResolver::build_resolver lines 30-40).

Custom Nameserver Configuration

You can force a specific nameserver using DnsResolver::with_nameserver(addr), which creates a resolver that talks exclusively to the supplied UDP server (lines 50-55):

use iroh_dns::DnsResolver;
use std::net::SocketAddr;

let resolver = DnsResolver::with_nameserver("10.0.0.53:53".parse::<SocketAddr>()?);

Transport Protocol Selection

The DnsProtocol enum selects between UDP, TCP, TLS, or HTTPS (lines 38-65). For TLS and HTTPS connections, you can provide custom certificate verification via Builder::tls_client_config (lines 14-18).

The high-level wrapper address_lookup::DnsAddressLookup internally holds a DnsResolver and implements AddressLookup, exposing the helper n0_dns() which configures the resolver to query the public DNS zone dns.iroh.link. (constants N0_DNS_ENDPOINT_ORIGIN_PROD in iroh-dns/src/dns.rs lines 44-48).

Publishing Endpoint Data with Pkarr

Pkarr lookup uses HTTP/TLS/DoH queries to a Pkarr relay that stores signed packets containing endpoint information. The implementation resides in iroh-dns/src/pkarr.rs.

The PkarrPublisher

PkarrPublisher signs endpoint data with the node's secret key and stores the result in a Pkarr relay. The published packet format follows:


<32-byte pubkey><64-byte signature><8-byte timestamp><DNS packet>

Signing Process – SignedPacket::from_bytes verifies the signature, parses the DNS packet, and returns a SignedPacket (lines 90-114). Timestamp Monotonicity – Timestamp::now guarantees strictly increasing values even when the system clock jumps backward (lines 25-62).

Construct a publisher using the builder pattern:

use iroh::address_lookup::PkarrPublisher;

let publisher = PkarrPublisher::builder(pkarr_url)
    .build(secret_key, tls_config);

The helper PkarrPublisher::n0_dns() creates a pre-configured publisher targeting the public relay at https://pkarr.n0.com/.

Combining DNS and Pkarr for Redundancy

A production configuration typically enables both services so peers remain discoverable even if one lookup mechanism fails. This pattern appears in iroh/src/address_lookup.rs lines 71-86:

use iroh::{
    Endpoint, SecretKey,
    address_lookup::{self, AddrFilter, PkarrPublisher},
    endpoint::presets,
};

let secret = SecretKey::generate();

let ep = Endpoint::builder(presets::Minimal)
    .addr_filter(AddrFilter::relay_only())
    .address_lookup(PkarrPublisher::n0_dns())
    .address_lookup(address_lookup::DnsAddressLookup::n0_dns())
    .bind()
    .await?;

Key configuration details:

  • AddrFilter::relay_only() – Removes direct IP addresses before publishing, leaving only the relay URL. This prevents leaking direct addresses while still allowing relay-based connectivity.
  • PkarrPublisher::n0_dns() – Targets the public Pkarr relay.
  • DnsAddressLookup::n0_dns() – Queries the public iroh DNS zone.

When resolving a remote EndpointId, the AddressLookupStream merges results from both services. The first usable address wins while the slower service continues in the background, ensuring minimal connection latency.

Customizing Lookups and Resolvers

You can replace the default resolver with your own implementation of the Resolver trait (see the custom_resolver test in iroh-dns/src/dns.rs lines 735-784). Similarly, implement AddressLookup and optionally AddressLookupBuilder to create entirely custom discovery mechanisms.

Publishing only via Pkarr (no DNS):

let endpoint = Endpoint::builder(presets::Minimal)
    .address_lookup(PkarrPublisher::n0_dns())
    .bind()
    .await?;

Custom DNS resolver with specific nameserver:

use iroh::address_lookup::DnsAddressLookup;
use iroh_dns::DnsResolver;

let resolver = DnsResolver::with_nameserver("10.0.0.53:53".parse::<SocketAddr>()?);
let dns_lookup = DnsAddressLookup::new(resolver);

let endpoint = Endpoint::builder(presets::Minimal)
    .address_lookup(dns_lookup)
    .bind()
    .await?;

Runtime Behavior and Error Handling

Understanding how these services behave at runtime helps diagnose connectivity issues.

Publishing – Operations are fire-and-forget. If the underlying service requires async work, it spawns its own task (see AddressLookup::publish default implementation lines 33-41).

Resolving – Each service returns a BoxStream. The AddressLookupStream merges them, buffers per-service errors, and only fails after all services have errored with AddressLookupFailed::NoResults. If no service is configured, the error NoServiceConfigured is emitted (lines 548-555).

This design guarantees that a fast-failing resolver (e.g., temporary network error) does not suppress later results from a slower but working resolver. A regression test covering this scenario exists in iroh/src/address_lookup.rs lines 913-951.

Resolving a remote peer manually:

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 in Iroh is implemented through the AddressLookup trait, registered via Endpoint::builder().address_lookup().
  • DNS resolution uses DnsResolver in iroh-dns/src/dns.rs, supporting custom nameservers, transport protocols (UDP/TCP/TLS/HTTPS), and the public dns.iroh.link. zone.
  • Pkarr publishing uses PkarrPublisher in iroh-dns/src/pkarr.rs, signing packets with your secret key and storing them at relays like https://pkarr.n0.com/.
  • Redundant lookups are achieved by registering multiple services; AddressLookupStream merges results and uses the first available.
  • Filtering via AddrFilter::relay_only() prevents direct IP leakage while maintaining relay connectivity.
  • Custom implementations are supported through the Resolver and AddressLookup traits for private DNS zones or alternative discovery protocols.

Frequently Asked Questions

What is the difference between DNS and Pkarr lookup in Iroh?

DNS lookup resolves _iroh.<z-base-32-pubkey>.<origin> TXT records from traditional DNS servers, while Pkarr lookup queries HTTP-based relays that store cryptographically signed packets containing the same endpoint data. DNS relies on the global DNS infrastructure and caching, whereas Pkarr provides a decentralized alternative where users publish signed records to specific relays. Iroh can use both simultaneously for redundancy.

How do I configure a custom DNS resolver for Iroh?

Use DnsResolver::with_nameserver(addr) to create a resolver targeting a specific UDP server, then wrap it in DnsAddressLookup::new(resolver). Pass this to the endpoint builder via .address_lookup(). You can also configure TLS/HTTPS DNS by providing a custom rustls::ClientConfig through the resolver builder.

Can I use Pkarr without DNS in Iroh?

Yes. Simply register only the PkarrPublisher with your endpoint builder using .address_lookup(PkarrPublisher::n0_dns()). The endpoint will publish its addresses to the configured Pkarr relay and resolve peers exclusively through Pkarr queries without performing any DNS lookups.

How does Iroh handle failures when multiple lookup services are configured?

The AddressLookupStream merges results from all configured services and returns the first successful result immediately. If one service fails quickly (e.g., network timeout), it does not block results from slower but working services. The operation only fails with AddressLookupFailed::NoResults after all services have returned errors, ensuring maximum connectivity across unreliable networks.

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 →