masque_compression_table (masque v0.7.0)

View Source

Per-session Connect-UDP-Bind compression table.

Two of these live in every bind session, one per role:

  • own table - context-IDs we have opened on the peer via outbound COMPRESSION_ASSIGN. Allocations follow the parity rule (client uses even IDs, proxy uses odd).
  • peer table - context-IDs the peer has opened on us via incoming COMPRESSION_ASSIGN. We installed them; we look up by ID on receipt of a datagram and by tuple when compressing outbound traffic to a peer we already have a mapping for.

Draft-ietf-masque-connect-udp-listen-11 invariants enforced here:

  • Parity: client-even / proxy-odd context-IDs on open. Cross-parity from the peer is malformed on install.
  • Duplicate context-ID is malformed.
  • Per-tuple uniqueness with the two distinct draft-11 cases:
    • Cross-side conflict: peer ASSIGNs a tuple our side already opened. The table emits {conflict, close_proxy_id} so the session can send the matching COMPRESSION_CLOSE.
    • Same-side conflict: peer ASSIGNs a tuple it already has open. Returned as {error, malformed_duplicate_tuple}; the session aborts the request stream.
  • Uncompressed (IP Version 0) asymmetry:
    • open_uncompressed/1 on a proxy-side own table returns {error, uncompressed_only_from_client}.
    • The proxy-side peer table accepts an incoming version-0 ASSIGN (the client opened it).
    • The client-side peer table rejects an incoming version-0 ASSIGN (only the client may originate uncompressed mappings).
  • Singleton uncompressed: at most one open IP Version 0 mapping per session. Second open_uncompressed/1 returns {error, uncompressed_context_already_open}; second incoming version-0 ASSIGN is malformed.
  • Post-close prohibition: once the proxy-side own uncompressed mapping has been closed, the proxy must not open new compressed mappings - the client could not deliver payloads that need the uncompressed channel. open_compressed/2 returns {error, uncompressed_closed} on a proxy-side own table after the closure.
  • Address-family gating: open_compressed/2 for a family not in the advertised list returns {error, unadvertised_family}; the same family check applies to install/2.
  • Bounds (max_in, max_out) on the peer / own tables to limit memory in the face of a hostile peer.

This module is pure data. No process, no I/O. Sessions thread the returned state() through their gen_server state.

Summary

Functions

Record a peer-originated COMPRESSION_ASSIGN on the peer table.

Record a peer COMPRESSION_ACK. Only valid against the own table (we sent the original ASSIGN).

Record a peer COMPRESSION_CLOSE. Removes the entry from whichever table holds it. The caller is expected to dispatch by direction first - typically the session looks up the ID in both tables and calls install_close on the matching one.

Build a fresh "own" table - the one that allocates outbound context-IDs. role decides the parity. Opts may set advertised_families (default [4,6]) and max_entries (default 1024).

Build a fresh "peer" table - the one that records incoming COMPRESSION_ASSIGNs from the peer. The peer's parity is the opposite of Role.

Allocate a fresh own context-ID for a compressed (IP Version 4 / 6) mapping to the given peer tuple. Returns the new entry plus the updated state. The session emits a matching COMPRESSION_ASSIGN on the wire and waits for ACK before using the ID.

Allocate a fresh own context-ID for an uncompressed (IP Version 0) mapping. Client-only.

Types

direction/0

-type direction() :: own | peer.

install_ack_error/0

-type install_ack_error() :: malformed_unknown_ack.

install_close_error/0

-type install_close_error() :: unknown_context.

install_error/0

-type install_error() ::
          bad_parity | duplicate_context_id | uncompressed_only_from_client |
          uncompressed_context_already_open | malformed_duplicate_tuple | unadvertised_family |
          table_full.

install_result/0

-type install_result() :: {ok, state()} | {ok, {conflict, close_proxy_id, pos_integer()}, state()}.

open_error/0

-type open_error() ::
          uncompressed_only_from_client | uncompressed_context_already_open | unadvertised_family |
          table_full.

peer_tuple/0

-type peer_tuple() :: {4 | 6, inet:ip_address(), inet:port_number()}.

role/0

-type role() :: client | proxy.

state/0

-opaque state()

Functions

entries(State)

-spec entries(state()) ->
                 [#compression_entry{context_id :: pos_integer(),
                                     ip_version :: 0 | 4 | 6,
                                     address :: undefined | inet:ip_address(),
                                     port :: undefined | inet:port_number(),
                                     state :: pending_ack | installed | closing,
                                     direction :: outbound | inbound}].

install(State, Compression_assign)

-spec install(state(),
              #compression_assign{context_id :: pos_integer(),
                                  ip_version :: 0 | 4 | 6,
                                  address :: undefined | inet:ip_address(),
                                  port :: undefined | inet:port_number()}) ->
                 install_result() | {error, install_error()}.

Record a peer-originated COMPRESSION_ASSIGN on the peer table.

Returns:

  • {ok, NewState} on a clean install.
  • {ok, {conflict, close_proxy_id, Id}, NewState} when the peer's tuple matches one our own side has already opened (the proxy must close the proxy-opened context per draft-11). The session emits the matching CLOSE; the close refers to the Id on the *own* table, not this one.
  • {error, _} on a malformed install.

The own table is passed in too because the cross-side conflict rule needs visibility into our own allocations. OwnTable is read-only; the conflict resolution itself happens in the session, which removes the conflicted entry from the own table after sending CLOSE.

install_ack(State, Compression_ack)

-spec install_ack(state(), #compression_ack{context_id :: pos_integer()}) ->
                     {ok, state()} | {error, install_ack_error()}.

Record a peer COMPRESSION_ACK. Only valid against the own table (we sent the original ASSIGN).

install_close(State, Compression_close)

-spec install_close(state(), #compression_close{context_id :: pos_integer()}) ->
                       {ok, state()} | {error, install_close_error()}.

Record a peer COMPRESSION_CLOSE. Removes the entry from whichever table holds it. The caller is expected to dispatch by direction first - typically the session looks up the ID in both tables and calls install_close on the matching one.

is_empty(State)

-spec is_empty(state()) -> boolean().

lookup_by_id(State, Id)

-spec lookup_by_id(state(), pos_integer()) ->
                      {ok,
                       #compression_entry{context_id :: pos_integer(),
                                          ip_version :: 0 | 4 | 6,
                                          address :: undefined | inet:ip_address(),
                                          port :: undefined | inet:port_number(),
                                          state :: pending_ack | installed | closing,
                                          direction :: outbound | inbound}} |
                      not_found.

lookup_by_tuple(State, Tuple)

-spec lookup_by_tuple(state(), peer_tuple()) ->
                         {ok,
                          #compression_entry{context_id :: pos_integer(),
                                             ip_version :: 0 | 4 | 6,
                                             address :: undefined | inet:ip_address(),
                                             port :: undefined | inet:port_number(),
                                             state :: pending_ack | installed | closing,
                                             direction :: outbound | inbound}} |
                         not_found.

new_own(Role, Opts)

-spec new_own(role(), map()) -> state().

Build a fresh "own" table - the one that allocates outbound context-IDs. role decides the parity. Opts may set advertised_families (default [4,6]) and max_entries (default 1024).

new_peer(Role, Opts)

-spec new_peer(role(), map()) -> state().

Build a fresh "peer" table - the one that records incoming COMPRESSION_ASSIGNs from the peer. The peer's parity is the opposite of Role.

open_compressed(State, Peer)

-spec open_compressed(state(), peer_tuple()) ->
                         {ok,
                          #compression_entry{context_id :: pos_integer(),
                                             ip_version :: 0 | 4 | 6,
                                             address :: undefined | inet:ip_address(),
                                             port :: undefined | inet:port_number(),
                                             state :: pending_ack | installed | closing,
                                             direction :: outbound | inbound},
                          state()} |
                         {error, open_error()}.

Allocate a fresh own context-ID for a compressed (IP Version 4 / 6) mapping to the given peer tuple. Returns the new entry plus the updated state. The session emits a matching COMPRESSION_ASSIGN on the wire and waits for ACK before using the ID.

open_uncompressed(State)

-spec open_uncompressed(state()) ->
                           {ok,
                            #compression_entry{context_id :: pos_integer(),
                                               ip_version :: 0 | 4 | 6,
                                               address :: undefined | inet:ip_address(),
                                               port :: undefined | inet:port_number(),
                                               state :: pending_ack | installed | closing,
                                               direction :: outbound | inbound},
                            state()} |
                           {error, open_error()}.

Allocate a fresh own context-ID for an uncompressed (IP Version 0) mapping. Client-only.