Table of Contents

Standard library mapping

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.

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:

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:

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.

// 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.

Next steps

Last updated: 2026-08-20