diff options
Diffstat (limited to 'src')
-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 |
5 files changed, 133 insertions, 15 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. |