Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support optional socket options #215

Merged
merged 7 commits into from
Oct 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 49 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ several authentication mechanisms and dynamic configuration
* [eradius configuration example 1](#eradius-configuration-example-1)
* [eradius configuration example 2](#eradius-configuration-example-2)
* [eradius configuration example 3](#eradius-configuration-example-3)
* [eradius configuration example 3](#eradius-configuration-example-4)
* [Support of failover for client](#support-of-failover-for-client)
* [Failover configuration](#failover-configuration)
* [Failover Erlang code usage](#failover-erlang-code-usage)
Expand Down Expand Up @@ -98,12 +99,13 @@ servers == { servers, [<Server>] }
```
Each server is tuple ({}):
```
Server == { <SymbolicName>, { <IP>, [<Ports>] } } | { <SymbolicName>, { <IP>, [<Ports>] }, <ExtraServerOptions> }
Server == { <SymbolicName>, { <IP>, [<Ports>] } } | { <SymbolicName>, { <IP>, [<Ports>], <ExtraSocketOptions> } }
ExtraServerOptions == [<ServerOption>]
ExtraSocketOptions == [{socket_opts, [socket_setopt()]}] (see: https://erlang.org/doc/man/inet.html#setopts-2)
ServerOption == {rate_config, <SymbolicNameLimit> | <RateConfigList>}
```

Rate configuration can be configurated per server, in extra configuration, with a symbolic name or directly in server
Rate configuration can be configured per server, in extra configuration, with a symbolic name or directly in server
```
{SymbolicNameLimit, RateConfigList}
RateConfigList == [<RateOption>]
Expand All @@ -114,12 +116,15 @@ Each server is assigned a list of handlers. This list defines the NASes that are
which handler is to process the request.

Handler assignment: `{<SymbolicName>, [<Handlers>]}`

```
SymbolicName == Reference to a previously defined server.
Handler == { <HandlerDefinition>, [<Sources>] }
```

If only one handler module is used, it can be defined globally as `{radius_callback, <HandlerMod>}`.
If more than one handler modules are used, they have to be given in the HandlerDefinition:

```
HandlerDefinition == {<HandlerMod>, <NasId>, <HandlerArgs>} | {<NasId>, <HandlerArgs>}
HandlerMod == Handler module to process the received requests.
Expand All @@ -140,8 +145,9 @@ Session nodes == {session_nodes, ['node@host', ...]} | {session_nodes, [{<GroupN

## eradius configuration example 1

All requests are forwarded to the same globally defined list of nodes.
All requests are forwarded to the same globally defined list of nodes.
Only one handler module is used.

```erlang
[{eradius, [
{session_nodes, ['node1@host1', 'node2@host2']},
Expand All @@ -164,8 +170,9 @@ Only one handler module is used.

## eradius configuration example 2

Requests of different sources are forwarded to different nodes.
Requests of different sources are forwarded to different nodes.
Different handlers are used for the sources.

```erlang
[{eradius, [
{session_nodes, [
Expand All @@ -178,23 +185,53 @@ Different handlers are used for the sources.
{root, [
{
{tposs_pcrf_handler1, "NAS1", [handler_arg1, handler_arg2]},
[ {"10.18.14.2", <<"secret1">>, [{group, "NodeGroup1"}]} ]
[ {"10.18.14.2", <<"secret1">>, [{group, "NodeGroup1"}]} ]
},
{
{tposs_pcrf_handler2, "NAS2", [handler_arg3, handler_arg4]},
[ {"10.18.14.3", <<"secret2">>, [{group, "NodeGroup2"}]} ]
[ {"10.18.14.3", <<"secret2">>, [{group, "NodeGroup2"}]} ]
}
]}
]}]
```

## eradius configuration example 3

Requests of different sources are forwarded to different nodes.
Different handlers are used for the sources.

```erlang
[{eradius, [
{session_nodes, [
{"NodeGroup1", ['node1@host1', 'node2@host2']},
{"NodeGroup2", ['node3@host3', 'node4@host4']}
]},
{servers, [
{root, {"127.0.0.1", [1812, 1813], [{socket_opts, [{recbuf, 8192},
{netns, "/var/run/netns/myns"}]}]}}
]},
{root, [
{
{tposs_pcrf_handler1, "NAS1", [handler_arg1, handler_arg2]},
[ {"10.18.14.2", <<"secret1">>, [{group, "NodeGroup1"}]} ]
},
{
{tposs_pcrf_handler2, "NAS2", [handler_arg3, handler_arg4]},
[ {"10.18.14.3", <<"secret2">>, [{group, "NodeGroup2"}]} ]
}
]}
]}]
```

## eradius configuration example 4

Example of full configuration with keys which can use in `eradius`:

```erlang
[{eradius, [
%% The IP address used to send RADIUS requests
{client_ip, {127, 0, 0, 1}},
%% The maximum number of open ports that will be used by RADIUS clients
%% The maximum number of open ports that will be used by RADIUS clients
{client_ports, 256},
%% how long the binary response is kept before re-sending it
{resend_timeout, 500},
Expand Down Expand Up @@ -234,7 +271,7 @@ Example of full configuration with keys which can use in `eradius`:
]},
{server_status_metrics_enabled, false},
{counter_aggregator, false},
%% Size of RADIUS receive buffer
%% Size of RADIUS receive buffer
{recbuf, 8192}
]}].
```
Expand All @@ -251,6 +288,7 @@ Secondary RADIUS servers could be specified via RADIUS proxy configuration, with
## Failover configuration

Configuration example of failover where the `pool_name` is `atom` specifies name of a pool of secondary RADIUS servers.

```erlang
[{eradius, [
%%% ...
Expand All @@ -274,9 +312,11 @@ All pools are configured via:

## Failover Erlang code usage
In a case when RADIUS proxy (eradius_proxy handler) is not used, a list of RADIUS upstream servers could be passed to the `eradius_client:send_radius_request/3` via options, for example:

```erlang
eradius_client:send_request(Server, Request, [{failover, [{"localhost", 1814, <<"secret">>}]}]).
```

If `failover` option was not passed to the client through the options or RADIUS proxy configuration there should not be any performance impact as RADIUS client will try to a RADIUS request to only one RADIUS server that is defined in `eradius_client:send_request/3` options.

For each secondary RADIUS server server status metrics could be enabled via boolean `server_status_metrics_enabled` configuration option.
Expand All @@ -300,6 +340,7 @@ A list of RADIUS dictionaries to be loaded at startup. The atoms in this list ar
the `priv` directory of the eradius application.

Example:

```
[dictionary, dictionary_cisco, dictionary_travelping]
```
Expand Down
64 changes: 52 additions & 12 deletions src/eradius_config.erl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
-export([validate_new_config/0, validate_new_config/2, validate_config/1]).
% Config validating API functions:
-export([get_app_env/2, validate_ip/1, validate_port/1, validate_ports/1,
map_helper/3, map_helper/2, ok_error_helper/2, validate_secret/1]).
map_helper/3, map_helper/2, ok_error_helper/2, validate_secret/1,
validate_options/1, validate_socket_options/1, validate_server/1]).
-export([generate_ip_list/2]).

%% ------------------------------------------------------------------------------------------
Expand All @@ -30,17 +31,21 @@ validate_new_config_start(Servers, Nodes) ->
map_helper(fun(Server) -> validate_new_server_config(Server, Nodes) end, Servers, flatten).

validate_new_server_config({Name, {IP, ListOfPorts}}, Nodes) ->
validate_new_server_config(Name, get_app_env(Name), validate_ip(IP), validate_ports(ListOfPorts), Nodes).
validate_new_server_config(Name, get_app_env(Name), validate_ip(IP), validate_ports(ListOfPorts), [], Nodes);

validate_new_server_config(_Server, {invalid, _} = Invalid, _IP, _ListOfPorts, _Nodes) -> Invalid;
validate_new_server_config(_Server, _NasList, {invalid, _} = Invalid, _ListOfPorts, _Nodes) -> Invalid;
validate_new_server_config(_Server, _NasList, _IP, {invalid, _} = Invalid, _Nodes) -> Invalid;
validate_new_server_config(Server, NasList, IP, ListOfPorts, Nodes) ->
validate_new_server_config({Name, {IP, ListOfPorts, Opts}}, Nodes) ->
validate_new_server_config(Name, get_app_env(Name), validate_ip(IP), validate_ports(ListOfPorts), validate_options(Opts), Nodes).

validate_new_server_config(_Server, {invalid, _} = Invalid, _IP, _ListOfPorts, _Opts, _Nodes) -> Invalid;
validate_new_server_config(_Server, _NasList, {invalid, _} = Invalid, _ListOfPorts, _Opts, _Nodes) -> Invalid;
validate_new_server_config(_Server, _NasList, _IP, {invalid, _} = Invalid, _Opts, _Nodes) -> Invalid;
validate_new_server_config(_Server, _NasList, _IP, _ListOfPorts, {invalid, _} = Invalid, _Nodes) -> Invalid;
validate_new_server_config(Server, NasList, IP, ListOfPorts, Opts, Nodes) ->
case validate_new_nas_list(NasList, {IP, ListOfPorts, Nodes}) of
{invalid, _} = Invalid ->
Invalid;
Values ->
lists:map(fun(Port) -> {Server, {IP, Port}, Values} end, ListOfPorts)
lists:map(fun(Port) -> {Server, {IP, Port, Opts}, Values} end, ListOfPorts)
end.

validate_new_nas_list(NasLists, ServerConfig) ->
Expand All @@ -65,11 +70,11 @@ validate_behavior({Module, Nas, _Args} = Value) when is_atom(Module) andalso ?is
false -> Value
end;
validate_behavior({Module, _, _}) when is_atom(Module) ->
?invalid("bad NAS Id in Behavior specifification: ~p", [Module]);
?invalid("bad NAS Id in Behavior specification: ~p", [Module]);
validate_behavior({Module, _, _}) ->
?invalid("bad module in Behavior specifification: ~p", [Module]);
?invalid("bad module in Behavior specification: ~p", [Module]);
validate_behavior(Term) ->
?invalid("bad Term in Behavior specifification: ~p", [Term]).
?invalid("bad Term in Behavior specification: ~p", [Term]).

validate_arguments({Module, Nas, Args} = Value) ->
case Module:validate_arguments(Args) of
Expand Down Expand Up @@ -125,6 +130,32 @@ validate_port(Port) when ?pos_int(Port) -> Port;
validate_port(Port) when is_integer(Port) -> ?invalid("port number out of range: ~p", [Port]);
validate_port(Port) -> ?invalid("bad port number: ~p", [Port]).

validate_options(Opts) when is_list(Opts) ->
SocketOpts = proplists:get_value(socket_opts, Opts, []),
case validate_socket_options(SocketOpts) of
{invalid, Reason} = E ->
io:format("validate_socket_options: ~p", [Reason]),
E;
_ ->
Opts
end;
validate_options(Opts) ->
?invalid("expect a list of options: ~p", Opts).

validate_socket_options(SocketOpts) when is_list(SocketOpts) ->
BannedOpts = [ip, binary, list, active],
IsBannedFn = fun(Opt) ->
proplists:is_defined(Opt, SocketOpts)
end,
case lists:any(IsBannedFn, BannedOpts) of
true ->
?invalid("bad socket options specified: ~p", [SocketOpts]);
false ->
SocketOpts
end;
validate_socket_options(Opts) ->
?invalid("expect a list of options: ~p", Opts).

check_root([First | _] = AllNodes) when is_tuple(First) ->
map_helper(fun({Name, List}) ->
case validate_handler_nodes(List) of
Expand Down Expand Up @@ -185,11 +216,11 @@ validate_server_config([]) ->
[];
validate_server_config([{Server, NasList} | ConfigRest]) ->
case validate_server(Server) of
E = {invalid, _} ->
{invalid, _} = E ->
E;
ValidServer ->
case validate_nas_list(NasList) of
E = {invalid, _} ->
{invalid, _} = E ->
E;
ValidNasList ->
case validate_server_config(ConfigRest) of
Expand Down Expand Up @@ -228,6 +259,15 @@ validate_server(String) when is_list(String) ->
_ ->
{invalid, io_lib:format("bad address/port combination: ~p", [String])}
end;
validate_server({IP, Port, Opts}) when is_list(Opts) ->
case {validate_server({IP, Port}), validate_options(Opts)} of
{{invalid, _Reason} = E, _} ->
E;
{_, {invalid, _Reason} = E} ->
E;
{{ValidIP, ValidPort}, ValidOpts} ->
{ValidIP, ValidPort, ValidOpts}
end;
validate_server(X) ->
{invalid, io_lib:format("bad address/port combination: ~p", [X])}.

Expand Down
30 changes: 23 additions & 7 deletions src/eradius_server.erl
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
%% }).
%% '''
-module(eradius_server).
-export([start_link/3]).
-export([start_link/3, start_link/4]).
-export_type([port_number/0, req_id/0]).

%% internal
Expand All @@ -69,6 +69,7 @@
-define(RESEND_TIMEOUT, 5000). % how long the binary response is kept after sending it on the socket
-define(RESEND_RETRIES, 3). % how often a reply may be resent
-define(HANDLER_REPLY_TIMEOUT, 15000). % how long to wait before a remote handler is considered dead
-define(DEFAULT_RADIUS_SERVER_OPTS(IP), [{active, once}, {ip, IP}, binary]).

-type port_number() :: 1..65535.
-type req_id() :: byte().
Expand All @@ -92,22 +93,27 @@
-callback radius_request(#radius_request{}, #nas_prop{}, HandlerData :: term()) ->
{reply, #radius_request{}} | noreply | {error, timeout}.

%% @private
-spec start_link(atom(), inet:ip4_address(), port_number()) -> {ok, pid()} | {error, term()}.
start_link(ServerName, IP = {A,B,C,D}, Port) ->
start_link(ServerName, IP, Port) ->
start_link(ServerName, IP, Port, []).

-spec start_link(atom(), inet:ip4_address(), port_number(), [inet:socket_setopt()]) -> {ok, pid()} | {error, term()}.
start_link(ServerName, IP = {A,B,C,D}, Port, Opts) ->
Name = list_to_atom(lists:flatten(io_lib:format("eradius_server_~b.~b.~b.~b:~b", [A,B,C,D,Port]))),
gen_server:start_link({local, Name}, ?MODULE, {ServerName, IP, Port}, []).
gen_server:start_link({local, Name}, ?MODULE, {ServerName, IP, Port, Opts}, []).

stats(Server, Function) ->
gen_server:call(Server, {stats, Function}).

%% ------------------------------------------------------------------------------------------
%% -- gen_server Callbacks
%% @private
init({ServerName, IP, Port}) ->
init({ServerName, IP, Port, Opts}) ->
process_flag(trap_exit, true),
RecBuf = application:get_env(eradius, recbuf, 8192),
case gen_udp:open(Port, [{active, once}, {ip, IP}, binary, {recbuf, RecBuf}]) of
ExtraServerOptions = proplists:get_value(socket_opts, Opts, []),
DefaultRecBuf = application:get_env(eradius, recbuf, 8192),
0xAX marked this conversation as resolved.
Show resolved Hide resolved
ExtraServerOptionsWithBuf = add_recbuf_to_options(DefaultRecBuf, ExtraServerOptions),
case gen_udp:open(Port, ?DEFAULT_RADIUS_SERVER_OPTS(IP) ++ ExtraServerOptionsWithBuf) of
{ok, Socket} ->
{ok, #state{socket = Socket,
ip = IP, port = Port, name = ServerName,
Expand Down Expand Up @@ -163,6 +169,16 @@ handle_info({'EXIT', HandlerPid, _Reason}, State = #state{transacts = Transacts}
handle_info(_Info, State) ->
{noreply, State}.

%% @private
-spec add_recbuf_to_options(pos_integer(), proplists:proplist()) -> proplists:proplist().
add_recbuf_to_options(RecBuf, Opts) ->
case proplists:get_value(recbuf, Opts) of
undefined ->
[{recbuf, RecBuf} | Opts];
_Val ->
Opts
end.

%% @private
terminate(_Reason, State) ->
gen_udp:close(State#state.socket),
Expand Down
28 changes: 18 additions & 10 deletions src/eradius_server_mon.erl
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ configure(#state{running = Running}) ->
{ok, #state{running = NewRunning}}
end.

server_naslist({ServerName, {IP, Port, _Opts}, HandlerList}) ->
server_naslist({ServerName, {IP, Port}, HandlerList});
server_naslist({ServerName, {IP, Port}, HandlerList}) ->
lists:map(fun({NasId, NasIP, Secret, HandlerNodes, HandlerMod, HandlerArgs}) ->
ServerInfo = eradius_lib:make_addr_info({ServerName, {IP, Port}}),
Expand All @@ -144,18 +146,24 @@ update_server(Running, ToStop, ToStart) ->
eradius_server_sup:stop_instance(ServerAddr, Pid),
StoppedServer
end, ToStop),
NewStarted = lists:map(fun(ServerAddr = {ServerName, Addr = {IP, Port}}) ->
case eradius_server_sup:start_instance(ServerAddr) of
{ok, Pid} ->
{ServerName, Addr, Pid};
{error, Error} ->
?LOG(error, "Could not start listener on host: ~s, occuring error: ~p",
[printable_peer(IP, Port), Error])
end
end, ToStart),
StartFn = fun({ServerName, Addr = {IP, Port, _Opts}}=ServerAddr) ->
case eradius_server_sup:start_instance(ServerAddr) of
{ok, Pid} ->
{ServerName, Addr, Pid};
{error, Error} ->
?LOG(error, "Could not start listener on host: ~s, occurring error: ~p",
[printable_peer(IP, Port), Error])
end
end,
NewStarted = lists:map(fun
({ServerName, {IP, Port}}) ->
StartFn({ServerName, {IP, Port, []}});
(ServerAddr) ->
StartFn(ServerAddr)
end,
ToStart),
(Running -- Stopped) ++ NewStarted.

update_nases(ToDelete, ToInsert) ->
lists:foreach(fun(Nas) -> ets:delete_object(?NAS_TAB, Nas) end, ToDelete),
lists:foreach(fun(Nas) -> ets:insert(?NAS_TAB, Nas) end, ToInsert).

Loading