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:
- Both sides are authenticated. After the handshake,
conn.RemoteID()is the peer’s actual public key, cryptographically proven. There is no certificate authority to trust or to be compromised. - The relay cannot read your traffic. A relay forwards encrypted QUIC packets between endpoints that authenticated to each other, not to it. It is a dumb pipe with respect to your data.
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:
- mDNS discovery (
iroh/mdns) broadcasts your direct addresses on the local link by design. That is its whole function; it does not reach beyond the local network, but everyone on that network sees it. - Endpoint tickets embed whatever addresses the
EndpointAddrcarried. A ticket built from direct addresses hands those addresses to whoever receives the ticket.
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
- Observing and rejecting connections — allow lists and custom authentication.
- Relays and discovery — self-hosting, and what each discovery service publishes.