masque (masque v0.7.0)

View Source

Public 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

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.

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

connect_opts/0

-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}.

ip_assignment/0

-type ip_assignment() ::
          #ip_assignment{request_id :: non_neg_integer(),
                         version :: 4 | 6,
                         address :: inet:ip_address(),
                         prefix_len :: 0..128}.

ip_ipproto/0

-type ip_ipproto() :: '*' | 0..255.

ip_prefix/0

-type ip_prefix() :: {4, inet:ip4_address(), 0..32} | {6, inet:ip6_address(), 0..128}.

ip_prefix_request/0

-type ip_prefix_request() ::
          #ip_prefix_request{request_id :: pos_integer(),
                             version :: 4 | 6,
                             address :: inet:ip_address(),
                             prefix_len :: 0..128}.

ip_route/0

-type ip_route() ::
          #ip_route{version :: 4 | 6,
                    start_addr :: inet:ip_address(),
                    end_addr :: inet:ip_address(),
                    ip_protocol :: 0..255}.

ip_target/0

-type ip_target() :: '*' | inet:ip4_address() | inet:ip6_address() | ip_prefix() | binary().

hostname

ip_version/0

-type ip_version() :: 4 | 6.

listener_opts/0

-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()]}.

nz_request_id/0

-type nz_request_id() :: pos_integer().

proxy_uri/0

-type proxy_uri() :: binary() | string().

request_id/0

-type request_id() :: non_neg_integer().

session/0

-type session() :: pid().

target/0

-type target() :: udp_target() | {ip_target(), ip_ipproto()}.

transport/0

-type transport() :: h3 | h2 | h1.

udp_target/0

-type udp_target() :: {binary() | inet:hostname() | inet:ip_address(), inet:port_number()}.

Functions

assign_addresses(Sess, Entries)

-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).

assign_compression(Sess, Peer)

-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.

bind_connect(ProxyURI, Target, Opts)

-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.

close(Sess)

-spec close(session()) -> ok.

Close a MASQUE session.

close_compression(Sess, Id)

-spec close_compression(session(), pos_integer()) -> ok | {error, term()}.

Retire a compression context.

connect(ProxyURI, Target)

-spec connect(proxy_uri(), target()) -> {ok, session()} | {error, term()}.

Equivalent to connect(ProxyURI, Target, #{}).

connect(ProxyURI, Target, Opts)

-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.

drain_listener(Name)

-spec drain_listener(atom()) -> ok.

Stop accepting new tunnels but let existing ones finish.

h2_handlers(Opts)

-spec h2_handlers(map()) ->
                     #{handler := fun((pid(), non_neg_integer(), binary(), binary(), list()) -> any())}.

h3_handlers(Opts)

-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).

info(Sess)

-spec info(session()) -> map().

Return a map describing the session's current state and peers.

ip_info(Sess)

-spec ip_info(session()) ->
                 #{assigned := [ip_assignment()],
                   routes := [ip_route()],
                   mtu := 1280..65535,
                   transport := transport()}.

Inspect the CONNECT-IP session state.

is_draining(Name)

-spec is_draining(atom() | undefined) -> boolean().

Check if a listener is draining.

open_uncompressed_context(Sess)

-spec open_uncompressed_context(session()) -> {ok, pos_integer()} | {error, term()}.

Open the singleton uncompressed (IP Version 0) context. Client-only per draft-11.

proxy_public_address(Sess)

-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.

recv(Sess, Timeout)

-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).

request_addresses(Sess, Prefixes)

-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(Sess, Data)

-spec send(session(), iodata()) -> ok | {error, term()}.

Send data through the tunnel.

For UDP tunnels: sends a UDP packet (context-id 0). For TCP tunnels: sends raw bytes on the stream.

send(Sess, ContextId, Data)

-spec send(session(), non_neg_integer(), iodata()) -> ok | {error, term()}.

Send data under an explicit context-id (UDP extension use).

send_capsule(Sess, Type, Value)

-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_ip_packet(Sess, Packet)

-spec send_ip_packet(session(), binary()) -> ok | {error, term()}.

Send a full IP packet (starting at the IP header) through a CONNECT-IP tunnel. Rejects packets larger than the session's MTU.

send_to(Sess, Peer, Bytes)

-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}.

set_mode(Sess, Mode)

-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.

shutdown_write(Sess)

-spec shutdown_write(session()) -> ok | {error, term()}.

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_chain_listener(Name, Opts)

-spec start_chain_listener(atom(), map()) -> {ok, pid()} | {error, term()}.

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.

start_chain_listener_h1(Name, Opts)

-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.

start_chain_listener_h2(Name, Opts)

-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.

start_listener(Name, Opts)

-spec start_listener(atom(), listener_opts()) -> {ok, pid()} | {error, term()}.

start_listener_h1(Name, Opts)

-spec start_listener_h1(atom(), map()) -> {ok, h1:server_ref()} | {error, term()}.

start_listener_h2(Name, Opts)

-spec start_listener_h2(atom(), map()) -> {ok, h2:server_ref()} | {error, term()}.

stop_listener(Name)

-spec stop_listener(atom()) -> ok | {error, term()}.

stop_listener_h1(Ref)

-spec stop_listener_h1(h1:server_ref() | atom()) -> ok | {error, term()}.

stop_listener_h2(Ref)

-spec stop_listener_h2(h2:server_ref() | atom()) -> ok | {error, term()}.

undrain_listener(Name)

-spec undrain_listener(atom()) -> ok.

Re-enable new tunnels after draining.

version()

-spec version() -> binary().

Returns the library version as declared in the application resource file.