diff options
-rw-r--r-- | src/gun.erl | 87 | ||||
-rw-r--r-- | src/gun_http2.erl | 25 | ||||
-rw-r--r-- | src/gun_http3.erl | 1 | ||||
-rw-r--r-- | src/gun_tls.erl | 2 | ||||
-rw-r--r-- | src/gun_tls_proxy.erl | 33 | ||||
-rw-r--r-- | test/gun_SUITE.erl | 328 | ||||
-rw-r--r-- | test/rfc7540_SUITE.erl | 3 |
7 files changed, 461 insertions, 18 deletions
diff --git a/src/gun.erl b/src/gun.erl index 085e47f..e23da3a 100644 --- a/src/gun.erl +++ b/src/gun.erl @@ -332,7 +332,7 @@ keepalive_ref :: undefined | reference(), socket :: undefined | inet:socket() | ssl:sslsocket() | pid(), transport :: module(), - active = true :: boolean(), + active = false :: boolean(), messages :: {atom(), atom(), atom()}, protocol :: module(), protocol_state :: any(), @@ -1331,10 +1331,18 @@ normal_tls_handshake(Socket, State=#state{ EvHandlerState1 = EvHandler:tls_handshake_start(HandshakeEvent, EvHandlerState0), case gun_tls:connect(Socket, TLSOpts, TLSTimeout) of {ok, TLSSocket} -> - %% This call may return {error,closed} when the socket has - %% been closed by the peer. This should be very rare (due to - %% timing) but can happen for example when client certificates - %% were required but not sent or invalid with some servers. + %% When initially connecting we are in passive mode and + %% in that state we expect this call to always succeed. + %% In rare scenarios (suspended Gun process) it may + %% return {error,closed}, but this indicates that the + %% socket process is gone and we cannot retrieve a potential + %% TLS alert. + %% + %% When using HTTP/1.1 CONNECT we are also in passive mode + %% because CONNECT involves a response that is received via + %% active mode, which automatically goes into passive mode + %% ({active,once}), and we only reenable active mode after + %% processing commands. case ssl:negotiated_protocol(TLSSocket) of {error, Reason = closed} -> EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ @@ -1368,7 +1376,8 @@ connected_protocol_init(internal, {connected, Retries, Socket, NewProtocol}, {ok, StateName, ProtoState} -> %% @todo Don't send gun_up and gun_down if active/1 fails here. reply(Owner, {gun_up, self(), Protocol:name()}), - State1 = State0#state{socket=Socket, protocol=Protocol, protocol_state=ProtoState}, + State1 = State0#state{socket=Socket, protocol=Protocol, + protocol_state=ProtoState, active=true}, case active(State1) of {ok, State2} -> State = case Protocol:has_keepalive() of @@ -1899,7 +1908,8 @@ commands([TLSHandshake={tls_handshake, _, _, _}], State) -> disconnect(State0=#state{owner=Owner, status=Status, opts=Opts, intermediaries=Intermediaries, socket=Socket, transport=Transport0, protocol=Protocol, protocol_state=ProtoState, - event_handler=EvHandler, event_handler_state=EvHandlerState0}, Reason) -> + event_handler=EvHandler, event_handler_state=EvHandlerState0}, Reason0) -> + Reason = maybe_tls_alert(State0, Reason0), EvHandlerState1 = Protocol:close(Reason, ProtoState, EvHandler, EvHandlerState0), _ = Transport0:close(Socket), EvHandlerState = EvHandler:disconnect(#{reason => Reason}, EvHandlerState1), @@ -1913,6 +1923,9 @@ disconnect(State0=#state{owner=Owner, status=Status, opts=Opts, %% We closed the socket, discard any remaining socket events. disconnect_flush(State1), KilledStreams = Protocol:down(ProtoState), + %% @todo Reason here may be {error, Reason1} which leads to + %% different behavior compared to down messages received + %% from failing to connect where Reason1 is what gets sent. reply(Owner, {gun_down, self(), Protocol:name(), Reason, KilledStreams}), Retry = maps:get(retry, Opts, 5), State2 = keepalive_cancel(State1#state{ @@ -1935,6 +1948,66 @@ disconnect(State0=#state{owner=Owner, status=Status, opts=Opts, {next_event, internal, {retries, Retry, Reason}}} end. +%% With TLS 1.3 the handshake may not have validated the certificate +%% by the time it completes. The validation may therefore fail at any +%% time afterwards. TLS 1.3 also introduced post-handshake authentication +%% which would produce the same results. Erlang/OTP's ssl has a number +%% of asynchronous functions which won't return the alert as an error +%% and instead return a plain {error,closed}, including ssl:send. +%% Gun must therefore check whether a close is resulting from a TLS alert +%% and use that alert as a more descriptive disconnect reason. +%% +%% Sometimes, ssl:send will return {error,einval}, because while the +%% TLS pseudo-socket still exists, the underlying TCP socket is already +%% gone. In that case we can still query the TLS pseudo-socket to get +%% the detailed TLS alert. +%% +%% @todo We currently do not support retrieving the alert from a gun_tls_proxy +%% socket. We need a test case to best understand what should be done there. +%% But since the socket belongs to that process we likely need additional +%% changes there to make it work. +maybe_tls_alert(#state{socket=Socket, transport=gun_tls, + active=true, messages={_, _, Error}}, Reason0) + %% The unwrapped tuple we get half the time makes this clause more complex. + when Reason0 =:= {error, closed}; Reason0 =:= {error, einval}; Reason0 =:= closed -> + %% When active mode is enabled we should have the alert in our + %% mailbox so we can just retrieve it. In case it is late we + %% use a short timeout to increase the chances of catching it. + receive + {Error, Socket, Reason} -> + Reason + after 200 -> + Reason0 + end; +maybe_tls_alert(#state{socket=Socket, transport=Transport=gun_tls, + active=false}, Reason0) + when Reason0 =:= {error, closed}; Reason0 =:= {error, einval}; Reason0 =:= closed -> + %% When active mode is disabled we can do a number of operations to + %% receive the alert. Enabling active mode is one of them. + case Transport:setopts(Socket, [{active, once}]) of + {error, Reason={tls_alert, _}} -> + Reason; + _ -> + Reason0 + end; +%% We unwrap the TLS alert error for consistency. +%% @todo Consistenly wrap/unwrap all errors instead of just this one. +maybe_tls_alert(_, {error, Reason={tls_alert, _}}) -> + Reason; +%% We may also need to receive the alert when proxying TLS. +maybe_tls_alert(#state{socket=Socket, transport=gun_tls_proxy, + active=true, messages={_, _, Error}}, Reason0) + %% The unwrapped tuple we get half the time makes this clause more complex. + when Reason0 =:= {error, closed}; Reason0 =:= {error, einval}; Reason0 =:= closed -> + receive + {Error, Socket, Reason} -> + Reason + after 200 -> + Reason0 + end; +maybe_tls_alert(_, Reason) -> + Reason. + disconnect_flush(State=#state{socket=Socket, messages={OK, Closed, Error}}) -> receive {OK, Socket, _} -> disconnect_flush(State); diff --git a/src/gun_http2.erl b/src/gun_http2.erl index b39be6e..0feb40e 100644 --- a/src/gun_http2.erl +++ b/src/gun_http2.erl @@ -206,6 +206,16 @@ init(ReplyTo, Socket, Transport, Opts0) -> {ok, connected, #http2_state{reply_to=ReplyTo, socket=Socket, transport=Transport, opts=Opts, base_stream_ref=BaseStreamRef, tunnel_transport=TunnelTransport, content_handlers=Handlers, http2_machine=HTTP2Machine}}; + Error0={error, R} when R =:= closed; R =:= einval -> + %% Check whether we have a TLS alert and in that case, + %% return it. We must do this here because Protocol:init + %% failure doesn't go through disconnect. + case Transport:setopts(Socket, [{active, once}]) of + Error={error, {tls_alert, _}} -> + Error; + _ -> + Error0 + end; Error={error, _Reason} -> Error end. @@ -488,8 +498,20 @@ tunnel_commands([{state, ProtoState}|Tail], Stream=#stream{tunnel=Tunnel}, State, EvHandler, EvHandlerState) -> tunnel_commands(Tail, Stream#stream{tunnel=Tunnel#tunnel{protocol_state=ProtoState}}, State, EvHandler, EvHandlerState); -tunnel_commands([{error, Reason}|_], #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo}, +tunnel_commands([{error, Reason0}|_], #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo}, State, _EvHandler, EvHandlerState) -> + %% See gun:maybe_tls_alert for details. + Reason = case Reason0 of + closed -> + receive + {handle_continue, StreamRef, {tls_proxy_error, _Socket, Reason1}} -> + Reason1 + after 200 -> + Reason0 + end; + _ -> + Reason0 + end, gun:reply(ReplyTo, {gun_error, self(), stream_ref(State, StreamRef), {stream_error, Reason, 'Tunnel closed unexpectedly.'}}), {{state, delete_stream(State, StreamID)}, EvHandlerState}; @@ -948,6 +970,7 @@ close(Reason0, State=#http2_state{streams=Streams}, _, EvHandlerState) -> end, [], Streams), EvHandlerState. +%% @todo This can get {error,closed} leading to {closed,{error,closed}}. close_reason(closed) -> closed; close_reason(Reason) -> {closed, Reason}. diff --git a/src/gun_http3.erl b/src/gun_http3.erl index 38d8970..26aa072 100644 --- a/src/gun_http3.erl +++ b/src/gun_http3.erl @@ -104,6 +104,7 @@ default_keepalive() -> infinity. init(ReplyTo, Conn, Transport, Opts) -> Handlers = maps:get(content_handlers, Opts, [gun_data_h]), {ok, SettingsBin, HTTP3Machine0} = cow_http3_machine:init(client, Opts), + %% @todo We may get a TLS 1.3 error/alert here in mTLS scenarios. {ok, ControlID} = Transport:start_unidi_stream(Conn, [<<0>>, SettingsBin]), {ok, EncoderID} = Transport:start_unidi_stream(Conn, [<<2>>]), {ok, DecoderID} = Transport:start_unidi_stream(Conn, [<<3>>]), diff --git a/src/gun_tls.erl b/src/gun_tls.erl index cf1bcfd..ef2d800 100644 --- a/src/gun_tls.erl +++ b/src/gun_tls.erl @@ -35,7 +35,7 @@ connect(Socket, Opts, Timeout) -> send(Socket, Packet) -> ssl:send(Socket, Packet). --spec setopts(ssl:sslsocket(), list()) -> ok | {error, atom()}. +-spec setopts(ssl:sslsocket(), list()) -> ok | {error, any()}. setopts(Socket, Opts) -> ssl:setopts(Socket, Opts). diff --git a/src/gun_tls_proxy.erl b/src/gun_tls_proxy.erl index 024c7a7..f3719b6 100644 --- a/src/gun_tls_proxy.erl +++ b/src/gun_tls_proxy.erl @@ -131,6 +131,8 @@ cb_send(Pid, Data) -> try gen_statem:call(Pid, {?FUNCTION_NAME, Data}) catch + exit:{{shutdown, close}, _} -> + {error, closed}; exit:{noproc, _} -> {error, closed} end. @@ -140,6 +142,8 @@ cb_setopts(Pid, Opts) -> try gen_statem:call(Pid, {?FUNCTION_NAME, Opts}) catch + exit:{{shutdown, close}, _} -> + {error, closed}; exit:{noproc, _} -> {error, einval} end. @@ -178,6 +182,9 @@ sockname(Pid) -> close(Pid) -> ?DEBUG_LOG("pid ~0p", [Pid]), try + %% We must unlink before closing otherwise the closing + %% will take down the Gun process with it. + unlink(Pid), gen_statem:call(Pid, ?FUNCTION_NAME) catch %% May happen for example when the handshake fails. @@ -236,11 +243,13 @@ not_connected(cast, Msg={setopts, _}, State) -> {keep_state_and_data, postpone}; not_connected(cast, Msg={connect_proc, {ok, Socket}}, State=#state{owner_pid=OwnerPid, extra=Extra}) -> ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), - OwnerPid ! {?MODULE, self(), {ok, ssl:negotiated_protocol(Socket)}, Extra}, - %% We need to spawn this call before OTP-21.2 because it triggers - %% a cb_setopts call that blocks us. Might be OK to just leave it - %% like this once we support 21.2+ only. - spawn(fun() -> ok = ssl:setopts(Socket, [{active, true}]) end), + Negotiated = ssl:negotiated_protocol(Socket), + _ = case ssl:setopts(Socket, [{active, true}]) of + ok -> + OwnerPid ! {?MODULE, self(), {ok, Negotiated}, Extra}; + Error -> + OwnerPid ! {?MODULE, self(), Error, Extra} + end, {next_state, connected, State#state{proxy_socket=Socket}}; not_connected(cast, Msg={connect_proc, Error}, State=#state{owner_pid=OwnerPid, extra=Extra}) -> ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), @@ -332,8 +341,20 @@ handle_common(cast, Msg={cb_controlling_process, ProxyPid}, State) -> handle_common(cast, Msg={setopts, Opts}, State) -> ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), {keep_state, owner_setopts(Opts, State)}; -handle_common(cast, Msg={send_result, From, Result}, State) -> +handle_common(cast, Msg={send_result, From, Result0}, State=#state{proxy_socket=Socket}) -> ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + %% See gun:maybe_tls_alert for details. + Result = case Result0 of + {error, closed} -> + receive + {ssl_error, Socket, Reason} -> + {error, Reason} + after 200 -> + Result0 + end; + _ -> + Result0 + end, gen_statem:reply(From, Result), keep_state_and_data; %% Messages from the real socket. diff --git a/test/gun_SUITE.erl b/test/gun_SUITE.erl index 766b1e7..7c0440b 100644 --- a/test/gun_SUITE.erl +++ b/test/gun_SUITE.erl @@ -29,10 +29,31 @@ suite() -> [{timetrap, 30000}]. all() -> - [{group, gun}]. + [ + {group, tls13_post_handshake_alert}, + {group, gun}, + {group, tls13_post_handshake_alert} + ]. groups() -> - [{gun, [parallel], ct_helper:all(?MODULE)}]. + [ + {gun, [parallel], ct_helper:all(?MODULE)}, + %% We run these tests in parallel in 'gun' as well as + %% sequentially to have more chances of hitting different + %% parts of the code. We also run them before/after 'gun'. + {tls13_post_handshake_alert, [], [ + tls13_post_handshake_alert_http1, + tls13_post_handshake_alert_http2, + tls13_post_handshake_alert_http1_via_http, + tls13_post_handshake_alert_http2_via_http, + tls13_post_handshake_alert_http1_via_https, + tls13_post_handshake_alert_http2_via_https, + tls13_post_handshake_alert_http1_via_h2c, + tls13_post_handshake_alert_http2_via_h2c, + tls13_post_handshake_alert_http1_via_h2, + tls13_post_handshake_alert_http2_via_h2 + ]} + ]. %% Tests. @@ -633,6 +654,309 @@ supervise_false(_) -> [] = [P || {_, P, _, _} <- supervisor:which_children(gun_sup), P =:= Pid], ok. +tls13_post_handshake_alert_http1(_) -> + case {os:type(), erlang:function_exported(lists, enumerate, 3)} of + {{unix, linux}, true} -> + do_tls13_post_handshake_alert_http1(); + {{unix, linux}, false} -> + {skip, "Handling of TLS 1.3 alerts was improved in an OTP-26 patch release."}; + _ -> + {skip, "This test is only enabled on Linux to avoid intermittent failures."} + end. + +do_tls13_post_handshake_alert_http1() -> + doc("Ensure that a TLS 1.3 post-handshake alert is properly " + "propagated when using HTTP/1.1 in mTLS scenarios."), + TestOpts = ct_helper:get_certs_from_ets(), + {ok, ListenSocket} = ssl:listen(0, [binary, + {log_level, none}, + {versions, ['tlsv1.3']}, + %% The alert will be triggered by the missing certificate. + {verify, verify_peer}, + {fail_if_no_peer_cert, true}, + %% We only use the certs, not other options, + %% as we require TLS 1.3 and want a specific behavior. + {cacerts, proplists:get_value(cacerts, TestOpts)}, + {cert, proplists:get_value(cert, TestOpts)}, + {key, proplists:get_value(key, TestOpts)} + ]), + {ok, {_, OriginPort}} = ssl:sockname(ListenSocket), + _ = spawn_link(fun() -> + {ok, ClientSocket} = ssl:transport_accept(ListenSocket, 5000), + {error, {tls_alert, _}} = ssl:handshake(ClientSocket, 5000), + receive after infinity -> ok end + end), + {ok, ConnPid} = gun:open("localhost", OriginPort, #{ + retry => 0, + transport => tls, + tls_opts => [ + {log_level, none}, + {verify, verify_none} + ] + }), + %% We want to send data immediately as soon as the connection is up. + %% So we do it before we receive the gun_up message. + StreamRef = gun:post(ConnPid, "/", [{<<"content-type">>, <<"application/octet-stream">>}]), + %% Yes, this was written on April 1st. + _ = [begin + gun:data(ConnPid, StreamRef, nofin, <<"April fools!">>) + end || _ <- lists:seq(1, 100)], + %% The alert will occur after the gun_up message has been sent + %% in the case of HTTP/1.1 (when active mode gets enabled). + {ok, http} = gun:await_up(ConnPid), + {error, {down, {shutdown, + {tls_alert, {certificate_required, _}}}}} = gun:await(ConnPid, undefined), + gun:close(ConnPid). + +tls13_post_handshake_alert_http2(_) -> + case {os:type(), erlang:function_exported(lists, enumerate, 3)} of + {{unix, linux}, true} -> + do_tls13_post_handshake_alert_http2(); + {{unix, linux}, false} -> + {skip, "Handling of TLS 1.3 alerts was improved in an OTP-26 patch release."}; + _ -> + {skip, "This test is only enabled on Linux to avoid intermittent failures."} + end. + +do_tls13_post_handshake_alert_http2() -> + doc("Ensure that a TLS 1.3 post-handshake alert is properly " + "propagated when using HTTP/2 in mTLS scenarios."), + TestOpts = ct_helper:get_certs_from_ets(), + {ok, ListenSocket} = ssl:listen(0, [binary, + {log_level, none}, + {versions, ['tlsv1.3']}, + %% Enable HTTP/2. + {alpn_preferred_protocols, [<<"h2">>]}, + %% The alert will be triggered by the missing certificate. + {verify, verify_peer}, + {fail_if_no_peer_cert, true}, + %% We only use the certs, not other options, + %% as we require TLS 1.3 and want a specific behavior. + {cacerts, proplists:get_value(cacerts, TestOpts)}, + {cert, proplists:get_value(cert, TestOpts)}, + {key, proplists:get_value(key, TestOpts)} + ]), + {ok, {_, OriginPort}} = ssl:sockname(ListenSocket), + _ = spawn_link(fun() -> + {ok, ClientSocket} = ssl:transport_accept(ListenSocket, 5000), + {error, {tls_alert, _}} = ssl:handshake(ClientSocket, 5000), + receive after infinity -> ok end + end), + {ok, ConnPid} = gun:open("localhost", OriginPort, #{ + retry => 0, + transport => tls, + tls_opts => [ + {log_level, none}, + {verify, verify_none} + ] + }), + case gun:await_up(ConnPid) of + {error, {down, {shutdown, {tls_alert, {certificate_required, _}}}}} -> + ct:log("TLS alert received in gun:await_up"); + {ok, http2} -> + {error, {down, {shutdown, + {tls_alert, {certificate_required, _}}}}} = gun:await(ConnPid, undefined), + ct:log("TLS alert received after gun:await_up") + end, + gun:close(ConnPid). + +tls13_post_handshake_alert_http1_via_http(_) -> + doc("Ensure that a TLS 1.3 post-handshake alert is properly " + "propagated when connecting to an HTTP/1.1 server " + "over an HTTP/1.1 clear-text tunnel in mTLS scenarios."), + case {os:type(), erlang:function_exported(lists, enumerate, 3)} of + {{unix, linux}, true} -> + do_tls13_post_handshake_alert_via_tunnel(http, http); + {{unix, linux}, false} -> + {skip, "Handling of TLS 1.3 alerts was improved in an OTP-26 patch release."}; + _ -> + {skip, "This test is only enabled on Linux to avoid intermittent failures."} + end. + +tls13_post_handshake_alert_http2_via_http(_) -> + doc("Ensure that a TLS 1.3 post-handshake alert is properly " + "propagated when connecting to an HTTP/2 server " + "over an HTTP/1.1 clear-text tunnel in mTLS scenarios."), + case {os:type(), erlang:function_exported(lists, enumerate, 3)} of + {{unix, linux}, true} -> + do_tls13_post_handshake_alert_via_tunnel(http, http2); + {{unix, linux}, false} -> + {skip, "Handling of TLS 1.3 alerts was improved in an OTP-26 patch release."}; + _ -> + {skip, "This test is only enabled on Linux to avoid intermittent failures."} + end. + +tls13_post_handshake_alert_http1_via_https(_) -> + doc("Ensure that a TLS 1.3 post-handshake alert is properly " + "propagated when connecting to an HTTP/1.1 server " + "over an HTTP/1.1 TLS tunnel in mTLS scenarios."), + case {os:type(), erlang:function_exported(lists, enumerate, 3)} of + {{unix, linux}, true} -> + do_tls13_post_handshake_alert_via_tunnel(https, http); + {{unix, linux}, false} -> + {skip, "Handling of TLS 1.3 alerts was improved in an OTP-26 patch release."}; + _ -> + {skip, "This test is only enabled on Linux to avoid intermittent failures."} + end. + +tls13_post_handshake_alert_http2_via_https(_) -> + doc("Ensure that a TLS 1.3 post-handshake alert is properly " + "propagated when connecting to an HTTP/2 server " + "over an HTTP/1.1 TLS tunnel in mTLS scenarios."), + case {os:type(), erlang:function_exported(lists, enumerate, 3)} of + {{unix, linux}, true} -> + do_tls13_post_handshake_alert_via_tunnel(https, http2); + {{unix, linux}, false} -> + {skip, "Handling of TLS 1.3 alerts was improved in an OTP-26 patch release."}; + _ -> + {skip, "This test is only enabled on Linux to avoid intermittent failures."} + end. + +tls13_post_handshake_alert_http1_via_h2c(_) -> + doc("Ensure that a TLS 1.3 post-handshake alert is properly " + "propagated when connecting to an HTTP/1.1 server " + "over an HTTP/2 clear-text tunnel in mTLS scenarios."), + case {os:type(), erlang:function_exported(lists, enumerate, 3)} of + {{unix, linux}, true} -> + do_tls13_post_handshake_alert_via_tunnel(h2c, http); + {{unix, linux}, false} -> + {skip, "Handling of TLS 1.3 alerts was improved in an OTP-26 patch release."}; + _ -> + {skip, "This test is only enabled on Linux to avoid intermittent failures."} + end. + +tls13_post_handshake_alert_http2_via_h2c(_) -> + doc("Ensure that a TLS 1.3 post-handshake alert is properly " + "propagated when connecting to an HTTP/2 server " + "over an HTTP/2 clear-text tunnel in mTLS scenarios."), + case {os:type(), erlang:function_exported(lists, enumerate, 3)} of + {{unix, linux}, true} -> + do_tls13_post_handshake_alert_via_tunnel(h2c, http2); + {{unix, linux}, false} -> + {skip, "Handling of TLS 1.3 alerts was improved in an OTP-26 patch release."}; + _ -> + {skip, "This test is only enabled on Linux to avoid intermittent failures."} + end. + +tls13_post_handshake_alert_http1_via_h2(_) -> + doc("Ensure that a TLS 1.3 post-handshake alert is properly " + "propagated when connecting to an HTTP/1.1 server " + "over an HTTP/2 TLS tunnel in mTLS scenarios."), + case {os:type(), erlang:function_exported(lists, enumerate, 3)} of + {{unix, linux}, true} -> + do_tls13_post_handshake_alert_via_tunnel(h2, http); + {{unix, linux}, false} -> + {skip, "Handling of TLS 1.3 alerts was improved in an OTP-26 patch release."}; + _ -> + {skip, "This test is only enabled on Linux to avoid intermittent failures."} + end. + +tls13_post_handshake_alert_http2_via_h2(_) -> + doc("Ensure that a TLS 1.3 post-handshake alert is properly " + "propagated when connecting to an HTTP/2 server " + "over an HTTP/2 TLS tunnel in mTLS scenarios."), + case {os:type(), erlang:function_exported(lists, enumerate, 3)} of + {{unix, linux}, true} -> + do_tls13_post_handshake_alert_via_tunnel(h2, http2); + {{unix, linux}, false} -> + {skip, "Handling of TLS 1.3 alerts was improved in an OTP-26 patch release."}; + _ -> + {skip, "This test is only enabled on Linux to avoid intermittent failures."} + end. + +do_tls13_post_handshake_alert_via_tunnel(ProxyType, OriginProtocol) -> + TestOpts = ct_helper:get_certs_from_ets(), + ExtraOpts = case OriginProtocol of + http -> []; + http2 -> [{alpn_preferred_protocols, [<<"h2">>]}] + end, + {ok, ListenSocket} = ssl:listen(0, [binary, + {log_level, none}, + {versions, ['tlsv1.3']}, + %% The alert will be triggered by the missing certificate. + {verify, verify_peer}, + {fail_if_no_peer_cert, true}, + %% We only use the certs, not other options, + %% as we require TLS 1.3 and want a specific behavior. + {cacerts, proplists:get_value(cacerts, TestOpts)}, + {cert, proplists:get_value(cert, TestOpts)}, + {key, proplists:get_value(key, TestOpts)} + |ExtraOpts]), + {ok, {_, OriginPort}} = ssl:sockname(ListenSocket), + _ = spawn_link(fun() -> + {ok, ClientSocket} = ssl:transport_accept(ListenSocket, 5000), + {error, {tls_alert, _}} = ssl:handshake(ClientSocket, 5000), + receive after infinity -> ok end + end), + {ok, ProxyPid, ProxyPort} = tunnel_SUITE:do_proxy_start(ProxyType), + {ProxyTransport, ProxyProtocol} = tunnel_SUITE:do_type(ProxyType), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + retry => 0, + transport => ProxyTransport, + tls_opts => [{verify, verify_none}], + protocols => [ProxyProtocol] + %,trace => true + }), + {ok, ProxyProtocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(ProxyProtocol, ProxyPid), + TunnelStreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => tls, + tls_opts => [ + {log_level, none}, + {verify, verify_none} + ] + }), + {response, fin, 200, _} = gun:await(ConnPid, TunnelStreamRef), + %% When going through an HTTP/2 tunnel we must wait for the protocol + %% to be up before sending data. + Continue = case ProxyProtocol of + http2 -> + case gun:await(ConnPid, TunnelStreamRef) of + {up, OriginProtocol} -> + ok; + {error, {stream_error, {stream_error, + {tls_alert, {certificate_required, _}}, _}}} -> + stop + end; + http -> + ok + end, + case Continue of + stop -> + ok; + ok -> + %% Otherwise we want to send data immediately as soon as the connection + %% is up. So we do it before we receive the gun_up message. + StreamRef = gun:post(ConnPid, "/", [{<<"content-type">>, <<"application/octet-stream">>}], + #{tunnel => TunnelStreamRef}), + %% Yes, this was written on April 1st. + _ = [begin + gun:data(ConnPid, StreamRef, nofin, <<"April fools!">>) + end || _ <- lists:seq(1, 100)], + _ = case ProxyProtocol of + http2 -> + %% We already received the gun_tunnel_up at this point for HTTP/2. + {error, {stream_error, {stream_error, + {tls_alert, {certificate_required, _}}, _}}} = gun:await(ConnPid, TunnelStreamRef); + http -> + %% The alert will occur after the gun_up message has been sent + %% in the case of HTTP/1.1 (when active mode gets enabled). + %% But when we are using TLS over TLS the timing is a little + %% different and we may get a failure before receiving gun_tunnel_up. + case gun:await(ConnPid, TunnelStreamRef) of + {up, OriginProtocol} -> + {error, {down, {shutdown, + {tls_alert, {certificate_required, _}}}}} = gun:await(ConnPid, undefined); + {error, {stream_error, {closed, + {tls_alert, {certificate_required, _}}}}} -> + ok + end + end + end, + gun:close(ConnPid). + tls_handshake_error_gun_http2_init_retry_0(_) -> doc("Ensure an early TLS connection close is propagated " "to the user of the connection."), diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl index 847449e..2293d6d 100644 --- a/test/rfc7540_SUITE.erl +++ b/test/rfc7540_SUITE.erl @@ -110,7 +110,8 @@ do_proxy_parse(<<Len:24, 0:8, _:8, StreamID:32, Payload:Len/binary, Rest/bits>>, ok -> do_proxy_parse(Rest, Proxy); {error, _} -> - ok + %% Wait forever when a connection gets closed. We will exit with the test process. + timer:sleep(infinity) end; do_proxy_parse(<<Len:24, 1:8, _:8, StreamID:32, ReqHeadersBlock:Len/binary, Rest/bits>>, Proxy=#proxy{parent=Parent, socket=Socket, transport=Transport, |