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

# P2P Tunnels — direct peer-to-peer connections without an exit node

> rustunnel P2P tunnels connect two clients directly with NAT hole punching over QUIC — no exit node, no relay tax once direct mode is established. Covers relay mode, direct mode, NAT classification, hole punching, and automatic fallback.

This page covers **rustunnel P2P tunnels** — direct peer-to-peer connections between two clients **without an exit node** in the data path. Once the two peers complete NAT hole punching, traffic flows directly between them over QUIC; the rustunnel server only signals during the handshake. If hole punching fails (symmetric NAT, restrictive firewall), rustunnel transparently falls back to relayed mode.

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.

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

```text theme={null}
App -> Subscriber (localhost:8000) --tunnel--> Publisher (localhost:27015) -> Service
```

Unlike HTTP/TCP tunnels, P2P tunnels don't require a public port or subdomain on the server. The server acts as a signaling relay and (optionally) a data relay.

***

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

The mode is selected automatically based on NAT compatibility. If direct mode fails or isn't possible, the connection transparently falls back to relayed mode.

***

## Server-Relayed Mode

This is the default mode and always works regardless of NAT type or firewall configuration.

### Connection flow

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Server bridges the streams">
    The server runs `copy_bidirectional` between the publisher's and subscriber's yamux streams. Data flows bidirectionally until either side disconnects.
  </Step>
</Steps>

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

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

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

### 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%           |

The key distinction: **Cone NATs** reuse the same public port for all outbound connections, making the mapped address predictable. **Symmetric NATs** assign a different port per destination, making prediction unreliable.

<Tip>
  STUN probing takes \~100ms (two UDP round trips) and happens once at tunnel registration time. It's cheap and non-invasive.
</Tip>

***

## 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:

```text theme={null}
1. Subscriber connects via P2pConnect
2. Server classifies NAT pair
3. If traversable:
   a. Server sends hole-punch instructions
   b. Peers send UDP probes (5-second timeout)
   c. Success -> QUIC session -> direct mode
   d. Failure -> relay mode
4. If not traversable:
   a. Relay immediately (no delay)
```

From the user's perspective:

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

The subscriber never needs to know which mode is active. The CLI command is identical in all cases.

***

## Security

### Shared secret authentication

1. Client computes `SHA-256(secret)` locally.
2. Only the **hash** is sent to the server.
3. The server compares hashes. Mismatch = connection rejected.
4. 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)

```bash theme={null}
rustunnel p2p 27015 --name my-game --secret "shared-secret-123"
```

Exposes `localhost:27015` as a P2P tunnel named `my-game`.

### Subscriber (connect to a service)

```bash theme={null}
rustunnel p2p 8000 --target my-game --secret "shared-secret-123"
```

Listens on `localhost:8000`. Any TCP connection to that port is forwarded through the tunnel to the publisher's `localhost:27015`.

### Config file

```yaml theme={null}
tunnels:
  # Publisher
  gameserver:
    proto: p2p
    local_port: 27015
    p2p_name: my-game
    p2p_secret: shared-secret-123

  # Subscriber
  connect:
    proto: p2p
    local_port: 8000
    p2p_target: my-game
    p2p_secret: shared-secret-123
```

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

***

## See also

<CardGroup cols={2}>
  <Card title="UDP Tunnels" icon="bolt" href="/reference/udp-tunnels">
    Forward UDP datagrams (game servers, DNS, QUIC, VoIP) to a public port on the rustunnel server.
  </Card>

  <Card title="Load Balancing & Health Checks" icon="scale-balanced" href="/reference/load-balancing">
    Run multiple backends behind one subdomain or TCP port with automatic failover.
  </Card>

  <Card title="Client Guide" icon="terminal" href="/guides/client-guide">
    Every flag and config option for the rustunnel CLI client.
  </Card>

  <Card title="Architecture" icon="diagram-project" href="/guides/architecture">
    How HTTP / TCP / UDP / P2P tunnels flow through the system.
  </Card>
</CardGroup>
