# P2P on QUIC

> Connect a client and a server directly across NATs, and fall back to a relay when the punch fails. One QUIC connection, one wire, the same authentication on every path.

The Python SDK can connect two machines over a direct QUIC path. It uses a
hole punch across NATs. When the punch fails, it dials a relay. The QUIC
connection is the same on both paths. The TLS session, the certificate pin and
the pair token do not change. Only the UDP path changes.

> **NOTE:**
> Two transports carry it. The bare-QUIC request wire (`NativeQuicOpClient`, the
> inference frontends) is Python only. The MoQT track wire (`MoqtClient`) carries
> it in every native SDK: Python, Go, Rust, Java, Kotlin, .NET, Swift, Flutter and
> Unity. The browser SDK stays relay-only: a browser cannot adopt a socket or pin a
> leaf certificate.

## How a connection is made

1. The server binds its QUIC port and learns its public address from a STUN
   reflector on that same socket.
2. The server registers a UDP tunnel on a relay PoP in front of the same port.
3. The server publishes its addresses, the relay address and the SHA-256 of
   its certificate to the rendezvous. The rendezvous needs an org API key.
4. The client binds a socket, learns its public address and publishes it.
5. Both sides send punch probes toward each other. The client dials the
   server's public address for 5 seconds with the certificate pinned.
6. If the direct dial fails, the client dials the relay address with the same
   pin.

The relay carries QUIC ciphertext only. It cannot read the traffic. The server
refuses every request that does not carry the pair token, on both paths.

## Environment

| Variable | Side | Meaning |
|---|---|---|
| `CLUTCHCALL_RENDEZVOUS` | both | The control-plane base URL, for example `https://api.rnp.sh` |
| `CLUTCHCALL_API_KEY` | both | An org API key. Tunnel, CTI and SMS keys are accepted. The two sides must use two different keys. |
| `CLUTCHCALL_PAIR_TOKEN` | both | The pair secret. It must differ from every API key. The SDK refuses a token that equals the API key. |
| `CLUTCHCALL_STUN` | both | A STUN reflector as `ip:port`. Public reflectors work. |
| `TUNNEL_AUTHTOKEN` | server | The tunnel key the relay registration uses. The old name `RNP_AUTHTOKEN` also works. |
| `RNP_TUNNEL_BIN` | server | Path to the `rnp-tunnel` engine. Default: `rnp-tunnel` on `PATH`. |

The pair id is a string of 6 to 64 characters from `A-Z`, `a-z`, `0-9`, `_`
and `-`. Share it and the pair token out of band.

## Server

```python
import asyncio
from clutchcall import p2p

async def serve():
    path = await p2p.serve_path(
        port=4701, pair_id="pair-7f3a9c21", cert_path="cert.pem",
        stun="74.125.250.129:19302", relay_pop="orin-p2p.rnp.sh")
    # Start your QUIC server on path.fd (the loop owns the socket from now on).
    # Then keep the rendezvous record alive beside it:
    asyncio.ensure_future(path.keepalive())
```

`serve_path` returns the punched socket in `path.fd`. Pass it as `adopt_fd`
to the native server. `keepalive` re-publishes the record every 30 seconds
and punches toward each client that joins the pair.

## Client

```python
from clutchcall import p2p
from clutchcall.native_quic import NativeQuicOpClient

async def dial(alpn):
    path = await p2p.client_path("pair-7f3a9c21", stun="74.125.250.129:19302")
    ip, port = p2p.split_ipv4_port(path.direct)
    try:
        cli = NativeQuicOpClient(ip, port, alpn, adopt_fd=path.fd,
                                 pin_sha256=path.pin_sha256)
        reply = await asyncio.wait_for(cli.request(b"..."), p2p.DIRECT_DIAL_S)
    except (asyncio.TimeoutError, ConnectionError):
        ip, port = p2p.resolve_host_port(path.relay)
        cli = NativeQuicOpClient(ip, port, alpn, pin_sha256=path.pin_sha256)
        reply = await cli.request(b"...")
```

`client_path` refuses to return a server record that has no certificate
fingerprint. A dial without a pin is not possible through this API.

## Native fields

The C ABI carries the same options for other bindings:

| Field or function | Where | Meaning |
|---|---|---|
| `adopt_fd` | server and client config | A bound UDP socket the loop takes over. `0` means none. |
| `pin_sha256` | client config | The server's leaf certificate SHA-256 as 64 hex characters. With a pin the certificate chain is not checked. |
| `clutch_p2p_punch()` | `clutch_p2p_ffi.h` | Bind, gather the reflexive address and punch on one socket. Returns the fd. |

## What to expect on real networks

Two subscribers of one carrier NAT often cannot reach each other directly,
even when both NATs map addresses in the endpoint-independent way. The punch
fails and the relay serves the connection. This is a normal case. Design for
the relay path first and treat the direct path as the faster option when it
is available.

## MoQT: the same paths for every SDK

A MoQT peer that ACCEPTS a direct connection runs an in-process WebTransport
server on its punched socket. It accepts one session whose CONNECT carries the
pair token as `tok=`. Any other CONNECT is answered 403. The DIALING peer pins
the accepting peer's certificate and dials it for 5 seconds, then dials the
relay it always dialed. The MoQT session is the same object on both paths.

| SDK | Accept | Dial direct | Punch and pin value |
|---|---|---|---|
| Python | `MoqtClient.listen(cert, key, pair_token)` | `MoqtClient.connect(url, token, adopt_fd=, pin_sha256=)` | `moqt.p2p_punch`, `moqt.cert_sha256` |
| Go | `ListenMoqt(MoqtListenOptions{...})` | `ConnectMoqtEx(url, token, onState, MoqtConnectOptions{...})` | `P2PPunch`, `CertSHA256` |
| Rust | `MoqtClient::listen(&ListenOptions)` | `MoqtClient::connect_ex(url, token, &ConnectOptions, ...)` | `p2p_punch`, `cert_sha256` |
| Java | `Moqt.Client.listen(ListenOptions, onState)` | `Moqt.Client.connectEx(url, token, ConnectOptions, onState)` | `Moqt.p2pPunch`, `Moqt.certSha256` |
| Kotlin | `MoqtClient.listen(certPath, keyPath, pairToken)` | `MoqtClient.connectEx(url, token, adoptFd, pinSha256)` | `MoqtClient.p2pPunch`, `MoqtClient.certSha256` |
| .NET | `MoqtClient.Listen(MoqtListenOptions)` | `MoqtClient.ConnectEx(url, token, MoqtConnectOptions)` | `MoqtClient.P2PPunch`, `MoqtClient.CertSha256` |
| Swift | `MoqtClient.listen(certPath:keyPath:pairToken:)` | `MoqtClient(url:token:adoptFd:pinSha256:)` | `MoqtClient.p2pPunch`, `MoqtClient.certSHA256` |
| Flutter | `MoqtClient.listen(certPath:, keyPath:, pairToken:)` | `MoqtClient.connectEx(url, adoptFd:, pinSha256:)` | `MoqtClient.p2pPunch`, `MoqtClient.certSha256` |
| Unity | `ClutchCallNative.Listen(certPath, keyPath, pairToken)` | `ClutchCallNative.ConnectEx(url, token, adoptFd, pinSha256)` | `ClutchCallNative.P2PPunch`, `ClutchCallNative.CertSha256` |

The rendezvous call and the dial ladder are in the SDK for Python
(`clutchcall.p2p.moqt_serve` / `moqt_dial`), Go (`MoqtP2P`), Rust
(`clutchcall::p2p::MoqtP2p`), Java (`MoqtP2P`), .NET (`MoqtP2P`) and Swift
(`MoqtP2P`). Kotlin, Flutter and Unity expose the primitives; the app makes the
one HTTPS POST to the rendezvous with the HTTP client it already has, then calls
`listen` or `connectEx`.

The C ABI behind every wrapper: `clutch_moqt_connect_ex`, `clutch_moqt_listen`,
`clutch_moqt_local_port`, `clutch_p2p_punch` and `clutch_p2p_cert_sha256`, all
in the MoQT library. Existing exports are unchanged.
