Skip to content

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 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.

A node has to learn which peers exist and how to dial them. Four mechanisms run in parallel, each suited to a different deployment.

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...",
]

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 = true

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 = 15
nanos = 0

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.

A node behind a home/cloud NAT is not directly dialable. Three cooperating libp2p behaviours make it reachable.

BehaviourRole
AutoNATProbes 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.
DCUTRDirect 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 addresses
external_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 = 10
nanos = 0

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:

StateMeaningActions on entering
Unknownno probe result yetnone — wait for the first verdict
Reachablea confirmed external address existsenable the AutoNAT server (so this node can probe others) and register with rendezvous so peers can find it
Unreachableno confirmed external addressdisable 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:

Terminal window
meroctl --node <name> network status

reports 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.

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.

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 topicRendezvous 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).

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:

ConstantValueMeaning
PEER_CACHE_TTL_SECS24hentries past this wall-clock age are dropped
MAX_PEER_CACHE_ENTRIES4096hard cap, least-recently-seen evicted
MAX_ADDRS_PER_PEER4addresses kept per peer, newest-first

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.

DeploymentBootstrapmDNSRendezvousNAT traversal
Single LAN / dev clusteroptionalyes — primarynot needednot needed
Hosted, static public IPyesoff (no effect)optionaladvertise_address + external_address; relay/DCUTR not needed
Behind NAT (home / cloud egress-only)yesoffyesrelay + DCUTR + AutoNAT
Joining an existing public networkyes (its boot nodes)offyes (its namespace)relay + DCUTR + AutoNAT