Table of Contents

Relays and discovery

Reach a peer you only know by public key — relay modes, DNS and pkarr lookup, mDNS, and self-hosting.

A direct IP address only works when both peers can route to each other. This page covers the two mechanisms that make an endpoint ID enough: relays (a path that always works) and discovery (turning an ID into an address).

Relay modes

Relays are configured with iroh.WithRelayMode and the relay package:

Mode Use when
relay.ModeDefault() you want the number0 production relays
relay.ModeStaging() you are testing against number0 staging
relay.ModeCustomURLs(urls...) you run your own relays
relay.ModeCustom(m) you need per-relay relay.Config (QUIC settings)
relay.ModeDisabled() direct paths only; no relay fallback
ep, err := iroh.Bind(ctx, iroh.WithRelayMode(relay.ModeDefault()))
if err != nil {
	return err
}
if err := ep.Online(ctx); err != nil { // wait for a home relay connection
	return err
}
fmt.Println(ep.Addr().RelayURLs())

Endpoint.Online blocks until a home relay connection exists. Until then your Addr() may have no relay URL in it, so a ticket minted too early is not reachable. Endpoint.HomeRelayStatus() observes the connection, and InsertRelay / RemoveRelay adjust the map at runtime.

When several relays are configured, relay.RankByLatency with a relay.Prober (relay.HTTPConnectProber) measures them; with WithNetReport() the endpoint’s own NetReport().PreferredRelay reports the relay with the best recent latency, chosen with hysteresis.

The relay is a fallback path, not the destination — see How go-iroh works for the upgrade to a direct path.

Discovery: from ID to address

Discovery is optional. If you already have an address — from a ticket, a config file, or a previous session — you can dial without any lookup service.

The abstraction is iroh.AddressLookupServices: a registry with publisher and resolver halves, registered on an endpoint with iroh.WithAddressLookup. It “publishes the endpoint’s own info to every publisher and merges resolver streams” (its doc comment). Resolution is an iterator of Item values, each tagged with the provenance of the service that produced it.

Shipped services:

Service Constructors Provenance
DNS NewDNSAddressLookup(origin, resolver), N0DNSAddressLookup(resolver) DNSProvenance
pkarr NewPkarrPublisher / NewPkarrResolver, N0PkarrPublisher / N0PkarrResolver PkarrProvenance
Static StaticLookupFromAddrs(addrs...) static_lookup
Memory NewMemoryLookup() + AddEndpointAddr memory_lookup
mDNS mdns.New(id, opts...) in iroh/mdns local link

The N0… constructors use the number0 production endpoints: iroh.N0DNSPkarrRelayProd is https://dns.iroh.link/pkarr, with N0DNSPkarrRelayStaging alongside it, and the DNS lookup uses dns.N0DNSEndpointOriginProd.

A typical internet-facing setup publishes with pkarr and resolves with both pkarr and DNS:

publisher, err := iroh.N0PkarrPublisher(sk, nil)
if err != nil {
	return err
}
resolver, err := iroh.N0PkarrResolver(nil)
if err != nil {
	return err
}
var services iroh.AddressLookupServices
services.AddPublisher(publisher)
services.AddResolver(resolver)
services.AddResolver(iroh.N0DNSAddressLookup(nil))

ep, err := iroh.Bind(ctx,
	iroh.WithSecretKey(sk),
	iroh.WithAddressLookup(&services),
)

Publishing is what makes a bare netaddr.NewEndpointAddr(id) dialable by someone who has only your ID.

pkarr records are signed with your endpoint’s secret key, so a pkarr relay distributes them without being trusted to be honest about their contents. The underlying codec is the pkarr package; the DNS/TXT encoding is in dns.

For LAN-only peers, iroh/mdns implements both halves over multicast DNS (default service name irohv1) and is not built for js.

Self-hosting

The core module ships both servers, so you do not need number0 infrastructure:

Point endpoints at them with relay.ModeCustomURLs(...) and NewPkarrPublisher / NewPkarrResolver / NewDNSAddressLookup with your own URL and origin. See Commands for flags and a health check.

Next steps

Last updated: 2026-08-09