Table of Contents
Endpoints and connections
Bind an endpoint, route by ALPN, open streams and datagrams, and observe paths.
This page is the working guide to the iroh package: creating endpoints,
accepting and dialing connections, and moving bytes.
Bind an endpoint
iroh.Bind(ctx, opts...) is the only constructor. Every knob is a functional
option:
ep, err := iroh.Bind(ctx,
iroh.WithSecretKey(sk), // stable identity across restarts
iroh.WithALPNs("example/echo/1"), // protocols this endpoint accepts
iroh.WithRelayMode(relay.ModeDefault()),
)
if err != nil {
return err
}
defer ep.Shutdown(ctx)
Options you are most likely to need:
| Option | Effect |
|---|---|
WithSecretKey(sk) |
use a fixed identity instead of a fresh random key |
WithALPNs(alpns...) |
the protocols this endpoint will accept |
WithBindAddr(ap) |
bind a specific address/port instead of the default |
WithRelayMode(m) |
which relays to use (relay.ModeDefault/Staging/Disabled/Custom) |
WithAddressLookup(s) |
discovery publishers and resolvers |
WithNetReport() |
run QAD probes and populate Endpoint.NetReport() |
WithPathSelector(sel) |
override path preference |
WithCustomTransport(t) |
carry traffic over a non-UDP transport |
WithoutIPTransports() / WithoutRelayTransports() |
compile a family out |
WithRelayFirstDial() |
dial the relay path first, then upgrade |
There is no Close; Endpoint.Shutdown(ctx) is the shutdown call, and
Endpoint.Closed() returns a channel that closes when it completes.
Endpoint.SetALPNs changes the accepted set after binding.
Identity and address
id := ep.ID() // key.EndpointID — the endpoint's public name
addr := ep.Addr() // netaddr.EndpointAddr — id plus current paths
local := ep.LocalAddr() // netip.AddrPort — the bound socket
ep.Addr() is a snapshot: relay assignment and discovered addresses change it
over time. Use ep.WatchAddr() when you need to re-publish or re-share the
address as it changes, and ep.Online(ctx) to block until a home relay
connection exists before handing your address to anyone.
You can add addresses the endpoint cannot discover for itself (a port-forwarded
public address, say) with ep.AddExternalAddr / ep.RemoveExternalAddr.
Serve protocols with a Router
For anything beyond a single connection, register handlers instead of writing an accept loop:
router, err := iroh.NewRouter(ep, map[string]iroh.ProtocolHandler{
"example/echo/1": iroh.ProtocolHandlerFunc(handleEcho),
"example/files/1": filesHandler{},
}, nil)
if err != nil {
return err
}
defer router.Shutdown(ctx)
A ProtocolHandler receives an accepted *iroh.Conn and owns it until it
returns. iroh.ProtocolHandlerFunc adapts a plain function. The Router is
the Go analog of Rust iroh’s Router, and it registers its ALPNs on the
endpoint for you.
For a single hand-rolled loop, ep.Accept(ctx) returns the next *Conn, and
ep.AcceptIncoming(ctx) returns an *Incoming you can inspect or reject
before completing the handshake. Only one accept loop may run at a time —
starting a second returns iroh.ErrEndpointAcceptLoopInUse.
Dial
conn, err := ep.Connect(ctx, addr, "example/echo/1")
addr is a netaddr.EndpointAddr. It must either carry at least one path or
be resolvable by a configured address-lookup service; a bare ID with neither
returns iroh.ErrNoAddress.
Variants:
ep.ConnectEarlyreturns a*Connectingso you can send 0-RTT data before the handshake completes. Checkconn.Used0RTT()afterwards.ep.Dialreturns anet.Connfor code that wants the standard interface.
Errors worth handling by name: ErrNoAddress, ErrNoRelay, ErrSelfConnect,
ErrConnectRejected, ErrHandshakeRejected, ErrConnClosedDuringHandshake,
ErrEndpointClosed.
Streams and datagrams
A *iroh.Conn multiplexes QUIC streams:
s, err := conn.OpenStreamSync(ctx) // bidirectional
u, err := conn.OpenUniStreamSync(ctx) // send-only
s, err := conn.AcceptStream(ctx) // the peer's bidirectional stream
r, err := conn.AcceptUniStream(ctx) // the peer's send-only stream
Streams implement the usual io interfaces. Closing a bidirectional stream
closes your send side and signals EOF to the peer while you keep reading —
that half-close is what makes request/response protocols terminate.
OpenStreamConn and AcceptStreamConn return net.Conn wrappers when you
want to hand a stream to code that expects one. ep.ListenStreams() returns a
*StreamListener, i.e. a net.Listener-shaped view of incoming streams.
Unreliable datagrams are available with conn.SendDatagram(p) and
conn.ReadDatagram(ctx).
Close with conn.CloseWithError(code, reason) so the peer learns why.
Observe the connection
for _, p := range conn.Paths() {
fmt.Println(p.ID, p.Validated, p.Addr, p.RTT)
}
conn.Paths()/conn.WatchPaths(ctx)— the live path set; this is how you tell a relayed connection from a direct one, and how you watch the upgrade happen.conn.Stats()— connection statistics.conn.MultipathNegotiated(),conn.Used0RTT(),conn.Side(),conn.StableID(),conn.ALPN(),conn.RemoteID().ep.RemoteInfo(id)— what the endpoint knows about a peer.ep.Metrics()— endpoint counters. TheSocketfield is aniroh.SocketMetricswith 27 counters covering sends and receives per transport (SendIPv4,SendRelay,RecvDataRelay, …), path composition (PathsDirect,PathsRelay),HolepunchAttempts,RelayHomeChange, andSendBlackholed. Each has a stable OpenMetrics name such assocket_relay_home_change; see themetricspackage for the registry.conn.KeyExchangeGroup()— the negotiated TLS key-exchange group; see Security and privacy for the post-quantum policies.
Choose among paths
When several paths to a peer are usable, a PathSelector decides which one
carries traffic:
type PathSelector interface {
Select(current netaddr.TransportAddr, candidates []PathCandidate) (netaddr.TransportAddr, bool)
}
Returning ok = false keeps the current selection. Implementations must not
block — they run on the path-management path.
The default is iroh.BiasedRttPathSelector, and its bias is worth knowing
because it explains behavior you will otherwise find surprising: it sorts by
(tier, biased RTT), so direct IP and custom paths always beat relay paths
regardless of RTT; within a tier the lowest biased RTT wins; IPv6 gets a 3ms
advantage; and switching within a tier requires the candidate to be at
least 5ms better than the current path. That last rule is hysteresis — it is
why a path that looks marginally faster in your metrics does not get selected.
Override with iroh.WithPathSelector when you need a different policy, for
example pinning to a relay for privacy or preferring a metered-link-avoiding
route.
Next steps
- Relays and discovery — making
addrresolvable across a NAT. - How go-iroh works — what the path set is doing underneath.
- Troubleshooting — when a dial hangs or a stream never ends.