An Erlang implementation of the MASQUE family - RFC 9298 - Proxying UDP in HTTP, RFC 9484 - Proxying IP in HTTP, and the draft-ietf-httpbis-connect-tcp variant - over HTTP/3 and HTTP/2, built on erlang_quic (quic_h3) and erlang_h2.

The client races both transports Apple-style (h3 first, h2 after 250 ms, first 2xx wins) so tunnels connect quickly even on networks that block QUIC.

masque lets you tunnel arbitrary UDP flows (DNS, QUIC, WireGuard, game traffic, …), TCP streams, and full IP packets through an authenticated HTTPS endpoint. Both proxy server and client are shipped in this library.

Features

  • RFC 9298 CONNECT-UDP (:protocol = connect-udp): built-in UDP proxy handler, allow / resolver policy hooks, client API in both message and queue (blocking recv) delivery modes.

  • RFC 9484 CONNECT-IP (:protocol = connect-ip): bidirectional control plane (ADDRESS_ASSIGN / ADDRESS_REQUEST / ROUTE_ADVERTISEMENT), default handler with address-pool allocator, listener-owned DNS resolution before accept/1, BCP-38 source filter, ICMPv4+v6 error synthesis (masque_icmp), H3 datagram MTU enforcement per §8. See docs/connect_ip.md for the §-mapped compliance table.

  • draft-ietf-httpbis-connect-tcp (:protocol = connect-tcp): TCP tunneling with END_STREAM = TCP FIN semantics.

  • One listener serves all three protocols; :protocol pseudo-header selects the handler.

  • RFC 9297 HTTP Datagrams (delegated to quic_h3) and RFC 9297 DATAGRAM-type capsules on HTTP/2 (for IP and UDP data planes).

  • Capsule-protocol dispatch for extension capsule types.

  • Handler behaviour for custom server-side logic; transport-generic client sessions for TCP and IP (one module per protocol serves both H3 and H2).

  • HTTP/2 fallback with Apple-style head-start racing (transports option; default [h3, h2]).

  • End-to-end compliance CT suites (UDP + IP) and eunit codecs (URI, capsule, datagram, ICMP) + skippable external-peer interop suite.

  • Usage guide - client modes, multiple tunnels, integration with an existing quic_h3 or h2 server, handler lifecycle, transport selection, connection pooling, metrics.

  • Design - architecture overview, supervision tree, client and server request paths, upstream pool internals, extension points.

  • Relay guide - end-to-end walkthrough of building an Apple-Private-Relay-shaped ingress + egress app on top of the library.

  • API reference - type signatures, option maps, handler callbacks, built-in handlers.

  • CONNECT-IP guide - RFC 9484 usage and section-by-section compliance mapping.

  • Feature matrix - RFC coverage and intentional non-goals.

Installation

Add to your rebar.config:

{deps, [
    {masque, {git, "https://github.com/benoitc/erlang_masque.git", {branch, "main"}}}
]}.

Quick start

Proxy server (serves both UDP and TCP tunnels)

{ok, _} = masque:start_listener(my_proxy, #{
    port => 4433,
    cert => CertDer,
    key  => KeyDer
    %% One listener dispatches both connect-udp and connect-tcp.
    %% Defaults: udp_handler = masque_udp_proxy_handler,
    %%           tcp_handler = masque_tcp_proxy_handler.
}).

Client

%% UDP tunnel (DNS, QUIC, game traffic)
{ok, Sess} = masque:connect(<<"https://proxy:4433">>,
                             {<<"1.1.1.1">>, 53},
                             #{protocol => udp}).

%% TCP tunnel (web, TLS)
{ok, Sess} = masque:connect(<<"https://proxy:4433">>,
                             {<<"example.com">>, 443},
                             #{protocol => tcp}).

%% Unified send/recv - works for both protocols
ok          = masque:send(Sess, Data),
{ok, Reply} = masque:recv(Sess, 5000),
ok          = masque:close(Sess).

%% Message mode (default): owner receives {masque_data, Sess, Data}
receive {masque_data, Sess, Bytes} -> Bytes end.

Custom server handler

-module(my_handler).
-behaviour(masque_handler).

-export([accept/1, init/2, handle_packet/2, handle_data/2, terminate/2]).

accept(#{target_host := Host}) ->
    case is_allowed(Host) of
        true  -> accept;
        false -> {reject, forbidden}
    end.

init(_Req, _Opts) -> {ok, #{}}.

%% UDP tunnels
handle_packet(Data, S) -> {ok, S, [{send, Data}]}.

%% TCP tunnels
handle_data(Data, S) -> {ok, S, [{send_data, Data}]}.

terminate(_Reason, _S) -> ok.

Examples

Building and testing

rebar3 compile
rebar3 eunit          # unit tests (codecs)
rebar3 as test proper # PropEr properties
rebar3 ct             # common_test (17 cases)
rebar3 xref
rebar3 dialyzer

External-peer interop:

MASQUE_GO_BIN=/path/to/masque-go rebar3 ct --suite=masque_interop_SUITE

License

Apache License 2.0. See LICENSE.