CONNECT-UDP is specified, shipped in browsers, and running inside at least one large privacy product. What it has not had is somewhere for an implementer to point a half-finished client. The reference implementations are inside CDNs and browser internals; there is no public endpoint that just answers.
This is that endpoint, plus a complete client short enough to read in one sitting.
The endpoint
- Relay
pqcrypta.com:443(HTTP/3, ALPNh3)- Template
/.well-known/masque/udp/{target_host}/{target_port}/- Method
- Extended
CONNECTwith:protocol = connect-udp - Allowed target
127.0.0.53:53— the relay host's own resolver- Session cap
- 8 concurrent sessions per QUIC connection
- Idle timeout
- 60 s with no datagram in either direction
The allowlist is the entire safety model, and it is why this can be public at all. An open UDP relay is a reflection and amplification weapon: whoever controls the target controls where the traffic lands. Here the target has to appear in the config, so a caller cannot aim it at anyone. The one entry resolves real public names, which is enough to prove a round trip end to end.
Try it
The reference client ships with the proxy. It resolves a name through the relay and prints the answer.
git clone https://github.com/PQCrypta/pqcrypta-proxy
cd pqcrypta-proxy
cargo run --example masque-client -- pqcrypta.com:443 127.0.0.53:53 example.com
Which prints:
relay : pqcrypta.com:443
target : 127.0.0.53:53
query : example.com IN A
[+] QUIC connected to 66.179.95.51:443
[+] CONNECT-UDP accepted (200 OK)
[+] stream client bidirectional stream 0 -> quarter stream id 0
[+] sent 29 byte DNS query
[+] received 61 byte reply
example.com resolved to 172.66.147.243 — through the relay, over HTTP/3.
The wire format, in full
CONNECT-UDP is three RFCs stacked, and the whole thing fits on a page.
RFC 9220 allows a :protocol pseudo-header on CONNECT.
RFC 9298 defines what connect-udp means and the URI
template that names the target. RFC 9297 defines how the payloads
travel.
The request
:method = CONNECT
:protocol = connect-udp
:scheme = https
:authority = pqcrypta.com
:path = /.well-known/masque/udp/127.0.0.53/53/
A 2xx opens the session. The relay binds a UDP socket toward the target and holds it for as long as the CONNECT stream is open.
The datagrams
QUIC DATAGRAM = varint(quarter stream id) | varint(context id) | UDP payload
QUIC datagrams are connection-global: unlike a stream, a datagram carries no association with the request that created the session. RFC 9297 puts that association back into the payload as the quarter stream ID — the CONNECT request's stream ID divided by four.
The division is not arbitrary. Client-initiated bidirectional stream IDs are always multiples of four, so dividing keeps the varint one byte for the first sixty-three sessions instead of burning two on trailing zeroes.
Context ID 0 means "a whole UDP payload follows". Anything else is an extension this relay does not implement, and per the spec it drops those rather than failing the session.
Three things that will cost you an afternoon
Do not finish the CONNECT stream
The stream is the session. Calling finish() after sending
the request — the reflex from every ordinary HTTP request — tells
the relay you are done, and it closes the UDP socket. Your datagrams then go
nowhere, silently, with a 200 OK already in hand. This is the single most
likely reason a first CONNECT-UDP client appears to connect and then hangs.
Negotiate both settings
SETTINGS_H3_DATAGRAM and SETTINGS_ENABLE_CONNECT_PROTOCOL
are separate, and you need both. Without the first there is nowhere to put the
payloads; without the second the :protocol pseudo-header is not
even legal on your request.
Filter inbound datagrams
Every datagram on the connection arrives at every reader, including ones belonging to other sessions. Check the quarter stream ID before treating the remainder as yours. With one session this looks unnecessary; with two it is the bug.
What MASQUE is for
Proxying UDP over an encrypted, authenticated HTTP/3 connection buys the same thing HTTPS proxying bought TCP: the path between you and the proxy sees one encrypted QUIC connection to one host, and nothing about what is inside it.
That is why it underpins multi-hop privacy designs — a first hop that knows your address but not your destination, a second that knows the destination but not you. It is also how you carry QUIC itself through a proxy, which CONNECT could never do, and how DNS-over-QUIC reaches a resolver without the local network learning which one.
Relay implementation: pqcrypta-proxy,
src/connect_udp.rs. Client: examples/masque-client.rs.
Both MIT/Apache-2.0.