NAT Traversal
+Most devices on the internet sit behind a NAT (Network Address Translator). Your router assigns your device a private address (like 192.168.1.100) and translates it to a public address when you connect outward. This works fine for browsing the web, but it creates a problem for P2P networks: two devices behind different NATs cannot directly connect to each other without help.
Tesseras solves this with a three-tier approach, trying the cheapest option first:
+-
+
- Direct connection — if both nodes have public IPs, they connect directly +
- UDP hole punching — a third node introduces the two peers so they can punch through their NATs +
- Relay — a public-IP node forwards packets between the two peers +
NAT type discovery
+When a node starts, it sends STUN (Session Traversal Utilities for NAT) requests to multiple public servers. By comparing the external addresses these servers report back, the node classifies its NAT:
+| NAT Type | What it means | Hole punching? |
|---|---|---|
| Public | No NAT — your device has a public IP | Not needed |
| Cone | NAT maps the same internal port to the same external port regardless of destination | Works well (~80%) |
| Symmetric | NAT assigns a different external port for each destination | Unreliable |
| Unknown | Could not reach STUN servers | Relay needed |
Your node advertises its NAT type in DHT Pong messages, so other nodes know whether hole punching is worth attempting.
+Hole punching
+When node A (behind a Cone NAT) wants to connect to node B (also behind a Cone NAT), neither can directly reach the other. The solution:
+-
+
-
+
A sends a PunchIntro message to node I (an introducer — any public-IP node they both know). The message includes A’s external address (from STUN) and an Ed25519 signature proving A’s identity.
+
+ -
+
I verifies the signature and forwards a PunchRequest to B, including A’s address and the original signature.
+
+ -
+
B verifies the signature (proving the request really came from A, not a spoofed source). B then sends a UDP packet to A’s external address — this opens a pinhole in B’s NAT. B also sends a PunchReady message back to A with B’s external address.
+
+ -
+
A sends a UDP packet to B’s external address. Both NATs now have pinholes, and the two nodes can communicate directly.
+
+
The entire process takes 2-5 seconds. The Ed25519 signatures prevent reflection attacks, where an attacker replays an old introduction to redirect traffic.
+Relay fallback
+When hole punching fails (Symmetric NAT, strict firewalls, or corporate networks), nodes fall back to relaying through a public-IP node:
+-
+
- A sends a RelayRequest to node R (a public-IP node with relay enabled). +
- R creates a session and sends a RelayOffer to both A and B, containing the relay address and a session token. +
- A and B send their packets to R, prefixed with the session token. R strips the token and forwards the payload to the other peer. +
Relay sessions have bandwidth limits:
+-
+
- 256 KB/s for peers with good reciprocity (they store fragments for others) +
- 64 KB/s for peers without reciprocity +
- Non-reciprocal sessions are limited to 10 minutes +
This encourages nodes to contribute storage — good network citizens get better relay service.
+Address migration
+When a mobile device switches networks (Wi-Fi to cellular), its IP address changes. Rather than tearing down and rebuilding relay sessions, the node sends a signed RelayMigrate message to update its address in the existing session. This avoids re-establishing connections from scratch.
+Configuration
+The [nat] section in the daemon config controls NAT traversal:
[nat]
+# STUN servers for NAT type detection
+stun_servers = ["stun.l.google.com:19302", "stun.cloudflare.com:3478"]
+
+# Enable relay (forward traffic for other NATed peers)
+relay_enabled = false
+
+# Maximum simultaneous relay sessions
+relay_max_sessions = 50
+
+# Bandwidth limit for reciprocal peers (KB/s)
+relay_reciprocal_kbps = 256
+
+# Bandwidth limit for non-reciprocal peers (KB/s)
+relay_bootstrap_kbps = 64
+
+# Relay session idle timeout (seconds)
+relay_idle_timeout_secs = 60
+
+To run a relay node, set relay_enabled = true. Your node must have a public IP (or a port-forwarded router) to serve as a relay.
Mobile reconnection
+When the Tesseras app detects a network change on a mobile device, it runs a three-phase reconnection sequence:
+-
+
- QUIC migration (0-2s) — QUIC supports connection migration natively. The app tries to migrate all active connections to the new address. +
- Re-STUN (2-5s) — discover the new external address and re-announce to the DHT. +
- Re-establish (5-10s) — reconnect peers that migration couldn’t save, in priority order: bootstrap nodes first, then nodes holding your fragments, then nodes whose fragments you hold. +
The app shows reconnection progress through the NetworkChanged event stream.
Monitoring
+NAT traversal exposes Prometheus metrics at /metrics:
-
+
tesseras_nat_type— current detected NAT type
+tesseras_stun_requests_total/tesseras_stun_failures_total— STUN reliability
+tesseras_punch_attempts_total{initiator_nat, target_nat}— punch success rate by NAT pair
+tesseras_relay_sessions_active— current relay load
+tesseras_relay_bytes_forwarded— total relay bandwidth
+tesseras_network_change_total— network change frequency on mobile
+