1008 lines
31 KiB
Erlang
1008 lines
31 KiB
Erlang
|
%% Copyright (c) 2018-2021, Loïc Hoguin <essen@ninenines.eu>
|
||
|
%%
|
||
|
%% 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(<<Addr:7/binary, $\s, Rest/binary>>, ipv4) -> parse_ipv4(Addr, Rest);
|
||
|
parse_ip(<<Addr:8/binary, $\s, Rest/binary>>, ipv4) -> parse_ipv4(Addr, Rest);
|
||
|
parse_ip(<<Addr:9/binary, $\s, Rest/binary>>, ipv4) -> parse_ipv4(Addr, Rest);
|
||
|
parse_ip(<<Addr:10/binary, $\s, Rest/binary>>, ipv4) -> parse_ipv4(Addr, Rest);
|
||
|
parse_ip(<<Addr:11/binary, $\s, Rest/binary>>, ipv4) -> parse_ipv4(Addr, Rest);
|
||
|
parse_ip(<<Addr:12/binary, $\s, Rest/binary>>, ipv4) -> parse_ipv4(Addr, Rest);
|
||
|
parse_ip(<<Addr:13/binary, $\s, Rest/binary>>, ipv4) -> parse_ipv4(Addr, Rest);
|
||
|
parse_ip(<<Addr:14/binary, $\s, Rest/binary>>, ipv4) -> parse_ipv4(Addr, Rest);
|
||
|
parse_ip(<<Addr:15/binary, $\s, Rest/binary>>, 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(<<Port:1/binary, C, Rest/bits>>, C) -> parse_port(Port, Rest);
|
||
|
parse_port(<<Port:2/binary, C, Rest/bits>>, C) -> parse_port(Port, Rest);
|
||
|
parse_port(<<Port:3/binary, C, Rest/bits>>, C) -> parse_port(Port, Rest);
|
||
|
parse_port(<<Port:4/binary, C, Rest/bits>>, C) -> parse_port(Port, Rest);
|
||
|
parse_port(<<Port:5/binary, C, Rest/bits>>, 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
|
||
|
<<Header:Len/binary, _/bits>> ->
|
||
|
parse_v2(Rest, Len, parse_family(Family), parse_protocol(Protocol),
|
||
|
<<Family:4, Protocol:4, Len:16, Header:Len/binary>>);
|
||
|
_ ->
|
||
|
{error, 'Missing data in the PROXY protocol binary header. (PP 2.2)'}
|
||
|
end;
|
||
|
%% Errors.
|
||
|
parse_v2(<<Version:4, _/bits>>) 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(<<SrcAddr0:108/binary, DestAddr0:108/binary, Rest/bits>>,
|
||
|
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,
|
||
|
<<Before:BeforeLen/binary, _:32, After:Len/binary>> = 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
|
||
|
<<Subs:SubsLen/binary, Rest/bits>> ->
|
||
|
SSL0 = #{
|
||
|
client => parse_client(<<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(<<TLVType, TLVLen:16, TLVValue:TLVLen/binary, Rest/bits>>, 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(<<TLVType, TLVLen:16, TLVValue:TLVLen/binary, Rest/bits>>, 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>>,
|
||
|
<<Len:16>>,
|
||
|
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 ->
|
||
|
<<S1, S2, S3, S4, D1, D2, D3, D4, SrcPort:16, DestPort:16>>;
|
||
|
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),
|
||
|
<<Type, Len:16, Bin/binary>>;
|
||
|
_ ->
|
||
|
<<>>
|
||
|
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),
|
||
|
<<Type, Len:16, Bin/binary>>
|
||
|
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.
|