---
title: Endpoints and connections
description: Bind an endpoint, route by ALPN, open streams and datagrams, and observe paths.
icon: plug
---

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:

```go
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

```go
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:

```go
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

```go
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.ConnectEarly` returns a `*Connecting` so you can send 0-RTT data before
  the handshake completes. Check `conn.Used0RTT()` afterwards.
- `ep.Dial` returns a `net.Conn` for 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:

```go
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

```go
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. The `Socket` field is an
  `iroh.SocketMetrics` with 27 counters covering sends and receives per
  transport (`SendIPv4`, `SendRelay`, `RecvDataRelay`, …), path composition
  (`PathsDirect`, `PathsRelay`), `HolepunchAttempts`, `RelayHomeChange`, and
  `SendBlackholed`. Each has a stable OpenMetrics name such as
  `socket_relay_home_change`; see the `metrics` package for the registry.
- `conn.KeyExchangeGroup()` — the negotiated TLS key-exchange group; see
  [Security and privacy](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:

```go
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](relays-and-discovery) — making `addr` resolvable
  across a NAT.
- [How go-iroh works](architecture) — what the path set is doing underneath.
- [Troubleshooting](troubleshooting) — when a dial hangs or a stream never
  ends.
