aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/gun.erl87
-rw-r--r--src/gun_http2.erl25
-rw-r--r--src/gun_http3.erl1
-rw-r--r--src/gun_tls.erl2
-rw-r--r--src/gun_tls_proxy.erl33
-rw-r--r--test/gun_SUITE.erl328
-rw-r--r--test/rfc7540_SUITE.erl3
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,