%%% @doc Encode/decode the inner Bound UDP Proxying Payload, the %%% bytes that follow the HTTP Datagram Context ID. Per %%% draft-ietf-masque-connect-udp-listen-11 sections 4 (uncompressed) %%% and 5 (compressed): %%% %%% %%% %%% This module never sees the Context ID; that is consumed by %%% `masque_datagram' one layer above. The session is responsible for %%% looking the Context ID up in its compression table and dispatching %%% to the right shape (compressed vs uncompressed) based on the %%% registered IP Version of the entry. %%% %%% Pure module - no I/O, no state. Address-family gating is enforced %%% on encode using the advertised public-address families. -module(masque_udp_bind_payload). -export([encode_compressed/1, encode_uncompressed/3, decode_compressed/1, decode_uncompressed/1, family_advertised/2]). -type families() :: [4 | 6]. -type encode_error() :: unadvertised_family. -type decode_error() :: truncated | bad_ip_version | bad_udp_port. -export_type([families/0, encode_error/0, decode_error/0]). %%==================================================================== %% Encode %%==================================================================== %% @doc Encode a payload onto a compressed Context ID. The IP-port %% tuple is implicit (the receiver looks it up in its table). %% Caller is expected to have verified the family is advertised via %% `family_advertised/2' before invoking this. -spec encode_compressed(binary()) -> binary(). encode_compressed(UdpPayload) when is_binary(UdpPayload) -> UdpPayload. %% @doc Encode a payload onto an uncompressed Context ID. The peer %% tuple is carried inline. Address family must be in the advertised %% list. -spec encode_uncompressed( {4 | 6, inet:ip_address(), inet:port_number()}, binary(), families()) -> {ok, binary()} | {error, encode_error()}. encode_uncompressed({V, _, _}, _Payload, Fams) when V =/= 4, V =/= 6 -> %% defensive; spec only allows 4 / 6 in the wire IP Version case lists:member(V, Fams) of true -> {error, bad_ip_version}; false -> {error, unadvertised_family} end; encode_uncompressed({V, IP, Port}, Payload, Fams) when (V =:= 4 orelse V =:= 6), is_integer(Port), Port >= 0, Port =< 65535, is_binary(Payload) -> case lists:member(V, Fams) of false -> {error, unadvertised_family}; true -> {ok, build_uncompressed(V, IP, Port, Payload)} end. %%==================================================================== %% Decode %%==================================================================== %% @doc Decode a compressed payload (just the UDP bytes). Trivial; %% provided for symmetry with `decode_uncompressed/1'. -spec decode_compressed(binary()) -> {ok, binary()}. decode_compressed(Payload) when is_binary(Payload) -> {ok, Payload}. %% @doc Decode an uncompressed payload into the carried peer tuple %% and the inner UDP bytes. -spec decode_uncompressed(binary()) -> {ok, {4 | 6, inet:ip_address(), inet:port_number()}, binary()} | {error, decode_error()}. decode_uncompressed(<<4:8, A:8, B:8, C:8, D:8, Port:16, Payload/binary>>) -> case Port of P when P >= 0, P =< 65535 -> {ok, {4, {A,B,C,D}, P}, Payload}; _ -> {error, bad_udp_port} end; decode_uncompressed(<<4:8, _/binary>>) -> {error, truncated}; decode_uncompressed(<<6:8, V6:16/binary, Port:16, Payload/binary>>) -> <> = V6, case Port of P when P >= 0, P =< 65535 -> {ok, {6, {A,B,C,D,E,F,G,H}, P}, Payload}; _ -> {error, bad_udp_port} end; decode_uncompressed(<<6:8, _/binary>>) -> {error, truncated}; decode_uncompressed(<>) when Ver =/= 4, Ver =/= 6 -> {error, bad_ip_version}; decode_uncompressed(_) -> {error, truncated}. %%==================================================================== %% Family gating helper %%==================================================================== %% @doc Returns true iff the given IP version is in the advertised %% list. Both encode paths consult this, and the session uses it on %% incoming uncompressed payloads to drop those whose family is not %% advertised (e.g. a peer reaching us over an IPv4-only proxy with %% an IPv6 source somehow set). -spec family_advertised(4 | 6, families()) -> boolean(). family_advertised(V, Fams) when is_integer(V), is_list(Fams) -> lists:member(V, Fams). %%==================================================================== %% Internal %%==================================================================== build_uncompressed(4, {A,B,C,D}, Port, Payload) -> <<4:8, A:8, B:8, C:8, D:8, Port:16, Payload/binary>>; build_uncompressed(6, {A,B,C,D,E,F,G,H}, Port, Payload) -> <<6:8, A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16, Port:16, Payload/binary>>.