Skip to content

Commit 6009ad8

Browse files
committed
ssl: Buffer unsent encrypted data on send timeout
When using gen_tcp with {inet_backend, socket} and send_timeout, gen_tcp:send may return {error, {timeout, RestData}} with the unsent encrypted data. Previously this fell through to the generic error handler which killed the connection. Buffer the RestData in a new #sync{} record in the tls_sender state and retry sending it together with new data on the next ssl:send call. Reply {error, timeout} to the caller to simulate {inet_backend, inet} behavior. When alerts, post-handshake data or renegotiation need to send while a #sync{} buffer exists, attempt to flush the buffer first. If the flush succeeds, proceed normally. If it times out again, postpone the event and re-buffer the remaining data. Signed-off-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
1 parent f65d56b commit 6009ad8

2 files changed

Lines changed: 176 additions & 13 deletions

File tree

lib/ssl/src/tls_sender.erl

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@
9191
low = undefined
9292
}).
9393

94+
%% Buffer for unsent encrypted data returned by gen_tcp:send
95+
%% as {error, {timeout, RestData}} when using {inet_backend, socket}
96+
-record(sync,
97+
{
98+
q_rev = [] %% Remaining encrypted data (iodata)
99+
}).
100+
94101
-define(IS_ASYNC(Tag), Tag =:= select; Tag =:= completion).
95102

96103
%%%===================================================================
@@ -285,24 +292,49 @@ connection({call, From}, {post_handshake_data, HSData}, #data{buff = Buff} = Sta
285292
case Buff of
286293
undefined ->
287294
send_post_handshake_data(HSData, From, connection, StateData, [{reply, From, ok}]);
288-
Async ->
289-
{next_state, async_wait, StateData#data{buff = Async#async{low = 0}}, [postpone]}
295+
#async{} = Async ->
296+
{next_state, async_wait, StateData#data{buff = Async#async{low = 0}}, [postpone]};
297+
#sync{} ->
298+
StateData1 = flush_sync_buffer(StateData),
299+
case StateData1#data.buff of
300+
undefined ->
301+
send_post_handshake_data(HSData, From, connection, StateData1, [{reply, From, ok}]);
302+
#sync{} ->
303+
{keep_state, StateData1, [postpone]}
304+
end
290305
end;
291306
connection({call, From}, {ack_alert, #alert{} = Alert}, #data{buff = Buff} = StateData0) ->
292307
case Buff of
293308
undefined ->
294309
StateData = send_tls_alert(Alert, StateData0),
295310
{next_state, connection, StateData, [{reply,From,ok}]};
296-
Async ->
297-
{next_state, async_wait, StateData0#data{buff = Async#async{low = 0}}, [postpone]}
311+
#async{} = Async ->
312+
{next_state, async_wait, StateData0#data{buff = Async#async{low = 0}}, [postpone]};
313+
#sync{} ->
314+
StateData1 = flush_sync_buffer(StateData0),
315+
case StateData1#data.buff of
316+
undefined ->
317+
StateData = send_tls_alert(Alert, StateData1),
318+
{next_state, connection, StateData, [{reply,From,ok}]};
319+
#sync{} ->
320+
{keep_state, StateData1, [postpone]}
321+
end
298322
end;
299323
connection({call, From}, renegotiate,
300324
#data{connection_states = #{current_write := Write}, buff = Buff} = StateData) ->
301325
case Buff of
302326
undefined ->
303327
{next_state, handshake, StateData, [{reply, From, {ok, Write}}]};
304-
Async ->
305-
{next_state, async_wait, StateData#data{buff = Async#async{low = 0}}, [postpone]}
328+
#async{} = Async ->
329+
{next_state, async_wait, StateData#data{buff = Async#async{low = 0}}, [postpone]};
330+
#sync{} ->
331+
StateData1 = flush_sync_buffer(StateData),
332+
case StateData1#data.buff of
333+
undefined ->
334+
{next_state, handshake, StateData1, [{reply, From, {ok, Write}}]};
335+
#sync{} ->
336+
{keep_state, StateData1, [postpone]}
337+
end
306338
end;
307339
connection({call, From}, downgrade, #data{connection_states =
308340
#{current_write := Write}} = StateData) ->
@@ -329,17 +361,34 @@ connection(internal, {post_handshake_data, From, HSData}, #data{buff = Buff} = S
329361
case Buff of
330362
undefined ->
331363
send_post_handshake_data(HSData, From, connection, StateData, []);
332-
Async ->
333-
{next_state, async_wait, StateData#data{buff = Async#async{low = 0}}, [postpone]}
364+
#async{} = Async ->
365+
{next_state, async_wait, StateData#data{buff = Async#async{low = 0}}, [postpone]};
366+
#sync{} ->
367+
StateData1 = flush_sync_buffer(StateData),
368+
case StateData1#data.buff of
369+
undefined ->
370+
send_post_handshake_data(HSData, From, connection, StateData1, []);
371+
#sync{} ->
372+
{keep_state, StateData1, [postpone]}
373+
end
334374
end;
335375

336376
connection(cast, #alert{} = Alert, #data{buff = Buff} = StateData0) ->
337377
case Buff of
338378
undefined ->
339379
StateData = send_tls_alert(Alert, StateData0),
340380
{next_state, connection, StateData};
341-
Async ->
342-
{next_state, async_wait, StateData0#data{buff = Async#async{low = 0}}, [postpone]}
381+
#async{} = Async ->
382+
{next_state, async_wait, StateData0#data{buff = Async#async{low = 0}}, [postpone]};
383+
#sync{} ->
384+
StateData1 = flush_sync_buffer(StateData0),
385+
case StateData1#data.buff of
386+
undefined ->
387+
StateData = send_tls_alert(Alert, StateData1),
388+
{next_state, connection, StateData};
389+
#sync{} ->
390+
{keep_state, StateData1, [postpone]}
391+
end
343392
end;
344393
connection(cast, {new_write, WritesState, Version, MaxFragLen},
345394
#data{connection_states = ConnectionStates0, env = Env} = StateData) ->
@@ -589,6 +638,12 @@ send_or_buffer(Transport, Socket, Msgs, From, #data{buff = undefined} = StateDat
589638
ok ->
590639
send_reply(From, ok),
591640
{ok, StateData0};
641+
{error, {timeout, RestData}} ->
642+
%% gen_tcp:send with {inet_backend, socket} returns unsent
643+
%% encrypted data on timeout. Buffer it for retry on next send.
644+
%% Reply {error, timeout} to simulate {inet_backend, inet} behavior.
645+
send_reply(From, {error, timeout}),
646+
{ok, StateData0#data{buff = #sync{q_rev = RestData}}};
592647
{error, timeout} = Error ->
593648
%% This clause is to retain some backwards compatibility with
594649
%% inet-driver behavior for gen_tcp:send timeout. That
@@ -626,7 +681,25 @@ send_or_buffer(Transport, Socket, Msgs, From, #data{buff = undefined} = StateDat
626681
{block, StateData0#data{buff = Async#async{reply_to = From}}}
627682
end
628683
end;
629-
%% Buffer exists, push more data to buffer
684+
%% Timeout buffer exists, flush buffered data together with new data.
685+
%% Transport is gen_tcp (not tls_socket_tcp) so only sync results.
686+
send_or_buffer(Transport, Socket, Msgs, From,
687+
#data{buff = #sync{q_rev = BuffData}} = StateData0) ->
688+
case tls_socket:send(Transport, Socket, [BuffData | Msgs], nowait) of
689+
ok ->
690+
send_reply(From, ok),
691+
{ok, StateData0#data{buff = undefined}};
692+
{error, {timeout, RestData}} ->
693+
send_reply(From, {error, timeout}),
694+
{ok, StateData0#data{buff = #sync{q_rev = RestData}}};
695+
{error, timeout} = Error ->
696+
send_reply(From, Error),
697+
{ok, StateData0#data{buff = undefined}};
698+
{error, _Err} = Error ->
699+
send_reply(From, Error),
700+
Error
701+
end;
702+
%% Async buffer exists, push more data to buffer
630703
send_or_buffer(_Transport, _Socket, Msgs, From, #data{buff = Async0} = StateData) ->
631704
#async{high = High, size = Sz0, q_rev = Q} = Async0,
632705
Sz = Sz0 + iolist_size(Msgs),
@@ -639,6 +712,22 @@ send_or_buffer(_Transport, _Socket, Msgs, From, #data{buff = Async0} = StateData
639712
{block, StateData#data{buff = Async#async{reply_to = From}}}
640713
end.
641714

715+
%% Try to flush the #sync{} buffer. Returns the updated #data{}
716+
%% with buff set to undefined on success, or a new #sync{} on timeout.
717+
flush_sync_buffer(#data{env = #env{socket = Socket,
718+
transport_cb = Transport},
719+
buff = #sync{q_rev = BuffData}} = StateData) ->
720+
case tls_socket:send(Transport, Socket, BuffData, nowait) of
721+
ok ->
722+
StateData#data{buff = undefined};
723+
{error, {timeout, RestData}} ->
724+
StateData#data{buff = #sync{q_rev = RestData}};
725+
{error, timeout} ->
726+
StateData#data{buff = undefined};
727+
{error, _} ->
728+
StateData#data{buff = undefined}
729+
end.
730+
642731
do_async_send(_Transport, _Socket, _Handle, _Nextstate, {error, Err} = Error,
643732
#data{buff = #async{reply_to = From}} = StateData) ->
644733
send_reply(From, Error),

lib/ssl/test/ssl_api_SUITE.erl

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
select_sha1_cert/1,
6464
inet_backend_option_order/0,
6565
inet_backend_option_order/1,
66+
send_timeout_buffering/0,
67+
send_timeout_buffering/1,
6668
root_any_sign/0,
6769
root_any_sign/1,
6870
connection_information/0,
@@ -227,6 +229,8 @@
227229
protocol_version_check/2,
228230
suite_check/2,
229231
check_peercert/2,
232+
send_timeout_sink/1,
233+
send_timeout_fill/1,
230234
%%TODO Keep?
231235
run_error_server/1,
232236
run_client_error/1
@@ -264,9 +268,11 @@ groups() ->
264268
{'tlsv1.1', [parallel], gen_api_tests() ++ handshake_paus_tests() ++ pre_1_3() ++ pre_1_2()},
265269
{'tlsv1', [parallel], gen_api_tests() ++ handshake_paus_tests() ++ pre_1_3() ++ pre_1_2() ++
266270
beast_mitigation_test()},
267-
{'dtlsv1.2', [parallel], gen_api_tests() -- [new_options_in_handshake, hibernate_server] ++
271+
{'dtlsv1.2', [parallel], gen_api_tests() -- [new_options_in_handshake, hibernate_server,
272+
send_timeout_buffering] ++
268273
handshake_paus_tests() -- [handshake_continue_tls13_client] ++ pre_1_3()},
269-
{'dtlsv1', [parallel], gen_api_tests() -- [new_options_in_handshake, hibernate_server] ++
274+
{'dtlsv1', [parallel], gen_api_tests() -- [new_options_in_handshake, hibernate_server,
275+
send_timeout_buffering] ++
270276
handshake_paus_tests() -- [handshake_continue_tls13_client] ++ pre_1_3() ++ pre_1_2()},
271277
{transport_socket, [parallel], gen_api_tests() -- [ssl_not_started, dh_params]}
272278
].
@@ -305,6 +311,7 @@ gen_api_tests() ->
305311
peercert_with_client_cert,
306312
select_sha1_cert,
307313
inet_backend_option_order,
314+
send_timeout_buffering,
308315
connection_information,
309316
secret_connection_info,
310317
keylog_connection_info,
@@ -673,6 +680,73 @@ inet_backend_option_order(Config) when is_list(Config) ->
673680
ssl_test_lib:close(Server),
674681
ssl_test_lib:close(Client).
675682

683+
%%--------------------------------------------------------------------
684+
send_timeout_buffering() ->
685+
[{doc,"Test that ssl buffers unsent encrypted data on send timeout "
686+
"when using {inet_backend, socket} and retries on next send"}].
687+
send_timeout_buffering(Config) when is_list(Config) ->
688+
ClientOpts = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config),
689+
ServerOpts = ssl_test_lib:ssl_options(server_rsa_opts, Config),
690+
{ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config),
691+
Server = ssl_test_lib:start_server([{node, ServerNode}, {port, 0},
692+
{from, self()},
693+
{mfa, {?MODULE, send_timeout_sink, []}},
694+
{options, [{inet_backend, socket},
695+
{active, false}
696+
| ServerOpts]}]),
697+
Port = ssl_test_lib:inet_port(Server),
698+
Client = ssl_test_lib:start_client([{node, ClientNode}, {port, Port},
699+
{host, Hostname},
700+
{from, self()},
701+
{mfa, {?MODULE, send_timeout_fill, []}},
702+
{options, [{inet_backend, socket},
703+
{active, false},
704+
{sndbuf, 4096},
705+
{send_timeout, 1}
706+
| ClientOpts]}]),
707+
708+
ssl_test_lib:check_result(Server, ok, Client, ok),
709+
710+
ssl_test_lib:close(Server),
711+
ssl_test_lib:close(Client).
712+
713+
send_timeout_sink(Socket) ->
714+
%% Server side: register, wait for signal, drain, signal back
715+
register(send_timeout_sink_server, self()),
716+
receive start_recv -> ok end,
717+
send_timeout_recv_loop(Socket),
718+
send_timeout_sink_client ! drained,
719+
ok.
720+
721+
send_timeout_recv_loop(Socket) ->
722+
case ssl:recv(Socket, 0, 1000) of
723+
{ok, _} -> send_timeout_recv_loop(Socket);
724+
{error, timeout} -> ok;
725+
{error, closed} -> ok
726+
end.
727+
728+
send_timeout_fill(Socket) ->
729+
%% Client side: fill buffer, signal server, wait for drain, send again
730+
register(send_timeout_sink_client, self()),
731+
Data = <<0:(1024*8)>>,
732+
send_timeout_fill_loop(Socket, Data, 0).
733+
734+
send_timeout_fill_loop(Socket, Data, N) ->
735+
case ssl:send(Socket, Data) of
736+
ok ->
737+
send_timeout_fill_loop(Socket, Data, N + 1);
738+
{error, timeout} when N > 0 ->
739+
%% Buffer filled. Signal server to start draining.
740+
send_timeout_sink_server ! start_recv,
741+
%% Wait for server to finish draining.
742+
receive drained -> ok end,
743+
%% Verify connection is still usable.
744+
ok = ssl:send(Socket, <<"still alive">>),
745+
ok;
746+
{error, _} = Error ->
747+
Error
748+
end.
749+
676750
%%--------------------------------------------------------------------
677751
connection_information() ->
678752
[{doc,"Test the API function ssl:connection_information/1"}].

0 commit comments

Comments
 (0)