---
title: Standard library mapping
description: How iroh concepts land on Go stdlib and x/ APIs, and the four places go-iroh must diverge — with the conditions that would let each divergence end.
icon: library
---

If you already know Go's networking and crypto packages, go-iroh's *public API*
is largely a translation exercise — the internals are a clean-room port with
two vendored forks. This page is the translation table, followed by an account
of where the standard library cannot be used yet and why.

The short version: **identity, addressing, and streams are stdlib.
The TLS handshake and the QUIC stack are not.**

## Concept translation

| iroh concept | Nearest Go API | How go-iroh spells it |
| --- | --- | --- |
| Endpoint identity | `crypto/ed25519` | `key.SecretKey`, `key.PublicKey`, `key.EndpointID` |
| Signature | `ed25519.Sign` / `Verify` | `key.Signature`, `SecretKey.Sign` |
| Socket address | `net/netip.AddrPort` | `netaddr.IPAddr` wraps `netip.AddrPort` |
| Peer address | *(no stdlib analog)* | `netaddr.EndpointAddr` — an ID plus zero or more paths |
| Relay address | `net/url.URL` | `netaddr.RelayURL`, built by `RelayURLFromURL` |
| Protocol selection | `tls.Config.NextProtos` (ALPN) | ALPN strings, `iroh.WithALPNs` |
| Accepting a protocol | `http.ServeMux` | `iroh.Router` maps ALPN → `iroh.ProtocolHandler` |
| A connection | `net.Conn` | `*iroh.Conn` — a QUIC connection, so many streams |
| A stream | `net.Conn` | `*iroh.Stream`, plus `net.Conn` adapters (below) |
| Dialing | `net.Dialer.DialContext` | `Endpoint.Dial` — returns a `net.Conn` directly |
| Listening | `net.Listener` | `Endpoint.ListenStreams` → `*iroh.StreamListener` |
| Unreliable message | `net.PacketConn` | `Conn.SendDatagram` / `Conn.ReadDatagram` |
| Name lookup | `net.Resolver.LookupTXT` | `dns.Resolver` — and its default really is `net.DefaultResolver` |
| Observable value | `sync.Cond`, or a channel | `watch.Value[T]` / `watch.Observer[T]` |
| Metrics | `expvar` | `metrics` — its snapshot type implements `expvar.Var` |
| Wire encoding | `encoding/gob`, `encoding/binary` | `postcard` — Rust's format, so gob is not an option |
| HTTP/3 | `net/http` | `quicconn` adapts an `iroh.Conn` for an HTTP/3 stack; it is not one |

Two of these deserve a note.

`iroh.Stream` is deliberately *not* a `net.Conn`: it has `Read`, `Write`,
`Close`, and all three `SetDeadline` methods, but a stream has no address of
its own — the address belongs to the connection. Rather than invent one, the
API offers explicit adapters when you need to hand a stream to code that
demands a `net.Conn`:

```go
c, err := conn.OpenStreamConn(ctx)  // net.Conn, LocalAddr/RemoteAddr from the connection
c, err := conn.AcceptStreamConn(ctx)
```

The listener side has the same escape hatch, and it is the half that matters
if you want to hand iroh to `http.Serve` or `grpc.Server.Serve`:

```go
l, err := ep.ListenStreams()  // *iroh.StreamListener: Accept() (net.Conn, error), Addr(), Close()
c, err := ep.Dial(ctx, addr, alpn)  // net.Conn in one call
```

Each `net.Conn` from `StreamListener.Accept` is one bidirectional QUIC stream,
and several may come from the same peer connection — so closing one closes
that stream only. Accepted conns also carry `RemoteID` and `Used0RTT` methods,
which is how you recover the peer identity that `net.Conn` has nowhere to put.

`dns.Resolver` is the clearest example of the general rule. Its `Lookuper`
field is a `TXTLookuper` interface, and when you leave it nil the lookup goes
to `net.DefaultResolver.LookupTXT`. The interface exists so `dns.DoHLookuper`
and `dns.DoTLookuper` can be substituted where the host resolver is untrusted
or blocked — not because the stdlib resolver was inadequate.

## Where the standard library is used directly

This is most of the module, and it is worth stating explicitly, because the
two forks below are easy to mistake for a general "go-iroh reimplements Go's
networking" posture. It does not.

| Need | Package used |
| --- | --- |
| Ed25519 keys and signatures | `crypto/ed25519` |
| UDP sockets | `net`, `net/netip` |
| Relay, pkarr, DoH and DoT transport | `net/http`, standard `crypto/tls` with WebPKI |
| Hex, base32, JSON | `encoding/hex`, `encoding/base32`, `encoding/json` |
| Cancellation and deadlines | `context`, `time` |
| Randomness | `crypto/rand` |

Note the third row in particular: everything that talks to *infrastructure*
(a relay, a pkarr server, a DoH endpoint) uses ordinary `crypto/tls` with
ordinary certificate verification. Only the direct peer-to-peer QUIC handshake
needs anything unusual.

## Where go-iroh must differ

**Two forks.** A fork means vendored, patched copies of code that is otherwise
stdlib or the community standard — the expensive kind of divergence, and the
only kind this section is really about. Two further dependencies get their own
subsections not because a dependency is unusual, but because in both cases the
obvious stdlib answer *exists and still cannot be used*; the rest of the
module's dependencies are ordinary and are listed further down.

Each of the four has a stated exit condition, collected in a table at the end.

### 1. RFC 7250 raw public keys — `internal/itls/tls`

**What stdlib gives you:** `crypto/tls`, which authenticates peers with X.509
certificate chains.

**What iroh needs:** TLS 1.3 Raw Public Keys (RFC 7250). The ed25519 public
key *is* the credential; there is no chain, no CA, and no name to verify. That
is what makes an endpoint ID a complete identity.

**Why stdlib cannot do it:** `crypto/tls` does not implement the
`client_certificate_type` / `server_certificate_type` extensions, so the
negotiation that selects raw public keys never happens. `VerifyConnection`
does not rescue this: the handshake fails while parsing a bare SPKI as an
X.509 chain, before any callback runs.

**What go-iroh does:** vendors `crypto/tls` under `internal/itls/tls` and
patches it, with eleven small packages under `internal/itls/shim` supplying the
GOROOT-private dependencies a copy outside the standard library cannot import —
`godebug`, `cpu`, `boring`, `byteorder`, the FIPS TLS 1.3 *and* 1.2 key
schedules, AES-GCM, and HKDF. The `internal/itls/README.md` Layout section is
the authoritative list.

This is not a preference. Raw public keys are what iroh puts on the wire, so
the alternative to the fork is not "use stdlib TLS" — it is "do not interoperate."

**When it ends** (from `internal/itls/README.md`): when upstream `crypto/tls`
provides certificate-type negotiation for raw public keys, QUIC support
through `tls.QUICClient` / `tls.QUICServer`, SPKI parsing in place of X.509
when raw keys are negotiated, mutual ed25519 raw-key authentication, and a
resumption story that does not skip identity verification.

### 2. The QUIC stack — `internal/qng`

**What exists:** `github.com/quic-go/quic-go`, the de facto standard Go QUIC
implementation. go-iroh pins v0.59.1 as the fork base.

**Why not `x/net/quic`?** The obvious question for this audience. The standard
library ships the `crypto/tls` QUIC *handshake* API but no QUIC transport;
`golang.org/x/net/quic` is the nearest thing, and it does not help here. It is
explicitly experimental with no compatibility promise, it drives concrete
`crypto/tls` exactly as quic-go does — so it offers no seam for RFC 7250 either
— and it implements none of the iroh/noq extensions. Forking it would buy the
same problems from a less battle-tested base.

**Why a fork:** quic-go calls the *concrete* `crypto/tls` QUIC API. There is
no interface seam to inject a different TLS implementation — the handshake
state machine is `crypto/tls`. So the moment fork 1 is necessary, fork 2
follows mechanically: quic-go must be repointed at the patched TLS. Because
that use is spread across quic-go's `internal/` tree, Go's internal-visibility
rule forces copying the whole transitive package set so the `tls.Config` type
is identical throughout the graph.

The fork then also carries the iroh/noq extension surface: QUIC multipath, QAD
observed-address reporting, and QNT NAT traversal, none of which upstream
quic-go implements. It additionally carries local admission behavior around
QUIC Retry — upstream quic-go does implement Retry itself, so this is a
policy change on an existing mechanism, not a missing feature.

**Cost:** only *half* a quic-go bump is mechanical. The import rewrite is
scripted (`internal/qng/regenerate.sh`) and reproducible from the module cache;
the extension additions are then re-applied by hand, which is the expensive
part. They are kept in plainly named files (`multipath_*`, `observed_addr_*`,
`qnt_*`, `retry_admission_test.go`) to make that re-application tractable.

**When it ends:** the TLS rewrite half can go when fork 1 goes. The extension
half needs upstream quic-go to carry the multipath and NAT-traversal
extensions. Its README is explicit that the fork should shrink only when a
specific part becomes unnecessary — not merely because quic-go grows a nearby
feature.

### 3. BLAKE3 — `lukechampine.com/blake3`

The iroh wire format is BLAKE3 throughout: blob hashes and BAO verified
streaming, gossip topic IDs, PlumTree message IDs, and the relay handshake.
Go's standard library has SHA-2 and SHA-3, and `golang.org/x/crypto` has no
BLAKE3 package. Like RFC 7250 above, this is not a choice go-iroh could make
differently — the hash is fixed by the protocol.

Note that `blobs.Hash` is a `[32]byte` value type, not a `hash.Hash`. BAO
verified streaming does not fit the `hash.Hash` interface: it verifies
subtrees against a root as bytes arrive, rather than reducing a stream to one
digest at the end.

Ends when BLAKE3 lands in the standard library or `x/crypto`.

### 4. Ed25519 point validation — `filippo.io/edwards25519`

`key` uses one function from this package: decoding a 32-byte public key as an
edwards25519 point, to reject non-canonical and off-curve bytes at parse time
rather than at first use.

```go
// key/key.go, in NewPublicKey
if _, err := new(edwards25519.Point).SetBytes(b[:]); err != nil {
	return PublicKey{}, ErrInvalidKeyData
}
```

`crypto/ed25519` deliberately exposes no point type, so there is no stdlib way
to ask "is this a valid public key?" without attempting a verification. Every
other ed25519 operation in `key` is stdlib.

## The x/ dependencies, and why they are not divergences

| Package | Used for | Why not stdlib |
| --- | --- | --- |
| `golang.org/x/net/dns/dnsmessage` | pkarr signed packets, the DNS server, DoH | pkarr signs raw DNS wire messages; `net` resolves names but cannot build or parse them |
| `golang.org/x/net/ipv4`, `ipv6` | batched UDP reads with control messages (in `qng`); multicast group joins (in `iroh/mdns`) | `net.UDPConn` has no per-packet control-message or `ReadBatch` API |
| `golang.org/x/crypto/{chacha20poly1305,cryptobyte,hkdf,chacha20}` | inside the two forks only | these are what `crypto/tls` and quic-go themselves depend on; the forks inherit them |
| `golang.org/x/sys` | syscall access in `qng`'s socket layer, `iroh/mdns`, and the shim's `cpu` package | by design: `x/sys` is where Go puts syscalls |
| `github.com/coder/websocket` | the relay client and server | `x/net/websocket` is documented as lacking features and not actively developed; the relay protocol needs context-aware binary messaging |
| `rsc.io/script` | test-only, for CLI integration tests | not a runtime dependency |
| `github.com/klauspost/cpuid` | indirect, via `blake3` | not imported directly |

The `x/crypto` row is the important one: nothing in go-iroh's *own* code
reaches for `x/crypto`. All nine files that import it are inside `internal/itls/tls` and
`internal/qng`, inherited from the upstream code those forks copy.

## Checking the exit conditions

| Divergence | Retired when |
| --- | --- |
| `internal/itls/tls` | `crypto/tls` gains certificate-type negotiation for RFC 7250, QUIC support, SPKI parsing, mutual raw-key auth, and a safe resumption story |
| `internal/qng` (TLS rewrite) | the row above — it is a consequence of it |
| `internal/qng` (extensions) | upstream quic-go carries multipath, QAD, and QNT |
| `lukechampine.com/blake3` | BLAKE3 lands in the standard library or `x/crypto` |
| `filippo.io/edwards25519` | `crypto/ed25519` exposes public-key validation without a verify |

Both forks carry a README section titled "When to break this fork" naming the
concrete upstream capability that would retire them. That is the standard to
hold them to: if you are evaluating go-iroh, read those two files and check
whether the stated conditions are still unmet.

- `internal/itls/README.md`
- `internal/qng/README.md`

## Next steps

- [How go-iroh works](architecture) — where the forks sit in the layer stack.
- [Concepts and terminology](concepts) — the iroh side of the table above.
- [API map](api-map) — task-to-call reference for the public packages.
