Networking & Discovery
A Calimero node is a libp2p peer. This page is the operator’s view of how nodes connect: the swarm transport and ports, the discovery mechanisms that find peers, and the NAT-traversal pieces that make a node reachable from behind a router. Every knob maps to a key in the configuration reference; the on-the-wire protocol details live in Networking & the wire protocol.
The swarm transport
Section titled “The swarm transport”The swarm is the peer-to-peer listener — separate from the local
HTTP server. It binds on port 2428 by default, over
two transports:
- TCP — with TLS / Noise encryption and yamux stream multiplexing.
- QUIC (
quic-v1, over UDP) — encrypted, multiplexed transport on the same port number.
merod init seeds [swarm] listen with all four combinations — TCP and QUIC,
on IPv4 (0.0.0.0) and IPv6 (::):
[swarm]listen = [ "/ip4/0.0.0.0/tcp/2428", "/ip4/0.0.0.0/udp/2428/quic-v1", "/ip6/::/tcp/2428", "/ip6/::/udp/2428/quic-v1",]The swarm also runs identify (peers exchange their observed addresses and
supported protocols) and ping (liveness) on every connection. These are always
on and have no operator config.
How peers discover each other
Section titled “How peers discover each other”A node has to learn which peers exist and how to dial them. Four mechanisms run in parallel, each suited to a different deployment.
Bootstrap nodes
Section titled “Bootstrap nodes”A static list of known peers to dial on startup — the entry point into a
network. merod init --boot-network seeds the list (calimero-dev by default,
or ipfs), and --boot-nodes <ADDR> adds explicit multiaddrs. Each bootstrap
multiaddr must end in a peer ID (/p2p/...).
[bootstrap]nodes = [ "/ip4/63.181.86.34/udp/4001/quic-v1/p2p/12D3KooW...",]mDNS (LAN)
Section titled “mDNS (LAN)”Local-network discovery via multicast DNS — nodes on the same LAN find each
other automatically with no bootstrap list. Enabled by default
([discovery] mdns = true); does nothing across routed networks or the public
internet, so leave it on for local clusters and ignore it for hosted ones.
[discovery]mdns = trueRendezvous
Section titled “Rendezvous”A rendezvous server is a meeting point: nodes register under a namespace and discover others registered there. This works across NATs and the internet where mDNS cannot. Nodes act as rendezvous clients, registering in the configured namespace and periodically querying for peers.
Beyond the configured global namespace, a node also registers and discovers under a separate key per overlay it follows, so discovery returns only the peers it actually collaborates with — see Reconnecting after a restart or partition below.
[discovery.rendezvous]namespace = "/calimero/devnet/global"discovery_rpm = 0.5 # query throttle floor (~1 query / 120s / peer)registrations_limit = 3
[discovery.rendezvous.discovery_interval]secs = 15nanos = 0DHT (Kademlia)
Section titled “DHT (Kademlia)”The node runs a Kademlia DHT (protocol /calimero/kad/1.0.0) and bootstraps it
from the configured boot nodes. It backs peer-routing — finding the addresses of
a known peer ID by walking the DHT — and grows the routing table as the node
meets peers. There are no dedicated [discovery] keys for the DHT; it is seeded
by [bootstrap] nodes and driven by the swarm.
NAT traversal
Section titled “NAT traversal”A node behind a home/cloud NAT is not directly dialable. Three cooperating libp2p behaviours make it reachable.
| Behaviour | Role |
|---|---|
| AutoNAT | Probes external peers to learn whether the node is publicly reachable and to confirm its observed external address. |
| Relay (client) | Reserves a slot on a relay so other peers can reach this node through the relay when a direct dial fails. |
| DCUTR | Direct Connection Upgrade Through Relay — once peers meet over a relay, they attempt a coordinated hole punch to upgrade to a direct connection. |
The relevant keys:
[discovery]advertise_address = false # whether to advertise external addressesexternal_address = [] # operator-supplied static external multiaddrs
[discovery.relay]registrations_limit = 3 # max relay reservations to hold
[discovery.autonat]max_candidates = 5 # max address candidates probed per round
[discovery.autonat.probe_interval]secs = 10nanos = 0The reachability state machine
Section titled “The reachability state machine”AutoNAT isn’t just a probe — its verdict drives a small state machine that switches the node’s whole posture between “publicly reachable server” and “NAT’d client”. The node runs a switchable AutoNAT v2 behaviour that starts as a client only and is flipped into a client-and-server as reachability is confirmed. Confirmed external addresses (from AutoNAT probes or the swarm) move the node between three states, and each transition fires a batch of discovery actions:
| State | Meaning | Actions on entering |
|---|---|---|
Unknown | no probe result yet | none — wait for the first verdict |
Reachable | a confirmed external address exists | enable the AutoNAT server (so this node can probe others) and register with rendezvous so peers can find it |
Unreachable | no confirmed external address | disable the AutoNAT server, refresh its rendezvous registration, request relay reservations, and re-run rendezvous discovery to reattach through relays |
Only real transitions act — re-confirming an address while already Reachable
is a no-op, so the machine doesn’t thrash. A separate trigger covers the
restart/partition case: losing the last connection to a regular peer forces
an immediate rendezvous re-discovery that bypasses the normal query throttle (see
below), so a node picks up a peer’s
fresh post-restart registration without waiting out the throttle window.
The latest probe is retained (one slot, newest only) and surfaced for operators:
meroctl --node <name> network statusreports the current reachability (Unknown / Public / Private) plus the last
AutoNAT test — the address that was tested, whether it came back reachable or
failed, and when — so “is this node behind NAT?” is answerable without scraping
RUST_LOG=debug. The same snapshot lists relay reservations, rendezvous
registrations, and any DCUtR direct-connection upgrades.
Reconnecting after a restart or partition
Section titled “Reconnecting after a restart or partition”Plain discovery finds peers; three further mechanisms keep a node attached to the peers it actually collaborates with and get it reconnected quickly after a restart or a network partition.
Per-overlay, demand-driven rendezvous
Section titled “Per-overlay, demand-driven rendezvous”Rendezvous is not one global namespace that returns every node on the network. A node registers and discovers under one key per overlay it follows, derived deterministically from the gossipsub topic string:
| Overlay topic | Rendezvous key |
|---|---|
ns/<hex> | /calimero/ns/<hex> |
group/<hex> | /calimero/grp/<hex> |
<context-id> | /calimero/ctx/<id> |
Because the key is derived from the topic, a registering member and a discovering
peer that hold the same id compute the identical key with no extra coordination,
and discover returns only co-members of that exact namespace, group, or context
— relevant peers by construction.
This per-overlay discovery is demand-driven: each tick, only overlays with
zero connected subscribers are queried (a topic that already has a connected
co-member can sync through it). The per-tick fan-out is bounded at
RENDEZVOUS_DISCOVER_BUDGET = 8 keys, and a rotating cursor advances across ticks
so a node belonging to many overlays eventually covers every under-connected key
without flooding the server in one pass. In steady state this cost is ≈0; a burst
only happens right after a restart or partition.
Global discovery in the configured namespace still runs every tick alongside it — that is the bootstrap / namespace-join path, used when a node needs to find the members of a namespace it does not belong to yet (and so cannot discover per-overlay).
Persistent peer-address cache
Section titled “Persistent peer-address cache”A node keeps a persistent cache of the addresses of the peers it collaborates with, so a restart doesn’t cost it a full discovery round-trip:
- On every
ConnectionEstablished, the node caches the address it connected on to a node-local datastore key (calimero-peercch). Unlike the in-memory discovery address book — which keeps direct addresses only — this cache keeps both direct and relayed-circuit addresses, because a/<relay>/p2p-circuit/p2p/<peer>address stays re-dialable after a restart as long as the peer re-reserves on the same relay, giving NAT’d co-members a fast path too. - On startup, the node loads the still-fresh entries and dials them in parallel with rendezvous rediscovery (and ahead of it), so a restarted node reconnects to its collaborators immediately rather than waiting a discovery round-trip. Dials are deduped at the swarm level; a stale cached address that fails is dropped and rendezvous supplies a fresh one. The cache only ever accelerates reconnection — it never blocks it.
Bounds:
| Constant | Value | Meaning |
|---|---|---|
PEER_CACHE_TTL_SECS | 24h | entries past this wall-clock age are dropped |
MAX_PEER_CACHE_ENTRIES | 4096 | hard cap, least-recently-seen evicted |
MAX_ADDRS_PER_PEER | 4 | addresses kept per peer, newest-first |
Liveness ping reaping
Section titled “Liveness ping reaping”The always-on ping behaviour probes each connection on a fixed interval
(~15s, 20s timeout). Three consecutive failures (MAX_PING_FAILURES = 3)
force-close the connection; the counter resets on the first success, so only
sustained failure trips it.
This is the only active detector for a silently-dead link — a partition with no FIN/RST, where the socket looks open but nothing is getting through. Tearing the connection down hands off to the reconnection cascade above, so recovery happens promptly instead of waiting on an OS-level timeout that may never come.
Which discovery for which deployment
Section titled “Which discovery for which deployment”| Deployment | Bootstrap | mDNS | Rendezvous | NAT traversal |
|---|---|---|---|---|
| Single LAN / dev cluster | optional | yes — primary | not needed | not needed |
| Hosted, static public IP | yes | off (no effect) | optional | advertise_address + external_address; relay/DCUTR not needed |
| Behind NAT (home / cloud egress-only) | yes | off | yes | relay + DCUTR + AutoNAT |
| Joining an existing public network | yes (its boot nodes) | off | yes (its namespace) | relay + DCUTR + AutoNAT |