Table of Contents

How go-iroh works

The layers between iroh.Bind and a packet on the wire — transports, paths, the TLS and QUIC forks, and what wire compatibility costs.

This page explains the runtime shape of go-iroh. It is background reading, not a set of steps; the how-to pages link back here when a decision depends on it.

The layer stack

your protocol handler          (ALPN-routed, iroh.Router)
   iroh.Conn / iroh.Stream     package iroh
   magic socket + path selection
   internal/qng                quic-go fork: multipath, QAD, QNT
   internal/itls/tls           crypto/tls fork: RFC 7250 raw public keys
   UDP socket │ relay client (WebSocket) │ custom transport

Only the top two boxes are public API. Everything from the magic socket down lives under internal/, which is deliberate: those are wire-compatibility implementation details that upstream iroh may change.

Endpoints own a socket, not a connection

iroh.Bind produces an Endpoint: a secret key, a UDP socket, and the QUIC transport used to dial and accept. One endpoint multiplexes every peer you talk to across that single socket, which is why NAT bindings and hole-punching results are reusable across connections.

Because the endpoint is the unit of identity, it is also the unit of addressability: Endpoint.Addr() returns the current netaddr.EndpointAddr, and Endpoint.WatchAddr() returns an observer that fires as relay assignment and address discovery change what that address contains.

Transports and paths

An endpoint can carry traffic over three kinds of transport:

Either built-in family can be switched off at bind time with iroh.WithoutIPTransports() or iroh.WithoutRelayTransports() — these are runtime options, not build tags; the browser build passes the former.

At runtime a connection may hold several paths at once. Conn.Paths() returns PathInfo values carrying the QUIC multipath path ID, whether the path is validated, its address, and its smoothed RTT when observed. The usual lifecycle is: connect over the relay immediately, discover direct candidates, validate one, migrate application data to it, keep the relay path as a fallback. Conn.WatchPaths lets you observe that transition instead of guessing at it.

Path preference is pluggable with iroh.WithPathSelector.

Learning your own address

An endpoint cannot know its public address by looking at its socket. go-iroh uses QAD (QUIC address discovery) round trips against relays to learn observed addresses, summarized in Endpoint.NetReport(): whether IPv4 and IPv6 round trips completed, whether the observed address varies by destination (the classic symmetric-NAT signal), and which relay currently has the best latency. Enable it with iroh.WithNetReport(); iroh.WithNATPMP adds port mapping.

Addresses learned this way are what get published to discovery services and what end up inside a ticket.

The two forks, and why they exist

Wire compatibility with Rust iroh is not reachable from the Go standard library, so the repository carries two vendored forks under internal/: internal/itls/tls, a crypto/tls fork supporting RFC 7250 raw public keys, and internal/qng, a quic-go v0.59.1 fork that drives it and adds the iroh/noq transport extensions.

Connections that are not peer-to-peer — relay, pkarr, DoH, DoT — use ordinary WebPKI TLS and the standard library.

The forks are a maintenance liability the project tracks explicitly, with a “When to break this fork” section in each fork’s own README (internal/itls/README.md, internal/qng/README.md). For why the standard library cannot serve here and what would retire each fork, see Standard library mapping.

Where discovery fits

Discovery is not part of the connection path; it is the step before it. An address-lookup service turns an endpoint ID into an EndpointAddr (resolver) and publishes your own address so others can do the same (publisher). It is configured on the endpoint with iroh.WithAddressLookup and is entirely optional — an address you already have, from a ticket or a config file, works without any lookup service at all.

See Relays and discovery.

Browser builds

GOOS=js GOARCH=wasm compiles. The platform, not the port, limits what runs: the relay WebSocket client has a js-specific dial path, but direct UDP QUIC, direct paths, and NAT traversal are not available in browser WebAssembly. A browser endpoint is therefore relay-only. The iroh/mdns package is excluded from js builds entirely.

The wasmrelay experiment module carries browser demos, and cmd/wasmrelaytest is the in-repo smoke test.

Reading upstream alongside this

The protocol these layers implement is designed and documented upstream. When you need to know why the wire looks the way it does — the relay protocol, the hole-punching sequence, ALPN conventions — read docs.iroh.computer and the Rust source at n0-computer/iroh. This site documents the Go realization of it.

Next steps

Last updated: 2026-08-20