Table of Contents

Security and privacy

What an iroh connection guarantees, what your endpoint ID and addresses reveal, and the knobs go-iroh gives you.

Two different questions get confused here. Security is whether someone can read or forge your traffic. Privacy is who learns that you exist and where you are. iroh is strong on the first and requires care on the second.

Upstream’s Security & Privacy page covers the protocol-level story for the whole project. This page covers what go-iroh specifically gives you and what its defaults do.

What a connection guarantees

Every direct peer-to-peer connection is TLS 1.3 with mutual authentication using RFC 7250 raw public keys — the ed25519 key is the credential. Two consequences:

This is why an endpoint ID is a complete identity: possession of the secret key is the only thing that lets anyone speak as that endpoint. Protect the secret key accordingly — iroh.WithSecretKey takes one you have stored, and iroh.Bind with no key generates a fresh identity every run.

Post-quantum key exchange

Direct connections negotiate a TLS key-exchange group, and the default is already post-quantum. iroh.WithKeyExchangePolicy selects the policy:

Policy Effect
KeyExchangeDefault the package default
KeyExchangeClassical disables post-quantum key exchange
KeyExchangePreferPQ prefers X25519MLKEM768, keeps classical fallback
KeyExchangePQOnly requires X25519MLKEM768

Conn.KeyExchangeGroup() reports what was actually negotiated. Binding two endpoints under each policy and connecting them prints:

default   -> X25519MLKEM768
classical -> X25519
pq-only   -> X25519MLKEM768

The threat this addresses is harvest-now-decrypt-later: an adversary recording traffic today to decrypt once quantum computers can break X25519. A hybrid group protects the session even if one half is broken later.

KeyExchangePQOnly refuses to talk to peers that cannot do X25519MLKEM768 — including older iroh peers. Use PreferPQ when interoperability matters more than a hard guarantee, and check Conn.KeyExchangeGroup() if you need to know what you got.

What a relay does learn

A relay does not see your plaintext, but it does see metadata: which endpoint IDs are connected to it, which pairs are exchanging traffic, timing, and volume. It also sees your IP address, because you connected to it.

That is the honest trade. If relay metadata is part of your threat model, run your own relay — see Commands for iroh-relay and Relays and discovery for pointing endpoints at it with relay.ModeCustomURLs.

What discovery publishes

This is where most privacy surprises live. Publishing to a public pkarr relay makes a record queryable by anyone who knows your endpoint ID.

go-iroh’s default is conservative, and it is worth knowing you are relying on it: PkarrPublisher publishes only relay addresses by default. The godoc is explicit that this avoids “leaking direct IP addresses to a public pkarr relay.” The mechanism is iroh.RelayOnlyFilter, the default value of PkarrPublisherConfig.AddrFilter:

// AddrFilter controls which addresses are published. If nil,
// [RelayOnlyFilter] is used. Use a filter that returns its input unchanged
// to publish all addresses.

go-iroh ships iroh.IPOnlyFilter as the inverse (direct IP and custom addresses, dropping relays), and any func([]netaddr.TransportAddr) []netaddr.TransportAddr works, so you can write your own policy — for example publishing only private-range addresses on a trusted LAN.

So if you set AddrFilter to publish everything, you are publishing your direct IP addresses — including local ones — to a public server, keyed by your endpoint ID. That may be exactly what you want on a trusted network. Do it knowingly.

Two related notes:

Direct connections reveal your IP to the peer

Once a direct path is established, the peer knows your IP address — that is what “direct” means. NAT traversal exists to make this happen. If you do not want a given peer to learn your address, you must keep the connection on a relay, which is a routing decision, not something the handshake can hide.

Controlling who may connect

Authentication answers “is this peer who they claim to be.” It does not answer “should this peer be allowed in.” For that, use endpoint hooks: AfterHandshake runs with an authenticated conn.RemoteID() and can reject with a close code, which is the supported way to implement an allow list.

Note the caveat documented on that page: a hook rejection is admission control on the accepting side, not a synchronous refusal the dialer sees immediately.

Deliberately not covered

go-iroh has no equivalent of upstream’s onion-routing and multi-hop-relay discussion, because those are deployment and roadmap topics for the Rust project rather than Go API surface. For the protocol-level threat model and what n0 plans, read upstream’s page.

Next steps

Last updated: 2026-08-20