---
title: How go-iroh works
description: The layers between iroh.Bind and a packet on the wire — transports, paths, the TLS and QUIC forks, and what wire compatibility costs.
icon: diagram-project
---

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

```text
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:

- **Direct IP.** UDP to an address the peer advertised or that hole punching
  discovered.
- **Relay.** A WebSocket connection to a relay server, which forwards to the
  peer's relay connection. Available whenever the relay is reachable; higher
  latency than a direct path.
- **Custom.** Anything you implement, via the `iroh.CustomTransport` interface
  and `netaddr.CustomAddr` addresses. This is how go-iroh runs over transports
  that are not UDP at all.

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](go-stdlib-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](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](https://docs.iroh.computer) and the Rust source at
[n0-computer/iroh](https://github.com/n0-computer/iroh). This site documents
the Go realization of it.

## Next steps

- [Endpoints and connections](endpoints-and-connections) — the API this
  describes.
- [Relays and discovery](relays-and-discovery) — configuring the pieces above.
- [Testing and interop](testing-and-interop) — how the wire claims are checked.
