Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
RoadRunnr committed Apr 19, 2024
1 parent 2e866d4 commit 341ca01
Show file tree
Hide file tree
Showing 11 changed files with 467 additions and 713 deletions.
201 changes: 72 additions & 129 deletions src/eradius_client.erl
Original file line number Diff line number Diff line change
Expand Up @@ -34,176 +34,154 @@
Req#radius_request.cmd == 'coareq' orelse
Req#radius_request.cmd == 'discreq')).

-type nas_address() :: {string() | binary() | inet:ip_address(),
eradius_server:port_number(),
eradius_lib:secret()}.
-type options() :: [{retries, pos_integer()} |
{timeout, timeout()} |
{server_name, atom()} |
{metrics_info, {atom(), atom(), atom()}}].
%% -type nas_address() :: {string() | binary() | inet:ip_address(),
%% eradius_server:port_number(),
%% eradius_lib:secret()}.
-type options() :: #{retries => pos_integer(),
timeout => timeout(),
server_name => atom(),
metrics_info => {atom(), atom(), atom()}}.

-export_type([nas_address/0, options/0]).
-export_type([options/0]).

-define(DEFAULT_REQUEST_OPTS, #{retries => 3, timeout => 5_000, failover => []}).
-define(SERVER, ?MODULE).

%%%=========================================================================
%%% API
%%%=========================================================================

%% @equiv send_request(NAS, Request, [])
-spec send_request(nas_address(), #radius_request{}) -> {ok, binary()} | {error, 'timeout' | 'socket_down'}.
-spec send_request(eradius_client_mngr:server_name(), #radius_request{}) -> {ok, binary()} | {error, 'timeout' | 'socket_down'}.
send_request(NAS, Request) ->
send_request(NAS, Request, []).
send_request(NAS, Request, #{}).

%% @doc Send a radius request to the given NAS.
%% If no answer is received within the specified timeout, the request will be sent again.
-spec send_request(nas_address(), #radius_request{}, options()) ->
-spec send_request(eradius_client_mngr:server_name(), #radius_request{}, options()) ->
{ok, binary(), eradius_lib:authenticator()} | {error, 'timeout' | 'socket_down'}.
send_request({Host, Port, Secret}, Request, Options)
when ?GOOD_CMD(Request) andalso is_binary(Host) ->
send_request({erlang:binary_to_list(Host), Port, Secret}, Request, Options);
send_request({Host, Port, Secret}, Request, Options)
when ?GOOD_CMD(Request) andalso is_list(Host) ->
IP = get_ip(Host),
send_request({IP, Port, Secret}, Request, Options);
send_request({IP, Port, Secret}, Request, Options) when ?GOOD_CMD(Request) andalso is_tuple(IP) ->
send_request(ServerName, Request, Opts0)
when ?GOOD_CMD(Request), is_map(Opts0), is_list(ServerName) ->
Opts = maps:merge(?DEFAULT_REQUEST_OPTS, Opts0),
do_send_request(ServerName, [], Request, Opts);
send_request(ServerName, Request, Opts) ->
send_request([ServerName], Request, Opts).

do_send_request([], _Tried, _Request, _Opts) ->
{error, no_active_servers};
do_send_request(Peers, Tried, Request, Opts) ->
TS1 = erlang:monotonic_time(),
ServerName = proplists:get_value(server_name, Options, undefined),
MetricsInfo = make_metrics_info(Options, {IP, Port}),
Retries = proplists:get_value(retries, Options, ?DEFAULT_RETRIES),
Timeout = proplists:get_value(timeout, Options, ?DEFAULT_TIMEOUT),
SendReqFn = fun () ->
Peer = {ServerName, {IP, Port}},
update_client_requests(MetricsInfo),
{Socket, ReqId} = eradius_client_mngr:wanna_send(Peer),
Response = send_request_loop(Socket, ReqId, Peer,
Request#radius_request{reqid = ReqId, secret = Secret},
Retries, Timeout, MetricsInfo),
proceed_response(Request, Response, Peer, TS1, MetricsInfo, Options)
end,
%% If we have other RADIUS upstream servers check current one,
%% maybe it is already marked as inactive and try to find another
%% one
case proplists:get_value(failover, Options, []) of
[] ->
SendReqFn();
UpstreamServers ->
case eradius_client_mngr:find_suitable_peer([{IP, Port, Secret} | UpstreamServers]) of
[] ->
no_active_servers;
{{IP, Port, Secret}, _NewPool} ->
SendReqFn();
{NewPeer, []} ->
%% Special case, we don't have servers in the pool anymore, but we need
%% to preserve `failover` option to mark current server as inactive if
%% it will fail
NewOptions = lists:keyreplace(failover, 1, Options, {failover, undefined}),
send_request(NewPeer, Request, NewOptions);
{NewPeer, NewPool} ->
%% current server is not in list of active servers, so use another one
NewOptions = lists:keyreplace(failover, 1, Options, {failover, NewPool}),
send_request(NewPeer, Request, NewOptions)
end
end;
send_request({_IP, _Port, _Secret}, _Request, _Options) ->
error(badarg).

proceed_response(Request, {ok, Response, Secret, Authenticator}, _Peer = {_ServerName, {ServerIP, Port}}, TS1, MetricsInfo, Options) ->
case eradius_client_mngr:wanna_send(Peers, Tried) of
{ok, {Socket, ReqId, ServerName, Server, ClientInfo}} ->
#{secret := Secret, ip := IP, port := Port} = Server,

MetricsInfo = make_metrics_info(ServerName, Server, ClientInfo),
update_client_requests(MetricsInfo),
Peer = {ServerName, {IP, Port}},

Response =
send_request_loop(
Socket, ReqId, Peer, Request#radius_request{reqid = ReqId, secret = Secret},
Opts, MetricsInfo),
proceed_response([ServerName | Tried], Request, Response, ServerName, Server, TS1, MetricsInfo, Opts);

{error, _} = Error ->
maybe_failover(Tried, Request, Error, Opts)
end.

proceed_response(Tried, Request, {ok, Response, Authenticator},
ServerName, #{secret := Secret, ip := ServerIP, port := Port},
TS1, MetricsInfo, Opts) ->
update_client_request(Request#radius_request.cmd, MetricsInfo, erlang:monotonic_time() - TS1, Request),
update_client_responses(MetricsInfo),
case eradius_lib:decode_request(Response, Secret, Authenticator) of
{bad_pdu, Reason} ->
eradius_client_mngr:request_failed(ServerIP, Port, Options),
update_server_status_metric(ServerIP, Port, false, Options),
eradius_client_mngr:request_failed(ServerName),
update_server_status_metric(ServerIP, Port, false, Opts),

case Reason of
"Message-Authenticator Attribute is invalid" ->
update_client_response(bad_authenticator, MetricsInfo, Request),
?LOG(error, "~s INF: Noreply for request ~p. "
"Message-Authenticator Attribute is invalid",
[printable_peer(ServerIP, Port), Request]),
noreply;
{error, noreply};
"Authenticator Attribute is invalid" ->
update_client_response(bad_authenticator, MetricsInfo, Request),
?LOG(error, "~s INF: Noreply for request ~p. "
"Authenticator Attribute is invalid",
[printable_peer(ServerIP, Port), Request]),
noreply;
{error, noreply};
"unknown request type" ->
update_client_response(unknown_req_type, MetricsInfo, Request),
?LOG(error, "~s INF: Noreply for request ~p. "
"unknown request type",
[printable_peer(ServerIP, Port), Request]),
noreply;
{error, noreply};
_ ->
update_client_response(dropped, MetricsInfo, Request),
?LOG(error, "~s INF: Noreply for request ~p. "
"Could not decode the request, reason: ~s",
[printable_peer(ServerIP, Port), Request, Reason]),
maybe_failover(Request, noreply, Options)
maybe_failover(Tried, Request, noreply, Opts)
end;
Decoded ->
update_server_status_metric(ServerIP, Port, true, Options),
update_server_status_metric(ServerIP, Port, true, Opts),
update_client_response(Decoded#radius_request.cmd, MetricsInfo, Request),
{ok, Response, Authenticator}
{ok, {Response, Authenticator}}
end;

proceed_response(Request, Response, {_ServerName, {ServerIP, Port}}, TS1, MetricsInfo, Options) ->
proceed_response(Tried, Request, Response, ServerName, #{ip := ServerIP, port := Port}, TS1, MetricsInfo, Opts) ->
update_client_responses(MetricsInfo),
update_client_request(Request#radius_request.cmd, MetricsInfo, erlang:monotonic_time() - TS1, Request),

eradius_client_mngr:request_failed(ServerIP, Port, Options),
update_server_status_metric(ServerIP, Port, false, Options),
eradius_client_mngr:request_failed(ServerName),
update_server_status_metric(ServerIP, Port, false, Opts),

maybe_failover(Request, Response, Options).
maybe_failover(Tried, Request, Response, Opts).

maybe_failover(Request, Response, Options) ->
UpstreamServers = proplists:get_value(failover, Options, []),
case eradius_client_mngr:find_suitable_peer(UpstreamServers) of
[] ->
Response;
{NewPeer, NewPool} ->
%% leave only active upstream servers
NewOptions = lists:keyreplace(failover, 1, Options, {failover, NewPool}),
send_request(NewPeer, Request, NewOptions)
end.
maybe_failover(Tried, Request, _Response, #{failover := [_|_] = FailOver} = Opts) ->
do_send_request(FailOver, Tried, Request, Opts);
maybe_failover(_, _, Response, _) ->
Response.

%% send_request_loop/7
send_request_loop(Socket, ReqId, Peer, Request = #radius_request{},
Retries, Timeout, undefined) ->
send_request_loop(Socket, ReqId, Peer, Request, Retries, Timeout, eradius_lib:make_addr_info(Peer));
send_request_loop(Socket, ReqId, Peer, Request,
Retries, Timeout, MetricsInfo) ->
Opts, undefined) ->
send_request_loop(Socket, ReqId, Peer, Request, Opts, eradius_lib:make_addr_info(Peer));
send_request_loop(Socket, ReqId, Peer, Request = #radius_request{},
Opts, MetricsInfo) ->
{Authenticator, EncRequest} = eradius_lib:encode_request(Request),
send_request_loop(Socket, Peer, ReqId, Authenticator, EncRequest,
Timeout, Retries, MetricsInfo, Request#radius_request.secret, Request).
Opts, MetricsInfo, Request).

%% send_request_loop/10
send_request_loop(_Socket, _Peer, _ReqId, _Authenticator, _EncRequest,
Timeout, 0, MetricsInfo, _Secret, Request) ->
#{timeout := Timeout, retries := 0}, MetricsInfo, Request) ->
TS = erlang:convert_time_unit(Timeout, millisecond, native),
update_client_request(timeout, MetricsInfo, TS, Request),
{error, timeout};
send_request_loop(Socket, Peer = {_ServerName, {IP, Port}}, ReqId, Authenticator, EncRequest,
Timeout, RetryN, MetricsInfo, Secret, Request) ->
send_request_loop(Socket, Peer = {_ServerName, PeerAddress}, ReqId, Authenticator, EncRequest,
#{timeout := Timeout, retries := RetryN} = Opts, MetricsInfo, Request) ->
Result =
try
update_client_request(pending, MetricsInfo, 1, Request),
eradius_client_socket:send_request(Socket, {IP, Port}, ReqId, EncRequest, Timeout)
eradius_client_socket:send_request(Socket, PeerAddress, ReqId, EncRequest, Timeout)
after
update_client_request(pending, MetricsInfo, -1, Request)
end,

case Result of
{ok, Response} ->
{ok, Response, Secret, Authenticator};
{ok, {Response, Authenticator}};
{error, close} ->
{error, socket_down};
{error, timeout} ->
TS = erlang:convert_time_unit(Timeout, millisecond, native),
update_client_request(retransmission, MetricsInfo, TS, Request),
send_request_loop(Socket, Peer, ReqId, Authenticator, EncRequest,
Timeout, RetryN - 1, MetricsInfo, Secret, Request);
Opts#{retries := RetryN - 1}, MetricsInfo, Request);
{error, _} = Error ->
Error
end.
Expand Down Expand Up @@ -261,21 +239,8 @@ update_client_response(_, _, _) -> ok.
%%% internal functions
%%%=========================================================================

parse_ip(undefined) ->
{ok, undefined};
parse_ip(Address) when is_list(Address) ->
inet_parse:address(Address);
parse_ip(T = {_, _, _, _}) ->
{ok, T};
parse_ip(T = {_, _, _, _, _, _, _, _}) ->
{ok, T}.

make_metrics_info(Options, {ServerIP, ServerPort}) ->
ServerName = proplists:get_value(server_name, Options, undefined),
ClientName = proplists:get_value(client_name, Options, undefined),
ClientIP = application:get_env(eradius, client_ip, undefined),
{ok, ParsedClientIP} = parse_ip(ClientIP),
ClientAddrInfo = eradius_lib:make_addr_info({ClientName, {ParsedClientIP, undefined}}),
make_metrics_info(ServerName, #{ip := ServerIP, port := ServerPort}, {ClientName, ClientIP}) ->
ClientAddrInfo = eradius_lib:make_addr_info({ClientName, {ClientIP, undefined}}),
ServerAddrInfo = eradius_lib:make_addr_info({ServerName, {ServerIP, ServerPort}}),
{ClientAddrInfo, ServerAddrInfo}.

Expand Down Expand Up @@ -352,25 +317,3 @@ client_response_counter_account_match_spec_compile() ->
MatchSpecCompile ->
MatchSpecCompile
end.

get_ip(Host, Family) ->
case inet:gethostbyname(Host, Family) of
{ok, #hostent{h_addrtype = Family, h_addr_list = [IP]}} ->
{ok, IP};
{ok, #hostent{h_addrtype = Family, h_addr_list = [_ | _] = IPs}} ->
Index = rand:uniform(length(IPs)),
{ok, lists:nth(Index, IPs)};
{error, _} = Error ->
Error
end.

get_ip(Host) ->
case get_ip(Host, inet6) of
{ok, IP6} ->
IP6;
{error, _} ->
case get_ip(Host, inet) of
{ok, IP4} -> IP4;
{error, _} -> error(badarg)
end
end.
Loading

0 comments on commit 341ca01

Please sign in to comment.