rustunnel supports direct peer-to-peer tunnels between two clients. This allows two machines — neither with a public IP — to communicate through the rustunnel server, with an optional upgrade to a direct connection that bypasses the server entirely.Documentation Index
Fetch the complete documentation index at: https://rustunnel.com/docs/llms.txt
Use this file to discover all available pages before exploring further.
Concepts
A P2P tunnel connects two rustunnel clients:- Publisher — the client exposing a local service (e.g., a game server on port 27015). Registers a named tunnel with a shared secret.
- Subscriber — the client connecting to the publisher’s service. Listens on a local port (e.g., 8000) and forwards incoming TCP connections through the tunnel.
- Shared secret — a password both sides must know. The SHA-256 hash is sent to the server for verification; the plaintext never leaves the client.
Connection Modes
P2P tunnels support two modes:| Mode | Data path | Latency | Server load | Metered |
|---|---|---|---|---|
| Relayed | App -> Subscriber -> Server -> Publisher -> Service | Higher (two WS hops) | Full bandwidth | Yes |
| Direct | App -> Subscriber -> Publisher -> Service (via UDP hole punch + QUIC) | Lower (peer-to-peer) | Signaling only | No |
Server-Relayed Mode
This is the default mode and always works regardless of NAT type or firewall configuration.Connection flow
Publisher registers
The publisher connects to the server and registers a P2P tunnel with a name (e.g.,
my-game) and a SHA-256 hash of the shared secret.Subscriber accepts a local connection
The subscriber listens on its local port (e.g., 8000). When an app connects, the subscriber sends a
P2pConnect request to the server with the tunnel name and secret hash.Server verifies and creates relay
The server looks up the publisher by name, verifies the secret hash matches, then sends
NewConnection to both the publisher and subscriber sessions.Both sides open yamux streams
The publisher opens a yamux stream and connects to its local service (e.g.,
localhost:27015). The subscriber bridges the already-accepted TCP connection with its yamux stream.Each incoming connection to the subscriber’s local port creates a new relay. Multiple concurrent connections are supported, each with its own yamux stream pair.
Key details
- On-demand relay: The relay is established per-connection, not at startup. Each app connection to the subscriber’s local port triggers a new
P2pConnect. - Yamux multiplexing: Both clients maintain persistent WebSocket connections to the server. Each relay uses a separate yamux stream over these existing connections.
- No public ports needed: Neither client needs to accept inbound connections from the internet.
Direct Mode (NAT Hole Punching)
Direct mode bypasses the server for the data path. After an initial signaling exchange through the server, the publisher and subscriber establish a direct UDP connection using NAT hole punching, then upgrade it to a QUIC session for reliable, encrypted transport.Direct mode is enabled per-server via the
[p2p] config section (direct_enabled = true). When enabled, the server automatically attempts hole punching for compatible NAT pairs. The relay is always available as a fallback.Why direct mode matters
- Lower latency: Data travels directly between peers instead of bouncing through the server.
- Lower server cost: The server only handles signaling (~1 KB), not the full data stream.
- Better for real-time applications: Game servers, VoIP, and live streaming benefit from reduced round-trip time.
NAT Classification via STUN
Before attempting hole punching, each client determines its NAT type using the STUN protocol (Session Traversal Utilities for NAT).How STUN probing works
The client sends a STUN Binding Request to two different STUN servers. Each server replies with the client’s mapped address — the public IP and port the server saw the request come from. By comparing the two responses, the client classifies its NAT:| STUN Server A reply | STUN Server B reply | NAT type |
|---|---|---|
1.2.3.4:5000 | 1.2.3.4:5000 | Cone NAT (traversable) |
1.2.3.4:5000 | 1.2.3.4:6001 | Symmetric NAT (hard to traverse) |
| Public IP = Local IP | Public IP = Local IP | Open (no NAT) |
| No response | No response | Unknown (use relay) |
NAT types explained
| NAT Type | Description | Hole punch success |
|---|---|---|
| Open | No NAT — client has a public IP | ~100% |
| Full Cone | Same public mapping for all destinations; any external host can send to the mapped port | ~100% |
| Restricted Cone | Same mapping, but only hosts the client has sent to can reply | ~95% |
| Port-Restricted Cone | Same mapping, restricted by both IP and port | ~90% |
| Symmetric | Different mapping for each destination (port changes per target) | ~10-60% |
Hole Punching Strategy
The server classifies the NAT pair and selects one of three strategies:Strategy 1: Direct Exchange (Cone + Cone)
Both peers have predictable mapped addresses. Both send a UDP probe to the other’s mapped address. The first probe “punches” the hole in each NAT by creating an outbound mapping. The second probe passes through. Success rate: ~95%Strategy 2: Port Prediction (Cone + Symmetric)
The Cone peer has a predictable address. The Symmetric peer’s port changes per destination, but the server observes the port increment pattern from the STUN probes and predicts the next port range. The Cone peer sends probes to the predicted range. Success rate: ~60-70%Strategy 3: Skip (Symmetric + Symmetric)
Both peers have unpredictable port mappings. Brute-force probing has a success rate under 10% and can trigger firewall alarms. Decision: fall back to relay immediately. No hole punching attempted, no delay.Decision matrix
| Publisher NAT | Subscriber NAT | Strategy | Expected success |
|---|---|---|---|
| Open/Cone | Open/Cone | Direct Exchange | ~95% |
| Cone | Symmetric | Port Prediction | ~60-70% |
| Symmetric | Cone | Port Prediction | ~60-70% |
| Symmetric | Symmetric | Skip -> Relay | N/A |
| Unknown | Any | Skip -> Relay | N/A |
Automatic Fallback
When direct mode is attempted but fails, the connection transparently falls back to server relay:| Outcome | Connection time | Latency |
|---|---|---|
| Direct succeeded | ~1-2 seconds | Low (peer-to-peer) |
| Direct failed (timeout) | ~6-7 seconds | Normal (relay) |
| Direct skipped (incompatible NATs) | ~1-2 seconds | Normal (relay) |
Security
Shared secret authentication
- Client computes
SHA-256(secret)locally. - Only the hash is sent to the server.
- The server compares hashes. Mismatch = connection rejected.
- The plaintext secret never leaves the client.
Transport encryption
| Mode | Encryption | Server visibility |
|---|---|---|
| Relayed | TLS (WebSocket) | Server can see plaintext (same as HTTP/TCP tunnels) |
| Direct | QUIC (TLS 1.3, end-to-end) | Server cannot see plaintext |
Tunnel name visibility
P2P tunnel names are visible to the server. Anyone who knows both the tunnel name and the shared secret can connect. Use strong, unique secrets for production.Billing and Metering
| Mode | Metered by server? | Billing |
|---|---|---|
| Relayed | Yes | Per-byte, same as TCP tunnels |
| Direct | No (data bypasses server) | Informational only (client-reported) |
CLI Usage
Publisher (expose a service)
localhost:27015 as a P2P tunnel named my-game.
Subscriber (connect to a service)
localhost:8000. Any TCP connection to that port is forwarded through the tunnel to the publisher’s localhost:27015.
Config file
Error cases
| Error | Cause |
|---|---|
P2P tunnel name 'X' is already in use | Another publisher is using this name |
P2P tunnel 'X' not found | No publisher registered with this name |
invalid P2P secret | Subscriber’s secret doesn’t match publisher’s |
P2P mode requires --name or --target | Neither publisher nor subscriber mode specified |

