%% Copyright (c) 2018-2021, Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(ranch_proxy_header). -export([parse/1]). -export([header/1]). -export([header/2]). -export([to_connection_info/1]). -type proxy_info() :: #{ %% Mandatory part. version := 1 | 2, command := local | proxy, transport_family => undefined | ipv4 | ipv6 | unix, transport_protocol => undefined | stream | dgram, %% Addresses. src_address => inet:ip_address() | binary(), src_port => inet:port_number(), dest_address => inet:ip_address() | binary(), dest_port => inet:port_number(), %% Extra TLV-encoded data. alpn => binary(), %% US-ASCII. authority => binary(), %% UTF-8. ssl => #{ client := [ssl | cert_conn | cert_sess], verified := boolean(), version => binary(), %% US-ASCII. cipher => binary(), %% US-ASCII. sig_alg => binary(), %% US-ASCII. key_alg => binary(), %% US-ASCII. cn => binary() %% UTF-8. }, netns => binary(), %% US-ASCII. %% Unknown TLVs can't be parsed so the raw data is given. raw_tlvs => [{0..255, binary()}] }. -export_type([proxy_info/0]). -type build_opts() :: #{ checksum => crc32c, padding => pos_integer() %% >= 3 }. %% Parsing. -spec parse(Data) -> {ok, proxy_info(), Data} | {error, atom()} when Data::binary(). parse(<<"\r\n\r\n\0\r\nQUIT\n", Rest/bits>>) -> parse_v2(Rest); parse(<<"PROXY ", Rest/bits>>) -> parse_v1(Rest); parse(_) -> {error, 'The PROXY protocol header signature was not recognized. (PP 2.1, PP 2.2)'}. -ifdef(TEST). parse_unrecognized_header_test() -> {error, _} = parse(<<"GET / HTTP/1.1\r\n">>), ok. -endif. %% Human-readable header format (Version 1). parse_v1(<<"TCP4 ", Rest/bits>>) -> parse_v1(Rest, ipv4); parse_v1(<<"TCP6 ", Rest/bits>>) -> parse_v1(Rest, ipv6); parse_v1(<<"UNKNOWN\r\n", Rest/bits>>) -> {ok, #{ version => 1, command => proxy, transport_family => undefined, transport_protocol => undefined }, Rest}; parse_v1(<<"UNKNOWN ", Rest0/bits>>) -> case binary:split(Rest0, <<"\r\n">>) of [_, Rest] -> {ok, #{ version => 1, command => proxy, transport_family => undefined, transport_protocol => undefined }, Rest}; [_] -> {error, 'Malformed or incomplete PROXY protocol header line. (PP 2.1)'} end; parse_v1(_) -> {error, 'The INET protocol and family string was not recognized. (PP 2.1)'}. parse_v1(Rest0, Family) -> try {ok, SrcAddr, Rest1} = parse_ip(Rest0, Family), {ok, DestAddr, Rest2} = parse_ip(Rest1, Family), {ok, SrcPort, Rest3} = parse_port(Rest2, $\s), {ok, DestPort, Rest4} = parse_port(Rest3, $\r), <<"\n", Rest/bits>> = Rest4, {ok, #{ version => 1, command => proxy, transport_family => Family, transport_protocol => stream, src_address => SrcAddr, src_port => SrcPort, dest_address => DestAddr, dest_port => DestPort }, Rest} catch throw:parse_ipv4_error -> {error, 'Failed to parse an IPv4 address in the PROXY protocol header line. (PP 2.1)'}; throw:parse_ipv6_error -> {error, 'Failed to parse an IPv6 address in the PROXY protocol header line. (PP 2.1)'}; throw:parse_port_error -> {error, 'Failed to parse a port number in the PROXY protocol header line. (PP 2.1)'}; _:_ -> {error, 'Malformed or incomplete PROXY protocol header line. (PP 2.1)'} end. parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); parse_ip(Data, ipv6) -> [Addr, Rest] = binary:split(Data, <<$\s>>), parse_ipv6(Addr, Rest). parse_ipv4(Addr0, Rest) -> case inet:parse_ipv4strict_address(binary_to_list(Addr0)) of {ok, Addr} -> {ok, Addr, Rest}; {error, einval} -> throw(parse_ipv4_error) end. parse_ipv6(Addr0, Rest) -> case inet:parse_ipv6strict_address(binary_to_list(Addr0)) of {ok, Addr} -> {ok, Addr, Rest}; {error, einval} -> throw(parse_ipv6_error) end. parse_port(<>, C) -> parse_port(Port, Rest); parse_port(<>, C) -> parse_port(Port, Rest); parse_port(<>, C) -> parse_port(Port, Rest); parse_port(<>, C) -> parse_port(Port, Rest); parse_port(<>, C) -> parse_port(Port, Rest); parse_port(Port0, Rest) -> try binary_to_integer(Port0) of Port when Port > 0, Port =< 65535 -> {ok, Port, Rest}; _ -> throw(parse_port_error) catch _:_ -> throw(parse_port_error) end. -ifdef(TEST). parse_v1_test() -> %% Examples taken from the PROXY protocol header specification. {ok, #{ version := 1, command := proxy, transport_family := ipv4, transport_protocol := stream, src_address := {255, 255, 255, 255}, src_port := 65535, dest_address := {255, 255, 255, 255}, dest_port := 65535 }, <<>>} = parse(<<"PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535\r\n">>), {ok, #{ version := 1, command := proxy, transport_family := ipv6, transport_protocol := stream, src_address := {65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535}, src_port := 65535, dest_address := {65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535}, dest_port := 65535 }, <<>>} = parse(<<"PROXY TCP6 " "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff " "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 65535 65535\r\n">>), {ok, #{ version := 1, command := proxy, transport_family := undefined, transport_protocol := undefined }, <<>>} = parse(<<"PROXY UNKNOWN\r\n">>), {ok, #{ version := 1, command := proxy, transport_family := undefined, transport_protocol := undefined }, <<>>} = parse(<<"PROXY UNKNOWN " "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff " "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 65535 65535\r\n">>), {ok, #{ version := 1, command := proxy, transport_family := ipv4, transport_protocol := stream, src_address := {192, 168, 0, 1}, src_port := 56324, dest_address := {192, 168, 0, 11}, dest_port := 443 }, <<"GET / HTTP/1.1\r\nHost: 192.168.0.11\r\n\r\n">>} = parse(<< "PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\n" "GET / HTTP/1.1\r\n" "Host: 192.168.0.11\r\n" "\r\n">>), %% Test cases taken from tomciopp/proxy_protocol. {ok, #{ version := 1, command := proxy, transport_family := ipv4, transport_protocol := stream, src_address := {192, 168, 0, 1}, src_port := 56324, dest_address := {192, 168, 0, 11}, dest_port := 443 }, <<"GET / HTTP/1.1\r">>} = parse(<< "PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\nGET / HTTP/1.1\r">>), {error, _} = parse(<<"PROXY TCP4 192.1638.0.1 192.168.0.11 56324 443\r\nGET / HTTP/1.1\r">>), {error, _} = parse(<<"PROXY TCP4 192.168.0.1 192.168.0.11 1111111 443\r\nGET / HTTP/1.1\r">>), {ok, #{ version := 1, command := proxy, transport_family := ipv6, transport_protocol := stream, src_address := {8193, 3512, 0, 66, 0, 35374, 880, 29492}, src_port := 4124, dest_address := {8193, 3512, 0, 66, 0, 35374, 880, 29493}, dest_port := 443 }, <<"GET / HTTP/1.1\r">>} = parse(<<"PROXY TCP6 " "2001:0db8:0000:0042:0000:8a2e:0370:7334 " "2001:0db8:0000:0042:0000:8a2e:0370:7335 4124 443\r\nGET / HTTP/1.1\r">>), {error, _} = parse(<<"PROXY TCP6 " "2001:0db8:0000:0042:0000:8a2e:0370:7334 " "2001:0db8:00;0:0042:0000:8a2e:0370:7335 4124 443\r\nGET / HTTP/1.1\r">>), {error, _} = parse(<<"PROXY TCP6 " "2001:0db8:0000:0042:0000:8a2e:0370:7334 " "2001:0db8:0000:0042:0000:8a2e:0370:7335 4124 foo\r\nGET / HTTP/1.1\r">>), {ok, #{ version := 1, command := proxy, transport_family := undefined, transport_protocol := undefined }, <<"GET / HTTP/1.1\r">>} = parse(<<"PROXY UNKNOWN 4124 443\r\nGET / HTTP/1.1\r">>), {ok, #{ version := 1, command := proxy, transport_family := undefined, transport_protocol := undefined }, <<"GET / HTTP/1.1\r">>} = parse(<<"PROXY UNKNOWN\r\nGET / HTTP/1.1\r">>), ok. -endif. %% Binary header format (version 2). %% LOCAL. parse_v2(<<2:4, 0:4, _:8, Len:16, Rest0/bits>>) -> case Rest0 of <<_:Len/binary, Rest/bits>> -> {ok, #{ version => 2, command => local }, Rest}; _ -> {error, 'Missing data in the PROXY protocol binary header. (PP 2.2)'} end; %% PROXY. parse_v2(<<2:4, 1:4, Family:4, Protocol:4, Len:16, Rest/bits>>) when Family =< 3, Protocol =< 2 -> case Rest of <> -> parse_v2(Rest, Len, parse_family(Family), parse_protocol(Protocol), <>); _ -> {error, 'Missing data in the PROXY protocol binary header. (PP 2.2)'} end; %% Errors. parse_v2(<>) when Version =/= 2 -> {error, 'Invalid version in the PROXY protocol binary header. (PP 2.2)'}; parse_v2(<<_:4, Command:4, _/bits>>) when Command > 1 -> {error, 'Invalid command in the PROXY protocol binary header. (PP 2.2)'}; parse_v2(<<_:8, Family:4, _/bits>>) when Family > 3 -> {error, 'Invalid address family in the PROXY protocol binary header. (PP 2.2)'}; parse_v2(<<_:12, Protocol:4, _/bits>>) when Protocol > 2 -> {error, 'Invalid transport protocol in the PROXY protocol binary header. (PP 2.2)'}. parse_family(0) -> undefined; parse_family(1) -> ipv4; parse_family(2) -> ipv6; parse_family(3) -> unix. parse_protocol(0) -> undefined; parse_protocol(1) -> stream; parse_protocol(2) -> dgram. parse_v2(Data, Len, Family, Protocol, _) when Family =:= undefined; Protocol =:= undefined -> <<_:Len/binary, Rest/bits>> = Data, {ok, #{ version => 2, command => proxy, %% In case only one value was undefined, we set both explicitly. %% It doesn't make sense to have only one known value. transport_family => undefined, transport_protocol => undefined }, Rest}; parse_v2(<< S1, S2, S3, S4, D1, D2, D3, D4, SrcPort:16, DestPort:16, Rest/bits>>, Len, Family=ipv4, Protocol, Header) when Len >= 12 -> parse_tlv(Rest, Len - 12, #{ version => 2, command => proxy, transport_family => Family, transport_protocol => Protocol, src_address => {S1, S2, S3, S4}, src_port => SrcPort, dest_address => {D1, D2, D3, D4}, dest_port => DestPort }, Header); parse_v2(<< S1:16, S2:16, S3:16, S4:16, S5:16, S6:16, S7:16, S8:16, D1:16, D2:16, D3:16, D4:16, D5:16, D6:16, D7:16, D8:16, SrcPort:16, DestPort:16, Rest/bits>>, Len, Family=ipv6, Protocol, Header) when Len >= 36 -> parse_tlv(Rest, Len - 36, #{ version => 2, command => proxy, transport_family => Family, transport_protocol => Protocol, src_address => {S1, S2, S3, S4, S5, S6, S7, S8}, src_port => SrcPort, dest_address => {D1, D2, D3, D4, D5, D6, D7, D8}, dest_port => DestPort }, Header); parse_v2(<>, Len, Family=unix, Protocol, Header) when Len >= 216 -> try [SrcAddr, _] = binary:split(SrcAddr0, <<0>>), true = byte_size(SrcAddr) > 0, [DestAddr, _] = binary:split(DestAddr0, <<0>>), true = byte_size(DestAddr) > 0, parse_tlv(Rest, Len - 216, #{ version => 2, command => proxy, transport_family => Family, transport_protocol => Protocol, src_address => SrcAddr, dest_address => DestAddr }, Header) catch _:_ -> {error, 'Invalid UNIX address in PROXY protocol binary header. (PP 2.2)'} end; parse_v2(_, _, _, _, _) -> {error, 'Invalid length in the PROXY protocol binary header. (PP 2.2)'}. -ifdef(TEST). parse_v2_test() -> %% Test cases taken from tomciopp/proxy_protocol. {ok, #{ version := 2, command := proxy, transport_family := ipv4, transport_protocol := stream, src_address := {127, 0, 0, 1}, src_port := 444, dest_address := {192, 168, 0, 1}, dest_port := 443 }, <<"GET / HTTP/1.1\r\n">>} = parse(<< 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, %% Signature. 33, %% Version and command. 17, %% Family and protocol. 0, 12, %% Length. 127, 0, 0, 1, %% Source address. 192, 168, 0, 1, %% Destination address. 1, 188, %% Source port. 1, 187, %% Destination port. "GET / HTTP/1.1\r\n">>), {ok, #{ version := 2, command := proxy, transport_family := ipv4, transport_protocol := dgram, src_address := {127, 0, 0, 1}, src_port := 444, dest_address := {192, 168, 0, 1}, dest_port := 443 }, <<"GET / HTTP/1.1\r\n">>} = parse(<< 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, %% Signature. 33, %% Version and command. 18, %% Family and protocol. 0, 12, %% Length. 127, 0, 0, 1, %% Source address. 192, 168, 0, 1, %% Destination address. 1, 188, %% Source port. 1, 187, %% Destination port. "GET / HTTP/1.1\r\n">>), {ok, #{ version := 2, command := proxy, transport_family := ipv6, transport_protocol := stream, src_address := {5532, 4240, 1, 0, 0, 0, 0, 0}, src_port := 444, dest_address := {8193, 3512, 1, 0, 0, 0, 0, 0}, dest_port := 443 }, <<"GET / HTTP/1.1\r\n">>} = parse(<< 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, %% Signature. 33, %% Version and command. 33, %% Family and protocol. 0, 36, %% Length. 21, 156, 16, 144, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, %% Source address. 32, 1, 13, 184, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, %% Destination address. 1, 188, %% Source port. 1, 187, %% Destination port. "GET / HTTP/1.1\r\n">>), {ok, #{ version := 2, command := proxy, transport_family := ipv6, transport_protocol := dgram, src_address := {5532, 4240, 1, 0, 0, 0, 0, 0}, src_port := 444, dest_address := {8193, 3512, 1, 0, 0, 0, 0, 0}, dest_port := 443 }, <<"GET / HTTP/1.1\r\n">>} = parse(<< 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, %% Signature. 33, %% Version and command. 34, %% Family and protocol. 0, 36, %% Length. 21, 156, 16, 144, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, %% Source address. 32, 1, 13, 184, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, %% Destination address. 1, 188, %% Source port. 1, 187, %% Destination port. "GET / HTTP/1.1\r\n">>), Path = <<"/var/pgsql_sock">>, Len = byte_size(Path), Padding = 8 * (108 - Len), {ok, #{ version := 2, command := proxy, transport_family := unix, transport_protocol := stream, src_address := Path, dest_address := Path }, <<"GET / HTTP/1.1\r\n">>} = parse(<< 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, 33, 49, 0, 216, Path/binary, 0:Padding, Path/binary, 0:Padding, "GET / HTTP/1.1\r\n">>), {ok, #{ version := 2, command := proxy, transport_family := unix, transport_protocol := dgram, src_address := Path, dest_address := Path }, <<"GET / HTTP/1.1\r\n">>} = parse(<< 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, 33, 50, 0, 216, Path/binary, 0:Padding, Path/binary, 0:Padding, "GET / HTTP/1.1\r\n">>), ok. parse_v2_regression_test() -> %% Real packet received from AWS. We confirm that the CRC32C %% check succeeds only (in other words that ok is returned). {ok, _, <<>>} = parse(<< 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, 33, 17, 0, 84, 172, 31, 7, 113, 172, 31, 10, 31, 200, 242, 0, 80, 3, 0, 4, 232, 214, 137, 45, 234, 0, 23, 1, 118, 112, 99, 101, 45, 48, 56, 100, 50, 98, 102, 49, 53, 102, 97, 99, 53, 48, 48, 49, 99, 57, 4, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>), ok. -endif. parse_tlv(Rest, 0, Info, _) -> {ok, Info, Rest}; %% PP2_TYPE_ALPN. parse_tlv(<<16#1, TLVLen:16, ALPN:TLVLen/binary, Rest/bits>>, Len, Info, Header) -> parse_tlv(Rest, Len - TLVLen - 3, Info#{alpn => ALPN}, Header); %% PP2_TYPE_AUTHORITY. parse_tlv(<<16#2, TLVLen:16, Authority:TLVLen/binary, Rest/bits>>, Len, Info, Header) -> parse_tlv(Rest, Len - TLVLen - 3, Info#{authority => Authority}, Header); %% PP2_TYPE_CRC32C. parse_tlv(<<16#3, TLVLen:16, CRC32C:32, Rest/bits>>, Len0, Info, Header) when TLVLen =:= 4 -> Len = Len0 - TLVLen - 3, BeforeLen = byte_size(Header) - Len - TLVLen, <> = Header, %% The initial CRC is ranch_crc32c:crc32c(<<"\r\n\r\n\0\r\nQUIT\n", 2:4, 1:4>>). case ranch_crc32c:crc32c(2900412422, [Before, <<0:32>>, After]) of CRC32C -> parse_tlv(Rest, Len, Info, Header); _ -> {error, 'Failed CRC32C verification in PROXY protocol binary header. (PP 2.2)'} end; %% PP2_TYPE_NOOP. parse_tlv(<<16#4, TLVLen:16, _:TLVLen/binary, Rest/bits>>, Len, Info, Header) -> parse_tlv(Rest, Len - TLVLen - 3, Info, Header); %% PP2_TYPE_SSL. parse_tlv(<<16#20, TLVLen:16, Client, Verify:32, Rest0/bits>>, Len, Info, Header) -> SubsLen = TLVLen - 5, case Rest0 of <> -> SSL0 = #{ client => parse_client(<>), verified => Verify =:= 0 }, case parse_ssl_tlv(Subs, SubsLen, SSL0) of {ok, SSL, <<>>} -> parse_tlv(Rest, Len - TLVLen - 3, Info#{ssl => SSL}, Header); Error={error, _} -> Error end; _ -> {error, 'Invalid TLV length in the PROXY protocol binary header. (PP 2.2)'} end; %% PP2_TYPE_NETNS. parse_tlv(<<16#30, TLVLen:16, NetNS:TLVLen/binary, Rest/bits>>, Len, Info, Header) -> parse_tlv(Rest, Len - TLVLen - 3, Info#{netns => NetNS}, Header); %% Unknown TLV. parse_tlv(<>, Len, Info, Header) -> RawTLVs = maps:get(raw_tlvs, Info, []), parse_tlv(Rest, Len - TLVLen - 3, Info#{raw_tlvs => [{TLVType, TLVValue}|RawTLVs]}, Header); %% Invalid TLV length. parse_tlv(_, _, _, _) -> {error, 'Invalid TLV length in the PROXY protocol binary header. (PP 2.2)'}. parse_client(<<_:5, ClientCertSess:1, ClientCertConn:1, ClientSSL:1>>) -> Client0 = case ClientCertSess of 0 -> []; 1 -> [cert_sess] end, Client1 = case ClientCertConn of 0 -> Client0; 1 -> [cert_conn|Client0] end, case ClientSSL of 0 -> Client1; 1 -> [ssl|Client1] end. parse_ssl_tlv(Rest, 0, Info) -> {ok, Info, Rest}; %% Valid TLVs. parse_ssl_tlv(<>, Len, Info) -> case ssl_subtype(TLVType) of undefined -> {error, 'Invalid TLV subtype for PP2_TYPE_SSL in PROXY protocol binary header. (PP 2.2)'}; Type -> parse_ssl_tlv(Rest, Len - TLVLen - 3, Info#{Type => TLVValue}) end; %% Invalid TLV length. parse_ssl_tlv(_, _, _) -> {error, 'Invalid TLV length in the PROXY protocol binary header. (PP 2.2)'}. ssl_subtype(16#21) -> version; ssl_subtype(16#22) -> cn; ssl_subtype(16#23) -> cipher; ssl_subtype(16#24) -> sig_alg; ssl_subtype(16#25) -> key_alg; ssl_subtype(_) -> undefined. %% Building. -spec header(proxy_info()) -> iodata(). header(ProxyInfo) -> header(ProxyInfo, #{}). -spec header(proxy_info(), build_opts()) -> iodata(). header(#{version := 2, command := local}, _) -> <<"\r\n\r\n\0\r\nQUIT\n", 2:4, 0:28>>; header(#{version := 2, command := proxy, transport_family := Family, transport_protocol := Protocol}, _) when Family =:= undefined; Protocol =:= undefined -> <<"\r\n\r\n\0\r\nQUIT\n", 2:4, 1:4, 0:24>>; header(ProxyInfo=#{version := 2, command := proxy, transport_family := Family, transport_protocol := Protocol}, Opts) -> Addresses = addresses(ProxyInfo), TLVs = tlvs(ProxyInfo, Opts), ExtraLen = case Opts of #{checksum := crc32c} -> 7; _ -> 0 end, Len = iolist_size(Addresses) + iolist_size(TLVs) + ExtraLen, Header = [ <<"\r\n\r\n\0\r\nQUIT\n", 2:4, 1:4>>, <<(family(Family)):4, (protocol(Protocol)):4>>, <>, Addresses, TLVs ], case Opts of #{checksum := crc32c} -> CRC32C = ranch_crc32c:crc32c([Header, <<16#3, 4:16, 0:32>>]), [Header, <<16#3, 4:16, CRC32C:32>>]; _ -> Header end; header(#{version := 1, command := proxy, transport_family := undefined, transport_protocol := undefined}, _) -> <<"PROXY UNKNOWN\r\n">>; header(#{version := 1, command := proxy, transport_family := Family0, transport_protocol := stream, src_address := SrcAddress, src_port := SrcPort, dest_address := DestAddress, dest_port := DestPort}, _) when SrcPort > 0, SrcPort =< 65535, DestPort > 0, DestPort =< 65535 -> [ <<"PROXY ">>, case Family0 of ipv4 when tuple_size(SrcAddress) =:= 4, tuple_size(DestAddress) =:= 4 -> [<<"TCP4 ">>, inet:ntoa(SrcAddress), $\s, inet:ntoa(DestAddress)]; ipv6 when tuple_size(SrcAddress) =:= 8, tuple_size(DestAddress) =:= 8 -> [<<"TCP6 ">>, inet:ntoa(SrcAddress), $\s, inet:ntoa(DestAddress)] end, $\s, integer_to_binary(SrcPort), $\s, integer_to_binary(DestPort), $\r, $\n ]. family(ipv4) -> 1; family(ipv6) -> 2; family(unix) -> 3. protocol(stream) -> 1; protocol(dgram) -> 2. addresses(#{transport_family := ipv4, src_address := {S1, S2, S3, S4}, src_port := SrcPort, dest_address := {D1, D2, D3, D4}, dest_port := DestPort}) when SrcPort > 0, SrcPort =< 65535, DestPort > 0, DestPort =< 65535 -> <>; addresses(#{transport_family := ipv6, src_address := {S1, S2, S3, S4, S5, S6, S7, S8}, src_port := SrcPort, dest_address := {D1, D2, D3, D4, D5, D6, D7, D8}, dest_port := DestPort}) when SrcPort > 0, SrcPort =< 65535, DestPort > 0, DestPort =< 65535 -> << S1:16, S2:16, S3:16, S4:16, S5:16, S6:16, S7:16, S8:16, D1:16, D2:16, D3:16, D4:16, D5:16, D6:16, D7:16, D8:16, SrcPort:16, DestPort:16 >>; addresses(#{transport_family := unix, src_address := SrcAddress, dest_address := DestAddress}) when byte_size(SrcAddress) =< 108, byte_size(DestAddress) =< 108 -> SrcPadding = 8 * (108 - byte_size(SrcAddress)), DestPadding = 8 * (108 - byte_size(DestAddress)), << SrcAddress/binary, 0:SrcPadding, DestAddress/binary, 0:DestPadding >>. tlvs(ProxyInfo, Opts) -> [ binary_tlv(ProxyInfo, alpn, 16#1), binary_tlv(ProxyInfo, authority, 16#2), ssl_tlv(ProxyInfo), binary_tlv(ProxyInfo, netns, 16#30), raw_tlvs(ProxyInfo), noop_tlv(Opts) ]. binary_tlv(Info, Key, Type) -> case Info of #{Key := Bin} -> Len = byte_size(Bin), <>; _ -> <<>> end. noop_tlv(#{padding := Len0}) when Len0 >= 3 -> Len = Len0 - 3, <<16#4, Len:16, 0:Len/unit:8>>; noop_tlv(_) -> <<>>. ssl_tlv(#{ssl := Info=#{client := Client0, verified := Verify0}}) -> Client = client(Client0, 0), Verify = if Verify0 -> 0; not Verify0 -> 1 end, TLVs = [ binary_tlv(Info, version, 16#21), binary_tlv(Info, cn, 16#22), binary_tlv(Info, cipher, 16#23), binary_tlv(Info, sig_alg, 16#24), binary_tlv(Info, key_alg, 16#25) ], Len = iolist_size(TLVs) + 5, [<<16#20, Len:16, Client, Verify:32>>, TLVs]; ssl_tlv(_) -> <<>>. client([], Client) -> Client; client([ssl|Tail], Client) -> client(Tail, Client bor 16#1); client([cert_conn|Tail], Client) -> client(Tail, Client bor 16#2); client([cert_sess|Tail], Client) -> client(Tail, Client bor 16#4). raw_tlvs(Info) -> [begin Len = byte_size(Bin), <> end || {Type, Bin} <- maps:get(raw_tlvs, Info, [])]. -ifdef(TEST). v1_test() -> Test1 = #{ version => 1, command => proxy, transport_family => undefined, transport_protocol => undefined }, {ok, Test1, <<>>} = parse(iolist_to_binary(header(Test1))), Test2 = #{ version => 1, command => proxy, transport_family => ipv4, transport_protocol => stream, src_address => {127, 0, 0, 1}, src_port => 1234, dest_address => {10, 11, 12, 13}, dest_port => 23456 }, {ok, Test2, <<>>} = parse(iolist_to_binary(header(Test2))), Test3 = #{ version => 1, command => proxy, transport_family => ipv6, transport_protocol => stream, src_address => {1, 2, 3, 4, 5, 6, 7, 8}, src_port => 1234, dest_address => {65535, 55555, 2222, 333, 1, 9999, 777, 8}, dest_port => 23456 }, {ok, Test3, <<>>} = parse(iolist_to_binary(header(Test3))), ok. v2_test() -> Test0 = #{ version => 2, command => local }, {ok, Test0, <<>>} = parse(iolist_to_binary(header(Test0))), Test1 = #{ version => 2, command => proxy, transport_family => undefined, transport_protocol => undefined }, {ok, Test1, <<>>} = parse(iolist_to_binary(header(Test1))), Test2 = #{ version => 2, command => proxy, transport_family => ipv4, transport_protocol => stream, src_address => {127, 0, 0, 1}, src_port => 1234, dest_address => {10, 11, 12, 13}, dest_port => 23456 }, {ok, Test2, <<>>} = parse(iolist_to_binary(header(Test2))), Test3 = #{ version => 2, command => proxy, transport_family => ipv6, transport_protocol => stream, src_address => {1, 2, 3, 4, 5, 6, 7, 8}, src_port => 1234, dest_address => {65535, 55555, 2222, 333, 1, 9999, 777, 8}, dest_port => 23456 }, {ok, Test3, <<>>} = parse(iolist_to_binary(header(Test3))), Test4 = #{ version => 2, command => proxy, transport_family => unix, transport_protocol => dgram, src_address => <<"/run/source.sock">>, dest_address => <<"/run/destination.sock">> }, {ok, Test4, <<>>} = parse(iolist_to_binary(header(Test4))), ok. v2_tlvs_test() -> Common = #{ version => 2, command => proxy, transport_family => ipv4, transport_protocol => stream, src_address => {127, 0, 0, 1}, src_port => 1234, dest_address => {10, 11, 12, 13}, dest_port => 23456 }, Test1 = Common#{alpn => <<"h2">>}, {ok, Test1, <<>>} = parse(iolist_to_binary(header(Test1))), Test2 = Common#{authority => <<"internal.example.org">>}, {ok, Test2, <<>>} = parse(iolist_to_binary(header(Test2))), Test3 = Common#{netns => <<"/var/run/netns/example">>}, {ok, Test3, <<>>} = parse(iolist_to_binary(header(Test3))), Test4 = Common#{ssl => #{ client => [ssl, cert_conn, cert_sess], verified => true, version => <<"TLSv1.3">>, cipher => <<"ECDHE-RSA-AES128-GCM-SHA256">>, sig_alg => <<"SHA256">>, key_alg => <<"RSA2048">>, cn => <<"example.com">> }}, {ok, Test4, <<>>} = parse(iolist_to_binary(header(Test4))), %% Note that the raw_tlvs order is not relevant and therefore %% the parser does not reverse the list it builds. Test5In = Common#{raw_tlvs => RawTLVs=[ %% The only custom TLV I am aware of is defined at: %% https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html#proxy-protocol {16#ea, <<16#1, "instance-id">>}, %% This TLV is entirely fictional. {16#ff, <<1, 2, 3, 4, 5, 6, 7, 8, 9, 0>>} ]}, Test5Out = Test5In#{raw_tlvs => lists:reverse(RawTLVs)}, {ok, Test5Out, <<>>} = parse(iolist_to_binary(header(Test5In))), ok. v2_checksum_test() -> Test = #{ version => 2, command => proxy, transport_family => ipv4, transport_protocol => stream, src_address => {127, 0, 0, 1}, src_port => 1234, dest_address => {10, 11, 12, 13}, dest_port => 23456 }, {ok, Test, <<>>} = parse(iolist_to_binary(header(Test, #{checksum => crc32c}))), ok. v2_padding_test() -> Test = #{ version => 2, command => proxy, transport_family => ipv4, transport_protocol => stream, src_address => {127, 0, 0, 1}, src_port => 1234, dest_address => {10, 11, 12, 13}, dest_port => 23456 }, {ok, Test, <<>>} = parse(iolist_to_binary(header(Test, #{padding => 123}))), ok. -endif. %% Helper to convert proxy_info() to ssl:connection_info(). %% %% Because there isn't a lot of fields common to both types %% this only ends up returning the keys protocol, selected_cipher_suite %% and sni_hostname *at most*. -spec to_connection_info(proxy_info()) -> ssl:connection_info(). to_connection_info(ProxyInfo=#{ssl := SSL}) -> ConnInfo0 = case ProxyInfo of #{authority := Authority} -> [{sni_hostname, Authority}]; _ -> [] end, ConnInfo = case SSL of #{cipher := Cipher} -> case ssl:str_to_suite(binary_to_list(Cipher)) of {error, {not_recognized, _}} -> ConnInfo0; CipherInfo -> [{selected_cipher_suite, CipherInfo}|ConnInfo0] end; _ -> ConnInfo0 end, %% https://www.openssl.org/docs/man1.1.1/man3/SSL_get_version.html case SSL of #{version := <<"TLSv1.3">>} -> [{protocol, 'tlsv1.3'}|ConnInfo]; #{version := <<"TLSv1.2">>} -> [{protocol, 'tlsv1.2'}|ConnInfo]; #{version := <<"TLSv1.1">>} -> [{protocol, 'tlsv1.1'}|ConnInfo]; #{version := <<"TLSv1">>} -> [{protocol, tlsv1}|ConnInfo]; #{version := <<"SSLv3">>} -> [{protocol, sslv3}|ConnInfo]; #{version := <<"SSLv2">>} -> [{protocol, sslv2}|ConnInfo]; %% <<"unknown">>, unsupported or missing version. _ -> ConnInfo end; %% No SSL/TLS information available. to_connection_info(_) -> []. -ifdef(TEST). to_connection_info_test() -> Common = #{ version => 2, command => proxy, transport_family => ipv4, transport_protocol => stream, src_address => {127, 0, 0, 1}, src_port => 1234, dest_address => {10, 11, 12, 13}, dest_port => 23456 }, %% Version 1. [] = to_connection_info(#{ version => 1, command => proxy, transport_family => undefined, transport_protocol => undefined }), [] = to_connection_info(Common#{version => 1}), %% Version 2, no ssl data. [] = to_connection_info(#{ version => 2, command => local }), [] = to_connection_info(#{ version => 2, command => proxy, transport_family => undefined, transport_protocol => undefined }), [] = to_connection_info(Common), [] = to_connection_info(#{ version => 2, command => proxy, transport_family => unix, transport_protocol => dgram, src_address => <<"/run/source.sock">>, dest_address => <<"/run/destination.sock">> }), [] = to_connection_info(Common#{netns => <<"/var/run/netns/example">>}), [] = to_connection_info(Common#{raw_tlvs => [ {16#ff, <<1, 2, 3, 4, 5, 6, 7, 8, 9, 0>>} ]}), %% Version 2, with ssl-related data. [] = to_connection_info(Common#{alpn => <<"h2">>}), %% The authority alone is not enough to deduce that this is SNI. [] = to_connection_info(Common#{authority => <<"internal.example.org">>}), [ {protocol, 'tlsv1.3'}, {selected_cipher_suite, #{ cipher := aes_128_gcm, key_exchange := ecdhe_rsa, mac := aead, prf := sha256 }} ] = to_connection_info(Common#{ssl => #{ client => [ssl, cert_conn, cert_sess], verified => true, version => <<"TLSv1.3">>, cipher => <<"ECDHE-RSA-AES128-GCM-SHA256">>, sig_alg => <<"SHA256">>, key_alg => <<"RSA2048">>, cn => <<"example.com">> }}), [ {protocol, 'tlsv1.3'}, {selected_cipher_suite, #{ cipher := aes_128_gcm, key_exchange := ecdhe_rsa, mac := aead, prf := sha256 }}, {sni_hostname, <<"internal.example.org">>} ] = to_connection_info(Common#{authority => <<"internal.example.org">>, ssl => #{ client => [ssl, cert_conn, cert_sess], verified => true, version => <<"TLSv1.3">>, cipher => <<"ECDHE-RSA-AES128-GCM-SHA256">>, sig_alg => <<"SHA256">>, key_alg => <<"RSA2048">>, cn => <<"example.com">> }}), ok. -endif.