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

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

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

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:

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