Skip to content

Commit

Permalink
Merge pull request #184 from keynslug/fix/dtls-udp-opts
Browse files Browse the repository at this point in the history
fix(setopts): pass only changed socket options to `setops/2`
  • Loading branch information
keynslug authored Dec 19, 2023
2 parents 36a0b25 + 55a2566 commit fe84af9
Show file tree
Hide file tree
Showing 11 changed files with 363 additions and 197 deletions.
120 changes: 95 additions & 25 deletions src/esockd.erl
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,26 @@
-export([start/0]).

%% Core API
-export([ open/4
, open_udp/4
, open_dtls/4
-export([ open/3
, open_udp/3
, open_dtls/3
, close/2
, close/1
%% Legacy API
, open/4
, open_udp/4
, open_dtls/4
]).

-export([ reopen/1
, reopen/2
]).

-export([ child_spec/4
-export([ child_spec/3
, udp_child_spec/3
, dtls_child_spec/3
%% Legacy API
, child_spec/4
, udp_child_spec/4
, dtls_child_spec/4
]).
Expand Down Expand Up @@ -65,7 +73,9 @@

%% Utility functions
-export([ merge_opts/2
, changed_opts/2
, parse_opt/1
, start_mfargs/3
, ulimit/0
, fixaddr/1
, to_string/1
Expand Down Expand Up @@ -94,6 +104,7 @@
-type(option() :: {acceptors, pos_integer()}
| {max_connections, pos_integer()}
| {max_conn_rate, conn_limit()}
| {connection_mfargs, mfargs()}
| {access_rules, [esockd_access:rule()]}
| {shutdown, brutal_kill | infinity | pos_integer()}
| tune_buffer | {tune_buffer, boolean()}
Expand All @@ -106,8 +117,10 @@

-type(host() :: inet:ip_address() | string()).
-type(listen_on() :: inet:port_number() | {host(), inet:port_number()}).
-type ssl_options() :: [{handshake_timeout, pos_integer()} | ssl_option()].
-type dtls_options() :: [{handshake_timeout, pos_integer()} | ssl_option()].
-type ssl_options() :: [ssl_custom_option() | ssl_option()].
-type dtls_options() :: [ssl_custom_option() | ssl_option()].
-type ssl_custom_option() :: {handshake_timeout, pos_integer()}
| {gc_after_handshake, boolean()}.
-type listener_ref() :: {proto(), listen_on()}.

%%--------------------------------------------------------------------
Expand All @@ -123,35 +136,51 @@ start() ->
%% Open & Close

%% @doc Open a TCP or SSL listener
-spec(open(atom(), listen_on(), [option()], mfargs()) -> {ok, pid()} | {error, term()}).
open(Proto, Port, Opts, MFA) when is_atom(Proto), is_integer(Port) ->
esockd_sup:start_listener(Proto, Port, Opts, MFA);
open(Proto, {Host, Port}, Opts, MFA) when is_atom(Proto), is_integer(Port) ->
-spec open(atom(), listen_on(), options()) -> {ok, pid()} | {error, term()}.
open(Proto, Port, Opts) when is_atom(Proto), is_integer(Port) ->
esockd_sup:start_child(child_spec(Proto, Port, Opts));
open(Proto, {Host, Port}, Opts) when is_atom(Proto), is_integer(Port) ->
{IPAddr, _Port} = fixaddr({Host, Port}),
case proplists:get_value(ip, tcp_options(Opts)) of
undefined -> ok;
IPAddr -> ok;
Other -> error({badmatch, Other})
end,
esockd_sup:start_listener(Proto, {IPAddr, Port}, Opts, MFA).
esockd_sup:start_child(child_spec(Proto, {IPAddr, Port}, Opts)).

%% @private
tcp_options(Opts) ->
proplists:get_value(tcp_options, Opts, []).

%% @doc Open a TCP or SSL listener
-spec open(atom(), listen_on(), [option()], mfargs()) -> {ok, pid()} | {error, term()}.
open(Proto, Port, Opts, MFA) ->
open(Proto, Port, merge_mfargs(Opts, MFA)).

%% @doc Open a UDP listener
-spec(open_udp(atom(), listen_on(), [option()], mfargs())
-> {ok, pid()}
| {error, term()}).
-spec open_udp(atom(), listen_on(), [option()])
-> {ok, pid()} | {error, term()}.
open_udp(Proto, Port, Opts) ->
esockd_sup:start_child(udp_child_spec(Proto, Port, Opts)).

%% @doc Open a UDP listener
-spec open_udp(atom(), listen_on(), [option()], mfargs())
-> {ok, pid()} | {error, term()}.
open_udp(Proto, Port, Opts, MFA) ->
esockd_sup:start_child(udp_child_spec(Proto, Port, Opts, MFA)).
open_udp(Proto, Port, merge_mfargs(Opts, MFA)).

%% @doc Open a DTLS listener
-spec open_dtls(atom(), listen_on(), options())
-> {ok, pid()} | {error, term()}.
open_dtls(Proto, ListenOn, Opts) ->
esockd_sup:start_child(dtls_child_spec(Proto, ListenOn, Opts)).

%% @doc Open a DTLS listener
-spec(open_dtls(atom(), listen_on(), options(), mfargs())
-> {ok, pid()}
| {error, term()}).
open_dtls(Proto, ListenOn, Opts, MFA) ->
esockd_sup:start_child(dtls_child_spec(Proto, ListenOn, Opts, MFA)).
open_dtls(Proto, ListenOn, merge_mfargs(Opts, MFA)).

%% @doc Close the listener
-spec(close({atom(), listen_on()}) -> ok | {error, term()}).
Expand All @@ -176,22 +205,42 @@ reopen(Proto, ListenOn) when is_atom(Proto) ->

%% @doc Create a Child spec for a TCP/SSL Listener. It is a convenient method
%% for creating a Child spec to hang on another Application supervisor.
-spec(child_spec(atom(), listen_on(), [option()], mfargs())
-> supervisor:child_spec()).
-spec child_spec(atom(), listen_on(), options())
-> supervisor:child_spec().
child_spec(Proto, ListenOn, Opts) when is_atom(Proto) ->
esockd_sup:child_spec(Proto, fixaddr(ListenOn), Opts).

-spec child_spec(atom(), listen_on(), options(), mfargs())
-> supervisor:child_spec().
child_spec(Proto, ListenOn, Opts, MFA) when is_atom(Proto) ->
esockd_sup:child_spec(Proto, fixaddr(ListenOn), Opts, MFA).
child_spec(Proto, ListenOn, merge_mfargs(Opts, MFA)).

%% @doc Create a Child spec for a UDP Listener.
-spec udp_child_spec(atom(), listen_on(), options())
-> supervisor:child_spec().
udp_child_spec(Proto, Port, Opts) ->
esockd_sup:udp_child_spec(Proto, fixaddr(Port), Opts).

%% @doc Create a Child spec for a UDP Listener.
-spec(udp_child_spec(atom(), listen_on(), options(), mfargs())
-> supervisor:child_spec()).
-spec udp_child_spec(atom(), listen_on(), options(), mfargs())
-> supervisor:child_spec().
udp_child_spec(Proto, Port, Opts, MFA) ->
esockd_sup:udp_child_spec(Proto, fixaddr(Port), Opts, MFA).
udp_child_spec(Proto, Port, merge_mfargs(Opts, MFA)).

%% @doc Create a Child spec for a DTLS Listener.
-spec(dtls_child_spec(atom(), listen_on(), options(), mfargs())
-> supervisor:child_spec()).
-spec dtls_child_spec(atom(), listen_on(), options())
-> supervisor:child_spec().
dtls_child_spec(Proto, ListenOn, Opts) ->
esockd_sup:dtls_child_spec(Proto, fixaddr(ListenOn), Opts).

%% @doc Create a Child spec for a DTLS Listener.
-spec dtls_child_spec(atom(), listen_on(), options(), mfargs())
-> supervisor:child_spec().
dtls_child_spec(Proto, ListenOn, Opts, MFA) ->
esockd_sup:dtls_child_spec(Proto, fixaddr(ListenOn), Opts, MFA).
dtls_child_spec(Proto, ListenOn, merge_mfargs(Opts, MFA)).

merge_mfargs(Opts, MFA) ->
[{connection_mfargs, MFA} | proplists:delete(connection_mfargs, Opts)].

%%--------------------------------------------------------------------
%% Get/Set APIs
Expand Down Expand Up @@ -276,6 +325,14 @@ deny({Proto, ListenOn}, CIDR) when is_atom(Proto) ->
%%--------------------------------------------------------------------
%% Utils

-spec start_mfargs(mfargs(), _Arg1, _Arg2) -> _Ret.
start_mfargs(M, A1, A2) when is_atom(M) ->
M:start_link(A1, A2);
start_mfargs({M, F}, A1, A2) when is_atom(M), is_atom(F) ->
M:F(A1, A2);
start_mfargs({M, F, Args}, A1, A2) when is_atom(M), is_atom(F), is_list(Args) ->
erlang:apply(M, F, [A1, A2 | Args]).

%% @doc Merge two options
-spec(merge_opts(proplists:proplist(), proplists:proplist())
-> proplists:proplist()).
Expand All @@ -300,6 +357,19 @@ merge_opt(udp_options, Opts1, Opts2) -> merge_opts(Opts1, Opts2);
merge_opt(dtls_options, Opts1, Opts2) -> merge_opts(Opts1, Opts2);
merge_opt(_, _Opt1, Opt2) -> Opt2.

-spec changed_opts(proplists:proplist(), proplists:proplist())
-> proplists:proplist().
changed_opts(Opts, OptsRef) ->
lists:filter(
fun(Opt) ->
[Name] = proplists:get_keys([Opt]),
Value = proplists:get_value(Name, [Opt]),
ValueRef = proplists:get_value(Name, OptsRef),
ValueRef =/= Value orelse ValueRef == undefined
end,
Opts
).

%% @doc Parse option.
parse_opt(Options) ->
parse_opt(Options, []).
Expand Down
42 changes: 22 additions & 20 deletions src/esockd_connection_sup.erl
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@

-behaviour(gen_server).

-import(proplists, [get_value/3]).
-import(proplists, [get_value/3, get_value/2]).

-export([start_link/2, start_supervised/2, stop/1]).
-export([start_link/1, start_supervised/1, stop/1]).

-export([ start_connection/3
, count_connections/1
Expand Down Expand Up @@ -67,16 +67,16 @@
error_logger:error_msg("[~s] " ++ Format, [?MODULE | Args])).

%% @doc Start connection supervisor.
-spec(start_link([esockd:option()], esockd:mfargs())
-> {ok, pid()} | ignore | {error, term()}).
start_link(Opts, MFA) ->
gen_server:start_link(?MODULE, [Opts, MFA], []).
-spec start_link([esockd:option()])
-> {ok, pid()} | ignore | {error, term()}.
start_link(Opts) ->
gen_server:start_link(?MODULE, Opts, []).

-spec start_supervised(esockd:listener_ref(), esockd:mfargs())
-spec start_supervised(esockd:listener_ref())
-> {ok, pid()} | ignore | {error, term()}.
start_supervised(ListenerRef, MFA) ->
start_supervised(ListenerRef) ->
Opts = esockd_server:get_listener_prop(ListenerRef, options),
case start_link(Opts, MFA) of
case start_link(Opts) of
{ok, Pid} ->
_ = esockd_server:set_listener_prop(ListenerRef, connection_sup, Pid),
{ok, Pid};
Expand Down Expand Up @@ -113,14 +113,10 @@ start_connection(Sup, Sock, UpgradeFuns) ->
end.

%% @doc Start the connection process.
-spec(start_connection_proc(esockd:mfargs(), esockd_transport:socket())
-> {ok, pid()} | ignore | {error, term()}).
start_connection_proc(M, Sock) when is_atom(M) ->
M:start_link(?TRANSPORT, Sock);
start_connection_proc({M, F}, Sock) when is_atom(M), is_atom(F) ->
M:F(?TRANSPORT, Sock);
start_connection_proc({M, F, Args}, Sock) when is_atom(M), is_atom(F), is_list(Args) ->
erlang:apply(M, F, [?TRANSPORT, Sock | Args]).
-spec start_connection_proc(esockd:mfargs(), esockd_transport:socket())
-> {ok, pid()} | ignore | {error, term()}.
start_connection_proc(MFA, Sock) ->
esockd:start_mfargs(MFA, ?TRANSPORT, Sock).

-spec(count_connections(pid()) -> integer()).
count_connections(Sup) ->
Expand Down Expand Up @@ -150,12 +146,13 @@ call(Sup, Req) ->
%% gen_server callbacks
%%--------------------------------------------------------------------

init([Opts, MFA]) ->
init(Opts) ->
process_flag(trap_exit, true),
Shutdown = get_value(shutdown, Opts, brutal_kill),
MaxConns = get_value(max_connections, Opts, ?DEFAULT_MAX_CONNS),
RawRules = get_value(access_rules, Opts, [{allow, all}]),
AccessRules = [esockd_access:compile(Rule) || Rule <- RawRules],
MFA = get_value(connection_mfargs, Opts),
{ok, #state{curr_connections = #{},
max_connections = MaxConns,
access_rules = AccessRules,
Expand Down Expand Up @@ -218,7 +215,8 @@ handle_call(get_options, _From, State) ->
Options = [
{shutdown, get_state_option(shutdown, State)},
{max_connections, get_state_option(max_connections, State)},
{access_rules, get_state_option(access_rules, State)}
{access_rules, get_state_option(access_rules, State)},
{connection_mfargs, get_state_option(connection_mfargs, State)}
],
{reply, Options, State};

Expand Down Expand Up @@ -280,7 +278,9 @@ get_state_option(max_connections, #state{max_connections = MaxConnections}) ->
get_state_option(shutdown, #state{shutdown = Shutdown}) ->
Shutdown;
get_state_option(access_rules, #state{access_rules = Rules}) ->
[raw(Rule) || Rule <- Rules].
[raw(Rule) || Rule <- Rules];
get_state_option(connection_mfargs, #state{mfargs = MFA}) ->
MFA.

set_state_option({max_connections, MaxConns}, State) ->
State#state{max_connections = MaxConns};
Expand All @@ -293,6 +293,8 @@ set_state_option({access_rules, Rules}, State) ->
catch
error:_Reason -> {error, bad_access_rules}
end;
set_state_option({connection_mfargs, MFA}, State) ->
State#state{mfargs = MFA};
set_state_option(_, State) ->
State.

Expand Down
38 changes: 25 additions & 13 deletions src/esockd_dtls_listener.erl
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@
listen_on :: esockd:listen_on(),
lsock :: ssl:sslsocket(),
laddr :: inet:ip_address(),
lport :: inet:port_number()
lport :: inet:port_number(),
sockopts :: [ssl:tls_server_option()]
}).

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

-define(DEFAULT_DTLS_OPTIONS,
[{protocol, dtls},
Expand Down Expand Up @@ -95,31 +96,39 @@ init({Proto, ListenOn, Opts}) ->
Port = port(ListenOn),
process_flag(trap_exit, true),
esockd_server:ensure_stats({Proto, ListenOn}),
SockOpts = merge_addr(ListenOn, dltsopts(Opts)),
SockOpts = merge_defaults(merge_addr(ListenOn, dltsopts(Opts))),
%% Don't active the socket...
case ssl:listen(Port, esockd:merge_opts(?DEFAULT_DTLS_OPTIONS, SockOpts)) of
case ssl:listen(Port, SockOpts) of
%%case ssl:listen(Port, [{active, false} | proplists:delete(active, SockOpts)]) of
{ok, LSock} ->
{ok, {LAddr, LPort}} = ssl:sockname(LSock),
%%error_logger:info_msg("~s listen on ~s:~p with ~p acceptors.~n",
%% [Proto, inet:ntoa(LAddr), LPort, AcceptorNum]),
{ok, #state{proto = Proto, listen_on = ListenOn,
lsock = LSock, laddr = LAddr, lport = LPort}};
{ok, #state{proto = Proto, listen_on = ListenOn, lsock = LSock,
laddr = LAddr, lport = LPort, sockopts = SockOpts}};
{error, Reason} ->
error_logger:error_msg("~s failed to listen on ~p - ~p (~s)",
[Proto, Port, Reason, inet:format_error(Reason)]),
{stop, Reason}
end.

dltsopts(Opts) ->
proplists:delete(
handshake_timeout,
proplists:get_value(dtls_options, Opts, [])
).
%% Filter out `esockd:ssl_custom_option()`, otherwise DTLS listener will
%% fail to start.
DTLSOpts = lists:foldl(
fun proplists:delete/2,
proplists:get_value(dtls_options, Opts, []),
[handshake_timeout, gc_after_handshake]
),
SockOpts = proplists:get_value(udp_options, Opts, []),
SockOpts ++ DTLSOpts.

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

merge_defaults(SockOpts) ->
esockd:merge_opts(?DEFAULT_DTLS_OPTIONS, SockOpts).

merge_addr(Port, SockOpts) when is_integer(Port) ->
SockOpts;
merge_addr({Addr, _Port}, SockOpts) ->
Expand All @@ -134,10 +143,13 @@ handle_call(get_state, _From, State = #state{lsock = LSock, lport = LPort}) ->
],
{reply, Reply, State};

handle_call({set_options, Opts}, _From, State = #state{lsock = LSock}) ->
case ssl:setopts(LSock, dltsopts(Opts)) of
handle_call({set_options, Opts}, _From, State = #state{lsock = LSock, sockopts = SockOpts}) ->
SockOptsIn = dltsopts(Opts),
SockOptsChanged = esockd:changed_opts(SockOptsIn, SockOpts),
case ssl:setopts(LSock, SockOptsChanged) of
ok ->
{reply, ok, State};
SockOptsMerged = esockd:merge_opts(SockOpts, SockOptsChanged),
{reply, ok, State#state{sockopts = SockOptsMerged}};
Error = {error, _} ->
%% Setting dTLS options on listening socket always succeeds,
%% even if the options are invalid.
Expand Down
Loading

0 comments on commit fe84af9

Please sign in to comment.