---
name: dart-libp2p-nat-relay
description: Use when making dart_libp2p peers reachable across NATs in Dart: running a Circuit Relay v2 relay, AutoRelay reservations, dialing via /p2p-circuit, AutoNAT v2 reachability, DCUtR hole punching over UDX, mDNS local discovery (MdnsDiscovery), Happy Eyeballs dialing, go-libp2p relay interop.
triggers: p2p-circuit, Circuit Relay v2, Libp2p.relay, Libp2p.autoRelay, relayServers, forceReachability, AutoRelay, EvtAutoRelayAddrsUpdated, AmbientAutoNATv2, EvtLocalReachabilityChanged, HolePunchService, DCUtR, dcutr, MdnsDiscovery, MdnsNotifee, hole punching
---

# Reachability: relay, AutoNAT, hole punching, mDNS

## What it is

`dart_libp2p` reaches peers behind NAT in the go-libp2p way:

1. A public **relay** runs Circuit Relay v2 (`/libp2p/circuit/relay/0.2.0/hop`).
2. A NAT'd peer uses **AutoRelay** to reserve a slot and advertises
   `<relay addr>/p2p/<relay>/p2p-circuit/p2p/<self>`.
3. Another peer dials that address; the relay splices the two streams, and the
   relayed connection gets its own Noise + Yamux inside.
4. **DCUtR** (`/libp2p/dcutr`) then hole-punches a direct UDX connection;
   new streams move to it.
5. **AutoNAT v2** tells a host whether it is public or private.

**mDNS** (`MdnsDiscovery`) finds peers on the same LAN only. For internet
discovery use the DHT (`dart-libp2p-kad-dht`). Host basics:
`dart-libp2p-host`. Browser-style WebRTC over a relay: `dart-libp2p-webrtc`.

## Install

```yaml
dependencies:
  dart_libp2p: ^4.12.0
```

```dart
import 'dart:async';                                             // Timer
import 'package:dart_libp2p/dart_libp2p.dart' hide Option;
import 'package:dart_libp2p/config/config.dart';
import 'package:dart_libp2p/p2p/transport/udx_transport.dart';
import 'package:dart_libp2p/p2p/transport/tcp_transport.dart';
import 'package:dart_libp2p/p2p/transport/connection_manager.dart';
import 'package:dart_libp2p/p2p/host/autorelay/autorelay.dart';   // EvtAutoRelayAddrsUpdated
import 'package:dart_libp2p/p2p/host/basic/basic_host.dart';      // BasicHost extras
import 'package:dart_libp2p/p2p/discovery/mdns/mdns.dart';        // MdnsDiscovery, MdnsNotifee
```

## Core workflow

The pattern below is the one the internet_chat demo runs in production
(`dart-libp2p-demos/internet_chat/lib/src/host.dart`).

### Shared host builder

```dart
Future<Host> buildHost({
  required KeyPair identity,
  required bool relay,
  int port = 0,
  List<MultiAddr> announce = const [],   // public addrs a cloud VM cannot see
  List<String> relayServers = const [],  // '/ip4/x/udp/4001/udx/p2p/12D3...'
}) async {
  final cm = ConnectionManager();
  return Libp2p.new_([
    Libp2p.identity(identity),                      // relays: a persisted key
    Libp2p.connManager(cm),
    Libp2p.transport(UDXTransport(connManager: cm)), // UDP: needed for punching
    Libp2p.transport(TCPTransport(connManager: cm)), // fallback where UDP is blocked
    Libp2p.listenAddrs([
      MultiAddr('/ip4/0.0.0.0/udp/$port/udx'),
      MultiAddr('/ip4/0.0.0.0/tcp/$port'),
    ]),
    Libp2p.addrsFactory((addrs) => [
      ...addrs.where((a) => !a.isLoopback() &&
          a.valueForProtocol('ip4') != '0.0.0.0' && a.valueForProtocol('ip6') != '::'),
      ...announce,
    ]),
    Libp2p.holePunching(true),
    if (relay) ...[
      Libp2p.relay(true),
      Libp2p.forceReachability(Reachability.public), // relay service starts only when public
    ] else ...[
      Libp2p.autoRelay(true),
      Libp2p.relayServers(relayServers),             // dialled during start()
      Libp2p.observedAddrActivationThreshold(1),     // trust the relay's observation
    ],
  ]);
}
```

### Run a relay

```dart
final relay = await buildHost(identity: await loadKey('relay.key'), relay: true,
    port: 4001, announce: [MultiAddr('/ip4/203.0.113.7/udp/4001/udx'),
                           MultiAddr('/ip4/203.0.113.7/tcp/4001')]);
await relay.start();
for (final a in relay.addrs.where((a) => !a.hasProtocol('p2p-circuit'))) {
  print('$a/p2p/${relay.id.toBase58()}');   // hand these to peers
}
```

Open UDP and TCP on the port. The relay also answers AutoNAT v2 checks
(server on by default) and Identify, so peers learn their public address from it.

### NAT'd peer: reserve and get advertised

```dart
final peer = await buildHost(identity: kp, relay: false, relayServers: relayAddrs);
await peer.start();   // connects to relayServers; AutoRelay starts at once
                      // (reachability unknown/private) and reserves

peer.eventBus.subscribe(EvtAutoRelayAddrsUpdated).stream.listen((dynamic e) {
  if (e is! EvtAutoRelayAddrsUpdated) return;
  for (final a in e.advertisableAddrs.where((a) => a.hasProtocol('p2p-circuit'))) {
    print('reachable at $a');   // .../p2p/<relay>/p2p-circuit/p2p/<self>
  }
});
```

The circuit addresses also appear in `peer.addrs`, in Identify and in the
signed peer record. AutoRelay refreshes the reservation and books again when
the relay connection returns (4.8.1). If the relay goes away, reconnect it
yourself, as the demo does:

```dart
for (final r in relayAddrs) {
  final info = AddrInfo.fromMultiaddr(MultiAddr(r));
  if (peer.network.connsToPeer(info.id).isEmpty) await peer.connect(info);
}
```

### Dial a peer through the relay

```dart
final viaRelay = [
  for (final r in relayAddrs) MultiAddr('$r/p2p-circuit/p2p/${target.toBase58()}'),
];
await host.connect(AddrInfo(target, viaRelay));
final relayed = host.network.connsToPeer(target)
    .any((c) => c.remoteMultiaddr.hasProtocol('p2p-circuit'));
```

The dialling host needs `Libp2p.relay(true)` or `Libp2p.autoRelay(true)`:
either registers the circuit transport (`CircuitV2Client`). The target must
hold a reservation on that relay, or the dial fails with `NO_RESERVATION`.

### Hole punching (DCUtR)

Automatic: when a host **accepts** a relayed connection, it starts DCUtR with
the dialer. Both sides exchange their public addresses over the relay and dial
each other at the same time over UDX. On success the direct connection is
added and new streams prefer it; the relayed one stays as a fallback.

```dart
// Watch for the switch:
Timer.periodic(const Duration(seconds: 2), (_) {
  final direct = host.network.connsToPeer(target)
      .where((c) => !c.remoteMultiaddr.hasProtocol('p2p-circuit'));
  if (direct.isNotEmpty) print('direct: ${direct.first.remoteMultiaddr}');
});

// Or trigger it explicitly over an existing relayed connection:
await host.holePunchService?.directConnect(target);
```

If both peers might dial each other at the same moment, let only one dial
(the demo dials only when `host.id.toBase58().compareTo(peer.toBase58()) < 0`),
or the two punches collide.

### Reachability (AutoNAT v2)

```dart
host.eventBus.subscribe(EvtLocalReachabilityChanged).stream.listen((dynamic e) {
  if (e is EvtLocalReachabilityChanged) print('reachability ${e.reachability}');
});
(host as BasicHost).autoNATService?.status;   // Reachability.unknown/public/private
```

Tune the client with `Libp2p.ambientAutoNATv2Config(AmbientAutoNATv2Config(
bootDelay: ..., retryInterval: ..., refreshInterval: ..., ipv4Only: ...))`
(`p2p/host/autonat/ambient_config.dart`; defaults 15 s / 1 min / 15 min).
Turn off answering others' checks on phones: `Libp2p.autoNATv2Server(false)`.
For local test networks a server needs `Libp2p.autoNATv2AllowPrivateAddrs()`.
Optional STUN NAT-type probe: `Libp2p.stunNatDetection(true, servers: [...])`
emits `EvtNATDeviceTypeChanged`; `BasicHost.udpNatDeviceType` reads it.

### Manual reservation (tests, custom relays)

```dart
import 'package:dart_libp2p/p2p/protocol/circuitv2/client/reservation.dart'; // reserve() extension

final client = (host as BasicHost).circuitV2Client!;   // needs relay or autoRelay on
final rsvp = await client.reserve(relayId);
print('expires ${rsvp.expire}, limit ${rsvp.limitDuration} / ${rsvp.limitData} bytes');
```

A manual reservation is not refreshed; AutoRelay's are.

### mDNS on the LAN

```dart
class Found implements MdnsNotifee {
  Found(this.host);
  final Host host;
  @override
  void handlePeerFound(AddrInfo peer) {
    if (host.network.connectedness(peer.id) != Connectedness.connected) {
      host.connect(peer).catchError((Object e) => print('dial ${peer.id}: $e'));
    }
  }
}

await host.start();                                 // first: mDNS advertises host.addrs
final mdns = MdnsDiscovery(host, notifee: Found(host));
await mdns.start();
// ...
await mdns.stop();
```

Service `_p2p._udp.local`, TXT records `dnsaddr=<addr>/p2p/<id>`.
`MdnsQueryOptions` (timeout, interface, IPv4/IPv6 only, `reusePort`,
`reuseAddress`, multicast TTL) goes in `queryOptions:`.

### Happy Eyeballs

`Swarm.dialPeer` ranks a peer's addresses (public IPv6, public IPv4, private
IPv4, then relay addresses) and starts them 250 ms apart; the first connection
wins and the losers are cancelled (UDX dials stop sending, 4.5.0). Direct
attempts time out after `Libp2p.dialTimeout` (15 s), relayed ones after
`Libp2p.relayDialTimeout` (30 s). Circuit addresses are deduplicated to one
attempt per relay, so a slow relay handshake does not open several relayed
connections to the same peer. Nothing to configure; give the peerstore all
addresses and let it race.

## Key concepts / API map

- `Libp2p.relay(true)`: `RelayManager` starts a Circuit Relay v2 service when reachability becomes public; stops it when private.
- `Libp2p.forceReachability(Reachability.public)`: emits public at start and skips ambient AutoNAT probing.
- `Libp2p.autoRelay(true)`: `AutoRelay` + `RelayFinder` reserve on connected peers that speak hop; emits `EvtAutoRelayAddrsUpdated(advertisableAddrs)`.
- `Libp2p.relayServers(List<String>)`: full `/p2p/<id>` multiaddr strings dialled in `start()` (10 s each, failures logged).
- `CircuitV2Client`: the `/p2p-circuit` transport and STOP handler; `BasicHost.circuitV2Client`; `reserve(relayId)` → `Reservation` (`expire`, `addrs`, `voucher`, `limitDuration`, `limitData`).
- `CircuitV2Protocol.protoIDv2Hop` / `protoIDv2Stop` (`p2p/protocol/circuitv2/proto.dart`).
- `HolePunchService.directConnect(peer)`; `host.holePunchService`; protocol `/libp2p/dcutr`; logger `p2p-holepunch`.
- `AmbientAutoNATv2` (`BasicHost.autoNATService`), `AmbientAutoNATv2Config`; AutoNAT v2 protocols `/libp2p/autonat/2/dial-request`, `/libp2p/autonat/2/dial-back`.
- `EvtLocalReachabilityChanged(reachability)`, `EvtNATDeviceTypeChanged`.
- `Conn.stat.stats.limited`: true when the relay announced limits; `Swarm.isRelayedRoute(addr)`.
- `MdnsDiscovery(host, {serviceName, notifee, queryOptions})`, `start()`, `stop()`, `findPeers(ns)`; `MdnsNotifee.handlePeerFound(AddrInfo)`.

## Gotchas

- **A relay with `relay(true)` alone does nothing until it is public.** Use
  `forceReachability(Reachability.public)` on a dedicated relay; turning
  AutoNAT off does not make it public.
- **Relayed connections are limited.** A dart_libp2p relay allows 1 h and
  1 MiB per relayed connection (fixed defaults; no `Libp2p` option changes
  them) and 10 HOP requests per minute per peer; a go-libp2p relay allows
  2 min and 128 KiB. Use the relay to coordinate and for small messages; move
  bulk data to the direct connection, or design the app to survive the
  relayed connection ending (the demo routes chat through the relay as a
  GossipSub member).
- **go-libp2p peers will not open streams on a limited connection** unless
  their code allows it (`network.WithAllowLimitedConn`). Expect Go
  services to answer only after DCUtR made a direct connection.
- **Hole punching needs UDX and public addresses.** TCP ignores the
  simultaneous-connect hint. From 4.1.8 a punch uses only public addresses: two
  peers whose only addresses are private do not punch (on one LAN they connect
  directly). Symmetric NATs (mobile carriers) defeat punching; with STUN
  detection on, the puncher tries once instead of three times.
- **A NAT'd peer must learn its public address.** It comes from Identify
  observations, which by default need 4 peers to agree. A peer that knows
  only its relay must set `Libp2p.observedAddrActivationThreshold(1)`, or DCUtR
  has nothing to send. UDX dials leave from the listen socket (4.1.0) so the
  observed port is the real mapping; do not listen only on `127.0.0.1`.
- **Advertised circuit addresses already end in `/p2p/<self>`.** Older docs say
  to append the destination ID; check before appending twice. When you build
  one yourself, the form is `<relay addr>/p2p/<relay id>/p2p-circuit/p2p/<target>`.
  A bare `/p2p-circuit` address is not dialable and is not advertised.
- **AutoRelay starts while reachability is unknown** (no need to emit a fake
  `private` event, which old docs show) and stops when it turns public.
  Its relay candidates are connected peers whose Identify listed the hop
  protocol, so connect to the relay first (`relayServers` does).
- **The default AutoRelay timing in `BasicHost` is a 5 s boot delay and 10 s
  re-check**; tests should wait for `EvtAutoRelayAddrsUpdated`, not a fixed sleep.
- **Before 4.8.1** a node behind NAT stayed undialable up to an hour after its
  relay connection dropped (no connectedness event, a 1-hour backoff). Use a
  current version.
- **Two peers dialling each other through one relay at once** closed each
  other's connection before 4.6.3; now kept. Still prefer one dialer.
- **Relay addresses need IPs.** `relayServers` and dial addresses with
  `/dns4/...` are not dialable by TCP or UDX; resolve first.
- **AutoNAT v2 ignores relay addresses** (4.1.7): a host whose only public
  address is a circuit address stays `unknown`. That is correct.
- **mDNS is LAN-only and silent on failure.** It advertises `host.addrs` at
  `start()`: start the host first, and do not listen only on loopback (filtered
  out), or nothing is advertised. On macOS the system responder holds UDP
  5353 and `mdns_dart` cannot bind it, so Mac instances do not find each other;
  test on Linux or in Docker. `reusePort` is off by default on Android. mDNS
  does not connect for you; connect in `handlePeerFound`.
- **AutoNAT v2 server dial-backs come from a separate host** with its own key
  (4.0.0, as go-libp2p); do not treat that peer ID as the server.

## Testing

- **In-process relay:** three hosts on loopback. The relay uses
  `Libp2p.relay(true)`, `forceReachability(Reachability.public)` and
  `Libp2p.addrsFactory((a) => a)` (keep loopback). Peers use `autoRelay(true)`
  and `relayServers(['<relay loopback addr>/p2p/<relay id>'])`. Wait for
  `EvtAutoRelayAddrsUpdated` with a timeout, dial
  `<relay>/p2p-circuit/p2p/<B>` from A, then assert the connection
  `hasProtocol('p2p-circuit')`. To force the relay path, keep only circuit
  addresses in the peerstore (`addrBook.clearAddrs(B)` first).
- **Hole punching cannot be tested on loopback**: no NAT, and private addresses
  are not punched. The repo uses Docker NAT harnesses
  (`test/integration/holepunch_network/`, demo `docker compose -f
  docker/compose.yml up`, `NAT_TYPE=symmetric` for the failing case).
- **go-libp2p interop:** `test/interop/go_interop_relay_test.dart` runs a Go
  relay and Go peers over TCP (Dart dials a Go peer through a Go relay; Go dials
  Dart through a reservation). DCUtR with Go needs a Go peer with UDX.
- Close every host in `tearDown`; relay and AutoRelay timers otherwise keep the
  test process alive.

## Red lines

- Never run a public relay with `FixedLimiter` / `NullResourceManager`; keep
  resource limits on a node strangers connect to.
- Never treat a relay as trusted for content: it sees who talks to whom and
  can drop traffic, but cannot read it (end-to-end Noise). Sign application data
  that matters.
- Never send large or long transfers over a limited relayed connection and call
  it done; detect `limited` and wait for (or require) a direct connection.
- Never hard-code a relay without its `/p2p/<id>`; dialling an address that a
  different peer answers fails with `PeerIdMismatchException`, by design.
- Never rotate a relay's key casually: every peer's configured relay address
  contains its peer ID.

## Reference

- Relay setup: https://cryptopeer.net/libp2p/core/relay-setup/
- Transports: https://cryptopeer.net/libp2p/core/transports/
- Configuration: https://cryptopeer.net/libp2p/core/configuration/
- Event bus: https://cryptopeer.net/libp2p/core/event-bus/
- Internet chat demo (relay + DHT + GossipSub + DCUtR): https://cryptopeer.net/demos/internet-chat/
- Source and CHANGELOG: https://github.com/stephanfeb/dart_libp2p
- Demo source: https://github.com/stephanfeb/dart-libp2p-demos
