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 targets
port 53 — plain DNS -
127.0.0.53:53(the relay host's own resolver) ·1.1.1.1:53·1.0.0.1:53·8.8.8.8:53·8.8.4.4:53·9.9.9.9:53 - port 853 — DNS-over-QUIC
-
1.1.1.1:853·9.9.9.9:853needs a DoQ client — the reference client below speaks plain DNS only - 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 destination has to appear in the config, so a caller cannot aim it at anyone. Everything on the list is a well-known public resolver on a DNS port.
The two port-853 targets speak DNS-over-QUIC, so the relay will happily carry
QUIC inside CONNECT-UDP inside HTTP/3 — it forwards opaque UDP and does not
care what is in it. But that needs a client that actually speaks DoQ. Point the
reference client below at :853 and you will get a clean
200 OK followed by silence, because it sends plain DNS wire format
and a DoQ resolver correctly ignores anything that is not a QUIC handshake. The
relay is fine; the payload is wrong. Use :53 to test the relay.
Point your own client at it
That is what this is for. You need nothing from us installed — open an
HTTP/3 connection to pqcrypta.com:443, send the Extended CONNECT
below, and start exchanging datagrams. The wire format is fully specified in the
next section.
A session that works looks like this:
- The
CONNECTreturns 2xx. A 4xx means the target is not on the allowlist. - Datagrams you send come back from the resolver within a few hundred milliseconds.
- Inbound datagrams carry your quarter stream ID and context ID 0 as their prefix.
Getting a 200 and then silence is almost always the first gotcha below — the CONNECT stream was closed.
Or start from a working client
Two files, upstream crates only. It does not depend on the PQCrypta proxy — there is nothing to clone and no fork to build.
mkdir masque-client && cd masque-client && mkdir src
curl -O https://pqcrypta.com/masque/client/Cargo.toml
curl -o src/main.rs https://pqcrypta.com/masque/client/main.rs
cargo run --release -- pqcrypta.com:443 1.1.1.1:53 example.com
Which prints:
relay : pqcrypta.com:443
target : 1.1.1.1: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 104.20.23.154 — through the relay, over HTTP/3.
Roughly 300 lines including the DNS encoder, written to be read rather than
depended on: main.rs ·
Cargo.toml. MIT/Apache-2.0, copy freely.
The proxy repository carries the same client as
examples/masque-client.rs if you happen to have it already.
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/1.1.1.1/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.