masque_compression_table (masque v0.7.0)
View SourcePer-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 oninstall. - 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 matchingCOMPRESSION_CLOSE. - Same-side conflict: peer ASSIGNs a tuple it already has open. Returned as
{error, malformed_duplicate_tuple}; the session aborts the request stream.
- Cross-side conflict: peer ASSIGNs a tuple our side already opened. The table emits
- Uncompressed (IP Version 0) asymmetry:
open_uncompressed/1on 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/1returns{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/2returns{error, uncompressed_closed}on a proxy-side own table after the closure. - Address-family gating:
open_compressed/2for a family not in the advertised list returns{error, unadvertised_family}; the same family check applies toinstall/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
-type direction() :: own | peer.
-type install_ack_error() :: malformed_unknown_ack.
-type install_close_error() :: unknown_context.
-type install_error() ::
bad_parity | duplicate_context_id | uncompressed_only_from_client |
uncompressed_context_already_open | malformed_duplicate_tuple | unadvertised_family |
table_full.
-type install_result() :: {ok, state()} | {ok, {conflict, close_proxy_id, pos_integer()}, state()}.
-type open_error() ::
uncompressed_only_from_client | uncompressed_context_already_open | unadvertised_family |
table_full.
-type peer_tuple() :: {4 | 6, inet:ip_address(), inet:port_number()}.
-type role() :: client | proxy.
-opaque state()
Functions
-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}].
-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 theIdon 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.
-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).
-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.
-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.
-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.
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.
-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.
-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.