Skip to main content

README.md

# masque

An Erlang implementation of the MASQUE family - [RFC 9298 - Proxying
UDP in HTTP][rfc9298], [RFC 9484 - Proxying IP in HTTP][rfc9484], and
the draft-ietf-httpbis-connect-tcp variant - over **HTTP/3** and
**HTTP/2**, built on [`erlang_quic`][quic] (`quic_h3`) and
[`erlang_h2`][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`](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](docs/usage.md)** - client modes, multiple tunnels,
  integration with an existing `quic_h3` or `h2` server, handler
  lifecycle, transport selection, connection pooling, metrics.
- **[Design](docs/design.md)** - architecture overview, supervision
  tree, client and server request paths, upstream pool internals,
  extension points.
- **[Relay guide](docs/relay.md)** - end-to-end walkthrough of
  building an Apple-Private-Relay-shaped ingress + egress app on
  top of the library.
- **[API reference](docs/api.md)** - type signatures, option maps,
  handler callbacks, built-in handlers.
- **[CONNECT-IP guide](docs/connect_ip.md)** - RFC 9484 usage and
  section-by-section compliance mapping.
- **[Feature matrix](docs/features.md)** - RFC coverage and
  intentional non-goals.

[rfc9484]: https://www.rfc-editor.org/rfc/rfc9484

## Installation

Add to your `rebar.config`:

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

## Quick start

### Proxy server (serves both UDP and TCP tunnels)

```erlang
{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

```erlang
%% 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

```erlang
-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

- [`examples/udp_echo_proxy.erl`](examples/udp_echo_proxy.erl) - a
  zero-code MASQUE proxy on port 4433.
- [`examples/udp_dig_client.erl`](examples/udp_dig_client.erl) -
  resolve a DNS name through a MASQUE proxy.

## Building and testing

```sh
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:

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

## License

Apache License 2.0. See [`LICENSE`](LICENSE).

[rfc9298]: https://www.rfc-editor.org/rfc/rfc9298
[quic]:    https://github.com/benoitc/erlang_quic
[h2]:      https://github.com/benoitc/erlang_h2