# How iroh's DNS/Pkarr Address Lookup Service Resolves Endpoints: A Deep Dive into the Resolution Pipeline

> Discover how irohs DNSPkarr address lookup service resolves endpoints by querying TXT records, verifying signatures, and parsing DNS data for structured connection info.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: deep-dive
- Published: 2026-06-19

---

**iroh's DNS/Pkarr address lookup service resolves endpoints by constructing a DNS name from the endpoint's public key, querying TXT records containing Pkarr-signed packets, verifying cryptographic signatures, and parsing the embedded DNS data into structured connection information.**

The n0-computer/iroh repository implements a decentralized endpoint resolution system that combines DNS infrastructure with Pkarr cryptographic signatures. When a client needs to connect to a remote peer, the `DnsResolver` constructs a specialized query, validates the cryptographic proof, and extracts connection details. This article examines the exact resolution flow implemented in [`iroh-dns/src/dns.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-dns/src/dns.rs) and [`iroh-dns/src/pkarr.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-dns/src/pkarr.rs).

## The Five-Step Resolution Process

The resolution pipeline follows a strict sequence from identifier to verified connection data. According to the iroh source code, the service completes endpoint resolution through five distinct phases:

1. **DNS name construction** – Formatting the `_iroh.<z-base-32-encoded-public-key>.<origin>` query string
2. **TXT record lookup** – Querying DNS with a 3-second timeout via the HickoryResolver backend
3. **Packet verification** – Validating the 32-byte public key and 64-byte Ed25519 signature in the Pkarr packet
4. **Data extraction** – Parsing the embedded DNS wire format into structured fields
5. **Caching and resilience** – Storing results and handling network failures through staggered retries

Each phase involves specific struct methods and constant definitions found in the iroh-dns crate.

## Constructing the DNS Query Name

The resolution begins in `DnsResolver::lookup_endpoint_by_id`, which transforms an `EndpointId` into a DNS-compatible hostname. The name format follows a strict pattern:

```

_iroh.<z-base-32-encoded-public-key>.<origin>

```

The `<origin>` suffix determines which DNS infrastructure to query. The production origin is `dns.iroh.link.`, while testing environments use `staging-dns.iroh.link.`. The public key is extracted from the `EndpointId` and encoded using Z-base-32, a URL-safe encoding that avoids ambiguous characters.

This formatted string becomes the query target for the underlying DNS resolver.

## Querying TXT Records with HickoryResolver

Once the DNS name is constructed, `DnsResolver::lookup_txt` initiates the network request. The implementation delegates to `HickoryResolver::lookup_txt`, which utilizes the `hickory_resolver` crate for actual DNS communication.

The lookup operates with a **hardcoded 3-second timeout** (`DNS_TIMEOUT` constant). The resolver queries for TXT records specifically, expecting a payload that contains a Pkarr-signed DNS packet. If the DNS server responds successfully, the raw TXT strings are passed to the decoding phase.

## Verifying Pkarr Signed Packets

The core security mechanism resides in [`iroh-dns/src/pkarr.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-dns/src/pkarr.rs). The `EndpointInfo::from_txt_lookup` method receives the TXT record iterator and processes it through `SignedPacket::from_bytes`.

The binary layout of a signed packet follows this exact structure:

- **32 bytes**: Public key
- **64 bytes**: Ed25519 signature
- **8 bytes**: Timestamp
- **Variable**: DNS wire-format packet

The constants `HEADER_SIZE` (104 bytes) and `MAX_SIGNED_PACKET_SIZE` define the buffer constraints. The verification process calls `PublicKey::verify` to validate the signature against the embedded public key and timestamp. After cryptographic verification, `simple_dns::Packet::parse` extracts the inner DNS packet.

Helper methods `SignedPacket::txt_records` and `SignedPacket::all_txt_records` convert the raw DNS answers into iterable strings that the endpoint parser consumes.

## Extracting EndpointInfo

After successful verification, `EndpointInfo::from_txt_lookup` constructs the final `EndpointInfo` struct. This structure contains the concrete connection details extracted from the DNS TXT records, including:

- **`relay_url`**: The relay server URL for NAT traversal
- **`quic_addr`**: QUIC protocol address and port
- **`tcp_addr`**: TCP protocol address and port

These fields provide the caller with multiple connection paths to the remote endpoint, supporting both direct UDP/QUIC connections and relayed TCP fallbacks.

## Caching and Network Resilience

The `DnsResolver` implements several resilience mechanisms. The resolver caches DNS responses internally to minimize redundant network traffic. Cache invalidation occurs explicitly through `DnsResolver::clear_cache`, while network interface changes trigger `DnsResolver::reset` to rebuild the underlying resolver instance.

For unreliable networks, `lookup_endpoint_by_id_staggered` implements jittered retries. This method accepts a slice of millisecond delays (e.g., `[200, 500]`) and attempts the lookup multiple times with the specified intervals, improving success rates on flaky connections.

## Code Examples

### Resolve an Endpoint by EndpointId

```rust
use iroh_dns::{DnsResolver, N0_DNS_ENDPOINT_ORIGIN_PROD};
use iroh_base::EndpointId;

async fn resolve_endpoint(id: EndpointId) -> Result<(), Box<dyn std::error::Error>> {
    // Create a resolver with default system DNS settings
    let resolver = DnsResolver::new();

    // Perform the lookup with the default 3-second timeout
    let info = resolver
        .lookup_endpoint_by_id(&id, N0_DNS_ENDPOINT_ORIGIN_PROD)
        .await?;

    println!("Resolved endpoint: {:?}", info);
    Ok(())
}

```

### Resolve by Human-Readable Domain Name

```rust
use iroh_dns::DnsResolver;

async fn resolve_by_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
    let resolver = DnsResolver::default();
    // Automatically adds the "_iroh." prefix if missing
    let info = resolver
        .lookup_endpoint_by_domain_name(name)
        .await?;
    println!("Endpoint info for {name}: {:?}", info);
    Ok(())
}

```

### Staggered Lookup with Retry Logic

```rust
use iroh_dns::DnsResolver;
use iroh_base::EndpointId;

async fn resolve_staggered(id: EndpointId) -> Result<(), Box<dyn std::error::Error>> {
    let resolver = DnsResolver::default();
    // Try up to three attempts with 0 ms, 200 ms and 500 ms delays
    let delays = [200u64, 500];
    let info = resolver
        .lookup_endpoint_by_id_staggered(&id, iroh_dns::N0_DNS_ENDPOINT_ORIGIN_PROD, &delays)
        .await?;
    println!("Staggered result: {:?}", info);
    Ok(())
}

```

## Key Source Files

The resolution pipeline spans several critical files in the n0-computer/iroh repository:

- **[`iroh-dns/src/dns.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-dns/src/dns.rs)**: Implements `DnsResolver`, the `Resolver` trait, and high-level lookup APIs including `lookup_endpoint_by_id` and `lookup_endpoint_by_domain_name`
- **[`iroh-dns/src/pkarr.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-dns/src/pkarr.rs)**: Defines the `SignedPacket` struct, signature verification logic, and packet parsing constants (`HEADER_SIZE`, `MAX_SIGNED_PACKET_SIZE`)
- **[`iroh-dns/src/endpoint_info.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-dns/src/endpoint_info.rs)**: Contains the `EndpointInfo` struct and `from_txt_lookup` method for extracting connection details
- **[`iroh-base/src/endpoint_addr.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-base/src/endpoint_addr.rs)**: Implements `EndpointId` and Z-base-32 encoding for DNS name construction

## Summary

- **iroh's DNS/Pkarr address lookup service** combines DNS TXT records with Pkarr cryptographic signatures to distribute endpoint information securely.
- The resolution process constructs a DNS name from the Z-base-32 encoded public key, queries the `_iroh.` subdomain, and expects a 3-second response timeout.
- **Security relies on Ed25519 signatures**: The 104-byte header (32-byte pubkey + 64-byte signature + 8-byte timestamp) verifies the authenticity of the embedded DNS packet.
- **Connection details** are extracted into an `EndpointInfo` struct containing relay URLs and direct QUIC/TCP addresses.
- **Resilience features** include DNS caching, automatic resolver reset on network changes, and staggered lookups with configurable retry delays.

## Frequently Asked Questions

### What is the exact format of the DNS name used by iroh's lookup service?

The DNS name follows the pattern `_iroh.<z-base-32-encoded-public-key>.<origin>`. The origin is typically `dns.iroh.link.` for production or `staging-dns.iroh.link.` for testing environments. The public key is extracted from the `EndpointId` and encoded using Z-base-32 encoding to create a URL-safe string suitable for DNS labels.

### How does iroh verify the authenticity of endpoint information?

Authenticity is ensured through Pkarr signed packets. The TXT record contains a binary payload with a 32-byte public key, 64-byte Ed25519 signature, and 8-byte timestamp. The `SignedPacket::from_bytes` method in [`iroh-dns/src/pkarr.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-dns/src/pkarr.rs) verifies the signature using `PublicKey::verify` before parsing the embedded DNS data. This cryptographic verification prevents tampering with endpoint addresses during DNS resolution.

### What is the timeout for DNS queries in iroh's resolver?

The default timeout for DNS queries is **3 seconds**, defined as a constant in the DNS resolver implementation. If the query exceeds this duration, the resolution fails and returns an error. For unreliable networks, developers can use `lookup_endpoint_by_id_staggered` to implement retry logic with custom delays.

### How does iroh handle network failures during endpoint resolution?

The resolver implements multiple resilience strategies. The `DnsResolver` caches successful responses and clears the cache via `clear_cache()` when necessary. On network interface changes, `reset()` rebuilds the underlying resolver. Additionally, the `lookup_endpoint_by_id_staggered` method accepts an array of millisecond delays (e.g., `[200, 500]`) to perform jittered retries, attempting the lookup multiple times before failing.