Skip to content

Commit bce8001

Browse files
committed
fix: ensure dTLS socket options are changed safely
1 parent b9321ea commit bce8001

File tree

2 files changed

+92
-25
lines changed

2 files changed

+92
-25
lines changed

src/esockd_dtls_listener.erl

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,19 @@
4545
lsock :: ssl:sslsocket(),
4646
laddr :: inet:ip_address(),
4747
lport :: inet:port_number(),
48-
sockopts :: [ssl:tls_server_option()]
48+
sockopts :: [gen_udp:option()],
49+
dtlsopts :: [ssl:tls_server_option()]
4950
}).
5051

5152
-type option() :: {dtls_options, [gen_udp:option()]}.
5253

53-
-define(DEFAULT_DTLS_OPTIONS,
54-
[{protocol, dtls},
55-
{mode, binary},
54+
-define(DEFAULT_SOCK_OPTIONS,
55+
[{mode, binary},
5656
{reuseaddr, true}]).
5757

58+
-define(DEFAULT_DTLS_OPTIONS,
59+
[{protocol, dtls}]).
60+
5861
-spec start_link(atom(), esockd:listen_on(), [esockd:option()])
5962
-> {ok, pid()} | ignore | {error, term()}.
6063
start_link(Proto, ListenOn, Opts) ->
@@ -96,37 +99,41 @@ init({Proto, ListenOn, Opts}) ->
9699
Port = port(ListenOn),
97100
process_flag(trap_exit, true),
98101
esockd_server:ensure_stats({Proto, ListenOn}),
99-
SockOpts = merge_defaults(merge_addr(ListenOn, dltsopts(Opts))),
102+
SockOpts = merge_sock_defaults(merge_addr(ListenOn, sockopts(Opts))),
103+
DTLSOpts = merge_dtls_defaults(dtlsopts(Opts)),
100104
%% Don't active the socket...
101-
case ssl:listen(Port, SockOpts) of
105+
case ssl:listen(Port, SockOpts ++ DTLSOpts) of
102106
%%case ssl:listen(Port, [{active, false} | proplists:delete(active, SockOpts)]) of
103107
{ok, LSock} ->
104108
{ok, {LAddr, LPort}} = ssl:sockname(LSock),
105-
%%error_logger:info_msg("~s listen on ~s:~p with ~p acceptors.~n",
106-
%% [Proto, inet:ntoa(LAddr), LPort, AcceptorNum]),
107-
{ok, #state{proto = Proto, listen_on = ListenOn, lsock = LSock,
108-
laddr = LAddr, lport = LPort, sockopts = SockOpts}};
109+
{ok, #state{proto = Proto, listen_on = ListenOn,
110+
lsock = LSock, laddr = LAddr, lport = LPort,
111+
sockopts = SockOpts, dtlsopts = DTLSOpts}};
109112
{error, Reason} ->
110113
error_logger:error_msg("~s failed to listen on ~p - ~p (~s)",
111114
[Proto, Port, Reason, inet:format_error(Reason)]),
112115
{stop, Reason}
113116
end.
114117

115-
dltsopts(Opts) ->
118+
dtlsopts(Opts) ->
116119
%% Filter out `esockd:ssl_custom_option()`, otherwise DTLS listener will
117120
%% fail to start.
118-
DTLSOpts = lists:foldl(
121+
lists:foldl(
119122
fun proplists:delete/2,
120123
proplists:get_value(dtls_options, Opts, []),
121124
[handshake_timeout, gc_after_handshake]
122-
),
123-
SockOpts = proplists:get_value(udp_options, Opts, []),
124-
SockOpts ++ DTLSOpts.
125+
).
126+
127+
sockopts(Opts) ->
128+
proplists:get_value(udp_options, Opts, []).
125129

126130
port(Port) when is_integer(Port) -> Port;
127131
port({_Addr, Port}) -> Port.
128132

129-
merge_defaults(SockOpts) ->
133+
merge_sock_defaults(SockOpts) ->
134+
esockd:merge_opts(?DEFAULT_SOCK_OPTIONS, SockOpts).
135+
136+
merge_dtls_defaults(SockOpts) ->
130137
esockd:merge_opts(?DEFAULT_DTLS_OPTIONS, SockOpts).
131138

132139
merge_addr(Port, SockOpts) when is_integer(Port) ->
@@ -143,17 +150,26 @@ handle_call(get_state, _From, State = #state{lsock = LSock, lport = LPort}) ->
143150
],
144151
{reply, Reply, State};
145152

146-
handle_call({set_options, Opts}, _From, State = #state{lsock = LSock, sockopts = SockOpts}) ->
147-
SockOptsIn = dltsopts(Opts),
148-
SockOptsChanged = esockd:changed_opts(SockOptsIn, SockOpts),
149-
case ssl:setopts(LSock, SockOptsChanged) of
150-
ok ->
151-
SockOptsMerged = esockd:merge_opts(SockOpts, SockOptsChanged),
152-
{reply, ok, State#state{sockopts = SockOptsMerged}};
153-
Error = {error, _} ->
153+
handle_call({set_options, Opts}
154+
, _From
155+
, State = #state{lsock = LSock, sockopts = SockOpts, dtlsopts = DTLSOpts}
156+
) ->
157+
SockOptsIn = sockopts(Opts),
158+
DTLSOptsIn = dtlsopts(Opts),
159+
case esockd:changed_opts(DTLSOptsIn, DTLSOpts) of
160+
[] ->
161+
SockOptsChanged = esockd:changed_opts(SockOptsIn, SockOpts),
162+
case ssl:setopts(LSock, SockOptsChanged) of
163+
ok ->
164+
{reply, ok, State#state{sockopts = SockOptsIn}};
165+
Error = {error, _} ->
166+
{reply, Error, State}
167+
end;
168+
[_ | _] ->
169+
%% If the dTLS option set is different, bail out.
154170
%% Setting dTLS options on listening socket always succeeds,
155171
%% even if the options are invalid.
156-
{reply, Error, State}
172+
{reply, {error, not_supported}, State}
157173
end;
158174

159175
handle_call(Req, _From, State) ->

test/esockd_SUITE.erl

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,57 @@ t_update_tls_options(Config) ->
551551

552552
ok = esockd:close(echo_tls, LPort).
553553

554+
t_update_dtls_options(Config) ->
555+
UdpOpts = [{read_packets, 16}],
556+
DtlsOpts = [{protocol, dtls},
557+
{certfile, esockd_ct:certfile(Config)},
558+
{keyfile, esockd_ct:keyfile(Config)},
559+
{verify, verify_none}
560+
],
561+
DtlsOpts1 = [{versions, ['dtlsv1.2']},
562+
{client_renegotiation, false}
563+
| DtlsOpts],
564+
DtlsOpts2 = [{versions, ['dtlsv1']},
565+
{beast_mitigation, disabled}
566+
| DtlsOpts],
567+
ClientOpts = [binary,
568+
{active, false},
569+
{protocol, dtls},
570+
{verify, verify_none}],
571+
{ok, _LSup1} = esockd:open_dtls(dtls_echo, 7000,
572+
[{dtls_options, DtlsOpts1},
573+
{connection_mfargs, dtls_echo_server}]),
574+
{ok, DtlsSock1} = ssl:connect({127,0,0,1}, 7000, ClientOpts, 1000),
575+
576+
?assertMatch(
577+
{error, not_supported},
578+
esockd:set_options({dtls_echo, 7000},
579+
[{udp_options, UdpOpts}, {dtls_options, DtlsOpts2}])
580+
),
581+
?assertMatch(
582+
{error, not_supported},
583+
esockd:reset_options({dtls_echo, 7000},
584+
[{udp_options, UdpOpts}, {dtls_options, DtlsOpts2}])
585+
),
586+
587+
{ok, DtlsSock2} = ssl:connect({127,0,0,1}, 7000, ClientOpts, 1000),
588+
589+
ok = ssl:send(DtlsSock1, <<"DtlsSock1">>),
590+
ok = ssl:send(DtlsSock2, <<"DtlsSock2">>),
591+
{ok, <<"DtlsSock1">>} = ssl:recv(DtlsSock1, 0),
592+
{ok, <<"DtlsSock2">>} = ssl:recv(DtlsSock2, 0),
593+
594+
%% NOTE
595+
%% Changing options involve restarting of acceptors, and this currently exposes
596+
%% `esockd` to what seems as an Erlang/OTP bug. Process acting as dTLS listener
597+
%% does not track acceptors liveness, and thus is happy to hand out connections
598+
%% to dead listeners.
599+
%% ok = esockd:set_options({dtls_echo, 7000},
600+
%% [{udp_options, UdpOpts}]),
601+
%% {ok, DtlsSock3} = ssl:connect({127,0,0,1}, 7000, ClientOpts, 1000),
602+
603+
ok = esockd:close(dtls_echo, 7000).
604+
554605
t_allow_deny(_) ->
555606
AccessRules = [{allow, "192.168.1.0/24"}],
556607
{ok, _LSup} = esockd:open(echo, 7000, [{access_rules, AccessRules}]),

0 commit comments

Comments
 (0)