masque (masque v0.7.0)
View SourcePublic API for the masque library.
masque implements RFC 9298 (Proxying UDP in HTTP) on top of erlang_quics HTTP/3 stack. The functions in this module are the stable surface used by applications; all other modules are internal and may change between versions.
The surface is filled in incrementally across the implementation plan. Step 1 only exposes the module so the application compiles and is loadable; the behavioural functions are added in later steps.
Summary
Types
hostname
Functions
Send a ROUTE_ADVERTISEMENT capsule.
Send an ADDRESS_ASSIGN capsule. Non-zero Request IDs must match an outstanding peer ADDRESS_REQUEST; ID 0 is always accepted (unprompted, RFC 9484 §4.7.1).
Open an outbound compressed context for Peer. Returns the allocated Context ID; the mapping is safe to use on send once the matching {masque_compression_acked, _, ContextId} message arrives.
Open a Connect-UDP-Bind tunnel to ProxyURI. Target is either unscoped (the bind socket on the proxy can talk to any peer the proxy's policy allows) or {Host, Port} for a scoped bind. The session emits {masque_bind_packet, _, Peer, Bytes} messages to the owner; use send_to/3 to send.
Close a MASQUE session.
Retire a compression context.
Equivalent to connect(ProxyURI, Target, #{}).
Dial a MASQUE proxy and open a CONNECT-UDP tunnel to Target.
Stop accepting new tunnels but let existing ones finish.
Return the handler and connection_handler funs needed to run MASQUE inside a user-owned quic_h3:start_server/3 call.
Return a map describing the session's current state and peers.
Inspect the CONNECT-IP session state.
Check if a listener is draining.
Open the singleton uncompressed (IP Version 0) context. Client-only per draft-11.
Read the parsed Proxy-Public-Address list the proxy advertised on the bind 2xx response.
Block until data is received or Timeout ms elapses.
Send an ADDRESS_REQUEST capsule asking the peer to assign one or more addresses. Returns the allocated Request IDs.
Send data through the tunnel.
Send data under an explicit context-id (UDP extension use).
Send a capsule on the tunnel's request stream (RFC 9297 §3.2).
Send a full IP packet (starting at the IP header) through a CONNECT-IP tunnel. Rejects packets larger than the session's MTU.
Send a UDP payload to Peer via the bind tunnel. The session picks a context-id from the compression table; if none exists it falls back to the uncompressed-context channel if open, otherwise returns {error, no_compression_context}.
Switch the session between message and queue delivery modes.
Half-close the write side of a TCP tunnel.
Start a chaining (two-hop) listener on HTTP/3.
Start a chaining (two-hop) listener on HTTP/1.1.
Start a chaining (two-hop) listener on HTTP/2.
Re-enable new tunnels after draining.
Returns the library version as declared in the application resource file.
Types
-type connect_opts() :: #{protocol => udp | tcp | ip, transports => [transport()], prefer_timeout_ms => non_neg_integer(), h1_prefer_timeout_ms => non_neg_integer(), proxy_authorization => binary(), uri_template => binary(), verify => verify_peer | verify_none, cacerts => [public_key:der_encoded()], timeout => pos_integer() | infinity, capsule_protocol => boolean(), owner => pid(), ssl_opts => [ssl:tls_client_option()], mtu => 1280..65535, upstream_pool => boolean(), upstream_pool_opts => map(), request_headers => [{binary(), binary()}], transport => transport(), proxy => {binary(), inet:port_number()}, alpn => [binary()], mode => message | queue}.
-type ip_assignment() :: #ip_assignment{request_id :: non_neg_integer(), version :: 4 | 6, address :: inet:ip_address(), prefix_len :: 0..128}.
-type ip_ipproto() :: '*' | 0..255.
-type ip_prefix() :: {4, inet:ip4_address(), 0..32} | {6, inet:ip6_address(), 0..128}.
-type ip_prefix_request() :: #ip_prefix_request{request_id :: pos_integer(), version :: 4 | 6, address :: inet:ip_address(), prefix_len :: 0..128}.
-type ip_route() :: #ip_route{version :: 4 | 6, start_addr :: inet:ip_address(), end_addr :: inet:ip_address(), ip_protocol :: 0..255}.
-type ip_target() :: '*' | inet:ip4_address() | inet:ip6_address() | ip_prefix() | binary().
hostname
-type ip_version() :: 4 | 6.
-type listener_opts() :: #{port := inet:port_number(), cert => term(), key => term(), uri_template => binary(), tcp_uri_template => binary(), ip_uri_template => binary(), handler => module(), tcp_handler => module(), ip_handler => module(), handler_opts => term(), address_pool => ip_prefix() | [ip_prefix()], routes => [ip_route()], mtu => 1280..65535, resolver => fun((binary()) -> {ok, [inet:ip_address()]} | {error, term()}), allow => fun((target()) -> boolean()), family => auto | inet | inet6, allow_private => boolean(), connect_timeout => pos_integer(), socket_opts => [gen_tcp:option()]}.
-type nz_request_id() :: pos_integer().
-type request_id() :: non_neg_integer().
-type session() :: pid().
-type target() :: udp_target() | {ip_target(), ip_ipproto()}.
-type transport() :: h3 | h2 | h1.
-type udp_target() :: {binary() | inet:hostname() | inet:ip_address(), inet:port_number()}.
Functions
Send a ROUTE_ADVERTISEMENT capsule.
-spec assign_addresses(session(), [ip_assignment()]) -> ok | {error, term()}.
Send an ADDRESS_ASSIGN capsule. Non-zero Request IDs must match an outstanding peer ADDRESS_REQUEST; ID 0 is always accepted (unprompted, RFC 9484 §4.7.1).
-spec assign_compression(session(), {inet:ip_address(), inet:port_number()}) -> {ok, pos_integer()} | {error, term()}.
Open an outbound compressed context for Peer. Returns the allocated Context ID; the mapping is safe to use on send once the matching {masque_compression_acked, _, ContextId} message arrives.
-spec bind_connect(proxy_uri(), unscoped | {binary() | inet:hostname(), 1..65535}, connect_opts()) -> {ok, session()} | {error, term()}.
Open a Connect-UDP-Bind tunnel to ProxyURI. Target is either unscoped (the bind socket on the proxy can talk to any peer the proxy's policy allows) or {Host, Port} for a scoped bind. The session emits {masque_bind_packet, _, Peer, Bytes} messages to the owner; use send_to/3 to send.
-spec close(session()) -> ok.
Close a MASQUE session.
-spec close_compression(session(), pos_integer()) -> ok | {error, term()}.
Retire a compression context.
Equivalent to connect(ProxyURI, Target, #{}).
-spec connect(proxy_uri(), target(), connect_opts()) -> {ok, session()} | {error, term()}.
Dial a MASQUE proxy and open a CONNECT-UDP tunnel to Target.
ProxyURI is an https://host:port URL identifying the proxy; Target is a {Host, Port} pair naming the UDP endpoint to reach. Returns {ok, Session} on 2xx, {error, Reason} otherwise.
-spec drain_listener(atom()) -> ok.
Stop accepting new tunnels but let existing ones finish.
-spec h3_handlers(map()) -> #{handler := masque_server:h3_handler_fun(), connection_handler := masque_server:connection_handler_fun()}.
Return the handler and connection_handler funs needed to run MASQUE inside a user-owned quic_h3:start_server/3 call.
See masque_server:h3_handlers/1 for the accepted option keys (including the fallback hook that routes non-MASQUE requests to the caller's own handler).
Return a map describing the session's current state and peers.
-spec ip_info(session()) -> #{assigned := [ip_assignment()], routes := [ip_route()], mtu := 1280..65535, transport := transport()}.
Inspect the CONNECT-IP session state.
Check if a listener is draining.
-spec open_uncompressed_context(session()) -> {ok, pos_integer()} | {error, term()}.
Open the singleton uncompressed (IP Version 0) context. Client-only per draft-11.
-spec proxy_public_address(session()) -> {ok, [{inet:ip_address(), inet:port_number()}]} | {error, term()}.
Read the parsed Proxy-Public-Address list the proxy advertised on the bind 2xx response.
-spec recv(session(), pos_integer()) -> {ok, binary()} | {error, timeout | term()}.
Block until data is received or Timeout ms elapses.
Requires the session to be in queue delivery mode (see set_mode/2).
-spec request_addresses(session(), [{ip_version(), inet:ip_address(), non_neg_integer()}]) -> {ok, [nz_request_id()]} | {error, term()}.
Send an ADDRESS_REQUEST capsule asking the peer to assign one or more addresses. Returns the allocated Request IDs.
Send data through the tunnel.
For UDP tunnels: sends a UDP packet (context-id 0). For TCP tunnels: sends raw bytes on the stream.
-spec send(session(), non_neg_integer(), iodata()) -> ok | {error, term()}.
Send data under an explicit context-id (UDP extension use).
-spec send_capsule(session(), non_neg_integer(), iodata()) -> ok | {error, term()}.
Send a capsule on the tunnel's request stream (RFC 9297 §3.2).
Send a full IP packet (starting at the IP header) through a CONNECT-IP tunnel. Rejects packets larger than the session's MTU.
-spec send_to(session(), {inet:ip_address(), inet:port_number()}, binary()) -> ok | {error, term()}.
Send a UDP payload to Peer via the bind tunnel. The session picks a context-id from the compression table; if none exists it falls back to the uncompressed-context channel if open, otherwise returns {error, no_compression_context}.
-spec set_mode(session(), message | queue) -> ok.
Switch the session between message and queue delivery modes.
message (default) delivers every incoming packet to the owner as {masque_data, Sess, Data}. queue buffers packets and requires the caller to pull them via recv/2.
Half-close the write side of a TCP tunnel.
Sends END_STREAM and prevents further writes. The session stays open for receiving data. Returns {error, not_supported} on UDP sessions. Returns {error, not_ready} if still connecting.
Start a chaining (two-hop) listener on HTTP/3.
Convenience wrapper: starts an h3 listener with masque_chain_handler wired up for all three tunnel protocols (UDP, TCP, IP). Every accepted tunnel is relayed to the upstream proxy specified in handler_opts.upstream_proxy.
Callers that want only a subset of protocols to chain can still call start_listener/2 directly and set handler, tcp_handler, ip_handler individually.
See start_chain_listener_h2/2 and start_chain_listener_h1/2 for the HTTP/2 and HTTP/1.1 siblings. A full Apple-Private-Relay-shaped ingress runs all three so the client can race them.
-spec start_chain_listener_h1(atom(), map()) -> {ok, h1:server_ref()} | {error, term()}.
Start a chaining (two-hop) listener on HTTP/1.1.
Same shape as start_chain_listener/2; only the outer transport differs. All three tunnel protocols chain; upstream proxy URI goes in handler_opts.upstream_proxy.
-spec start_chain_listener_h2(atom(), map()) -> {ok, h2:server_ref()} | {error, term()}.
Start a chaining (two-hop) listener on HTTP/2.
Same shape as start_chain_listener/2; only the outer transport differs. All three tunnel protocols chain; upstream proxy URI goes in handler_opts.upstream_proxy.
-spec start_listener(atom(), listener_opts()) -> {ok, pid()} | {error, term()}.
-spec start_listener_h1(atom(), map()) -> {ok, h1:server_ref()} | {error, term()}.
-spec start_listener_h2(atom(), map()) -> {ok, h2:server_ref()} | {error, term()}.
-spec stop_listener_h1(h1:server_ref() | atom()) -> ok | {error, term()}.
-spec stop_listener_h2(h2:server_ref() | atom()) -> ok | {error, term()}.
-spec undrain_listener(atom()) -> ok.
Re-enable new tunnels after draining.
-spec version() -> binary().
Returns the library version as declared in the application resource file.