Table of Contents
- Standard library mapping
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.
internal/itls/README.mdinternal/qng/README.md
Next steps
- How go-iroh works — where the forks sit in the layer stack.
- Concepts and terminology — the iroh side of the table above.
- API map — task-to-call reference for the public packages.