🎭 MASQUE proxies
MASQUE is a set of IETF protocols for proxying traffic other than TCP through HTTP. Rama
implements CONNECT-UDP (RFC 9298): a client asks the proxy for a UDP tunnel to a target,
and the UDP payloads travel inside that HTTP request. Its main use is HTTP/3, whose QUIC
connections run over UDP and so cannot go through an ordinary CONNECT tunnel.
CONNECT-UDP
Establishing a tunnel starts with an HTTP request to the proxy. HTTP/3 and HTTP/2 send an
Extended CONNECT with :protocol = connect-udp, HTTP/1.1 upgrades a GET request. The
request path names the target through the proxy’s URI template, by default
/.well-known/masque/udp/{target_host}/{target_port}/.
The proxy resolves the target and checks it against its policy before it accepts the tunnel:
101 Switching Protocols on HTTP/1.1, a 2xx on HTTP/2 and HTTP/3. Rama refuses private,
loopback and link-local targets unless they are allowed. After that, UDP payloads travel as
HTTP Datagrams (RFC 9297): in QUIC DATAGRAM frames on HTTP/3 when both peers support them,
otherwise as DATAGRAM capsules on the request stream.
A MASQUE proxy is also an HTTPS proxy: CONNECT still tunnels TCP. A rama client with a
masque:// proxy tunnels HTTP/3 with CONNECT-UDP and HTTP/1.1 and HTTP/2 with CONNECT.
A MASQUE proxy is addressed with the masque scheme, port 443 by default, such as
masque://user:pass@proxy.example. A path in the address is the proxy’s URI template; an
https:// address whose path is a template with both variables is a MASQUE proxy as well.
Packet sizes
A QUIC packet needs at least 1200 bytes of UDP payload (RFC 9000 §14), and a tunneled packet has to fit in a single QUIC datagram of the connection to the proxy. Both ends of that connection therefore start with room above that minimum; rama’s MASQUE client and server set this up, other QUIC stacks need the same. Inside the tunnel, rama sizes the inner QUIC connection to what the tunnel can carry, in both directions. The return direction is an estimate, since the proxy’s own path is not known to the client.
A datagram too large for the tunnel is dropped, never split or resent reliably: a QUIC DATAGRAM frame cannot be fragmented (RFC 9221 §5), and dropping keeps path MTU discovery working end to end (RFC 9297 §3.5).
Idle tunnels
A quiet tunnel closes after a while without traffic; RFC 9298 §3.1 recommends at least two
minutes. The QUIC connections that carry tunnels stay open longer than that, so the tunnel’s
own policy decides. The API documentation of rama::proxy::masque describes the values in
use.
Rama as a MASQUE client and proxy
The building blocks live in rama::proxy::masque and the HTTP client:
DefaultHttpWebClient::default_masque_with_executoris the default HTTP client with its HTTP/3 also going through MASQUE proxies. Variants take TLS configurations of your own.ConnectUdpConnectoropens CONNECT-UDP tunnels as UDP sockets, for any QUIC or UDP client.ConnectUdpServiceserves CONNECT-UDP requests: target policy, DNS, and a relay that can be swapped for your own, such as one inspecting the traffic.
The masque_connect_udp_proxy example has a complete proxy and client.
The rama CLI
rama send uses a MASQUE proxy with --proxy masque://…: --http3 requests go through a
CONNECT-UDP tunnel, other HTTP versions through CONNECT.
rama serve proxy --protocol masque serves a MASQUE proxy, TLS over TCP and HTTP/3 over UDP
on the same port:
| Flag | Meaning |
|---|---|
--protocol masque | serve MASQUE on --bind, next to other protocols |
--masque-bind ADDR | an additional MASQUE listener; repeatable |
--masque-template T | the CONNECT-UDP URI template |
--masque-tunnels connect,connect-udp | the tunnel methods served; both by default |
--masque-allow-private | allow private, loopback and link-local targets |
--proxy-auth user:pass | require this credential on every ingress; never forwarded |
With --upstream-proxy masque://… the proxy sends its own egress through a MASQUE upstream:
TCP through CONNECT, UDP and QUIC through CONNECT-UDP. Combined with --mitm, intercepted
HTTP/3 leaves through an upstream tunnel and QUIC that is not intercepted is relayed untouched
through one. Without --mitm the proxies chain. A tunnel is never sent around a configured
upstream. --upstream-proxy-insecure skips verification of the upstream’s certificate only;
through an untouched tunnel the end client still verifies the origin, while intercepted egress
does not verify the origin, as rama’s TLS MITM relay.
Troubleshooting
407 Proxy Authentication Required: put the credentials in the proxy address.- The tunnel is refused with
502 Bad Gateway: theProxy-Statusheader says why, such asdestination_ip_prohibitedfor a private target ordns_errorfor one that does not resolve. - The HTTP/3 handshake through the tunnel times out: the connection to the proxy may not have room for tunneled packets, see Packet sizes.
- A quiet tunnel closes: see Idle tunnels; keep-alives such as QUIC PINGs keep it open.
- Logs:
RUST_LOG=rama_masque=debuglogs each tunnel’s close and why.