NAT traversal.
- STATUS
- Testnet
- CRATE
- tenzro-network
- TRANSPORTS
- TCP + QUIC (port 9000)
- DEFAULT
- Permissionless
Universal listen-addr default
tenzro-node binds both /ip4/0.0.0.0/tcp/9000 and /ip4/0.0.0.0/udp/9000/quic-v1 by default when --listen-addr is omitted. This is the universal transport set: any device — cloud VM, residential wifi, mobile tether, Raspberry Pi, ESP32 — can reach the node via whichever transport NAT permits.
QUIC's structural port-reuse gives Identify observed_addr a stable listening UDP port that AutoNAT v2 can probe back, which is what makes hole punching reliable.
Four behaviours composed
The NAT stack is built into TenzroBehaviour via libp2p's SwarmBuilder::with_relay_client:
- Identify — peers exchange listen addresses + observed-address feedback.
- AutoNAT v2 — dial-back probes confirm public reachability.
- Circuit-Relay v2 — public validators serve as relay hops for joiners.
- DCUtR — direct connection upgrade through relay, i.e. hole punching.
Validators with a confirmed public address run the server halves (relay::Behaviour, autonat::v2::server::Behaviour). Joiner-class roles (LightClient, ModelProvider, TeeProvider) run the client halves. All five sub-behaviours are wrapped in Toggle<> so disabled halves are no-ops.
Config knobs
[network]
enable_relay = true # server-side; default-on for Validator
enable_hole_punching = true # client-side; default-on for all rolesPermissionless observed_addr
We never require --external-p2p-addr. The node uses Identify observed-address tally (with quorum N≥3) to learn its public address, AutoNAT v2 to confirm it, and falls back to Circuit-Relay v2 + DCUtR when direct reachability fails. This matches libp2p-class permissionless discovery.
Runtime-adaptive DHT + relay
The stack promotes itself as reachability improves rather than reading a fixed role from config. A node starts Kademlia in client mode and promotes to server only once its reachability tier holds at direct — so a node advertises itself as a routable DHT server after it has confirmed it is dialable, and a node behind a NAT never pollutes the routing table with an address that cannot be reached.
A joiner that learns a peer is relay-capable during the Identify handshake books a Circuit-Relay v2 reservation on it right away, so a relayed /p2p-circuit address exists before the first DCUtR hole-punch attempt — the fallback path is ready ahead of the direct path failing.
Bootstrap fallback
On testnet, bootstrap peers are advertised in both /ip4/.../tcp/9000/p2p/... and /ip4/.../udp/9000/quic-v1/p2p/... forms; the dialer picks whichever its NAT permits. --boot-nodes accepts a comma-separated list. The built-in testnet set additionally carries both DNS and raw fleet-IP multiaddrs for the same peers, so a node whose resolver cannot answer the DNS names still reaches the fleet over the pinned IP addresses — a first dial does not depend on DNS.
Cloud firewalls are operator runbook
Cloud NAT/security-group/network-policy rules are not part of the protocol. Open TCP/9000 and UDP/9000 in whatever firewall your host provides (cloud security group, VM firewall, K8s NetworkPolicy). Some container-optimized host images default their local iptables to DROP for everything except lo, icmp, tcp:22, and ESTABLISHED — on those, also append an ACCEPT rule for TCP/UDP 9000 via cloud-init.