From 572eb2b60f11c0eac9bc40972071b01b3bd1a50d Mon Sep 17 00:00:00 2001 From: Andreas Schultz Date: Mon, 15 Apr 2024 18:53:06 +0200 Subject: [PATCH 01/38] add rebar3_fmt plugin and apply consistent formating --- .../eradius_prometheus_collector/rebar.config | 4 +- .../src/eradius_prometheus_collector.app.src | 2 +- .../src/eradius_prometheus_collector.erl | 104 ++++---- dicts_compiler.erl | 54 ++-- include/eradius_dict.hrl | 22 +- include/eradius_lib.hrl | 172 ++++++------- priv/dev.config | 36 +-- rebar.config | 20 +- sample/example.config | 28 +-- sample/rebar.config | 4 +- sample/src/eradius_server_sample.app.src | 16 +- sample/src/eradius_server_sample.erl | 10 +- src/eradius.app.src | 58 ++--- src/eradius_auth.erl | 10 +- src/eradius_client.erl | 232 +++++++++--------- src/eradius_client_socket.erl | 6 +- src/eradius_config.erl | 22 +- src/eradius_counter.erl | 66 ++--- src/eradius_counter_aggregator.erl | 112 ++++----- src/eradius_dict.erl | 28 +-- src/eradius_eap_packet.erl | 28 +-- src/eradius_lib.erl | 114 ++++----- src/eradius_log.erl | 74 +++--- src/eradius_node_mon.erl | 20 +- src/eradius_proxy.erl | 82 +++---- src/eradius_server.erl | 64 +++-- src/eradius_server_mon.erl | 56 ++--- test/eradius_auth_SUITE.erl | 70 +++--- test/eradius_client_SUITE.erl | 120 ++++----- test/eradius_client_socket_test.erl | 42 ++-- test/eradius_config_SUITE.erl | 212 ++++++++-------- test/eradius_lib_SUITE.erl | 44 ++-- test/eradius_logtest.erl | 52 ++-- test/eradius_metrics_SUITE.erl | 50 ++-- test/eradius_proxy_SUITE.erl | 66 ++--- test/eradius_test.hrl | 12 +- test/eradius_test_handler.erl | 6 +- 37 files changed, 1071 insertions(+), 1047 deletions(-) diff --git a/applications/eradius_prometheus_collector/rebar.config b/applications/eradius_prometheus_collector/rebar.config index b767a711..d07757cd 100644 --- a/applications/eradius_prometheus_collector/rebar.config +++ b/applications/eradius_prometheus_collector/rebar.config @@ -1,3 +1,3 @@ {deps, [ - {prometheus, "4.10.0"} -]}. + {prometheus, "4.10.0"} + ]}. diff --git a/applications/eradius_prometheus_collector/src/eradius_prometheus_collector.app.src b/applications/eradius_prometheus_collector/src/eradius_prometheus_collector.app.src index 76e7b648..9c1762e5 100644 --- a/applications/eradius_prometheus_collector/src/eradius_prometheus_collector.app.src +++ b/applications/eradius_prometheus_collector/src/eradius_prometheus_collector.app.src @@ -9,4 +9,4 @@ {env,[]}, {modules, [eradius_prometheus_collector]}, {licenses, ["MIT"]} -]}. + ]}. diff --git a/applications/eradius_prometheus_collector/src/eradius_prometheus_collector.erl b/applications/eradius_prometheus_collector/src/eradius_prometheus_collector.erl index 31cc6cc1..fd2b1546 100644 --- a/applications/eradius_prometheus_collector/src/eradius_prometheus_collector.erl +++ b/applications/eradius_prometheus_collector/src/eradius_prometheus_collector.erl @@ -80,15 +80,15 @@ collect_metrics(_, {PromMetricType, Fun, Stats}) -> fetch_histogram(Name, Labels) -> try lists:flatten(lists:map(fun ({LabelsFromStat, Buckets, DurationUnit}) -> - case compare_labels(Labels, LabelsFromStat) of - true -> - {Buckets1, Values} = lists:unzip(Buckets), - Values1 = augment_counters(Values), - Buckets2 = lists:zip(Buckets1, Values1), - {Buckets2, LabelsFromStat, DurationUnit}; - _ -> [] - end - end, prometheus_histogram:values(default, Name))) + case compare_labels(Labels, LabelsFromStat) of + true -> + {Buckets1, Values} = lists:unzip(Buckets), + Values1 = augment_counters(Values), + Buckets2 = lists:zip(Buckets1, Values1), + {Buckets2, LabelsFromStat, DurationUnit}; + _ -> [] + end + end, prometheus_histogram:values(default, Name))) catch _:_ -> [] end. fetch_counter(Name, Labels) -> @@ -98,45 +98,45 @@ fetch_counter(Name, Labels) -> fetch_counter(uptime_milliseconds, Stat, Labels) -> {{ServerMetrics, _}, _, _} = Stat, lists:flatten(lists:map(fun (#server_counter{} = Cnt) -> - fetch_server_value(Labels, Cnt, fun () -> erlang:system_time(milli_seconds) - Cnt#server_counter.startTime end) - end, ServerMetrics)); + fetch_server_value(Labels, Cnt, fun () -> erlang:system_time(milli_seconds) - Cnt#server_counter.startTime end) + end, ServerMetrics)); fetch_counter(since_last_reset_milliseconds, Stat, Labels) -> {{ServerMetrics, _}, _, _} = Stat, lists:flatten(lists:map(fun (#server_counter{} = Cnt) -> - fetch_server_value(Labels, Cnt, fun () -> erlang:system_time(milli_seconds) - Cnt#server_counter.resetTime end) - end, ServerMetrics)); + fetch_server_value(Labels, Cnt, fun () -> erlang:system_time(milli_seconds) - Cnt#server_counter.resetTime end) + end, ServerMetrics)); fetch_counter(Name, Stat, Labels) -> case get_metric_info(Name, Stat) of {_, undefined, _} -> []; {Metrics, MetricIdx, RadiusMetricType} -> lists:flatten(lists:map(fun (Cnt) -> - case get_labels_and_val(MetricIdx, {Cnt, RadiusMetricType}, {Name, Labels}) of - {Value, LabelsFromStat} -> - case compare_labels(Labels, LabelsFromStat) of - true -> {Value, LabelsFromStat}; - _ -> [] - end; - List -> - lists:map(fun ({Value, LabelsFromStat}) -> - case compare_labels(Labels, LabelsFromStat) of - true -> {Value, LabelsFromStat}; - _ -> [] - end - end, List) - end - end, Metrics)) + case get_labels_and_val(MetricIdx, {Cnt, RadiusMetricType}, {Name, Labels}) of + {Value, LabelsFromStat} -> + case compare_labels(Labels, LabelsFromStat) of + true -> {Value, LabelsFromStat}; + _ -> [] + end; + List -> + lists:map(fun ({Value, LabelsFromStat}) -> + case compare_labels(Labels, LabelsFromStat) of + true -> {Value, LabelsFromStat}; + _ -> [] + end + end, List) + end + end, Metrics)) end. %% from prometheus.erl as prometheus_histogram:values/1 returns %% non-cumulative values augment_counters([Start | Counters]) -> - augment_counters(Counters, [Start], Start). + augment_counters(Counters, [Start], Start). augment_counters([], LAcc, _CAcc) -> - LAcc; + LAcc; augment_counters([Counter | Counters], LAcc, CAcc) -> - augment_counters(Counters, LAcc ++ [CAcc + Counter], CAcc + Counter). + augment_counters(Counters, LAcc ++ [CAcc + Counter], CAcc + Counter). build_metric(uptime_milliseconds, Type, Stat) -> {{ServerMetrics, _}, _, _} = Stat, @@ -154,25 +154,25 @@ build_metric(MetricName, Type, Stat) MetricName =:= accounting_requests_total; MetricName =:= accounting_responses_total -> lists:flatten(lists:map(fun (AcctType) -> - case get_metric_info(MetricName, Stat) of - {_, undefined, _} -> - []; - {Metrics, MetricIdx, RadiusMetricType} -> - lists:map(fun (Cnt) -> - {Value, Labels} = get_labels_and_val(MetricIdx, {Cnt, RadiusMetricType}, {MetricName, [{acct_type, AcctType}]}), - metric(Type, Value, Labels) - end, Metrics) - end - end, [start, stop, update])); + case get_metric_info(MetricName, Stat) of + {_, undefined, _} -> + []; + {Metrics, MetricIdx, RadiusMetricType} -> + lists:map(fun (Cnt) -> + {Value, Labels} = get_labels_and_val(MetricIdx, {Cnt, RadiusMetricType}, {MetricName, [{acct_type, AcctType}]}), + metric(Type, Value, Labels) + end, Metrics) + end + end, [start, stop, update])); build_metric(MetricName, Type, Stat) -> case get_metric_info(MetricName, Stat) of {_, undefined, _} -> []; {Metrics, MetricIdx, RadiusMetricType} -> lists:flatten(lists:map(fun (Cnt) -> - {Value, Labels} = get_labels_and_val(MetricIdx, {Cnt, RadiusMetricType}, {}), - metric(Type, Value, Labels) - end, Metrics)) + {Value, Labels} = get_labels_and_val(MetricIdx, {Cnt, RadiusMetricType}, {}), + metric(Type, Value, Labels) + end, Metrics)) end. metric(_, [], []) -> undefined; @@ -281,9 +281,9 @@ get_labels_and_val(_, {#nas_counter{} = Cnt, server}, {Name, Labels}) case get_value(Name, Type, Cnt) of undefined -> lists:map(fun (AcctType) -> - ResLabels = get_labels(Cnt, ServerIP, ServerPort, NasId, NasIP), - {get_value(Name, AcctType, Cnt), [{acct_type, AcctType} | ResLabels]} - end, ?ACCT_TYPES); + ResLabels = get_labels(Cnt, ServerIP, ServerPort, NasId, NasIP), + {get_value(Name, AcctType, Cnt), [{acct_type, AcctType} | ResLabels]} + end, ?ACCT_TYPES); Value -> ResLabels = get_labels(Cnt, ServerIP, ServerPort, NasId, NasIP), {Value, [{acct_type, Type} | ResLabels]} @@ -299,16 +299,16 @@ get_labels_and_val(_, {#client_counter{} = Cnt, client}, {Name, Labels}) case get_value(Name, Type, Cnt) of undefined -> lists:map(fun (AcctType) -> - ResLabels = get_labels(Cnt, ServerIP, ServerPort, ClientName, ClientIP), - {get_value(Name, AcctType, Cnt), [{acct_type, AcctType} | ResLabels]} - end, ?ACCT_TYPES); + ResLabels = get_labels(Cnt, ServerIP, ServerPort, ClientName, ClientIP), + {get_value(Name, AcctType, Cnt), [{acct_type, AcctType} | ResLabels]} + end, ?ACCT_TYPES); Value -> ResLabels = get_labels(Cnt, ServerIP, ServerPort, ClientName, ClientIP), {Value, [{acct_type, Type} | ResLabels]} end; get_labels_and_val(MetricIdx, {#client_counter{} = Cnt, client}, _) -> - {{ClientName, ClientIP, _ClientPort}, {_, ServerIP, ServerPort}} = Cnt#client_counter.key, - {element(MetricIdx + 1, Cnt), get_labels(Cnt, ServerIP, ServerPort, ClientName, ClientIP)}; + {{ClientName, ClientIP, _ClientPort}, {_, ServerIP, ServerPort}} = Cnt#client_counter.key, + {element(MetricIdx + 1, Cnt), get_labels(Cnt, ServerIP, ServerPort, ClientName, ClientIP)}; get_labels_and_val(_, _, _) -> {[], []}. diff --git a/dicts_compiler.erl b/dicts_compiler.erl index 1e1c360b..2aad5743 100755 --- a/dicts_compiler.erl +++ b/dicts_compiler.erl @@ -15,16 +15,16 @@ main(_) -> ok. compile() -> case find_files("priv/dictionaries", "^dictionary.*") of - [] -> + [] -> ok; Dictionaries0 -> {ok, Basedir} = file:get_cwd(), IncludeDir = filename:join([Basedir, "include"]), ok = filelib:ensure_dir(IncludeDir), - % sort dictionaries in alphabetical order to be sure that - % basic `priv/dictionaries/dictionary` builds first - % because it contains some attributes which may be needed - % for vendor's dictionaries + %% sort dictionaries in alphabetical order to be sure that + %% basic `priv/dictionaries/dictionary` builds first + %% because it contains some attributes which may be needed + % for vendor's dictionaries Dictionaries = lists:sort(Dictionaries0), Targets = [{Dictionary, out_files(Dictionary)} || Dictionary <- Dictionaries], compile_each(Targets) @@ -32,7 +32,7 @@ compile() -> clean() -> case find_files("priv/dictionaries", "^dictionary.*") of - [] -> + [] -> ok; Dictionaries -> Targets = [{Dictionary, out_files(Dictionary)} || Dictionary <- Dictionaries], @@ -87,12 +87,12 @@ clean_each([{_Dictionary, {Headerfile, Mapfile}}|Rest]) -> %%% -------------------------------------------------------------------- emit([A|T], {HrlPid, _} = Hrl, Map) when is_record(A, attribute) -> io:format(HrlPid, "-define( '~s' , ~w ).~n", - [d2u(A#attribute.name), A#attribute.id]), + [d2u(A#attribute.name), A#attribute.id]), io:format(Map, "~110p.~n", [A]), emit(T, Hrl, Map); emit([V|T], {HrlPid, _} = Hrl, Map) when is_record(V, vendor) -> io:format(HrlPid, "-define( '~s' , ~w ).~n", - [d2u(V#vendor.name), V#vendor.type]), + [d2u(V#vendor.name), V#vendor.type]), io:format(Map, "~p.~n", [V]), emit(T, Hrl, Map); emit([V|T], Hrl, Map) when is_record(V, value) -> @@ -101,8 +101,8 @@ emit([V|T], Hrl, Map) when is_record(V, value) -> emit([{include, Include}|T], {HrlPid, _} = Hrl, Map) -> EscapedInclude = re:replace(Include, "\\.", "_", [global, {return, list}]), io:format(HrlPid, "-include( \"~s.hrl\" ).~n", [EscapedInclude]), - % No need to add ".map" extension here. It will be added by eradius_dict - % server automatically. + %% No need to add ".map" extension here. It will be added by eradius_dict + %% server automatically. io:format(Map, "{include, ~p}.~n", [EscapedInclude]), emit(T, Hrl, Map); emit([header|T], {HrlPid, HrlName} = Hrl, Map) -> @@ -122,20 +122,20 @@ emit([], _, _) -> parse_dict(File) when is_list(File) -> {ok,B} = file:read_file(File), F = fun(Line,{undefined = Vendor, AccList}) -> - case pd(string:tokens(Line,"\s\t\r")) of - {ok,E} -> {Vendor, [E|AccList]}; - {include,Hrl} -> {Vendor, [{include, Hrl}|AccList]}; - {begin_vendor, VendId} -> {{vendor, VendId}, AccList}; - _ -> {Vendor, AccList} - end; - (Line, {{vendor, VendId} = Vendor, AccList}) -> - case pd(string:tokens(Line, "\s\t\r"), VendId) of - {end_vendor} -> {undefined, AccList}; - {include,Hrl} -> {Vendor, [{include, Hrl}|AccList]}; - {ok,E} -> {Vendor, [E|AccList]}; - _ -> {Vendor, AccList} - end - end, + case pd(string:tokens(Line,"\s\t\r")) of + {ok,E} -> {Vendor, [E|AccList]}; + {include,Hrl} -> {Vendor, [{include, Hrl}|AccList]}; + {begin_vendor, VendId} -> {{vendor, VendId}, AccList}; + _ -> {Vendor, AccList} + end; + (Line, {{vendor, VendId} = Vendor, AccList}) -> + case pd(string:tokens(Line, "\s\t\r"), VendId) of + {end_vendor} -> {undefined, AccList}; + {include,Hrl} -> {Vendor, [{include, Hrl}|AccList]}; + {ok,E} -> {Vendor, [E|AccList]}; + _ -> {Vendor, AccList} + end + end, {_, L} = lists:foldl(F,{undefined, []},string:tokens(b2l(B),"\n")), [header|L] ++ [footer]. @@ -170,7 +170,7 @@ parse_attribute_attrs(Attr, [Attribute|Tail]) -> parse_attribute_attrs(NewAttr, Rest ++ Tail). pd(["$INCLUDE", Name]) -> - {include, Name}; + {include, Name}; pd(["BEGIN-VENDOR", Name]) -> case get({vendor, Name}) of @@ -240,6 +240,6 @@ d2u(L) when is_list(L) -> repl(L,X,Y) when is_list(L) -> F = fun(Z) when Z == X -> Y; - (C) -> C - end, + (C) -> C + end, lists:map(F,L). diff --git a/include/eradius_dict.hrl b/include/eradius_dict.hrl index 4b1dc872..c2d12678 100644 --- a/include/eradius_dict.hrl +++ b/include/eradius_dict.hrl @@ -1,16 +1,16 @@ -record(attribute, { - id :: eradius_dict:attribute_id(), - type = 'octets' :: eradius_dict:attribute_type(), - name :: string(), - enc = 'no' :: eradius_dict:attribute_encryption() -}). + id :: eradius_dict:attribute_id(), + type = 'octets' :: eradius_dict:attribute_type(), + name :: string(), + enc = 'no' :: eradius_dict:attribute_encryption() + }). -record(vendor, { - type :: eradius_dict:vendor_id(), - name :: string() -}). + type :: eradius_dict:vendor_id(), + name :: string() + }). -record(value, { - id :: eradius_dict:value_id(), - name :: string() -}). + id :: eradius_dict:value_id(), + name :: string() + }). diff --git a/include/eradius_lib.hrl b/include/eradius_lib.hrl index eef81b4f..e5236be4 100644 --- a/include/eradius_lib.hrl +++ b/include/eradius_lib.hrl @@ -57,100 +57,100 @@ -define(RTCNAS_Reboot, 11). -record(client_counter, { - key :: term(), - requests = 0 :: non_neg_integer(), - replies = 0 :: non_neg_integer(), - accessRequests = 0 :: non_neg_integer(), - accessAccepts = 0 :: non_neg_integer(), - accessRejects = 0 :: non_neg_integer(), - accessChallenges = 0 :: non_neg_integer(), - accountRequestsStart = 0 :: non_neg_integer(), - accountRequestsStop = 0 :: non_neg_integer(), - accountRequestsUpdate = 0 :: non_neg_integer(), - accountResponsesStart = 0 :: non_neg_integer(), - accountResponsesStop = 0 :: non_neg_integer(), - accountResponsesUpdate = 0 :: non_neg_integer(), - coaRequests = 0 :: non_neg_integer(), - coaAcks = 0 :: non_neg_integer(), - coaNaks = 0 :: non_neg_integer(), - discRequests = 0 :: non_neg_integer(), - discAcks = 0 :: non_neg_integer(), - discNaks = 0 :: non_neg_integer(), - retransmissions = 0 :: non_neg_integer(), - badAuthenticators = 0 :: non_neg_integer(), - packetsDropped = 0 :: non_neg_integer(), - unknownTypes = 0 :: non_neg_integer(), - server_name :: string(), - pending = 0 :: non_neg_integer(), - timeouts = 0 :: non_neg_integer() - }). + key :: term(), + requests = 0 :: non_neg_integer(), + replies = 0 :: non_neg_integer(), + accessRequests = 0 :: non_neg_integer(), + accessAccepts = 0 :: non_neg_integer(), + accessRejects = 0 :: non_neg_integer(), + accessChallenges = 0 :: non_neg_integer(), + accountRequestsStart = 0 :: non_neg_integer(), + accountRequestsStop = 0 :: non_neg_integer(), + accountRequestsUpdate = 0 :: non_neg_integer(), + accountResponsesStart = 0 :: non_neg_integer(), + accountResponsesStop = 0 :: non_neg_integer(), + accountResponsesUpdate = 0 :: non_neg_integer(), + coaRequests = 0 :: non_neg_integer(), + coaAcks = 0 :: non_neg_integer(), + coaNaks = 0 :: non_neg_integer(), + discRequests = 0 :: non_neg_integer(), + discAcks = 0 :: non_neg_integer(), + discNaks = 0 :: non_neg_integer(), + retransmissions = 0 :: non_neg_integer(), + badAuthenticators = 0 :: non_neg_integer(), + packetsDropped = 0 :: non_neg_integer(), + unknownTypes = 0 :: non_neg_integer(), + server_name :: string(), + pending = 0 :: non_neg_integer(), + timeouts = 0 :: non_neg_integer() + }). -record(nas_counter, { - key :: term(), - requests = 0 :: non_neg_integer(), - replies = 0 :: non_neg_integer(), - dupRequests = 0 :: non_neg_integer(), - malformedRequests = 0 :: non_neg_integer(), - accessRequests = 0 :: non_neg_integer(), - accessAccepts = 0 :: non_neg_integer(), - accessRejects = 0 :: non_neg_integer(), - accessChallenges = 0 :: non_neg_integer(), - accountRequestsStart = 0 :: non_neg_integer(), - accountRequestsStop = 0 :: non_neg_integer(), - accountRequestsUpdate = 0 :: non_neg_integer(), - accountResponsesStart = 0 :: non_neg_integer(), - accountResponsesStop = 0 :: non_neg_integer(), - accountResponsesUpdate = 0 :: non_neg_integer(), - noRecords = 0 :: non_neg_integer(), - badAuthenticators = 0 :: non_neg_integer(), - packetsDropped = 0 :: non_neg_integer(), - unknownTypes = 0 :: non_neg_integer(), - handlerFailure = 0 :: non_neg_integer(), - coaRequests = 0 :: non_neg_integer(), - coaAcks = 0 :: non_neg_integer(), - coaNaks = 0 :: non_neg_integer(), - discRequests = 0 :: non_neg_integer(), - discAcks = 0 :: non_neg_integer(), - discNaks = 0 :: non_neg_integer(), - retransmissions = 0 :: non_neg_integer(), - server_name :: string(), - pending = 0 :: non_neg_integer() - }). + key :: term(), + requests = 0 :: non_neg_integer(), + replies = 0 :: non_neg_integer(), + dupRequests = 0 :: non_neg_integer(), + malformedRequests = 0 :: non_neg_integer(), + accessRequests = 0 :: non_neg_integer(), + accessAccepts = 0 :: non_neg_integer(), + accessRejects = 0 :: non_neg_integer(), + accessChallenges = 0 :: non_neg_integer(), + accountRequestsStart = 0 :: non_neg_integer(), + accountRequestsStop = 0 :: non_neg_integer(), + accountRequestsUpdate = 0 :: non_neg_integer(), + accountResponsesStart = 0 :: non_neg_integer(), + accountResponsesStop = 0 :: non_neg_integer(), + accountResponsesUpdate = 0 :: non_neg_integer(), + noRecords = 0 :: non_neg_integer(), + badAuthenticators = 0 :: non_neg_integer(), + packetsDropped = 0 :: non_neg_integer(), + unknownTypes = 0 :: non_neg_integer(), + handlerFailure = 0 :: non_neg_integer(), + coaRequests = 0 :: non_neg_integer(), + coaAcks = 0 :: non_neg_integer(), + coaNaks = 0 :: non_neg_integer(), + discRequests = 0 :: non_neg_integer(), + discAcks = 0 :: non_neg_integer(), + discNaks = 0 :: non_neg_integer(), + retransmissions = 0 :: non_neg_integer(), + server_name :: string(), + pending = 0 :: non_neg_integer() + }). -record(server_counter, { - key :: term(), - server_name :: string(), - startTime :: erlang:timestamp(), - resetTime :: erlang:timestamp(), - invalidRequests = 0 :: non_neg_integer(), - discardNoHandler = 0 :: non_neg_integer() -}). + key :: term(), + server_name :: string(), + startTime :: erlang:timestamp(), + resetTime :: erlang:timestamp(), + invalidRequests = 0 :: non_neg_integer(), + discardNoHandler = 0 :: non_neg_integer() + }). -record(nas_prop, { - server_ip :: inet:ip_address(), - server_port :: eradius_server:port_number(), - nas_id :: term(), - nas_ip :: inet:ip_address(), - nas_port :: eradius_server:port_number(), - metrics_info :: {atom_address(), atom_address()}, - secret :: eradius_lib:secret(), - handler_nodes = local :: list(atom()) | local -}). + server_ip :: inet:ip_address(), + server_port :: eradius_server:port_number(), + nas_id :: term(), + nas_ip :: inet:ip_address(), + nas_port :: eradius_server:port_number(), + metrics_info :: {atom_address(), atom_address()}, + secret :: eradius_lib:secret(), + handler_nodes = local :: list(atom()) | local + }). -record(radius_request, { - reqid = 0 :: byte(), - cmd = request :: eradius_lib:command(), - attrs = [] :: eradius_lib:attribute_list(), - secret :: eradius_lib:secret(), - authenticator :: eradius_lib:authenticator(), - msg_hmac = false :: boolean(), - eap_msg = <<>> :: binary() -}). + reqid = 0 :: byte(), + cmd = request :: eradius_lib:command(), + attrs = [] :: eradius_lib:attribute_list(), + secret :: eradius_lib:secret(), + authenticator :: eradius_lib:authenticator(), + msg_hmac = false :: boolean(), + eap_msg = <<>> :: binary() + }). -record(decoder_state, { - request_authenticator :: 'undefined' | binary(), - attrs = [] :: eradius_lib:attribute_list(), - hmac_pos :: 'undefined' | non_neg_integer(), - eap_msg = [] :: [binary()] -}). + request_authenticator :: 'undefined' | binary(), + attrs = [] :: eradius_lib:attribute_list(), + hmac_pos :: 'undefined' | non_neg_integer(), + eap_msg = [] :: [binary()] + }). diff --git a/priv/dev.config b/priv/dev.config index b93ff762..14ebf2f0 100644 --- a/priv/dev.config +++ b/priv/dev.config @@ -1,29 +1,29 @@ -%-*-Erlang-*- + %-*-Erlang-*- {kernel, [{logger, [{handler, default, logger_std_h, #{level => info, config => - #{sync_mode_qlen => 10000, - drop_mode_qlen => 10000, - flush_qlen => 10000} + #{sync_mode_qlen => 10000, + drop_mode_qlen => 10000, + flush_qlen => 10000} } } ]} ]}. {eradius, [ - {description, "Erlang RADIUS server"}, - {registered, [eradius_dict, eradius_sup, eradius_server_top_sup, eradius_server_sup, eradius_server_mon]}, - {applications, [kernel, stdlib, crypto]}, - {mod, {eradius, []}}, - {env, [ - {servers, []}, - {logging, true}, - {logfile, "./radius.log"}, - {tables, [dictionary]}, - {client_ip, undefined}, - {client_ports, 20}, - {resend_timeout, 30000} - ]} -]}. + {description, "Erlang RADIUS server"}, + {registered, [eradius_dict, eradius_sup, eradius_server_top_sup, eradius_server_sup, eradius_server_mon]}, + {applications, [kernel, stdlib, crypto]}, + {mod, {eradius, []}}, + {env, [ + {servers, []}, + {logging, true}, + {logfile, "./radius.log"}, + {tables, [dictionary]}, + {client_ip, undefined}, + {client_ports, 20}, + {resend_timeout, 30000} + ]} + ]}. diff --git a/rebar.config b/rebar.config index 237c1725..be584e49 100644 --- a/rebar.config +++ b/rebar.config @@ -1,22 +1,24 @@ -%-*-Erlang-*- +%%-*-Erlang-*- {erl_opts, [debug_info]}. {minimum_otp_vsn, "22"}. {pre_hooks, [{compile, "escript dicts_compiler.erl compile"}, - {clean, "escript dicts_compiler.erl clean"}]}. + {clean, "escript dicts_compiler.erl clean"}]}. {profiles, [ - {test, [ - {erl_opts, [nowarn_export_all]}, + {test, [ + {erl_opts, [nowarn_export_all]}, {project_app_dirs, ["applications/*", "src/*", "."]}, - {deps, [{meck, "0.9.0"}]} - ]} - ]}. + {deps, [{meck, "0.9.0"}]} + ]} + ]}. + +{project_plugins, [rebar3_fmt]}. %% xref checks to run {xref_checks, [undefined_function_calls, undefined_functions, - locals_not_used, deprecated_function_calls, - deprecated_functions]}. + locals_not_used, deprecated_function_calls, + deprecated_functions]}. {xref_ignores, [{prometheus_histogram, declare, 1}, {prometheus_histogram, observe, 3}, diff --git a/sample/example.config b/sample/example.config index 0ce5d579..5371dff7 100644 --- a/sample/example.config +++ b/sample/example.config @@ -1,16 +1,16 @@ %% vim: ft=erlang [{eradius, [ - {client_ip, {127, 0, 0, 1}}, - {client_ports, 256}, - {resend_timeout, 500}, - {recbuf, 4000000}, - {tables, [dictionary]}, - {session_nodes, local}, - {radius_callback, eradius_server_sample}, - {root, [ - {{"root", []}, [{"127.0.0.1", "secret"}]} - ]}, - {servers, [ - {root, {"127.0.0.1", [1812, 1813]}} - ]} -]}]. + {client_ip, {127, 0, 0, 1}}, + {client_ports, 256}, + {resend_timeout, 500}, + {recbuf, 4000000}, + {tables, [dictionary]}, + {session_nodes, local}, + {radius_callback, eradius_server_sample}, + {root, [ + {{"root", []}, [{"127.0.0.1", "secret"}]} + ]}, + {servers, [ + {root, {"127.0.0.1", [1812, 1813]}} + ]} + ]}]. diff --git a/sample/rebar.config b/sample/rebar.config index e921f643..630facb2 100644 --- a/sample/rebar.config +++ b/sample/rebar.config @@ -1,3 +1,3 @@ {deps, [ - eradius -]}. + eradius + ]}. diff --git a/sample/src/eradius_server_sample.app.src b/sample/src/eradius_server_sample.app.src index 175d17f1..23c55315 100644 --- a/sample/src/eradius_server_sample.app.src +++ b/sample/src/eradius_server_sample.app.src @@ -1,10 +1,10 @@ %% vim: ft=erlang {application, eradius_server_sample, [ - {description, "Erlang RADIUS sample server"}, - {vsn, "0.0.1"}, - {registered, []}, - {applications, [kernel, stdlib, eradius]}, - {mod, {eradius_server_sample, []}}, - {env, []}, - {modules, []} -]}. + {description, "Erlang RADIUS sample server"}, + {vsn, "0.0.1"}, + {registered, []}, + {applications, [kernel, stdlib, eradius]}, + {mod, {eradius_server_sample, []}}, + {env, []}, + {modules, []} + ]}. diff --git a/sample/src/eradius_server_sample.erl b/sample/src/eradius_server_sample.erl index b0207736..6af51c73 100644 --- a/sample/src/eradius_server_sample.erl +++ b/sample/src/eradius_server_sample.erl @@ -32,12 +32,12 @@ test() -> do_test(Count) -> Tasks = [run_task(fun() -> - Port = erlang:phash2(self(), 1) + 1812, % 1812, 1813 - {ok, R, A} = eradius_client:send_request({{127,0,0,1}, Port, "secret"}, - #radius_request{cmd = request}, []), - #radius_request{} = eradius_lib:decode_request(R, <<"secret">>, A) + Port = erlang:phash2(self(), 1) + 1812, % 1812, 1813 + {ok, R, A} = eradius_client:send_request({{127,0,0,1}, Port, "secret"}, + #radius_request{cmd = request}, []), + #radius_request{} = eradius_lib:decode_request(R, <<"secret">>, A) end) - || _ <- lists:seq(1, Count)], + || _ <- lists:seq(1, Count)], [await(Task) || Task <- Tasks], Count. diff --git a/src/eradius.app.src b/src/eradius.app.src index ced16030..3af15b11 100644 --- a/src/eradius.app.src +++ b/src/eradius.app.src @@ -1,31 +1,31 @@ %% vim: ft=erlang {application, eradius, [ - {description, "Erlang RADIUS server"}, - {vsn, semver}, - {registered, [eradius_dict, eradius_sup, eradius_server_top_sup, eradius_server_sup, eradius_server_mon]}, - {applications, [kernel, stdlib, crypto]}, - {mod, {eradius, []}}, - {env, [ - {servers, []}, - {tables, [dictionary]}, - {client_ip, undefined}, - {client_ports, 100}, - {resend_timeout, 30000}, - {logging, false}, - {counter_aggregator, false}, - {server_status_metrics_enabled, false}, - {logfile, "./radius.log"}, - {recbuf, 8192}, - {sndbuf, 131072} - ]}, - {maintainers, ["Andreas Schultz", "Vladimir Tarasenko", "Yury Gargay"]}, - {licenses, ["MIT"]}, - {links, [{"Github", "https://github.com/travelping/eradius"}]}, - %% List copied from rebar3_hex.hrl ?DEFAULT_FILES, adding "Makefile" - {files, ["applications", "src", "c_src", "include/eradius_*.hrl", "rebar.config.script" - ,"priv/dictionaries", "rebar.config", "rebar.lock" - ,"README*", "readme*" - ,"LICENSE*", "license*" - ,"NOTICE" - ,"dicts_compiler.erl"]} -]}. + {description, "Erlang RADIUS server"}, + {vsn, semver}, + {registered, [eradius_dict, eradius_sup, eradius_server_top_sup, eradius_server_sup, eradius_server_mon]}, + {applications, [kernel, stdlib, crypto]}, + {mod, {eradius, []}}, + {env, [ + {servers, []}, + {tables, [dictionary]}, + {client_ip, undefined}, + {client_ports, 100}, + {resend_timeout, 30000}, + {logging, false}, + {counter_aggregator, false}, + {server_status_metrics_enabled, false}, + {logfile, "./radius.log"}, + {recbuf, 8192}, + {sndbuf, 131072} + ]}, + {maintainers, ["Andreas Schultz", "Vladimir Tarasenko", "Yury Gargay"]}, + {licenses, ["MIT"]}, + {links, [{"Github", "https://github.com/travelping/eradius"}]}, + %% List copied from rebar3_hex.hrl ?DEFAULT_FILES, adding "Makefile" + {files, ["applications", "src", "c_src", "include/eradius_*.hrl", "rebar.config.script" + ,"priv/dictionaries", "rebar.config", "rebar.lock" + ,"README*", "readme*" + ,"LICENSE*", "license*" + ,"NOTICE" + ,"dicts_compiler.erl"]} + ]}. diff --git a/src/eradius_auth.erl b/src/eradius_auth.erl index 6124f2a9..bc3f7e63 100644 --- a/src/eradius_auth.erl +++ b/src/eradius_auth.erl @@ -22,7 +22,7 @@ %% @doc check the request password using all available authentication mechanisms. %% Tries CHAP, then MS-CHAP, then MS-CHAPv2, finally PAP. -spec check_password(binary(), #radius_request{}) -> - false | {boolean(), eradius_lib:attribute_list()}. + false | {boolean(), eradius_lib:attribute_list()}. check_password(Password, Req = #radius_request{authenticator = Authenticator, attrs = AVPs}) -> case lookup_auth_attrs(AVPs) of {false, Chap_pass, false, false, false, false} -> @@ -191,7 +191,7 @@ ms_chap(Passwd, Challenge, <<_Ident:1/binary, Flags:1/integer-unit:8, LMResponse NTResponse =:= challenge_response(Challenge, NtPasswdHash); true -> false - end, + end, Resp2 = if Resp1 == false -> LMResponse =:= challenge_response(Challenge, LmPasswdHash); @@ -282,9 +282,9 @@ v2_magic2() -> %% ------------------------------------------------------------------------------------------ %% -- MPPE magic constants when used with MS-CHAP-V2 mppe_magic1() -> - <<16#54, 16#68, 16#69, 16#73, 16#20, 16#69, 16#73, 16#20, 16#74, - 16#68, 16#65, 16#20, 16#4d, 16#50, 16#50, 16#45, 16#20, 16#4d, - 16#61, 16#73, 16#74, 16#65, 16#72, 16#20, 16#4b, 16#65, 16#79>>. + <<16#54, 16#68, 16#69, 16#73, 16#20, 16#69, 16#73, 16#20, 16#74, + 16#68, 16#65, 16#20, 16#4d, 16#50, 16#50, 16#45, 16#20, 16#4d, + 16#61, 16#73, 16#74, 16#65, 16#72, 16#20, 16#4b, 16#65, 16#79>>. mppe_magic2() -> <<16#4F, 16#6E, 16#20, 16#74, 16#68, 16#65, 16#20, 16#63, 16#6C, 16#69, diff --git a/src/eradius_client.erl b/src/eradius_client.erl index da192b84..6a8f2513 100644 --- a/src/eradius_client.erl +++ b/src/eradius_client.erl @@ -50,19 +50,19 @@ %% ------------------------------------------------------------------------------------------ %% -- API -% @private +%% @private start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -% @equiv send_request(NAS, Request, []) +%% @equiv send_request(NAS, Request, []) -spec send_request(nas_address(), #radius_request{}) -> {ok, binary()} | {error, 'timeout' | 'socket_down'}. 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. +%% @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()) -> - {ok, binary(), eradius_lib:authenticator()} | {error, 'timeout' | 'socket_down'}. + {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); @@ -77,17 +77,17 @@ send_request({IP, Port, Secret}, Request, Options) when ?GOOD_CMD(Request) andal 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} = gen_server:call(?SERVER, {wanna_send, Peer, MetricsInfo}), - 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 + Peer = {ServerName, {IP, Port}}, + update_client_requests(MetricsInfo), + {Socket, ReqId} = gen_server:call(?SERVER, {wanna_send, Peer, MetricsInfo}), + 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(); @@ -98,21 +98,21 @@ send_request({IP, Port, Secret}, Request, Options) when ?GOOD_CMD(Request) andal {{IP, Port, Secret, Options}, _NewPool} -> SendReqFn(); {{NewPeerAddr, NewPeerPort, NewPeerSecret, PeerOpts}, NewPool} -> - % current server is not in list of active servers, so use another one + %% current server is not in list of active servers, so use another one send_request({NewPeerAddr, NewPeerPort, NewPeerSecret}, Request, [{failover, NewPool} | PeerOpts]) end end; send_request({_IP, _Port, _Secret}, _Request, _Options) -> error(badarg). -% @equiv send_remote_request(Node, NAS, Request, []) +%% @equiv send_remote_request(Node, NAS, Request, []) -spec send_remote_request(node(), nas_address(), #radius_request{}) -> {ok, binary()} | {error, 'timeout' | 'node_down' | 'socket_down'}. send_remote_request(Node, NAS, Request) -> send_remote_request(Node, NAS, Request, []). -% @doc Send a radius request to the given NAS through a socket on the specified node. -% If no answer is received within the specified timeout, the request will be sent again. -% The request will not be sent again if the remote node is unreachable. +%% @doc Send a radius request to the given NAS through a socket on the specified node. +%% If no answer is received within the specified timeout, the request will be sent again. +%% The request will not be sent again if the remote node is unreachable. -spec send_remote_request(node(), nas_address(), #radius_request{}, options()) -> {ok, binary()} | {error, 'timeout' | 'node_down' | 'socket_down'}. send_remote_request(Node, {IP, Port, Secret}, Request, Options) when ?GOOD_CMD(Request) -> TS1 = erlang:monotonic_time(), @@ -132,15 +132,15 @@ send_remote_request(Node, {IP, Port, Secret}, Request, Options) when ?GOOD_CMD(R Retries = proplists:get_value(retries, Options, ?DEFAULT_RETRIES), Timeout = proplists:get_value(timeout, Options, ?DEFAULT_TIMEOUT), SenderPid = spawn(Node, ?MODULE, send_remote_request_loop, - [self(), Socket, ReqId, Peer, Request1, Retries, Timeout, MetricsInfo]), + [self(), Socket, ReqId, Peer, Request1, Retries, Timeout, MetricsInfo]), SenderMonitor = monitor(process, SenderPid), Response = receive - {SenderPid, Result} -> - erlang:demonitor(SenderMonitor, [flush]), - Result; - {'DOWN', SenderMonitor, process, SenderPid, _Reason} -> - {error, socket_down} - end, + {SenderPid, Result} -> + erlang:demonitor(SenderMonitor, [flush]), + Result; + {'DOWN', SenderMonitor, process, SenderPid, _Reason} -> + {error, socket_down} + end, proceed_response(Request, Response, Peer, TS1, MetricsInfo, Options) catch exit:{{nodedown, Node}, _} -> @@ -196,18 +196,18 @@ handle_failed_request(Request, {ServerIP, Port} = _FailedServer, UpstreamServers case ets:lookup(?MODULE, {ServerIP, Port}) of [{{ServerIP, Port}, Retries, InitialRetries}] -> FailedTries = proplists:get_value(retries, Options, ?DEFAULT_RETRIES), - % Mark the given RADIUS server as 'non-active' if there were more tries - % than possible + %% Mark the given RADIUS server as 'non-active' if there were more tries + %% than possible if FailedTries >= Retries -> ets:delete(?MODULE, {ServerIP, Port}), Timeout = application:get_env(eradius, unreachable_timeout, 60), timer:apply_after(Timeout * 1000, ?MODULE, restore_upstream_server, [{ServerIP, Port, InitialRetries, InitialRetries}]); true -> - % RADIUS client tried to send a request to the {ServierIP, Port} RADIUS - % server. There were done FailedTries tries and all of them failed. - % So decrease amount of tries for the given RADIUS server that - % that will be used for next RADIUS requests towards this RADIUS server. + %% RADIUS client tried to send a request to the {ServierIP, Port} RADIUS + %% server. There were done FailedTries tries and all of them failed. + %% So decrease amount of tries for the given RADIUS server that + %% that will be used for next RADIUS requests towards this RADIUS server. ets:update_counter(?MODULE, {ServerIP, Port}, -FailedTries) end; [] -> @@ -220,7 +220,7 @@ handle_failed_request(Request, {ServerIP, Port} = _FailedServer, UpstreamServers send_request({NewPeerAddr, NewPeerPort, NewPeerSecret}, Request, [{failover, NewPool} | PeerOpts]) end. -% @private +%% @private send_remote_request_loop(ReplyPid, Socket, ReqId, Peer, EncRequest, Retries, Timeout, MetricsInfo) -> ReplyPid ! {self(), send_request_loop(Socket, ReqId, Peer, EncRequest, Retries, Timeout, MetricsInfo)}. @@ -254,11 +254,11 @@ send_request_loop(Socket, SMon, Peer = {_ServerName, {IP, Port}}, ReqId, Authent send_request_loop(Socket, SMon, Peer, ReqId, Authenticator, EncRequest, Timeout, RetryN - 1, MetricsInfo, Secret, Request) end. -% @private +%% @private update_client_requests(MetricsInfo) -> eradius_counter:inc_counter(requests, MetricsInfo). -% @private +%% @private update_client_request(pending, MetricsInfo, Pending, _) -> if Pending =< 0 -> eradius_counter:dec_counter(pending, MetricsInfo); true -> eradius_counter:inc_counter(pending, MetricsInfo) @@ -267,7 +267,7 @@ update_client_request(Cmd, MetricsInfo, Ms, Request) -> eradius_counter:observe(eradius_client_request_duration_milliseconds, MetricsInfo, Ms, "Execution time of a RADIUS request"), update_client_request_by_type(Cmd, MetricsInfo, Ms, Request). -% @private +%% @private update_client_request_by_type(request, MetricsInfo, Ms, _) -> eradius_counter:observe(eradius_client_access_request_duration_milliseconds, MetricsInfo, Ms, "Access-Request execution time"), eradius_counter:inc_counter(accessRequests, MetricsInfo); @@ -310,13 +310,13 @@ reconfigure() -> %% ------------------------------------------------------------------------------------------ %% -- socket process manager -record(state, { - socket_ip :: null | inet:ip_address(), - no_ports = 1 :: pos_integer(), - idcounters = maps:new() :: map(), - sockets = array:new() :: array:array(), - sup :: pid(), - clients = [] :: [{{integer(),integer(),integer(),integer()}, integer()}] -}). + socket_ip :: null | inet:ip_address(), + no_ports = 1 :: pos_integer(), + idcounters = maps:new() :: map(), + sockets = array:new() :: array:array(), + sup :: pid(), + clients = [] :: [{{integer(),integer(),integer(),integer()}, integer()}] + }). %% @private init([]) -> @@ -377,9 +377,9 @@ configure(State) -> undefined -> prepare_pools(); _ -> - % if ets table is already exists - which could be in a case of - % reconfigure, just re-create the table and fill it with newly - % configured pools of RADIUS upstream servers + %% if ets table is already exists - which could be in a case of + %% reconfigure, just re-create the table and fill it with newly + %% configured pools of RADIUS upstream servers ets:delete(?MODULE), prepare_pools() end, @@ -416,20 +416,20 @@ store_upstream_servers({Server, _, _}) -> store_upstream_servers(Server) -> HandlerDefinitions = application:get_env(eradius, Server, []), UpdatePoolFn = fun (HandlerOpts) -> - {DefaultRoute, Routes, Retries} = eradius_proxy:get_routes_info(HandlerOpts), - eradius_proxy:put_default_route_to_pool(DefaultRoute, Retries), - eradius_proxy:put_routes_to_pool(Routes, Retries) - end, + {DefaultRoute, Routes, Retries} = eradius_proxy:get_routes_info(HandlerOpts), + eradius_proxy:put_default_route_to_pool(DefaultRoute, Retries), + eradius_proxy:put_routes_to_pool(Routes, Retries) + end, lists:foreach(fun (HandlerDefinition) -> - case HandlerDefinition of - {{_, []}, _} -> ok; - {{_, _, []}, _} -> ok; - {{_, HandlerOpts}, _} -> UpdatePoolFn(HandlerOpts); - {{_, _, HandlerOpts}, _} -> UpdatePoolFn(HandlerOpts); - _HandlerDefinition -> ok - end - end, - HandlerDefinitions). + case HandlerDefinition of + {{_, []}, _} -> ok; + {{_, _, []}, _} -> ok; + {{_, HandlerOpts}, _} -> UpdatePoolFn(HandlerOpts); + {{_, _, HandlerOpts}, _} -> UpdatePoolFn(HandlerOpts); + _HandlerDefinition -> ok + end + end, + HandlerDefinitions). %% private store_radius_server_from_pool(Addr, Port, Retries) when is_tuple(Addr) and is_integer(Port) and is_integer(Retries) -> @@ -461,7 +461,7 @@ configure_address(State = #state{socket_ip = OAdd, sockets = Sockts}, NPorts, NA undefined -> done; _ -> Pid ! close end - end, Sockts), + end, Sockts), {ok, State#state{sockets = array:new(), socket_ip = NAdd, no_ports = NPorts}} end. @@ -487,12 +487,13 @@ close_sockets(NPorts, Sockets) -> false -> List = array:to_list(Sockets), {_, Rest} = lists:split(NPorts, List), - lists:map( fun(Pid) -> - case Pid of - undefined -> done; - _ -> Pid ! close - end - end, Rest), + lists:map( + fun(Pid) -> + case Pid of + undefined -> done; + _ -> Pid ! close + end + end, Rest), array:resize(NPorts, Sockets) end. @@ -514,14 +515,14 @@ find_socket_process(PortIdx, Sockets, SocketIP, Sup) -> case array:get(PortIdx, Sockets) of undefined -> Res = supervisor:start_child(Sup, {PortIdx, - {eradius_client_socket, start, [SocketIP, self(), PortIdx]}, - transient, brutal_kill, worker, [eradius_client_socket]}), + {eradius_client_socket, start, [SocketIP, self(), PortIdx]}, + transient, brutal_kill, worker, [eradius_client_socket]}), Pid = case Res of - {ok, P} -> P; - {error, already_present} -> - {ok, P} = supervisor:restart_child(Sup, PortIdx), - P - end, + {ok, P} -> P; + {error, already_present} -> + {ok, P} = supervisor:restart_child(Sup, PortIdx), + P + end, {Pid, array:set(PortIdx, Pid, Sockets)}; Pid when is_pid(Pid) -> {Pid, Sockets} @@ -544,12 +545,12 @@ init_server_status_metrics() -> false -> ok; true -> - % That will be called at eradius startup and we must be sure that prometheus - % application already started if server status metrics supposed to be used + %% That will be called at eradius startup and we must be sure that prometheus + %% application already started if server status metrics supposed to be used application:ensure_all_started(prometheus), ets:foldl(fun ({{Addr, Port}, _, _}, _Acc) -> - eradius_counter:set_boolean_metric(server_status, [Addr, Port], false) - end, [], ?MODULE) + eradius_counter:set_boolean_metric(server_status, [Addr, Port], false) + end, [], ?MODULE) end. make_metrics_info(Options, {ServerIP, ServerPort}) -> @@ -579,35 +580,38 @@ update_server_status_metric(IP, Port, false, _Options) -> eradius_counter:set_boolean_metric(server_status, [IP, Port], false); update_server_status_metric(IP, Port, true, Options) -> UpstreamServers = proplists:get_value(failover, Options, []), - % set all servesr from pool as inactive + %% set all servesr from pool as inactive if is_list(UpstreamServers) -> - lists:foreach(fun (Server) -> - case Server of - {ServerIP, ServerPort, _} -> - eradius_counter:set_boolean_metric(server_status, [ServerIP, ServerPort], false); - {ServerIP, ServerPort, _, _} -> - eradius_counter:set_boolean_metric(server_status, [ServerIP, ServerPort], false); - _ -> - ok - end - - end, UpstreamServers); - true -> - ok + lists:foreach( + fun (Server) -> + case Server of + {ServerIP, ServerPort, _} -> + eradius_counter:set_boolean_metric(server_status, [ServerIP, ServerPort], false); + {ServerIP, ServerPort, _, _} -> + eradius_counter:set_boolean_metric(server_status, [ServerIP, ServerPort], false); + _ -> + ok + end + + end, UpstreamServers); + true -> + ok end, - % set current service as active + %% set current service as active eradius_counter:set_boolean_metric(server_status, [IP, Port], true). client_request_counter_account_match_spec_compile() -> case persistent_term:get({?MODULE, ?FUNCTION_NAME}, undefined) of undefined -> - MatchSpecCompile = ets:match_spec_compile(ets:fun2ms(fun - ({?RStatus_Type, ?RStatus_Type_Start}) -> accountRequestsStart; - ({?RStatus_Type, ?RStatus_Type_Stop}) -> accountRequestsStop; - ({?RStatus_Type, ?RStatus_Type_Update}) -> accountRequestsUpdate; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Start}) -> accountRequestsStart; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Stop}) -> accountRequestsStop; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Update}) -> accountRequestsUpdate end)), + MatchSpecCompile = + ets:match_spec_compile( + ets:fun2ms( + fun ({?RStatus_Type, ?RStatus_Type_Start}) -> accountRequestsStart; + ({?RStatus_Type, ?RStatus_Type_Stop}) -> accountRequestsStop; + ({?RStatus_Type, ?RStatus_Type_Update}) -> accountRequestsUpdate; + ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Start}) -> accountRequestsStart; + ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Stop}) -> accountRequestsStop; + ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Update}) -> accountRequestsUpdate end)), persistent_term:put({?MODULE, ?FUNCTION_NAME}, MatchSpecCompile), MatchSpecCompile; MatchSpecCompile -> @@ -617,13 +621,15 @@ client_request_counter_account_match_spec_compile() -> client_response_counter_account_match_spec_compile() -> case persistent_term:get({?MODULE, ?FUNCTION_NAME}, undefined) of undefined -> - MatchSpecCompile = ets:match_spec_compile(ets:fun2ms(fun - ({?RStatus_Type, ?RStatus_Type_Start}) -> accountResponsesStart; - ({?RStatus_Type, ?RStatus_Type_Stop}) -> accountResponsesStop; - ({?RStatus_Type, ?RStatus_Type_Update}) -> accountResponsesUpdate; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Start}) -> accountResponsesStart; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Stop}) -> accountResponsesStop; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Update}) -> accountResponsesUpdate end)), + MatchSpecCompile = + ets:match_spec_compile( + ets:fun2ms( + fun ({?RStatus_Type, ?RStatus_Type_Start}) -> accountResponsesStart; + ({?RStatus_Type, ?RStatus_Type_Stop}) -> accountResponsesStop; + ({?RStatus_Type, ?RStatus_Type_Update}) -> accountResponsesUpdate; + ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Start}) -> accountResponsesStart; + ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Stop}) -> accountResponsesStop; + ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Update}) -> accountResponsesUpdate end)), persistent_term:put({?MODULE, ?FUNCTION_NAME}, MatchSpecCompile), MatchSpecCompile; MatchSpecCompile -> @@ -634,12 +640,12 @@ find_suitable_peer([]) -> []; find_suitable_peer([{Host, Port, Secret, Opts} | Pool]) when is_list(Host) -> try - IP = get_ip(Host), - find_suitable_peer([{IP, Port, Secret, Opts} | Pool]) + IP = get_ip(Host), + find_suitable_peer([{IP, Port, Secret, Opts} | Pool]) catch _:_ -> - ?LOG(error, "Can't resolve hostname - ~p", [Host]), - % can't resolve ip by some reasons, just ignore it - find_suitable_peer(Pool) + ?LOG(error, "Can't resolve hostname - ~p", [Host]), + %% can't resolve ip by some reasons, just ignore it + find_suitable_peer(Pool) end; find_suitable_peer([{IP, Port, Secret, Opts} | Pool]) -> case ets:lookup(?MODULE, {IP, Port}) of diff --git a/src/eradius_client_socket.erl b/src/eradius_client_socket.erl index d03ff2b3..f53a8f3f 100644 --- a/src/eradius_client_socket.erl +++ b/src/eradius_client_socket.erl @@ -20,7 +20,7 @@ init([SocketIP, Client, PortIdx]) -> end, RecBuf = application:get_env(eradius, recbuf, 8192), SndBuf = application:get_env(eradius, sndbuf, 131072), - {ok, Socket} = gen_udp:open(0, [{active, once}, binary , {recbuf, RecBuf}, {sndbuf, SndBuf} | ExtraOptions]), + {ok, Socket} = gen_udp:open(0, [{active, once}, binary, {recbuf, RecBuf}, {sndbuf, SndBuf} | ExtraOptions]), {ok, #state{client = Client, socket = Socket, pending = maps:new(), mode = active, counter = 0}}. handle_call(_Request, _From, State) -> @@ -30,7 +30,7 @@ handle_cast(_Msg, State) -> {noreply, State}. handle_info({SenderPid, send_request, {IP, Port}, ReqId, EncRequest}, - State = #state{socket = Socket, pending = Pending, counter = Counter}) -> + State = #state{socket = Socket, pending = Pending, counter = Counter}) -> case gen_udp:send(Socket, IP, Port, EncRequest) of ok -> ReqKey = {IP, Port, ReqId}, @@ -42,7 +42,7 @@ handle_info({SenderPid, send_request, {IP, Port}, ReqId, EncRequest}, end; handle_info({udp, Socket, FromIP, FromPort, EncRequest}, - State = #state{socket = Socket, pending = Pending, mode = Mode, counter = Counter}) -> + State = #state{socket = Socket, pending = Pending, mode = Mode, counter = Counter}) -> case eradius_lib:decode_request_id(EncRequest) of {ReqId, EncRequest} -> case maps:find({FromIP, FromPort, ReqId}, Pending) of diff --git a/src/eradius_config.erl b/src/eradius_config.erl index cd6b6930..b0ea1e2b 100644 --- a/src/eradius_config.erl +++ b/src/eradius_config.erl @@ -1,7 +1,7 @@ -module(eradius_config). -% Eradius API's: + % Eradius API's: -export([validate_new_config/0, validate_new_config/2, validate_config/1]). -% Config validating API functions: + % 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, validate_options/1, validate_socket_options/1, validate_server/1]). @@ -12,7 +12,7 @@ -define(pos_int(X), is_integer(X), X >= 0). -define(ip4_address_num(X), ?pos_int(X), X < 256). -define(ip4_address(T), ?ip4_address_num(element(1, T)), ?ip4_address_num(element(2, T)), - ?ip4_address_num(element(3, T)), ?ip4_address_num(element(4, T))). + ?ip4_address_num(element(3, T)), ?ip4_address_num(element(4, T))). -define(valid_atom(Value), Value =/= invalid). -define(valid(X), is_tuple(X), ?valid_atom(element(1, X))). -define(is_io(IO), is_list(IO) orelse is_binary(IO)). @@ -114,8 +114,8 @@ validate_nas(NasId, _IP, Secret, _Name, _) when ?is_io(Secret) -> validate_nas(_NasId, _IP, Secret, _Name, _) -> ?invalid("bad RADIUS secret: ~p", [Secret]). -% -------------------------------------------------------------------------------------------------- -% -- direct validation function +%% -------------------------------------------------------------------------------------------------- +%% -- direct validation function validate_ip(IP) when is_list(IP) -> ok_error_helper(inet_parse:address(IP), {"bad IP address: ~p", [IP]}); @@ -145,8 +145,8 @@ validate_options(Opts) -> validate_socket_options(SocketOpts) when is_list(SocketOpts) -> BannedOpts = [ip, binary, list, active], IsBannedFn = fun(Opt) -> - proplists:is_defined(Opt, SocketOpts) - end, + proplists:is_defined(Opt, SocketOpts) + end, case lists:any(IsBannedFn, BannedOpts) of true -> ?invalid("bad socket options specified: ~p", [SocketOpts]); @@ -173,8 +173,8 @@ check_root(Nodes) -> {root, Values} end. -% -------------------------------------------------------------------------------------------------- -% -- build right format function +%% -------------------------------------------------------------------------------------------------- +%% -- build right format function build_nas_behavior_list({Module, Nas, Args}, ListOfNases) -> lists:map(fun({undefined, IP, Secret, Nodes}) -> @@ -351,8 +351,8 @@ plmerge(List1, List2) -> M1 = [{K, V} || {K, V} <- List1, not proplists:is_defined(K, List2)], lists:keysort(1, M1 ++ List2). -% -------------------------------------------------------------------------------------------------- -% -- helpers +%% -------------------------------------------------------------------------------------------------- +%% -- helpers get_app_env(Env) -> get_app_env(eradius, Env). diff --git a/src/eradius_counter.erl b/src/eradius_counter.erl index 9e922550..9d245e59 100644 --- a/src/eradius_counter.erl +++ b/src/eradius_counter.erl @@ -14,8 +14,8 @@ -include("eradius_lib.hrl"). -record(state, { - reset :: erlang:timestamp() - }). + reset :: erlang:timestamp() + }). -type srv_counters() :: [#server_counter{}]. -type nas_counters() :: {erlang:timestamp(), [#nas_counter{}]}. @@ -91,9 +91,9 @@ set_boolean_metric(Name, Labels, Value) -> try prometheus_boolean:set(Name, Labels, Value) catch _:_ -> - prometheus_boolean:declare([{name, server_status}, {labels, [server_ip, server_port]}, - {help, "Status of an upstream RADIUS Server"}]), - prometheus_boolean:set(Name, Labels, Value) + prometheus_boolean:declare([{name, server_status}, {labels, [server_ip, server_port]}, + {help, "Status of an upstream RADIUS Server"}]), + prometheus_boolean:set(Name, Labels, Value) end; _ -> ok @@ -270,32 +270,32 @@ counter_idx(accountResponsesUpdate, client) -> #client_counter.accountResponsesU add_counter(Cnt1 = #nas_counter{}, Cnt2 = #nas_counter{}) -> #nas_counter{ - key = Cnt1#nas_counter.key, - requests = Cnt1#nas_counter.requests + Cnt2#nas_counter.requests, - replies = Cnt1#nas_counter.replies + Cnt2#nas_counter.replies, - dupRequests = Cnt1#nas_counter.dupRequests + Cnt2#nas_counter.dupRequests, - malformedRequests = Cnt1#nas_counter.malformedRequests + Cnt2#nas_counter.malformedRequests, - accessRequests = Cnt1#nas_counter.accessRequests + Cnt2#nas_counter.accessRequests, - accessAccepts = Cnt1#nas_counter.accessAccepts + Cnt2#nas_counter.accessAccepts, - accessRejects = Cnt1#nas_counter.accessRejects + Cnt2#nas_counter.accessRejects, - accessChallenges = Cnt1#nas_counter.accessChallenges + Cnt2#nas_counter.accessChallenges, - accountRequestsStart = Cnt1#nas_counter.accountRequestsStart + Cnt2#nas_counter.accountRequestsStart, - accountRequestsStop = Cnt1#nas_counter.accountRequestsStop + Cnt2#nas_counter.accountRequestsStop, - accountRequestsUpdate = Cnt1#nas_counter.accountRequestsUpdate + Cnt2#nas_counter.accountRequestsUpdate, - accountResponsesStart = Cnt1#nas_counter.accountResponsesStart + Cnt2#nas_counter.accountResponsesStart, - accountResponsesStop = Cnt1#nas_counter.accountResponsesStop + Cnt2#nas_counter.accountResponsesStop, - accountResponsesUpdate = Cnt1#nas_counter.accountResponsesUpdate + Cnt2#nas_counter.accountResponsesUpdate, - noRecords = Cnt1#nas_counter.noRecords + Cnt2#nas_counter.noRecords, - badAuthenticators = Cnt1#nas_counter.badAuthenticators + Cnt2#nas_counter.badAuthenticators, - packetsDropped = Cnt1#nas_counter.packetsDropped + Cnt2#nas_counter.packetsDropped, - unknownTypes = Cnt1#nas_counter.unknownTypes + Cnt2#nas_counter.unknownTypes, - handlerFailure = Cnt1#nas_counter.handlerFailure + Cnt2#nas_counter.handlerFailure, - coaRequests = Cnt1#nas_counter.coaRequests + Cnt2#nas_counter.coaRequests, - coaAcks = Cnt1#nas_counter.coaAcks + Cnt2#nas_counter.coaAcks, - coaNaks = Cnt1#nas_counter.coaNaks + Cnt2#nas_counter.coaNaks, - discRequests = Cnt1#nas_counter.discRequests + Cnt2#nas_counter.discRequests, - discAcks = Cnt1#nas_counter.discAcks + Cnt2#nas_counter.discAcks, - discNaks = Cnt1#nas_counter.discNaks + Cnt2#nas_counter.discNaks, - retransmissions = Cnt1#nas_counter.retransmissions + Cnt2#nas_counter.retransmissions, - pending = Cnt1#nas_counter.pending + Cnt2#nas_counter.pending + key = Cnt1#nas_counter.key, + requests = Cnt1#nas_counter.requests + Cnt2#nas_counter.requests, + replies = Cnt1#nas_counter.replies + Cnt2#nas_counter.replies, + dupRequests = Cnt1#nas_counter.dupRequests + Cnt2#nas_counter.dupRequests, + malformedRequests = Cnt1#nas_counter.malformedRequests + Cnt2#nas_counter.malformedRequests, + accessRequests = Cnt1#nas_counter.accessRequests + Cnt2#nas_counter.accessRequests, + accessAccepts = Cnt1#nas_counter.accessAccepts + Cnt2#nas_counter.accessAccepts, + accessRejects = Cnt1#nas_counter.accessRejects + Cnt2#nas_counter.accessRejects, + accessChallenges = Cnt1#nas_counter.accessChallenges + Cnt2#nas_counter.accessChallenges, + accountRequestsStart = Cnt1#nas_counter.accountRequestsStart + Cnt2#nas_counter.accountRequestsStart, + accountRequestsStop = Cnt1#nas_counter.accountRequestsStop + Cnt2#nas_counter.accountRequestsStop, + accountRequestsUpdate = Cnt1#nas_counter.accountRequestsUpdate + Cnt2#nas_counter.accountRequestsUpdate, + accountResponsesStart = Cnt1#nas_counter.accountResponsesStart + Cnt2#nas_counter.accountResponsesStart, + accountResponsesStop = Cnt1#nas_counter.accountResponsesStop + Cnt2#nas_counter.accountResponsesStop, + accountResponsesUpdate = Cnt1#nas_counter.accountResponsesUpdate + Cnt2#nas_counter.accountResponsesUpdate, + noRecords = Cnt1#nas_counter.noRecords + Cnt2#nas_counter.noRecords, + badAuthenticators = Cnt1#nas_counter.badAuthenticators + Cnt2#nas_counter.badAuthenticators, + packetsDropped = Cnt1#nas_counter.packetsDropped + Cnt2#nas_counter.packetsDropped, + unknownTypes = Cnt1#nas_counter.unknownTypes + Cnt2#nas_counter.unknownTypes, + handlerFailure = Cnt1#nas_counter.handlerFailure + Cnt2#nas_counter.handlerFailure, + coaRequests = Cnt1#nas_counter.coaRequests + Cnt2#nas_counter.coaRequests, + coaAcks = Cnt1#nas_counter.coaAcks + Cnt2#nas_counter.coaAcks, + coaNaks = Cnt1#nas_counter.coaNaks + Cnt2#nas_counter.coaNaks, + discRequests = Cnt1#nas_counter.discRequests + Cnt2#nas_counter.discRequests, + discAcks = Cnt1#nas_counter.discAcks + Cnt2#nas_counter.discAcks, + discNaks = Cnt1#nas_counter.discNaks + Cnt2#nas_counter.discNaks, + retransmissions = Cnt1#nas_counter.retransmissions + Cnt2#nas_counter.retransmissions, + pending = Cnt1#nas_counter.pending + Cnt2#nas_counter.pending }. diff --git a/src/eradius_counter_aggregator.erl b/src/eradius_counter_aggregator.erl index b972002f..b137b370 100644 --- a/src/eradius_counter_aggregator.erl +++ b/src/eradius_counter_aggregator.erl @@ -10,9 +10,9 @@ -define(INTERVAL_HB, 5000). -record(state, { - me :: reference(), - reset :: erlang:timestamp() - }). + me :: reference(), + reset :: erlang:timestamp() + }). %% @doc reset all counters to zero reset() -> @@ -94,69 +94,69 @@ update_stats(Rec = #nas_counter{key = Key}) -> Cnt0 = case ets:lookup(?MODULE, Key) of [] -> #nas_counter{key = Key}; [Cnt] -> Cnt - end, + end, ets:insert(?MODULE, add_counter(Cnt0, Rec)); update_stats(Rec = #client_counter{key = Key}) -> Cnt0 = case ets:lookup(?MODULE, Key) of [] -> #client_counter{key = Key}; [Cnt] -> Cnt - end, + end, ets:insert(?MODULE, add_counter(Cnt0, Rec)). add_counter(Cnt1 = #nas_counter{}, Cnt2 = #nas_counter{}) -> #nas_counter{ - key = Cnt1#nas_counter.key, - requests = Cnt1#nas_counter.requests + Cnt2#nas_counter.requests, - replies = Cnt1#nas_counter.replies + Cnt2#nas_counter.replies, - dupRequests = Cnt1#nas_counter.dupRequests + Cnt2#nas_counter.dupRequests, - malformedRequests = Cnt1#nas_counter.malformedRequests + Cnt2#nas_counter.malformedRequests, - accessRequests = Cnt1#nas_counter.accessRequests + Cnt2#nas_counter.accessRequests, - accessAccepts = Cnt1#nas_counter.accessAccepts + Cnt2#nas_counter.accessAccepts, - accessRejects = Cnt1#nas_counter.accessRejects + Cnt2#nas_counter.accessRejects, - accessChallenges = Cnt1#nas_counter.accessChallenges + Cnt2#nas_counter.accessChallenges, - accountRequestsStart = Cnt1#nas_counter.accountRequestsStart + Cnt2#nas_counter.accountRequestsStart, - accountRequestsStop = Cnt1#nas_counter.accountRequestsStop + Cnt2#nas_counter.accountRequestsStop, - accountRequestsUpdate = Cnt1#nas_counter.accountRequestsUpdate + Cnt2#nas_counter.accountRequestsUpdate, - accountResponsesStart = Cnt1#nas_counter.accountResponsesStart + Cnt2#nas_counter.accountResponsesStart, - accountResponsesStop = Cnt1#nas_counter.accountResponsesStop + Cnt2#nas_counter.accountResponsesStop, - accountResponsesUpdate = Cnt1#nas_counter.accountResponsesUpdate + Cnt2#nas_counter.accountResponsesUpdate, - noRecords = Cnt1#nas_counter.noRecords + Cnt2#nas_counter.noRecords, - badAuthenticators = Cnt1#nas_counter.badAuthenticators + Cnt2#nas_counter.badAuthenticators, - packetsDropped = Cnt1#nas_counter.packetsDropped + Cnt2#nas_counter.packetsDropped, - unknownTypes = Cnt1#nas_counter.unknownTypes + Cnt2#nas_counter.unknownTypes, - handlerFailure = Cnt1#nas_counter.handlerFailure + Cnt2#nas_counter.handlerFailure, - coaRequests = Cnt1#nas_counter.coaRequests + Cnt2#nas_counter.coaRequests, - coaAcks = Cnt1#nas_counter.coaAcks + Cnt2#nas_counter.coaAcks, - coaNaks = Cnt1#nas_counter.coaNaks + Cnt2#nas_counter.coaNaks, - discRequests = Cnt1#nas_counter.discRequests + Cnt2#nas_counter.discRequests, - discAcks = Cnt1#nas_counter.discAcks + Cnt2#nas_counter.discAcks, - discNaks = Cnt1#nas_counter.discNaks + Cnt2#nas_counter.discNaks + key = Cnt1#nas_counter.key, + requests = Cnt1#nas_counter.requests + Cnt2#nas_counter.requests, + replies = Cnt1#nas_counter.replies + Cnt2#nas_counter.replies, + dupRequests = Cnt1#nas_counter.dupRequests + Cnt2#nas_counter.dupRequests, + malformedRequests = Cnt1#nas_counter.malformedRequests + Cnt2#nas_counter.malformedRequests, + accessRequests = Cnt1#nas_counter.accessRequests + Cnt2#nas_counter.accessRequests, + accessAccepts = Cnt1#nas_counter.accessAccepts + Cnt2#nas_counter.accessAccepts, + accessRejects = Cnt1#nas_counter.accessRejects + Cnt2#nas_counter.accessRejects, + accessChallenges = Cnt1#nas_counter.accessChallenges + Cnt2#nas_counter.accessChallenges, + accountRequestsStart = Cnt1#nas_counter.accountRequestsStart + Cnt2#nas_counter.accountRequestsStart, + accountRequestsStop = Cnt1#nas_counter.accountRequestsStop + Cnt2#nas_counter.accountRequestsStop, + accountRequestsUpdate = Cnt1#nas_counter.accountRequestsUpdate + Cnt2#nas_counter.accountRequestsUpdate, + accountResponsesStart = Cnt1#nas_counter.accountResponsesStart + Cnt2#nas_counter.accountResponsesStart, + accountResponsesStop = Cnt1#nas_counter.accountResponsesStop + Cnt2#nas_counter.accountResponsesStop, + accountResponsesUpdate = Cnt1#nas_counter.accountResponsesUpdate + Cnt2#nas_counter.accountResponsesUpdate, + noRecords = Cnt1#nas_counter.noRecords + Cnt2#nas_counter.noRecords, + badAuthenticators = Cnt1#nas_counter.badAuthenticators + Cnt2#nas_counter.badAuthenticators, + packetsDropped = Cnt1#nas_counter.packetsDropped + Cnt2#nas_counter.packetsDropped, + unknownTypes = Cnt1#nas_counter.unknownTypes + Cnt2#nas_counter.unknownTypes, + handlerFailure = Cnt1#nas_counter.handlerFailure + Cnt2#nas_counter.handlerFailure, + coaRequests = Cnt1#nas_counter.coaRequests + Cnt2#nas_counter.coaRequests, + coaAcks = Cnt1#nas_counter.coaAcks + Cnt2#nas_counter.coaAcks, + coaNaks = Cnt1#nas_counter.coaNaks + Cnt2#nas_counter.coaNaks, + discRequests = Cnt1#nas_counter.discRequests + Cnt2#nas_counter.discRequests, + discAcks = Cnt1#nas_counter.discAcks + Cnt2#nas_counter.discAcks, + discNaks = Cnt1#nas_counter.discNaks + Cnt2#nas_counter.discNaks }; add_counter(Cnt1 = #client_counter{}, Cnt2 = #client_counter{}) -> #client_counter{ - key = Cnt1#client_counter.key, - requests = Cnt1#client_counter.requests + Cnt2#client_counter.requests, - replies = Cnt1#client_counter.replies + Cnt2#client_counter.replies, - accessRequests = Cnt1#client_counter.accessRequests + Cnt2#client_counter.accessRequests, - accessAccepts = Cnt1#client_counter.accessAccepts + Cnt2#client_counter.accessAccepts, - accessRejects = Cnt1#client_counter.accessRejects + Cnt2#client_counter.accessRejects, - accessChallenges = Cnt1#client_counter.accessChallenges + Cnt2#client_counter.accessChallenges, - accountRequestsStart = Cnt1#client_counter.accountRequestsStart + Cnt2#client_counter.accountRequestsStart, - accountRequestsStop = Cnt1#client_counter.accountRequestsStop + Cnt2#client_counter.accountRequestsStop, - accountRequestsUpdate = Cnt1#client_counter.accountRequestsUpdate + Cnt2#client_counter.accountRequestsUpdate, - accountResponsesStart = Cnt1#client_counter.accountResponsesStart + Cnt2#client_counter.accountResponsesStart, - accountResponsesStop = Cnt1#client_counter.accountResponsesStop + Cnt2#client_counter.accountResponsesStop, - accountResponsesUpdate = Cnt1#client_counter.accountResponsesUpdate + Cnt2#client_counter.accountResponsesUpdate, - badAuthenticators = Cnt1#client_counter.badAuthenticators + Cnt2#client_counter.badAuthenticators, - packetsDropped = Cnt1#client_counter.packetsDropped + Cnt2#client_counter.packetsDropped, - unknownTypes = Cnt1#client_counter.unknownTypes + Cnt2#client_counter.unknownTypes, - coaRequests = Cnt1#client_counter.coaRequests + Cnt2#client_counter.coaRequests, - coaAcks = Cnt1#client_counter.coaAcks + Cnt2#client_counter.coaAcks, - coaNaks = Cnt1#client_counter.coaNaks + Cnt2#client_counter.coaNaks, - discRequests = Cnt1#client_counter.discRequests + Cnt2#client_counter.discRequests, - discAcks = Cnt1#client_counter.discAcks + Cnt2#client_counter.discAcks, - discNaks = Cnt1#client_counter.discNaks + Cnt2#client_counter.discNaks, - retransmissions = Cnt1#client_counter.retransmissions + Cnt2#client_counter.retransmissions, - timeouts = Cnt1#client_counter.timeouts + Cnt2#client_counter.timeouts, - pending = Cnt1#client_counter.pending + Cnt2#client_counter.pending + key = Cnt1#client_counter.key, + requests = Cnt1#client_counter.requests + Cnt2#client_counter.requests, + replies = Cnt1#client_counter.replies + Cnt2#client_counter.replies, + accessRequests = Cnt1#client_counter.accessRequests + Cnt2#client_counter.accessRequests, + accessAccepts = Cnt1#client_counter.accessAccepts + Cnt2#client_counter.accessAccepts, + accessRejects = Cnt1#client_counter.accessRejects + Cnt2#client_counter.accessRejects, + accessChallenges = Cnt1#client_counter.accessChallenges + Cnt2#client_counter.accessChallenges, + accountRequestsStart = Cnt1#client_counter.accountRequestsStart + Cnt2#client_counter.accountRequestsStart, + accountRequestsStop = Cnt1#client_counter.accountRequestsStop + Cnt2#client_counter.accountRequestsStop, + accountRequestsUpdate = Cnt1#client_counter.accountRequestsUpdate + Cnt2#client_counter.accountRequestsUpdate, + accountResponsesStart = Cnt1#client_counter.accountResponsesStart + Cnt2#client_counter.accountResponsesStart, + accountResponsesStop = Cnt1#client_counter.accountResponsesStop + Cnt2#client_counter.accountResponsesStop, + accountResponsesUpdate = Cnt1#client_counter.accountResponsesUpdate + Cnt2#client_counter.accountResponsesUpdate, + badAuthenticators = Cnt1#client_counter.badAuthenticators + Cnt2#client_counter.badAuthenticators, + packetsDropped = Cnt1#client_counter.packetsDropped + Cnt2#client_counter.packetsDropped, + unknownTypes = Cnt1#client_counter.unknownTypes + Cnt2#client_counter.unknownTypes, + coaRequests = Cnt1#client_counter.coaRequests + Cnt2#client_counter.coaRequests, + coaAcks = Cnt1#client_counter.coaAcks + Cnt2#client_counter.coaAcks, + coaNaks = Cnt1#client_counter.coaNaks + Cnt2#client_counter.coaNaks, + discRequests = Cnt1#client_counter.discRequests + Cnt2#client_counter.discRequests, + discAcks = Cnt1#client_counter.discAcks + Cnt2#client_counter.discAcks, + discNaks = Cnt1#client_counter.discNaks + Cnt2#client_counter.discNaks, + retransmissions = Cnt1#client_counter.retransmissions + Cnt2#client_counter.retransmissions, + timeouts = Cnt1#client_counter.timeouts + Cnt2#client_counter.timeouts, + pending = Cnt1#client_counter.pending + Cnt2#client_counter.pending }. diff --git a/src/eradius_dict.erl b/src/eradius_dict.erl index 095bcfd7..3fca6f30 100644 --- a/src/eradius_dict.erl +++ b/src/eradius_dict.erl @@ -3,7 +3,7 @@ -module(eradius_dict). -export([start_link/0, lookup/2, load_tables/1, load_tables/2, unload_tables/1, unload_tables/2]). -export_type([attribute/0, attr_value/0, table_name/0, attribute_id/0, attribute_type/0, - attribute_prim_type/0, attribute_encryption/0, vendor_id/0, value_id/0]). + attribute_prim_type/0, attribute_encryption/0, vendor_id/0, value_id/0]). -behaviour(gen_server). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -18,7 +18,7 @@ -type attribute_encryption() :: 'no' | 'scramble' | 'salt_crypt' | 'ascend'. -type attribute_type() :: attribute_prim_type() | {tagged, attribute_prim_type()}. -type attribute_prim_type() :: 'string' | 'integer' | 'integer64' | 'ipaddr' | 'ipv6addr' - | 'ipv6prefix' | 'date' | 'abinary' | 'binary' | 'octets'. + | 'ipv6prefix' | 'date' | 'abinary' | 'binary' | 'octets'. -type value_id() :: {attribute_id(), pos_integer()}. -type vendor_id() :: pos_integer(). @@ -98,9 +98,9 @@ do_unload_tables(_Dir, []) -> ok; do_unload_tables(Dir, Tables) -> try - % Unlike of what we do in do_load_tables, we don't treat includes here. - % Usually when you want to purge some tables you want to do exactly - % this, and you don't want to purge some extra tables. + %% Unlike of what we do in do_load_tables, we don't treat includes here. + %% Usually when you want to purge some tables you want to do exactly + %% this, and you don't want to purge some extra tables. {_, Defs} = prepare_tables(Dir, Tables), dict_delete(Defs), ?LOG(info, "Unloaded RADIUS tables: ~p", [Tables]) @@ -113,12 +113,12 @@ do_unload_tables(Dir, Tables) -> -spec prepare_tables(file:filename(), [table_name()]) -> {list(), list()}. prepare_tables(Dir, Tables) -> All = lists:flatmap(fun (Tab) -> - TabFile = filename:join(Dir, mapfile(Tab)), - case file:consult(TabFile) of - {ok, Res} -> Res; - {error, _Error} -> throw({consult, TabFile}) - end - end, Tables), + TabFile = filename:join(Dir, mapfile(Tab)), + case file:consult(TabFile) of + {ok, Res} -> Res; + {error, _Error} -> throw({consult, TabFile}) + end + end, Tables), lists:partition(fun({include, _}) -> true; (_) -> false end, All). dict_insert(Value) when is_list(Value) -> @@ -135,8 +135,8 @@ dict_delete(Value) when is_tuple(Value) -> dict_lookup(Type, Id) -> try - persistent_term:get({?MODULE, Type, Id}) + persistent_term:get({?MODULE, Type, Id}) catch - error:badarg -> - false + error:badarg -> + false end. diff --git a/src/eradius_eap_packet.erl b/src/eradius_eap_packet.erl index c2e79c07..0f55cbd4 100644 --- a/src/eradius_eap_packet.erl +++ b/src/eradius_eap_packet.erl @@ -61,10 +61,10 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. decode(<>) -> DataLen = Len - 4, case Rest of - <> -> - do_decode_payload(code(Code), Id, Data); - _ -> - {error, invalid_length} + <> -> + do_decode_payload(code(Code), Id, Data); + _ -> + {error, invalid_length} end. %% @doc endecode a EPA message @@ -75,9 +75,9 @@ encode(Code, Id, Msg) -> do_decode_payload(Code, Id, Data) -> try - decode_payload(Code, Id, Data) + decode_payload(Code, Id, Data) catch - _ -> {error, invalid_payload} + _ -> {error, invalid_payload} end. decode_payload(Code, Id, <>) @@ -87,9 +87,9 @@ decode_payload(Code, Id, <>) decode_payload(Code, Id, Data) when Code == success; Code == failure -> case Data of - <<>> -> {ok, {Code, Id}}; - _ -> {error, invalid_length} - end. + <<>> -> {ok, {Code, Id}}; + _ -> {error, invalid_length} + end. %% @doc EAP decoder functions for RFC-3784 types @@ -130,11 +130,11 @@ decode_eap_type({0, 3}, Data) -> {nak_ext, [{Vendor,Type} || <<_T:8, Vendor:24, Type:32>> <= Data]}; decode_eap_type(Type, Data) -> - case lookup_type(Type) of - [Module] -> - Module:decode_eap_type(Type, Data); - _ -> {Type, Data} - end. + case lookup_type(Type) of + [Module] -> + Module:decode_eap_type(Type, Data); + _ -> {Type, Data} + end. encode_payload(Code, Msg) when Code == request; Code == response -> diff --git a/src/eradius_lib.erl b/src/eradius_lib.erl index 71ac2de9..650cbde3 100644 --- a/src/eradius_lib.erl +++ b/src/eradius_lib.erl @@ -4,7 +4,7 @@ -export([timestamp/0, timestamp/1, printable_peer/2, make_addr_info/1]). -export_type([command/0, secret/0, authenticator/0, attribute_list/0]). -% -compile(bin_opt_info). +%% -compile(bin_opt_info). -ifdef(TEST). -export([encode_value/2, decode_value/2, scramble/3, ascend/3]). @@ -21,8 +21,8 @@ -define(IS_ATTR(Key, Attr), ?IS_KEY(Key, element(1, Attr))). -define(IS_KEY(Key, Attr), ((is_record(Attr, attribute) andalso (element(2, Attr) == Key)) - orelse - (Attr == Key)) ). + orelse + (Attr == Key)) ). %% ------------------------------------------------------------------------------------------ %% -- Request Accessors -spec random_authenticator() -> authenticator(). @@ -114,8 +114,8 @@ encode_message_authenticator(Req = #radius_request{reqid = ReqID, cmd = Command, chunk(Bin, Length) -> case Bin of - <> -> {First, Rest}; - _ -> {Bin, <<>>} + <> -> {First, Rest}; + _ -> {Bin, <<>>} end. encode_eap_attribute({<<>>, _}, EncReq) -> @@ -158,7 +158,7 @@ encode_attribute(_Req, _Attr = #attribute{id = ?REAP_Message}, _) -> encode_attribute(Req, Attr = #attribute{id = {Vendor, ID}}, Value) -> EncValue = encode_attribute(Req, Attr#attribute{id = ID}, Value), if byte_size(EncValue) + 6 > 255 -> - error(badarg, [{Vendor, ID}, Value]); + error(badarg, [{Vendor, ID}, Value]); true -> ok end, <>; @@ -169,14 +169,14 @@ encode_attribute(Req, #attribute{type = {tagged, Type}, id = ID, enc = Enc}, Val end, EncValue = encrypt_value(Req, encode_value(Type, UntaggedValue), Enc), if byte_size(EncValue) + 3 > 255 -> - error(badarg, [ID, Value]); + error(badarg, [ID, Value]); true -> ok end, <>; encode_attribute(Req, #attribute{type = Type, id = ID, enc = Enc}, Value)-> EncValue = encrypt_value(Req, encode_value(Type, Value), Enc), if byte_size(EncValue) + 2 > 255 -> - error(badarg, [ID, Value]); + error(badarg, [ID, Value]); true -> ok end, <>. @@ -241,24 +241,24 @@ decode_request0(< GivenBodySize -> - throw({bad_pdu, "false packet size"}); - ActualBodySize == GivenBodySize -> - Body0; - true -> - binary:part(Body0, 0, GivenBodySize) + ActualBodySize > GivenBodySize -> + throw({bad_pdu, "false packet size"}); + ActualBodySize == GivenBodySize -> + Body0; + true -> + binary:part(Body0, 0, GivenBodySize) end, Command = decode_command(Cmd), PartialRequest = #radius_request{cmd = Command, reqid = ReqId, authenticator = PacketAuthenticator, secret = Secret, msg_hmac = false}, DecodedState = decode_attributes(PartialRequest, RequestAuthenticator, Body), Request = PartialRequest#radius_request{attrs = lists:reverse(DecodedState#decoder_state.attrs), - eap_msg = list_to_binary(lists:reverse(DecodedState#decoder_state.eap_msg))}, + eap_msg = list_to_binary(lists:reverse(DecodedState#decoder_state.eap_msg))}, validate_authenticator(Command, <>, RequestAuthenticator, PacketAuthenticator, Body, Secret), if - is_integer(DecodedState#decoder_state.hmac_pos) -> - validate_packet_authenticator(Cmd, ReqId, Len, Body, DecodedState#decoder_state.hmac_pos, Secret, PacketAuthenticator, RequestAuthenticator), - Request#radius_request{msg_hmac = true}; - true -> Request + is_integer(DecodedState#decoder_state.hmac_pos) -> + validate_packet_authenticator(Cmd, ReqId, Len, Body, DecodedState#decoder_state.hmac_pos, Secret, PacketAuthenticator, RequestAuthenticator), + Request#radius_request{msg_hmac = true}; + true -> Request end. -spec validate_packet_authenticator(non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer(), binary(), binary(), authenticator(), authenticator() | 'undefined') -> ok. @@ -272,10 +272,10 @@ validate_packet_authenticator(Cmd, ReqId, Len, Auth, Body, Pos, Secret) -> case Body of <> -> case message_authenticator(Secret, [<>, Auth, Before, zero_authenticator(), After]) of - Value -> - ok; - _ -> - throw({bad_pdu, "Message-Authenticator Attribute is invalid"}) + Value -> + ok; + _ -> + throw({bad_pdu, "Message-Authenticator Attribute is invalid"}) end; _ -> throw({bad_pdu, "Message-Authenticator Attribute is malformed"}) @@ -284,15 +284,15 @@ validate_packet_authenticator(Cmd, ReqId, Len, Auth, Body, Pos, Secret) -> validate_authenticator(accreq, Head, _RequestAuthenticator, PacketAuthenticator, Body, Secret) -> compare_authenticator(crypto:hash(md5, [Head, zero_authenticator(), Body, Secret]), PacketAuthenticator); validate_authenticator(Cmd, Head, RequestAuthenticator, PacketAuthenticator, Body, Secret) - when - (Cmd =:= accept) orelse - (Cmd =:= reject) orelse - (Cmd =:= accresp) orelse - (Cmd =:= coaack) orelse - (Cmd =:= coanak) orelse - (Cmd =:= discack) orelse - (Cmd =:= discnak) orelse - (Cmd =:= challenge) -> + when + (Cmd =:= accept) orelse + (Cmd =:= reject) orelse + (Cmd =:= accresp) orelse + (Cmd =:= coaack) orelse + (Cmd =:= coanak) orelse + (Cmd =:= discack) orelse + (Cmd =:= discnak) orelse + (Cmd =:= challenge) -> compare_authenticator(crypto:hash(md5, [Head, RequestAuthenticator, Body, Secret]), PacketAuthenticator); validate_authenticator(_Cmd, _Head, _RequestAuthenticator, _PacketAuthenticator, _Body, _Secret) -> @@ -332,11 +332,11 @@ decode_attributes(Req, <>, Pos, State) ValueLength = ChunkLength - 2, <> = ChunkRest, NewState = case eradius_dict:lookup(attribute, Type) of - AttrRec = #attribute{} -> - decode_attribute(Value, Req, AttrRec, Pos + 2, State); - _ -> - append_attr({Type, Value}, State) - end, + AttrRec = #attribute{} -> + decode_attribute(Value, Req, AttrRec, Pos + 2, State); + _ -> + append_attr({Type, Value}, State) + end, decode_attributes(Req, PacketRest, Pos + ChunkLength, NewState). %% gotcha: the function returns a LIST of attribute-value pairs because @@ -354,12 +354,12 @@ decode_attribute(<>, Req, Attr = #attribute{type = Type, enc = decode_attribute(WholeBin = <>, Req, Attr = #attribute{type = {tagged, Type}}, _Pos, State) -> case {decode_tag_value(Tag), Attr#attribute.enc} of {0, no} -> - % decode including tag byte if tag is out of range + %% decode including tag byte if tag is out of range append_attr({Attr, {0, decode_value(WholeBin, Type)}}, State); {TagV, no} -> append_attr({Attr, {TagV, decode_value(Bin, Type)}}, State); {TagV, Encryption} -> - % for encrypted attributes, tag byte is never part of the value + %% for encrypted attributes, tag byte is never part of the value append_attr({Attr, {TagV, decode_value(decrypt_value(Req, State, Bin, Encryption), Type)}}, State) end. @@ -412,17 +412,17 @@ decode_integer(Bin) -> end. -spec decrypt_value(#radius_request{}, #decoder_state{}, binary(), - eradius_dict:attribute_encryption()) -> eradius_dict:attr_value(). + eradius_dict:attribute_encryption()) -> eradius_dict:attr_value(). decrypt_value(#radius_request{secret = Secret, authenticator = Authenticator}, - _, <>, scramble) -> + _, <>, scramble) -> scramble(Secret, Authenticator, Val); decrypt_value(#radius_request{secret = Secret}, - #decoder_state{request_authenticator = RequestAuthenticator}, - <>, salt_crypt) -when is_binary(RequestAuthenticator) -> + #decoder_state{request_authenticator = RequestAuthenticator}, + <>, salt_crypt) + when is_binary(RequestAuthenticator) -> salt_decrypt(Secret, RequestAuthenticator, Val); decrypt_value(#radius_request{secret = Secret, authenticator = Authenticator}, - _, <>, ascend) -> + _, <>, ascend) -> ascend(Secret, Authenticator, Val); decrypt_value(_Req, _State, <>, _Type) -> Val. @@ -435,11 +435,11 @@ decode_vendor_specific_attribute(Req, VendorID, <> = ChunkRest, VendorAttrKey = {VendorID, Type}, NewState = case eradius_dict:lookup(attribute, VendorAttrKey) of - Attr = #attribute{} -> - decode_attribute(Value, Req, Attr, Pos + 2, State); - _ -> - append_attr({VendorAttrKey, Value}, State) - end, + Attr = #attribute{} -> + decode_attribute(Value, Req, Attr, Pos + 2, State); + _ -> + append_attr({VendorAttrKey, Value}, State) + end, decode_vendor_specific_attribute(Req, VendorID, PacketRest, Pos + ChunkLength, NewState). %% ------------------------------------------------------------------------------------------ @@ -484,9 +484,9 @@ do_salt_crypt(Op, Salt, SharedSecret, RequestAuthenticator, < salt_crypt(Op, SharedSecret, B, <>, CipherText) -> NewCipherText = crypto:exor(PlainText, B), Bnext = case Op of - decrypt -> crypto:hash(md5, [SharedSecret, PlainText]); - encrypt -> crypto:hash(md5, [SharedSecret, NewCipherText]) - end, + decrypt -> crypto:hash(md5, [SharedSecret, PlainText]); + encrypt -> crypto:hash(md5, [SharedSecret, NewCipherText]) + end, salt_crypt(Op, SharedSecret, Bnext, Remaining, <>); salt_crypt(_Op, _SharedSecret, _B, << >>, CipherText) -> @@ -503,10 +503,10 @@ ascend(SharedSecret, RequestAuthenticator, <>) -> %% </a> -compile({inline, pad_to/2}). pad_to(Width, Binary) -> - case (Width - byte_size(Binary) rem Width) rem Width of - 0 -> Binary; - N -> <<Binary/binary, 0:(N*8)>> - end. + case (Width - byte_size(Binary) rem Width) rem Width of + 0 -> Binary; + N -> <<Binary/binary, 0:(N*8)>> + end. -spec timestamp() -> erlang:timestamp(). timestamp() -> diff --git a/src/eradius_log.erl b/src/eradius_log.erl index 20232fe3..f2974fc4 100644 --- a/src/eradius_log.erl +++ b/src/eradius_log.erl @@ -1,24 +1,24 @@ -% Copyright (c) 2010-2011 by Travelping GmbH <info@travelping.com> - -% Permission is hereby granted, free of charge, to any person obtaining a -% copy of this software and associated documentation files (the "Software"), -% to deal in the Software without restriction, including without limitation -% the rights to use, copy, modify, merge, publish, distribute, sublicense, -% and/or sell copies of the Software, and to permit persons to whom the -% Software is furnished to do so, subject to the following conditions: - -% The above copyright notice and this permission notice shall be included in -% all copies or substantial portions of the Software. - -% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -% DEALINGS IN THE SOFTWARE. - -% @private +%% Copyright (c) 2010-2011 by Travelping GmbH <info@travelping.com> + +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: + +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. + +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. + +%% @private -module(eradius_log). -behaviour(gen_server). @@ -81,7 +81,7 @@ handle_call(reconfigure, _From, State) -> file:close(State), {reply, ok, init_logger()}; -% for tests +%% for tests handle_call(get_state, _From, State) -> {reply, State, State}; @@ -99,7 +99,7 @@ handle_cast({write_request, Time, Sender, Request}, State) -> catch _:Error -> ?LOG(error, "Failed to log RADIUS request: error: ~p, request: ~p, sender: ~p, " - "logging will be disabled", [Error, Request, Sender]), + "logging will be disabled", [Error, Request, Sender]), {noreply, logger_disabled} end. @@ -245,8 +245,8 @@ collectable_attr_value(_Attr, Val) -> radius_date({{YYYY,MM,DD},{Hour,Min,Sec}}) -> DayNumber = calendar:day_of_the_week(YYYY, MM, DD), list_to_binary( - io_lib:format("~s ~3.s ~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w ~4.4.0w", - [day(DayNumber), month(MM), DD, Hour, Min, Sec, YYYY])). + io_lib:format("~s ~3.s ~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w ~4.4.0w", + [day(DayNumber), month(MM), DD, Hour, Min, Sec, YYYY])). format_unknown({VendId, Id}) -> case eradius_dict:lookup(vendor, VendId) of @@ -299,16 +299,16 @@ bin_to_hexstr(Bin) -> format_acct_status_type(Request) -> StatusType = eradius_lib:get_attr(Request, ?Acct_Status_Type), case StatusType of - undefined -> - ""; - 1 -> - "Start"; - 2 -> - "Stop"; - 3 -> - "Interim Update"; - 7 -> - "Accounting-On"; - 8 -> - "Accounting-Off" + undefined -> + ""; + 1 -> + "Start"; + 2 -> + "Stop"; + 3 -> + "Interim Update"; + 7 -> + "Accounting-On"; + 8 -> + "Accounting-Off" end. diff --git a/src/eradius_node_mon.erl b/src/eradius_node_mon.erl index 7d323724..f352ab40 100644 --- a/src/eradius_node_mon.erl +++ b/src/eradius_node_mon.erl @@ -52,11 +52,11 @@ get_remote_version(Node) -> %% ------------------------------------------------------------------------------------------ %% -- gen_server callbacks -record(state, { - live_registrar_nodes = sets:new() :: sets:set(), - dead_registrar_nodes = sets:new() :: sets:set(), - app_masters = maps:new() :: map(), - ping_timer :: reference() -}). + live_registrar_nodes = sets:new() :: sets:set(), + dead_registrar_nodes = sets:new() :: sets:set(), + app_masters = maps:new() :: map(), + ping_timer :: reference() + }). init([]) -> ets:new(?NODE_TAB, [bag, named_table, protected, {read_concurrency, true}]), @@ -80,8 +80,8 @@ handle_cast({remote_modules_ready_v1, ApplicationMaster, Modules}, State) -> handle_cast({modules_ready, ApplicationMaster, Modules}, State) -> NewState = State#state{app_masters = register_locally({ApplicationMaster, Modules}, State#state.app_masters)}, lists:foreach(fun (Node) -> - check_eradius_version(Node), - gen_server:cast({?SERVER, Node}, {remote_modules_ready_v1, ApplicationMaster, Modules}) + check_eradius_version(Node), + gen_server:cast({?SERVER, Node}, {remote_modules_ready_v1, ApplicationMaster, Modules}) end, nodes()), {noreply, NewState}. @@ -136,9 +136,9 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. dict_prepend(Key, List, Map) -> update_with(Key, fun (Old) -> List ++ Old end, List, Map). -% NOTE: -% copy-pasted from maps.erl to have backward compatability with OTP 18 -% it can be rewmoved if minimal version of OTP will be set to 19. +%% NOTE: +%% copy-pasted from maps.erl to have backward compatability with OTP 18 +%% it can be rewmoved if minimal version of OTP will be set to 19. update_with(Key,Fun,Init,Map) when is_function(Fun,1), is_map(Map) -> case maps:find(Key,Map) of {ok,Val} -> maps:update(Key,Fun(Val),Map); diff --git a/src/eradius_proxy.erl b/src/eradius_proxy.erl index 6812459b..bdca5761 100644 --- a/src/eradius_proxy.erl +++ b/src/eradius_proxy.erl @@ -35,7 +35,7 @@ -behaviour(eradius_server). -export([radius_request/3, validate_arguments/1, get_routes_info/1, - put_default_route_to_pool/2, put_routes_to_pool/2]). + put_default_route_to_pool/2, put_routes_to_pool/2]). -ifdef(TEST). -export([resolve_routes/4, validate_options/1, new_request/3, @@ -90,18 +90,18 @@ validate_arguments(Args) -> compile_routes(undefined) -> []; compile_routes(Routes) -> RoutesOpts = lists:map(fun (Route) -> - {Name, Relay, RouteOptions} = route(Route), - case re:compile(Name) of - {ok, R} -> - case validate_route({Relay, RouteOptions}) of - false -> - false; - _ -> {R, Relay, RouteOptions} - end; - {error, {Error, Position}} -> - throw("Error during regexp compilation - " ++ Error ++ " at position " ++ integer_to_list(Position)) - end - end, Routes), + {Name, Relay, RouteOptions} = route(Route), + case re:compile(Name) of + {ok, R} -> + case validate_route({Relay, RouteOptions}) of + false -> + false; + _ -> {R, Relay, RouteOptions} + end; + {error, {Error, Position}} -> + throw("Error during regexp compilation - " ++ Error ++ " at position " ++ integer_to_list(Position)) + end + end, Routes), RelaysRegexps = lists:any(fun(Route) -> Route == false end, RoutesOpts), if RelaysRegexps == false -> RoutesOpts; @@ -109,11 +109,11 @@ compile_routes(Routes) -> false end. -% @private + % @private -spec send_to_server(Request :: #radius_request{}, Route :: undefined_route() | route(), Options :: eradius_client:options()) -> - {reply, Reply :: #radius_request{}} | term(). + {reply, Reply :: #radius_request{}} | term(). send_to_server(_Request, {undefined, 0, []}, _) -> {error, no_route}; @@ -123,8 +123,8 @@ send_to_server(#radius_request{reqid = ReqID} = Request, {{Server, Port, Secret} {ok, Result, Auth} -> decode_request(Result, ReqID, Secret, Auth); no_active_servers -> - % If all RADIUS servers are marked as inactive for now just use - % just skip fail-over mechanism and use default given Peer + % If all RADIUS servers are marked as inactive for now just use + % just skip fail-over mechanism and use default given Peer send_to_server(Request, {Server, Port, Secret}, Options); Error -> ?LOG(error, "~p: error during send_request (~p)", [?MODULE, Error]), @@ -138,7 +138,7 @@ send_to_server(#radius_request{reqid = ReqID} = Request, {Server, Port, Secret}, Error end. -% @private + % @private decode_request(Result, ReqID, Secret, Auth) -> case eradius_lib:decode_request(Result, Secret, Auth) of Reply = #radius_request{} -> @@ -148,7 +148,7 @@ decode_request(Result, ReqID, Secret, Auth) -> Error end. -% @private + % @private -spec validate_route(Route :: route()) -> boolean(). validate_route({{Host, Port, Secret}, RouteOpts}) -> validate_route_options(RouteOpts) and validate_route({Host, Port, Secret}); @@ -163,7 +163,7 @@ validate_route({Host, Port, Secret}) when is_tuple(Host) -> validate_route({Host, _Port, _Secret}) when is_binary(Host) -> true; validate_route(_) -> false. -% @private + % @private -spec validate_route_options(Options :: [proplists:property()] | pool_name()) -> boolean(). validate_route_options(PoolName) when is_atom(PoolName) -> true; @@ -173,7 +173,7 @@ validate_route_options(Options) -> Keys = proplists:get_keys(Options), lists:all(fun(Key) -> validate_route_option(Key, proplists:get_value(Key, Options)) end, Keys). -% @private + % @private -spec validate_route_option(Key :: atom(), Value :: term()) -> boolean(). validate_route_option(timeout, Value) when is_integer(Value) -> true; @@ -184,13 +184,13 @@ validate_route_option(pool, Value) when is_atom(Value) -> validate_route_option(_, _) -> false. -% @private + % @private -spec validate_options(Options :: [proplists:property()]) -> boolean(). validate_options(Options) -> Keys = proplists:get_keys(Options), lists:all(fun(Key) -> validate_option(Key, proplists:get_value(Key, Options)) end, Keys). -% @private + % @private -spec validate_option(Key :: atom(), Value :: term()) -> boolean(). validate_option(type, Value) when Value =:= realm; Value =:= prefix -> true; validate_option(type, _Value) -> false; @@ -202,21 +202,21 @@ validate_option(retries, Value) when is_integer(Value) -> true; validate_option(_, _) -> false. -% @private + % @private -spec new_request(Request :: #radius_request{}, Username :: undefined | binary(), NewUsername :: string()) -> - NewRequest :: #radius_request{}. + NewRequest :: #radius_request{}. new_request(Request, Username, Username) -> Request; new_request(Request, _Username, NewUsername) -> eradius_lib:set_attr(eradius_lib:del_attr(Request, ?User_Name), ?User_Name, NewUsername). -% @private + % @private -spec resolve_routes(Username :: undefined | binary(), DefaultRoute :: undefined_route() | route(), Routes :: routes(), Options :: [proplists:property()]) -> - {NewUsername :: string(), Route :: route()}. + {NewUsername :: string(), Route :: route()}. resolve_routes( undefined, DefaultRoute, _Routes, _Options) -> {undefined, DefaultRoute}; resolve_routes(Username, DefaultRoute, Routes, Options) -> @@ -242,9 +242,9 @@ find_suitable_relay(Key, [{Regexp, Relay, RelayOpts} | Routes], DefaultRoute) -> _ -> {Relay, RelayOpts} end. -% @private + % @private -spec get_key(Username :: binary() | string() | [], Type :: atom(), Strip :: boolean(), Separator :: list()) -> - {Key :: not_found | string(), NewUsername :: string()}. + {Key :: not_found | string(), NewUsername :: string()}. get_key([], _, _, _) -> {not_found, []}; get_key(Username, Type, Strip, Separator) when is_binary(Username) -> get_key(binary_to_list(Username), Type, Strip, Separator); @@ -256,9 +256,9 @@ get_key(Username, prefix, Strip, Separator) -> {Prefix, strip(Username, prefix, Strip, Separator)}; get_key(Username, _, _, _) -> {not_found, Username}. -% @private + % @private -spec strip(Username :: string(), Type :: atom(), Strip :: boolean(), Separator :: list()) -> - NewUsername :: string(). + NewUsername :: string(). strip(Username, _, false, _) -> Username; strip(Username, realm, true, Separator) -> case string:tokens(Username, Separator) of @@ -298,16 +298,16 @@ put_default_route_to_pool(_, _) -> ok. put_routes_to_pool(false, _Retries) -> ok; put_routes_to_pool({routes, Routes}, Retries) -> lists:foreach(fun (Route) -> - case Route of - {_RouteName, {Host, Port, _Secret}} -> - eradius_client:store_radius_server_from_pool(Host, Port, Retries); - {_RouteName, {Host, Port, _Secret}, _Pool} -> - eradius_client:store_radius_server_from_pool(Host, Port, Retries); - {Host, Port, _Secret, _Opts} -> - eradius_client:store_radius_server_from_pool(Host, Port, Retries); - _ -> ok - end - end, Routes). + case Route of + {_RouteName, {Host, Port, _Secret}} -> + eradius_client:store_radius_server_from_pool(Host, Port, Retries); + {_RouteName, {Host, Port, _Secret}, _Pool} -> + eradius_client:store_radius_server_from_pool(Host, Port, Retries); + {Host, Port, _Secret, _Opts} -> + eradius_client:store_radius_server_from_pool(Host, Port, Retries); + _ -> ok + end + end, Routes). get_proxy_opt(_, [], Default) -> Default; get_proxy_opt(OptName, [{OptName, AddrOrRoutes} | _], _) -> AddrOrRoutes; diff --git a/src/eradius_server.erl b/src/eradius_server.erl index 76a5d14c..c77bae2f 100644 --- a/src/eradius_server.erl +++ b/src/eradius_server.erl @@ -28,7 +28,9 @@ %% ``` %% -record(radius_request, { %% reqid :: byte(), -%% cmd :: 'request' | 'accept' | 'challenge' | 'reject' | 'accreq' | 'accresp' | 'coareq' | 'coaack' | 'coanak' | 'discreq' | 'discack' | 'discnak'm +%% cmd :: 'request' | 'accept' | 'challenge' | 'reject' | +%% 'accreq' | 'accresp' | 'coareq' | 'coaack' | 'coanak' | +%% 'discreq' | 'discack' | 'discnak' %% attrs :: eradius_lib:attribute_list(), %% secret :: eradius_lib:secret(), %% authenticator :: eradius_lib:authenticator(), @@ -77,13 +79,13 @@ -type udp_packet() :: {udp, udp_socket(), inet:ip_address(), port_number(), binary()}. -record(state, { - socket :: udp_socket(), % Socket Reference of opened UDP port - ip = {0,0,0,0} :: inet:ip_address(), % IP to which this socket is bound - port = 0 :: port_number(), % Port number we are listening on - transacts :: ets:tid(), % ETS table containing current transactions - counter :: #server_counter{}, % statistics counter, - name :: atom() % server name -}). + socket :: udp_socket(), % Socket Reference of opened UDP port + ip = {0,0,0,0} :: inet:ip_address(), % IP to which this socket is bound + port = 0 :: port_number(), % Port number we are listening on + transacts :: ets:tid(), % ETS table containing current transactions + counter :: #server_counter{}, % statistics counter, + name :: atom() % server name + }). -optional_callbacks([validate_arguments/1]). @@ -143,7 +145,7 @@ handle_info(ReqUDP = {udp, Socket, FromIP, FromPortNo, Packet}, [{_ReqKey, {handling, HandlerPid}}] -> %% handler process is still working on the request ?LOG(debug, "~s From: ~s INF: Handler process ~p is still working on the request. duplicate request (being handled) ~p", - [printable_peer(ServerIP, Port), printable_peer(FromIP, FromPortNo), HandlerPid, ReqKey]), + [printable_peer(ServerIP, Port), printable_peer(FromIP, FromPortNo), HandlerPid, ReqKey]), eradius_counter:inc_counter(dupRequests, NasProp); [{_ReqKey, {replied, HandlerPid}}] -> %% handler process waiting for resend message @@ -220,22 +222,24 @@ do_radius(ServerPid, ServerName, ReqKey, Handler = {HandlerMod, _}, NasProp, {ud Nodes = eradius_node_mon:get_module_nodes(HandlerMod), case run_handler(Nodes, NasProp, Handler, EncRequest) of {reply, EncReply, {ReqCmd, RespCmd}, Request} -> + ?LOG(debug, "~s From: ~s INF: Sending response for request ~p", + [printable_peer(ServerIP, Port), printable_peer(FromIP, FromPort), ReqKey]), TS2 = erlang:monotonic_time(), inc_counter({ReqCmd, RespCmd}, ServerName, NasProp, TS2 - TS1, Request), gen_udp:send(Socket, FromIP, FromPort, EncReply), case application:get_env(eradius, resend_timeout, 2000) of ResendTimeout when ResendTimeout > 0, is_integer(ResendTimeout) -> - ServerPid ! {replied, ReqKey, self()}, - wait_resend_init(ServerPid, ReqKey, FromIP, FromPort, EncReply, ResendTimeout, ?RESEND_RETRIES); + ServerPid ! {replied, ReqKey, self()}, + wait_resend_init(ServerPid, ReqKey, FromIP, FromPort, EncReply, ResendTimeout, ?RESEND_RETRIES); _ -> ok end; {discard, Reason} -> ?LOG(debug, "~s From: ~s INF: Handler discarded the request ~p for reason ~1000.p", - [printable_peer(ServerIP, Port), printable_peer(FromIP, FromPort), Reason, ReqKey]), + [printable_peer(ServerIP, Port), printable_peer(FromIP, FromPort), Reason, ReqKey]), inc_discard_counter(Reason, NasProp); {exit, Reason} -> ?LOG(debug, "~s From: ~s INF: Handler exited for reason ~p, discarding request ~p", - [printable_peer(ServerIP, Port), printable_peer(FromIP, FromPort), Reason, ReqKey]), + [printable_peer(ServerIP, Port), printable_peer(FromIP, FromPort), Reason, ReqKey]), inc_discard_counter(packetsDropped, NasProp) end, eradius_counter:dec_counter(pending, NasProp). @@ -340,7 +344,7 @@ apply_handler_mod(HandlerMod, HandlerArg, Request, NasProp) -> {reply, EncReply, {Request#radius_request.cmd, ReplyCmd}, Request}; noreply -> ?LOG(error, "~s INF: Noreply for request ~p from handler ~p: returned value: ~p", - [printable_peer(ServerIP, Port), Request, HandlerArg, noreply]), + [printable_peer(ServerIP, Port), Request, HandlerArg, noreply]), {discard, handler_returned_noreply}; {error, timeout} -> ReqType = eradius_log:format_cmd(Request#radius_request.cmd), @@ -354,12 +358,12 @@ apply_handler_mod(HandlerMod, HandlerArg, Request, NasProp) -> {discard, {bad_return, {error, timeout}}}; OtherReturn -> ?LOG(error, "~s INF: Unexpected return for request ~p from handler ~p: returned value: ~p", - [printable_peer(ServerIP, Port), Request, HandlerArg, OtherReturn]), + [printable_peer(ServerIP, Port), Request, HandlerArg, OtherReturn]), {discard, {bad_return, OtherReturn}} catch Class:Reason:S -> ?LOG(error, "~s INF: Handler crashed after request ~p, radius handler class: ~p, reason of crash: ~p, stacktrace: ~p", - [printable_peer(ServerIP, Port), Request, Class, Reason, S]), + [printable_peer(ServerIP, Port), Request, Class, Reason, S]), {exit, {Class, Reason}} end. @@ -447,10 +451,16 @@ inc_discard_counter(_Reason, NasProp) -> server_request_counter_account_match_spec_compile() -> case persistent_term:get({?MODULE, ?FUNCTION_NAME}, undefined) of undefined -> - MatchSpecCompile = ets:match_spec_compile(ets:fun2ms(fun - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Start}) -> accountRequestsStart; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Stop}) -> accountRequestsStop; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Update}) -> accountRequestsUpdate end)), + MatchSpecCompile = + ets:match_spec_compile( + ets:fun2ms( + fun ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Start}) -> + accountRequestsStart; + ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Stop}) -> + accountRequestsStop; + ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Update}) -> + accountRequestsUpdate + end)), persistent_term:put({?MODULE, ?FUNCTION_NAME}, MatchSpecCompile), MatchSpecCompile; MatchSpecCompile -> @@ -460,10 +470,16 @@ server_request_counter_account_match_spec_compile() -> server_response_counter_account_match_spec_compile() -> case persistent_term:get({?MODULE, ?FUNCTION_NAME}, undefined) of undefined -> - MatchSpecCompile = ets:match_spec_compile(ets:fun2ms(fun - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Start}) -> accountResponsesStart; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Stop}) -> accountResponsesStop; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Update}) -> accountResponsesUpdate end)), + MatchSpecCompile = + ets:match_spec_compile( + ets:fun2ms( + fun ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Start}) -> + accountResponsesStart; + ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Stop}) -> + accountResponsesStop; + ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Update}) -> + accountResponsesUpdate + end)), persistent_term:put({?MODULE, ?FUNCTION_NAME}, MatchSpecCompile), MatchSpecCompile; MatchSpecCompile -> diff --git a/src/eradius_server_mon.erl b/src/eradius_server_mon.erl index 6f4b6f84..dabaa386 100644 --- a/src/eradius_server_mon.erl +++ b/src/eradius_server_mon.erl @@ -22,11 +22,11 @@ -import(eradius_lib, [printable_peer/2]). -record(nas, { - key :: {server(), inet:ip_address()}, - server_name :: server_name(), - handler :: handler(), - prop :: #nas_prop{} -}). + key :: {server(), inet:ip_address()}, + server_name :: server_name(), + handler :: handler(), + prop :: #nas_prop{} + }). %% ------------------------------------------------------------------------------------------ %% -- API @@ -65,7 +65,7 @@ lookup_pid(ServerIP, ServerPort) -> %% @doc returns the list of all currently configured NASs -spec all_nas_keys() -> [term()]. all_nas_keys() -> - ets:select(?NAS_TAB, [{#nas{key = '$1', _ = '_'}, [], ['$1']}]). + ets:select(?NAS_TAB, [{#nas{key = '$1', _ = '_'}, [], ['$1']}]). %% ------------------------------------------------------------------------------------------ %% -- gen_server callbacks @@ -130,11 +130,11 @@ 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}}), - NasInfo = eradius_lib:make_addr_info({NasId, {NasIP, undefined}}), - #nas{key = {{IP, Port}, NasIP}, server_name = ServerName, handler = {HandlerMod, HandlerArgs}, - prop = #nas_prop{handler_nodes = HandlerNodes, nas_id = NasId, nas_ip = NasIP, secret = Secret, - metrics_info = {ServerInfo, NasInfo}}} + ServerInfo = eradius_lib:make_addr_info({ServerName, {IP, Port}}), + NasInfo = eradius_lib:make_addr_info({NasId, {NasIP, undefined}}), + #nas{key = {{IP, Port}, NasIP}, server_name = ServerName, handler = {HandlerMod, HandlerArgs}, + prop = #nas_prop{handler_nodes = HandlerNodes, nas_id = NasId, nas_ip = NasIP, secret = Secret, + metrics_info = {ServerInfo, NasInfo}}} end, HandlerList). config_nodes(NasHandler) -> @@ -142,26 +142,26 @@ config_nodes(NasHandler) -> update_server(Running, ToStop, ToStart) -> Stopped = lists:map(fun(ServerAddr = {_ServerName, Addr}) -> - StoppedServer = {_, _, Pid} = lists:keyfind(Addr, 2, Running), - eradius_server_sup:stop_instance(ServerAddr, Pid), - StoppedServer + StoppedServer = {_, _, Pid} = lists:keyfind(Addr, 2, Running), + eradius_server_sup:stop_instance(ServerAddr, Pid), + StoppedServer end, ToStop), 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, + 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), + ({ServerName, {IP, Port}}) -> + StartFn({ServerName, {IP, Port, []}}); + (ServerAddr) -> + StartFn(ServerAddr) + end, + ToStart), (Running -- Stopped) ++ NewStarted. update_nases(ToDelete, ToInsert) -> diff --git a/test/eradius_auth_SUITE.erl b/test/eradius_auth_SUITE.erl index f49f839a..aec33608 100644 --- a/test/eradius_auth_SUITE.erl +++ b/test/eradius_auth_SUITE.erl @@ -1,44 +1,44 @@ -% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> - -% Permission is hereby granted, free of charge, to any person obtaining a -% copy of this software and associated documentation files (the "Software"), -% to deal in the Software without restriction, including without limitation -% the rights to use, copy, modify, merge, publish, distribute, sublicense, -% and/or sell copies of the Software, and to permit persons to whom the -% Software is furnished to do so, subject to the following conditions: - -% The above copyright notice and this permission notice shall be included in -% all copies or substantial portions of the Software. - -% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -% DEALINGS IN THE SOFTWARE. +%% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> + +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: + +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. + +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. -module(eradius_auth_SUITE). -compile(export_all). -include("eradius_test.hrl"). all() -> [ - unicode_test, - password_hash_test, - unicode_password_hash_test, - password_hashhash_test, - password_hash2_test, - des_key_test, - nt_response_test, - v2_authenticator_test, - mppe_master_key_test, - mppe_sendstart_key40_test, - mppe_sendstart_key56_test, - mppe_sendstart_key128_test, - mppe_start_key40_test, - mppe_start_key56_test, - mppe_start_key128_test - ]. + unicode_test, + password_hash_test, + unicode_password_hash_test, + password_hashhash_test, + password_hash2_test, + des_key_test, + nt_response_test, + v2_authenticator_test, + mppe_master_key_test, + mppe_sendstart_key40_test, + mppe_sendstart_key56_test, + mppe_sendstart_key128_test, + mppe_start_key40_test, + mppe_start_key56_test, + mppe_start_key128_test + ]. %% 0-to-256-char UserName diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index a25f82cb..43d6c28f 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -1,22 +1,22 @@ -% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> - -% Permission is hereby granted, free of charge, to any person obtaining a -% copy of this software and associated documentation files (the "Software"), -% to deal in the Software without restriction, including without limitation -% the rights to use, copy, modify, merge, publish, distribute, sublicense, -% and/or sell copies of the Software, and to permit persons to whom the -% Software is furnished to do so, subject to the following conditions: - -% The above copyright notice and this permission notice shall be included in -% all copies or substantial portions of the Software. - -% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -% DEALINGS IN THE SOFTWARE. +%% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> + +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: + +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. + +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. -module(eradius_client_SUITE). -compile(export_all). @@ -46,17 +46,17 @@ ?GOOD_SERVER_2_TUPLE]). all() -> [ - send_request, - wanna_send, - reconf_address, - wanna_send, - reconf_ports_30, - wanna_send, - reconf_ports_10, - wanna_send, - send_request_failover, - check_upstream_servers - ]. + send_request, + wanna_send, + reconf_address, + wanna_send, + reconf_ports_30, + wanna_send, + reconf_ports_10, + wanna_send, + send_request_failover, + check_upstream_servers + ]. init_per_suite(Config) -> {ok, _} = application:ensure_all_started(eradius), @@ -132,13 +132,13 @@ testSocket(Pid) -> end. -record(state, { - socket_ip :: inet:ip_address(), - no_ports = 1 :: pos_integer(), - idcounters = maps:new() :: map(), - sockets = array:new() :: array:array(), - sup :: pid(), - subscribed_clients = [] :: [{{integer(),integer(),integer(),integer()}, integer()}] -}). + socket_ip :: inet:ip_address(), + no_ports = 1 :: pos_integer(), + idcounters = maps:new() :: map(), + sockets = array:new() :: array:array(), + sup :: pid(), + subscribed_clients = [] :: [{{integer(),integer(),integer(),integer()}, integer()}] + }). split(N, List) -> split2(N, [], List). @@ -178,25 +178,25 @@ test(false, Msg) -> check(OldState, NewState = #state{no_ports = P}, null, A) -> check(OldState, NewState, P, A); check(OldState, NewState = #state{socket_ip = A}, P, null) -> check(OldState, NewState, P, A); check(#state{sockets = OS, no_ports = _OP, idcounters = _OC, socket_ip = OA}, - #state{sockets = NS, no_ports = NP, idcounters = NC, socket_ip = NA}, - P, A) -> + #state{sockets = NS, no_ports = NP, idcounters = NC, socket_ip = NA}, + P, A) -> {ok, PA} = parse_ip(A), test(PA == NA, "Adress not configured") and - case NA of - OA -> - {_, Rest} = split(NP, array:to_list(OS)), - test(P == NP,"Ports not configured") and - test(maps:fold( fun(_Peer, {NextPortIdx, _NextReqId}, Akk) -> - Akk and (NextPortIdx =< NP) - end, true, NC), "Invalid port counter") and - test(getSocketCount() =< NP, "Sockets not closed") and - test(array:size(NS) =< NP, "Socket array not resized") and - test(lists:all(fun(Pid) -> testSocket(Pid) end, Rest), "Sockets still available"); - _ -> - test(array:size(NS) == 0, "Socket array not cleaned") and - test(getSocketCount() == 0, "Sockets not closed") and - test(lists:all(fun(Pid) -> testSocket(Pid) end, array:to_list(OS)), "Sockets still available") - end. + case NA of + OA -> + {_, Rest} = split(NP, array:to_list(OS)), + test(P == NP,"Ports not configured") and + test(maps:fold( fun(_Peer, {NextPortIdx, _NextReqId}, Akk) -> + Akk and (NextPortIdx =< NP) + end, true, NC), "Invalid port counter") and + test(getSocketCount() =< NP, "Sockets not closed") and + test(array:size(NS) =< NP, "Socket array not resized") and + test(lists:all(fun(Pid) -> testSocket(Pid) end, Rest), "Sockets still available"); + _ -> + test(array:size(NS) == 0, "Socket array not cleaned") and + test(getSocketCount() == 0, "Sockets not closed") and + test(lists:all(fun(Pid) -> testSocket(Pid) end, array:to_list(OS)), "Sockets still available") + end. %% TESTS @@ -217,12 +217,12 @@ send(FUN, Ports, Address) -> wanna_send(_Config) -> lists:map(fun(_) -> - IP = {rand:uniform(100), rand:uniform(100), rand:uniform(100), rand:uniform(100)}, - Port = rand:uniform(100), - MetricsInfo = {{undefined, undefined, undefined}, {undefined, undefined, undefined}}, - FUN = fun() -> gen_server:call(eradius_client, {wanna_send, {undefined, {IP, Port}}, MetricsInfo}) end, - send(FUN, null, null) - end, lists:seq(1, 10)). + IP = {rand:uniform(100), rand:uniform(100), rand:uniform(100), rand:uniform(100)}, + Port = rand:uniform(100), + MetricsInfo = {{undefined, undefined, undefined}, {undefined, undefined, undefined}}, + FUN = fun() -> gen_server:call(eradius_client, {wanna_send, {undefined, {IP, Port}}, MetricsInfo}) end, + send(FUN, null, null) + end, lists:seq(1, 10)). %% I've catched some data races with `delSocket()' and `getSocketCount()' when %% `delSocket()' happens after `getSocketCount()' (because `delSocket()' is sent from another process). diff --git a/test/eradius_client_socket_test.erl b/test/eradius_client_socket_test.erl index ee465c59..c2a3bf12 100644 --- a/test/eradius_client_socket_test.erl +++ b/test/eradius_client_socket_test.erl @@ -1,22 +1,22 @@ -% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> - -% Permission is hereby granted, free of charge, to any person obtaining a -% copy of this software and associated documentation files (the "Software"), -% to deal in the Software without restriction, including without limitation -% the rights to use, copy, modify, merge, publish, distribute, sublicense, -% and/or sell copies of the Software, and to permit persons to whom the -% Software is furnished to do so, subject to the following conditions: - -% The above copyright notice and this permission notice shall be included in -% all copies or substantial portions of the Software. - -% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -% DEALINGS IN THE SOFTWARE. +%% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> + +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: + +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. + +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. -module(eradius_client_socket_test). @@ -42,13 +42,13 @@ handle_cast(_Msg, State) -> {noreply, State}. handle_info({SenderPid, send_request, {IP, Port}, ReqId, _EncRequest}, - State = #state{pending = Pending, counter = Counter}) -> + State = #state{pending = Pending, counter = Counter}) -> ReqKey = {IP, Port, ReqId}, NPending = Pending#{ReqKey => SenderPid}, {noreply, State#state{pending = NPending, counter = Counter+1}}; handle_info(close, State) -> - %~ {noreply, State#state{mode = inactive}}; + %~ {noreply, State#state{mode = inactive}}; {stop, normal, State}; handle_info({status, Pid}, State = #state{mode = Mode}) -> diff --git a/test/eradius_config_SUITE.erl b/test/eradius_config_SUITE.erl index 39edc763..2f35e1e6 100644 --- a/test/eradius_config_SUITE.erl +++ b/test/eradius_config_SUITE.erl @@ -1,22 +1,22 @@ -% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> +%% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> -% Permission is hereby granted, free of charge, to any person obtaining a -% copy of this software and associated documentation files (the "Software"), -% to deal in the Software without restriction, including without limitation -% the rights to use, copy, modify, merge, publish, distribute, sublicense, -% and/or sell copies of the Software, and to permit persons to whom the -% Software is furnished to do so, subject to the following conditions: +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: -% The above copyright notice and this permission notice shall be included in -% all copies or substantial portions of the Software. +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. -% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -% DEALINGS IN THE SOFTWARE. +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. -module(eradius_config_SUITE). -compile(export_all). @@ -40,40 +40,40 @@ config_1(_Config) -> Conf = [{session_nodes, ['node1@host1', 'node2@host2']}, {radius_callback, ?MODULE}, {servers, [ - {root, {eradius_test_handler:localhost(ip), [1812, 1813]}} + {root, {eradius_test_handler:localhost(ip), [1812, 1813]}} ]}, {root, [ - { {"NAS1", [arg1, arg2]}, - [{"10.18.14.2/30", <<"secret1">>}]}, - { {"NAS2", [arg1, arg2]}, - [{{10, 18, 14, 3}, <<"secret2">>, [{nas_id, <<"name">>}]}]} + { {"NAS1", [arg1, arg2]}, + [{"10.18.14.2/30", <<"secret1">>}]}, + { {"NAS2", [arg1, arg2]}, + [{{10, 18, 14, 3}, <<"secret2">>, [{nas_id, <<"name">>}]}]} ]}], ok = apply_conf(Conf), LocalHost = eradius_test_handler:localhost(tuple), ?match({ok, {?MODULE,[arg1,arg2]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1813, - nas_id = <<"name">>, - nas_ip = {10,18,14,3}, - handler_nodes = ['node1@host1', 'node2@host2'] - }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,3})), + #nas_prop{ + server_ip = LocalHost, + server_port = 1813, + nas_id = <<"name">>, + nas_ip = {10,18,14,3}, + handler_nodes = ['node1@host1', 'node2@host2'] + }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,3})), ?match({ok, {?MODULE,[arg1,arg2]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1812, - nas_id = <<"name">>, - nas_ip = {10,18,14,3}, - handler_nodes = ['node1@host1', 'node2@host2'] - }}, eradius_server_mon:lookup_handler(LocalHost, 1812, {10,18,14,3})), + #nas_prop{ + server_ip = LocalHost, + server_port = 1812, + nas_id = <<"name">>, + nas_ip = {10,18,14,3}, + handler_nodes = ['node1@host1', 'node2@host2'] + }}, eradius_server_mon:lookup_handler(LocalHost, 1812, {10,18,14,3})), ?match({ok, {?MODULE,[arg1,arg2]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1813, - nas_id = <<"NAS1_10.18.14.2">>, - nas_ip = {10,18,14,2}, - handler_nodes = ['node1@host1', 'node2@host2'] - }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,2})), + #nas_prop{ + server_ip = LocalHost, + server_port = 1813, + nas_id = <<"NAS1_10.18.14.2">>, + nas_ip = {10,18,14,2}, + handler_nodes = ['node1@host1', 'node2@host2'] + }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,2})), ok. config_2(_Config) -> @@ -83,41 +83,41 @@ config_2(_Config) -> ] }, {servers, [ - {root, {eradius_test_handler:localhost(ip), [1812, 1813]}} + {root, {eradius_test_handler:localhost(ip), [1812, 1813]}} ]}, {root, [ - { {handler1, "NAS1", [arg1, arg2]}, - [ {"10.18.14.3", <<"secret1">>, [{group, "NodeGroup1"}]}, - {"10.18.14.4", <<"secret1">>, [{group, "NodeGroup1"}]} ] }, - { {handler2, "NAS2", [arg3, arg4]}, - [ {"10.18.14.2", <<"secret2">>, [{group, "NodeGroup2"}]} ] } - ]}], + { {handler1, "NAS1", [arg1, arg2]}, + [ {"10.18.14.3", <<"secret1">>, [{group, "NodeGroup1"}]}, + {"10.18.14.4", <<"secret1">>, [{group, "NodeGroup1"}]} ] }, + { {handler2, "NAS2", [arg3, arg4]}, + [ {"10.18.14.2", <<"secret2">>, [{group, "NodeGroup2"}]} ] } + ]}], ok = apply_conf(Conf), LocalHost = eradius_test_handler:localhost(tuple), ?match({ok, {handler1,[arg1,arg2]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1813, - nas_id = <<"NAS1_10.18.14.3">>, - nas_ip = {10,18,14,3}, - handler_nodes = ['node1@host1', 'node2@host2'] - }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,3})), + #nas_prop{ + server_ip = LocalHost, + server_port = 1813, + nas_id = <<"NAS1_10.18.14.3">>, + nas_ip = {10,18,14,3}, + handler_nodes = ['node1@host1', 'node2@host2'] + }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,3})), ?match({ok, {handler1,[arg1,arg2]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1813, - nas_id = <<"NAS1_10.18.14.4">>, - nas_ip = {10,18,14,4}, - handler_nodes = ['node1@host1', 'node2@host2'] - }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,4})), + #nas_prop{ + server_ip = LocalHost, + server_port = 1813, + nas_id = <<"NAS1_10.18.14.4">>, + nas_ip = {10,18,14,4}, + handler_nodes = ['node1@host1', 'node2@host2'] + }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,4})), ?match({ok, {handler2,[arg3,arg4]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1813, - nas_id = <<"NAS2_10.18.14.2">>, - nas_ip = {10,18,14,2}, - handler_nodes = ['node3@host3', 'node4@host4'] - }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,2})), + #nas_prop{ + server_ip = LocalHost, + server_port = 1813, + nas_id = <<"NAS2_10.18.14.2">>, + nas_ip = {10,18,14,2}, + handler_nodes = ['node3@host3', 'node4@host4'] + }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,2})), ok. config_socket_options(_Config) -> @@ -159,46 +159,46 @@ config_with_ranges(_Config) -> ] }, {servers, [ - {root, {eradius_test_handler:localhost(ip), [1812, 1813]}} + {root, {eradius_test_handler:localhost(ip), [1812, 1813]}} ]}, {root, [ - { {handler, "NAS", []}, - [ {"10.18.14.2/30", <<"secret2">>, [{group, "NodeGroup"}]} ] } - ]}], + { {handler, "NAS", []}, + [ {"10.18.14.2/30", <<"secret2">>, [{group, "NodeGroup"}]} ] } + ]}], ok = apply_conf(Conf), LocalHost = eradius_test_handler:localhost(tuple), ?match({ok, {handler,[]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1812, - nas_id = <<"NAS_10.18.14.2">>, - nas_ip = {10,18,14,2}, - handler_nodes = Nodes - }}, eradius_server_mon:lookup_handler(LocalHost, 1812, {10,18,14,2})), + #nas_prop{ + server_ip = LocalHost, + server_port = 1812, + nas_id = <<"NAS_10.18.14.2">>, + nas_ip = {10,18,14,2}, + handler_nodes = Nodes + }}, eradius_server_mon:lookup_handler(LocalHost, 1812, {10,18,14,2})), ?match({ok, {handler,[]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1812, - nas_id = <<"NAS_10.18.14.3">>, - nas_ip = {10,18,14,3}, - handler_nodes = Nodes - }}, eradius_server_mon:lookup_handler(LocalHost, 1812, {10,18,14,3})), + #nas_prop{ + server_ip = LocalHost, + server_port = 1812, + nas_id = <<"NAS_10.18.14.3">>, + nas_ip = {10,18,14,3}, + handler_nodes = Nodes + }}, eradius_server_mon:lookup_handler(LocalHost, 1812, {10,18,14,3})), ?match({ok, {handler,[]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1813, - nas_id = <<"NAS_10.18.14.1">>, - nas_ip = {10,18,14,1}, - handler_nodes = Nodes - }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,1})), + #nas_prop{ + server_ip = LocalHost, + server_port = 1813, + nas_id = <<"NAS_10.18.14.1">>, + nas_ip = {10,18,14,1}, + handler_nodes = Nodes + }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,1})), ?match({ok, {handler,[]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1813, - nas_id = <<"NAS_10.18.14.2">>, - nas_ip = {10,18,14,2}, - handler_nodes = Nodes - }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,2})), + #nas_prop{ + server_ip = LocalHost, + server_port = 1813, + nas_id = <<"NAS_10.18.14.2">>, + nas_ip = {10,18,14,2}, + handler_nodes = Nodes + }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,2})), ok. log_test(_Config) -> @@ -208,7 +208,7 @@ log_test(_Config) -> LogOn1 = [{logging, true}, {logfile, LogFile1}], LogOff = [{logging, false}], - % via eradius_log:reconfigure/0 + %% via eradius_log:reconfigure/0 set_env(LogOn0), ok = eradius_log:reconfigure(), ?match(true, logger_disabled /= gen_server:call(eradius_log, get_state)), @@ -224,7 +224,7 @@ log_test(_Config) -> ?match(true, logger_disabled /= gen_server:call(eradius_log, get_state)), ?match(true, filelib:is_file(LogFile1)), - % via eradius:config_change/3 + %% via eradius:config_change/3 set_env(LogOff), eradius:config_change([], LogOff, []), logger_disabled = gen_server:call(eradius_log, get_state), @@ -233,7 +233,7 @@ log_test(_Config) -> eradius:config_change([], LogOn1, []), ?match(true, logger_disabled /= gen_server:call(eradius_log, get_state)), - % check default value for logging + %% check default value for logging application:unset_env(eradius, logging), eradius:config_change([], [], [logging]), logger_disabled = gen_server:call(eradius_log, get_state), @@ -249,7 +249,7 @@ apply_conf(Config) -> generate_ip_list_test(_) -> ?equal([{192, 168, 11, 148}, {192, 168, 11, 149}, {192, 168, 11, 150}, {192, 168, 11, 151}], - eradius_config:generate_ip_list({192, 168, 11, 150}, "30")), + eradius_config:generate_ip_list({192, 168, 11, 150}, "30")), eradius_config:generate_ip_list({192, 168, 11, 150}, 24), ?equal(256, length(eradius_config:generate_ip_list({192, 168, 11, 150}, 24))), ?equal(2048, length(eradius_config:generate_ip_list({192, 168, 11, 10}, 21))), diff --git a/test/eradius_lib_SUITE.erl b/test/eradius_lib_SUITE.erl index 1496213d..8dc884c9 100644 --- a/test/eradius_lib_SUITE.erl +++ b/test/eradius_lib_SUITE.erl @@ -1,22 +1,22 @@ -% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> - -% Permission is hereby granted, free of charge, to any person obtaining a -% copy of this software and associated documentation files (the "Software"), -% to deal in the Software without restriction, including without limitation -% the rights to use, copy, modify, merge, publish, distribute, sublicense, -% and/or sell copies of the Software, and to permit persons to whom the -% Software is furnished to do so, subject to the following conditions: - -% The above copyright notice and this permission notice shall be included in -% all copies or substantial portions of the Software. - -% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -% DEALINGS IN THE SOFTWARE. +%% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> + +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: + +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. + +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. -module(eradius_lib_SUITE). -include("eradius_lib.hrl"). @@ -63,9 +63,9 @@ init_per_suite(Config) -> Config. end_per_suite(_Config) -> ok. init_per_testcase(Test, Config) when Test == dec_vendor_integer_t - orelse Test == dec_vendor_string_t - orelse Test == dec_vendor_ipv4_t - orelse Test == vendor_attribute_id_conflict_test -> + orelse Test == dec_vendor_string_t + orelse Test == dec_vendor_ipv4_t + orelse Test == vendor_attribute_id_conflict_test -> application:set_env(eradius, tables, [dictionary]), eradius_dict:start_link(), eradius_dict:unload_tables([dictionary_3gpp]), diff --git a/test/eradius_logtest.erl b/test/eradius_logtest.erl index a093e27b..aa410aad 100644 --- a/test/eradius_logtest.erl +++ b/test/eradius_logtest.erl @@ -1,22 +1,22 @@ -% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> - -% Permission is hereby granted, free of charge, to any person obtaining a -% copy of this software and associated documentation files (the "Software"), -% to deal in the Software without restriction, including without limitation -% the rights to use, copy, modify, merge, publish, distribute, sublicense, -% and/or sell copies of the Software, and to permit persons to whom the -% Software is furnished to do so, subject to the following conditions: - -% The above copyright notice and this permission notice shall be included in -% all copies or substantial portions of the Software. - -% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -% DEALINGS IN THE SOFTWARE. +%% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> + +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: + +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. + +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. -module(eradius_logtest). @@ -53,14 +53,14 @@ start() -> ]}, {session_nodes, [node()]}, {root, [ - { {eradius_logtest, "root", [] }, [{"127.0.0.1/24", ?SECRET, [{nas_id, <<"Test_Nas_Id">>}]}] } - ]}, + { {eradius_logtest, "root", [] }, [{"127.0.0.1/24", ?SECRET, [{nas_id, <<"Test_Nas_Id">>}]}] } + ]}, {test, [ - { {eradius_logtest, "test", [] }, [{eradius_test_handler:localhost(ip), ?SECRET3, [{nas_id, <<"Test_Nas_Id_test">>}]}] } - ]}, + { {eradius_logtest, "test", [] }, [{eradius_test_handler:localhost(ip), ?SECRET3, [{nas_id, <<"Test_Nas_Id_test">>}]}] } + ]}, {proxy, [ { {eradius_proxy, "proxy", ProxyConfig }, [{eradius_test_handler:localhost(ip), ?SECRET2, [{nas_id, <<"Test_Nas_proxy">>}]}] } - ]} + ]} ], [application:set_env(eradius, Key, Value) || {Key, Value} <- Config], {ok, _} = application:ensure_all_started(eradius), @@ -92,7 +92,7 @@ radius_request(#radius_request{cmd = accreq}, _NasProp, _) -> validate_arguments(_Args) -> true. test_client() -> - test_client(request). + test_client(request). test_client(Command) -> eradius_dict:load_tables([dictionary, dictionary_3gpp]), @@ -100,7 +100,7 @@ test_client(Command) -> send_request(eradius_test_handler:localhost(tuple), 1813, ?SECRET, Request). test_proxy() -> - test_proxy(request). + test_proxy(request). test_proxy(Command) -> eradius_dict:load_tables([dictionary, dictionary_3gpp]), diff --git a/test/eradius_metrics_SUITE.erl b/test/eradius_metrics_SUITE.erl index 11e2f2c3..60cc0fbb 100644 --- a/test/eradius_metrics_SUITE.erl +++ b/test/eradius_metrics_SUITE.erl @@ -1,22 +1,22 @@ -% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> - -% Permission is hereby granted, free of charge, to any person obtaining a -% copy of this software and associated documentation files (the "Software"), -% to deal in the Software without restriction, including without limitation -% the rights to use, copy, modify, merge, publish, distribute, sublicense, -% and/or sell copies of the Software, and to permit persons to whom the -% Software is furnished to do so, subject to the following conditions: - -% The above copyright notice and this permission notice shall be included in -% all copies or substantial portions of the Software. - -% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -% DEALINGS IN THE SOFTWARE. +%% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> + +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: + +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. + +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. -module(eradius_metrics_SUITE). -compile(export_all). @@ -53,8 +53,8 @@ init_per_suite(Config) -> { {"good", [] }, [{"127.0.0.2", ?SECRET, [{nas_id, <<"good_nas">>}]}] } ]}, {bad, [ - { {"bad", [] }, [{"127.0.0.2", ?SECRET, [{nas_id, <<"bad_nas">>}]}] } - ]}, + { {"bad", [] }, [{"127.0.0.2", ?SECRET, [{nas_id, <<"bad_nas">>}]}] } + ]}, {error, [ { {"error", [] }, [{"127.0.0.2", ?SECRET, [{nas_id, <<"error_nas">>}]}] } ]}, @@ -66,8 +66,8 @@ init_per_suite(Config) -> ], [application:set_env(eradius, Key, Value) || {Key, Value} <- EradiusConfig], application:set_env(prometheus, collectors, [eradius_prometheus_collector]), - % prometheus is not included directly to eradius but prometheus_eradius_collector - % should include it + %% prometheus is not included directly to eradius but prometheus_eradius_collector + %% should include it application:ensure_all_started(prometheus), {ok, _} = application:ensure_all_started(eradius), spawn(fun() -> @@ -92,7 +92,7 @@ good_requests(_Config) -> {coareq, coa, coa_ack}, {discreq, disconnect, disconnect_ack}], [check_single_request(good, EradiusRequestType, RequestType, ResponseType) || - {EradiusRequestType, RequestType, ResponseType} <- Requests ], + {EradiusRequestType, RequestType, ResponseType} <- Requests ], check_total_requests(good, length(Requests)). bad_requests(_Config) -> @@ -100,7 +100,7 @@ bad_requests(_Config) -> {coareq, coa, coa_nak}, {discreq, disconnect, disconnect_nak}], [check_single_request(bad, EradiusRequestType, RequestType, ResponseType) || - {EradiusRequestType, RequestType, ResponseType} <- Requests ], + {EradiusRequestType, RequestType, ResponseType} <- Requests ], check_total_requests(bad, length(Requests)). error_requests(_Config) -> diff --git a/test/eradius_proxy_SUITE.erl b/test/eradius_proxy_SUITE.erl index f2f348b4..b76ee2f1 100644 --- a/test/eradius_proxy_SUITE.erl +++ b/test/eradius_proxy_SUITE.erl @@ -1,22 +1,22 @@ -% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> +%% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> -% Permission is hereby granted, free of charge, to any person obtaining a -% copy of this software and associated documentation files (the "Software"), -% to deal in the Software without restriction, including without limitation -% the rights to use, copy, modify, merge, publish, distribute, sublicense, -% and/or sell copies of the Software, and to permit persons to whom the -% Software is furnished to do so, subject to the following conditions: +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: -% The above copyright notice and this permission notice shall be included in -% all copies or substantial portions of the Software. +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. -% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -% DEALINGS IN THE SOFTWARE. +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. -module(eradius_proxy_SUITE). -compile(export_all). @@ -25,13 +25,13 @@ -include("eradius_test.hrl"). all() -> [ - resolve_routes_test, - validate_arguments_test, - validate_options_test, - new_request_test, - get_key_test, - strip_test - ]. + resolve_routes_test, + validate_arguments_test, + validate_options_test, + new_request_test, + get_key_test, + strip_test + ]. resolve_routes_test(_) -> DefaultRoute = {eradius_test_handler:localhost(tuple), 1813, <<"secret">>}, @@ -42,12 +42,12 @@ resolve_routes_test(_) -> {ok, R2} = re:compile("test"), {ok, R3} = re:compile("^dev_.*"), Routes = [{R1, Prod}, {R2, Test, [{pool, test_pool}]}, {R3, Dev}], - % default + %% default ?equal({undefined, DefaultRoute}, eradius_proxy:resolve_routes(undefined, DefaultRoute, Routes,[])), ?equal({"user", DefaultRoute}, eradius_proxy:resolve_routes(<<"user">>, DefaultRoute, Routes, [])), ?equal({"user@prod", Prod}, eradius_proxy:resolve_routes(<<"user@prod">>, DefaultRoute, Routes,[])), ?equal({"user@test", {Test, [{pool, test_pool}]}}, eradius_proxy:resolve_routes(<<"user@test">>, DefaultRoute, Routes,[])), - % strip + %% strip Opts = [{strip, true}], ?equal({"user", DefaultRoute}, eradius_proxy:resolve_routes(<<"user">>, DefaultRoute, Routes, Opts)), ?equal({"user", Prod}, eradius_proxy:resolve_routes(<<"user@prod">>, DefaultRoute, Routes, Opts)), @@ -55,11 +55,11 @@ resolve_routes_test(_) -> ?equal({"user", Dev}, eradius_proxy:resolve_routes(<<"user@dev_server">>, DefaultRoute, Routes, Opts)), ?equal({"user", DefaultRoute}, eradius_proxy:resolve_routes(<<"user@dev-server">>, DefaultRoute, Routes, Opts)), - % prefix + %% prefix Opts1 = [{type, prefix}, {separator, "/"}], ?equal({"user/example", DefaultRoute}, eradius_proxy:resolve_routes(<<"user/example">>, DefaultRoute, Routes, Opts1)), ?equal({"test/user", {Test, [{pool, test_pool}]}}, eradius_proxy:resolve_routes(<<"test/user">>, DefaultRoute, Routes, Opts1)), - % prefix and strip + %% prefix and strip Opts2 = Opts ++ Opts1, ?equal({"example", DefaultRoute}, eradius_proxy:resolve_routes(<<"user/example">>, DefaultRoute, Routes, Opts2)), ?equal({"user", {Test, [{pool, test_pool}]}}, eradius_proxy:resolve_routes(<<"test/user">>, DefaultRoute, Routes, Opts2)), @@ -73,15 +73,15 @@ validate_arguments_test(_) -> ]} ], GoodOldConfig = [{default_route, {eradius_test_handler:localhost(tuple), 1813, <<"secret">>}, test_pool}, - {options, [{type, realm}, {strip, true}, {separator, "@"}]}, - {routes, [{"test_1", {eradius_test_handler:localhost(tuple), 1815, <<"secret1">>}, [{pool, test_pool}]}, - {"test_2", {<<"localhost">>, 1816, <<"secret2">>}} - ]} - ], + {options, [{type, realm}, {strip, true}, {separator, "@"}]}, + {routes, [{"test_1", {eradius_test_handler:localhost(tuple), 1815, <<"secret1">>}, [{pool, test_pool}]}, + {"test_2", {<<"localhost">>, 1816, <<"secret2">>}} + ]} + ], BadConfig = [{default_route, {eradius_test_handler:localhost(tuple), 1813, <<"secret">>}}, {options, [{type, abc}]} - ], + ], BadConfig1 = [{default_route, {eradius_test_handler:localhost(tuple), 0, <<"secret">>}}], BadConfig2 = [{default_route, {abc, 123, <<"secret">>}}], BadConfig3 = [{default_route, {eradius_test_handler:localhost(tuple), 1813, <<"secret">>}}, diff --git a/test/eradius_test.hrl b/test/eradius_test.hrl index 5e03be47..4e9727d8 100644 --- a/test/eradius_test.hrl +++ b/test/eradius_test.hrl @@ -10,9 +10,9 @@ end)())). -define(equal(Expected, Actual), - (fun (Expected@@@, Expected@@@) -> true; - (Expected@@@, Actual@@@) -> - ct:pal("MISMATCH(~s:~b, ~s)~nExpected: ~p~nActual: ~p~n", - [?FILE, ?LINE, ??Actual, Expected@@@, Actual@@@]), - false - end)(Expected, Actual) orelse error(badmatch)). + (fun (Expected@@@, Expected@@@) -> true; + (Expected@@@, Actual@@@) -> + ct:pal("MISMATCH(~s:~b, ~s)~nExpected: ~p~nActual: ~p~n", + [?FILE, ?LINE, ??Actual, Expected@@@, Actual@@@]), + false + end)(Expected, Actual) orelse error(badmatch)). diff --git a/test/eradius_test_handler.erl b/test/eradius_test_handler.erl index 94b50c40..2d7e6e41 100644 --- a/test/eradius_test_handler.erl +++ b/test/eradius_test_handler.erl @@ -19,7 +19,7 @@ start() -> {two, {localhost(ip), [1813]}}]), application:set_env(eradius, unreachable_timeout, 2), application:set_env(eradius, servers_pool, [{test_pool, [{localhost(tuple), 1812, "secret"}, - % fake upstream server for fail-over + %% fake upstream server for fail-over {localhost(string), 1820, "secret", [{timeout, 1000}, {retries, 2}]}]}]), application:ensure_all_started(eradius), eradius:modules_ready([?MODULE]). @@ -52,8 +52,8 @@ radius_request(#radius_request{cmd = request}, _Nasprop, _Args) -> %% triggers the load balancing in eradius_client. localhost(string) -> case os:getenv("TRAVIS") of - false -> "localhost"; - _ -> "ip4-loopback" + false -> "localhost"; + _ -> "ip4-loopback" end; localhost(binary) -> list_to_binary(localhost(string)); From 6760d24260518768eaf26debd44efd4fc7f4539d Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Mon, 15 Apr 2024 18:54:52 +0200 Subject: [PATCH 02/38] [client] remove unused message from init --- src/eradius_client_socket.erl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/eradius_client_socket.erl b/src/eradius_client_socket.erl index f53a8f3f..4e16ef38 100644 --- a/src/eradius_client_socket.erl +++ b/src/eradius_client_socket.erl @@ -11,7 +11,6 @@ start(SocketIP, Client, PortIdx) -> gen_server:start_link(?MODULE, [SocketIP, Client, PortIdx], []). init([SocketIP, Client, PortIdx]) -> - Client ! {PortIdx, self()}, case SocketIP of undefined -> ExtraOptions = []; From 7d739c7ffef0d8220e6df9974ef0017ad31c8872 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Tue, 16 Apr 2024 09:58:59 +0200 Subject: [PATCH 03/38] [client] rewrite socket handler logic to be OTP like --- src/eradius_client.erl | 106 ++++++++++++--------- src/eradius_client_socket.erl | 142 ++++++++++++++++++++-------- src/eradius_client_sup.erl | 50 +++++++++- test/eradius_client_SUITE.erl | 90 ++++++------------ test/eradius_client_socket_test.erl | 66 ------------- 5 files changed, 234 insertions(+), 220 deletions(-) delete mode 100644 test/eradius_client_socket_test.erl diff --git a/src/eradius_client.erl b/src/eradius_client.erl index 6a8f2513..ed727204 100644 --- a/src/eradius_client.erl +++ b/src/eradius_client.erl @@ -11,7 +11,9 @@ %% parameter. Changing it currently requires a restart. It can be given as a string or ip address tuple, %% or the atom ``undefined'' (the default), which uses whatever address the OS selects. -module(eradius_client). + -export([start_link/0, send_request/2, send_request/3, send_remote_request/3, send_remote_request/4]). + %% internal -export([reconfigure/0, send_remote_request_loop/8, restore_upstream_server/1, store_radius_server_from_pool/3, @@ -22,6 +24,10 @@ -import(eradius_lib, [printable_peer/2]). +-ifdef(TEST). +-export([get_state/0]). +-endif. + -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("kernel/include/logger.hrl"). -include("eradius_dict.hrl"). @@ -221,37 +227,48 @@ handle_failed_request(Request, {ServerIP, Port} = _FailedServer, UpstreamServers end. %% @private +%% send_remote_request_loop/8 send_remote_request_loop(ReplyPid, Socket, ReqId, Peer, EncRequest, Retries, Timeout, MetricsInfo) -> ReplyPid ! {self(), send_request_loop(Socket, ReqId, Peer, EncRequest, Retries, Timeout, MetricsInfo)}. -send_request_loop(Socket, ReqId, Peer, Request = #radius_request{}, Retries, Timeout, undefined) -> +%% send_remote_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) -> +send_request_loop(Socket, ReqId, Peer, Request, + Retries, Timeout, MetricsInfo) -> {Authenticator, EncRequest} = eradius_lib:encode_request(Request), - SMon = erlang:monitor(process, Socket), - send_request_loop(Socket, SMon, Peer, ReqId, Authenticator, EncRequest, Timeout, Retries, MetricsInfo, Request#radius_request.secret, Request). + send_request_loop(Socket, Peer, ReqId, Authenticator, EncRequest, + Timeout, Retries, MetricsInfo, Request#radius_request.secret, Request). -send_request_loop(_Socket, SMon, _Peer, _ReqId, _Authenticator, _EncRequest, Timeout, 0, MetricsInfo, _Secret, Request) -> +%% send_remote_request_loop/10 +send_request_loop(_Socket, _Peer, _ReqId, _Authenticator, _EncRequest, + Timeout, 0, MetricsInfo, _Secret, Request) -> TS = erlang:convert_time_unit(Timeout, millisecond, native), update_client_request(timeout, MetricsInfo, TS, Request), - erlang:demonitor(SMon, [flush]), {error, timeout}; -send_request_loop(Socket, SMon, Peer = {_ServerName, {IP, Port}}, ReqId, Authenticator, EncRequest, Timeout, RetryN, MetricsInfo, Secret, Request) -> - Socket ! {self(), send_request, {IP, Port}, ReqId, EncRequest}, - update_client_request(pending, MetricsInfo, 1, Request), - receive - {Socket, response, ReqId, Response} -> - update_client_request(pending, MetricsInfo, -1, Request), +send_request_loop(Socket, Peer = {_ServerName, {IP, Port}}, ReqId, Authenticator, EncRequest, + Timeout, RetryN, MetricsInfo, Secret, Request) -> + Result = + try + update_client_request(pending, MetricsInfo, 1, Request), + eradius_client_socket:send_request(Socket, {IP, Port}, ReqId, EncRequest, Timeout) + after + update_client_request(pending, MetricsInfo, -1, Request) + end, + + case Result of + {response, ReqId, Response} -> {ok, Response, Secret, Authenticator}; - {'DOWN', SMon, process, Socket, _} -> + {error, close} -> {error, socket_down}; - {Socket, error, Error} -> - {error, Error} - after - Timeout -> + {error, timeout} -> TS = erlang:convert_time_unit(Timeout, millisecond, native), update_client_request(retransmission, MetricsInfo, TS, Request), - send_request_loop(Socket, SMon, Peer, ReqId, Authenticator, EncRequest, Timeout, RetryN - 1, MetricsInfo, Secret, Request) + send_request_loop(Socket, Peer, ReqId, Authenticator, EncRequest, + Timeout, RetryN - 1, MetricsInfo, Secret, Request); + {error, _} = Error -> + Error end. %% @private @@ -320,7 +337,7 @@ reconfigure() -> %% @private init([]) -> - {ok, Sup} = eradius_client_sup:start(), + {ok, Sup} = eradius_client_sup:start_link(), case configure(#state{socket_ip = null, sup = Sup}) of {error, Error} -> {stop, Error}; Else -> Else @@ -346,10 +363,6 @@ handle_call(reconfigure, _From, State) -> {ok, NState} -> {reply, ok, NState} end; -%% @private -handle_call(debug, _From, State) -> - {reply, {ok, State}, State}; - %% @private handle_call(_OtherCall, _From, State) -> {noreply, State}. @@ -393,6 +406,16 @@ configure(State) -> {error, {bad_client_ip, ClientIP}} end. +-ifdef(TEST). + +get_state() -> + State = sys:get_state(?SERVER), + Keys = record_info(fields, state), + Values = tl(tuple_to_list(State)), + maps:from_list(lists:zip(Keys, Values)). + +-endif. + %% private prepare_pools() -> ets:new(?MODULE, [ordered_set, public, named_table, {keypos, 1}, {write_concurrency,true}]), @@ -454,14 +477,14 @@ configure_address(State = #state{socket_ip = OAdd, sockets = Sockts}, NPorts, NA {ok, State#state{socket_ip = NAdd, no_ports = NPorts}}; NAdd -> configure_ports(State, NPorts); - _ -> + _ -> ?LOG(info, "Reopening RADIUS client sockets (client_ip changed to ~s)", [inet:ntoa(NAdd)]), - array:map( fun(_PortIdx, Pid) -> - case Pid of - undefined -> done; - _ -> Pid ! close - end - end, Sockts), + array:map( + fun(_PortIdx, undefined) -> + ok; + (_PortIdx, Socket) -> + eradius_client_socket:close(Socket) + end, Sockts), {ok, State#state{sockets = array:new(), socket_ip = NAdd, no_ports = NPorts}} end. @@ -488,11 +511,8 @@ close_sockets(NPorts, Sockets) -> List = array:to_list(Sockets), {_, Rest} = lists:split(NPorts, List), lists:map( - fun(Pid) -> - case Pid of - undefined -> done; - _ -> Pid ! close - end + fun(undefined) -> ok; + (Socket) -> eradius_client_socket:close(Socket) end, Rest), array:resize(NPorts, Sockets) end. @@ -514,18 +534,10 @@ next_port_and_req_id(Peer, NumberOfPorts, Counters) -> find_socket_process(PortIdx, Sockets, SocketIP, Sup) -> case array:get(PortIdx, Sockets) of undefined -> - Res = supervisor:start_child(Sup, {PortIdx, - {eradius_client_socket, start, [SocketIP, self(), PortIdx]}, - transient, brutal_kill, worker, [eradius_client_socket]}), - Pid = case Res of - {ok, P} -> P; - {error, already_present} -> - {ok, P} = supervisor:restart_child(Sup, PortIdx), - P - end, - {Pid, array:set(PortIdx, Pid, Sockets)}; - Pid when is_pid(Pid) -> - {Pid, Sockets} + {ok, Socket} = eradius_client_socket:new(Sup, SocketIP), + {Socket, array:set(PortIdx, Socket, Sockets)}; + Socket -> + {Socket, Sockets} end. update_socket_process(PortIdx, Sockets, Pid) -> diff --git a/src/eradius_client_socket.erl b/src/eradius_client_socket.erl index 4e16ef38..7472853e 100644 --- a/src/eradius_client_socket.erl +++ b/src/eradius_client_socket.erl @@ -1,76 +1,125 @@ +%% Copyright (c) 2002-2007, Martin Björklund and Torbjörn Törnkvist +%% Copyright (c) 2011, Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT +%% -module(eradius_client_socket). -behaviour(gen_server). --export([start/3]). +%% API +-export([new/2, start_link/1, send_request/5, close/1]). + +%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {client, socket, pending, mode, counter}). +-record(state, {socket, active_n, pending, mode, counter}). + +%%%========================================================================= +%%% API +%%%========================================================================= + +new(Sup, SocketIP) -> + eradius_client_sup:new(Sup, SocketIP). + +start_link(SocketIP) -> + gen_server:start_link(?MODULE, [SocketIP], []). -start(SocketIP, Client, PortIdx) -> - gen_server:start_link(?MODULE, [SocketIP, Client, PortIdx], []). +send_request(Socket, Peer, ReqId, Request, Timeout) -> + try + gen_server:call(Socket, {send_request, Peer, ReqId, Request}, Timeout) + catch + exit:{timeout, _} -> + {error, timeout}; + exit:{noproc, _} -> + {error, closed}; + {nodedown, _} -> + {error, closed} + end. -init([SocketIP, Client, PortIdx]) -> +close(Socket) -> + gen_server:cast(Socket, close). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +init([SocketIP]) -> case SocketIP of undefined -> ExtraOptions = []; SocketIP when is_tuple(SocketIP) -> ExtraOptions = [{ip, SocketIP}] end, + ActiveN = application:get_env(eradius, active_n, 100), RecBuf = application:get_env(eradius, recbuf, 8192), SndBuf = application:get_env(eradius, sndbuf, 131072), - {ok, Socket} = gen_udp:open(0, [{active, once}, binary, {recbuf, RecBuf}, {sndbuf, SndBuf} | ExtraOptions]), - {ok, #state{client = Client, socket = Socket, pending = maps:new(), mode = active, counter = 0}}. + Opts = [{active, ActiveN}, binary, {recbuf, RecBuf}, {sndbuf, SndBuf} | ExtraOptions], + {ok, Socket} = gen_udp:open(0, Opts), + + State = #state{ + socket = Socket, + active_n = ActiveN, + pending = #{}, + mode = active + }, + {ok, State}. + +handle_call({send_request, {IP, Port}, ReqId, Request}, From, + #state{socket = Socket, pending = Pending} = State) -> + case gen_udp:send(Socket, IP, Port, Request) of + ok -> + ReqKey = {IP, Port, ReqId}, + NPending = Pending#{ReqKey => From}, + {noreply, State#state{pending = NPending}}; + {error, Reason} -> + {reply, {error, Reason}, State} + end; handle_call(_Request, _From, State) -> {noreply, State}. +handle_cast(close, #state{pending = Pending} = State) + when map_size(Pending) =:= 0 -> + {stop, normal, State}; +handle_cast(close, State) -> + {noreply, State#state{mode = inactive}}; + handle_cast(_Msg, State) -> {noreply, State}. -handle_info({SenderPid, send_request, {IP, Port}, ReqId, EncRequest}, - State = #state{socket = Socket, pending = Pending, counter = Counter}) -> - case gen_udp:send(Socket, IP, Port, EncRequest) of - ok -> - ReqKey = {IP, Port, ReqId}, - NPending = maps:put(ReqKey, SenderPid, Pending), - {noreply, State#state{pending = NPending, counter = Counter+1}}; - {error, Reason} -> - SenderPid ! {error, Reason}, - {noreply, State} - end; +handle_info({udp_passive, _Socket}, #state{socket = Socket, active_n = ActiveN} = State) -> + inet:setopts(Socket, [{active, ActiveN}]), + {noreply, State}; -handle_info({udp, Socket, FromIP, FromPort, EncRequest}, - State = #state{socket = Socket, pending = Pending, mode = Mode, counter = Counter}) -> - case eradius_lib:decode_request_id(EncRequest) of - {ReqId, EncRequest} -> - case maps:find({FromIP, FromPort, ReqId}, Pending) of - error -> - %% discard reply because we didn't expect it - inet:setopts(Socket, [{active, once}]), - {noreply, State}; - {ok, WaitingSender} -> - WaitingSender ! {self(), response, ReqId, EncRequest}, - inet:setopts(Socket, [{active, once}]), +handle_info({udp, Socket, FromIP, FromPort, Request}, + State = #state{socket = Socket, pending = Pending, mode = Mode}) -> + case eradius_lib:decode_request_id(Request) of + {ReqId, Request} -> + case Pending of + #{{FromIP, FromPort, ReqId} := From} -> + gen_server:reply(From, {response, ReqId, Request}), + + flow_control(State), NPending = maps:remove({FromIP, FromPort, ReqId}, Pending), - NState = State#state{pending = NPending, counter = Counter-1}, - case {Mode, Counter-1} of - {inactive, 0} -> {stop, normal, NState}; - _ -> {noreply, NState} - end + NState = State#state{pending = NPending}, + case Mode of + inactive when map_size(NPending) =:= 0 -> + {stop, normal, NState}; + _ -> + {noreply, NState} + end; + _ -> + %% discard reply because we didn't expect it + flow_control(State), + {noreply, State} end; {bad_pdu, _} -> %% discard reply because it was malformed - inet:setopts(Socket, [{active, once}]), + flow_control(State), {noreply, State} end; -handle_info(close, State = #state{counter = Counter}) -> - case Counter of - 0 -> {stop, normal, State}; - _ -> {noreply, State#state{mode = inactive}} - end; - handle_info(_Info, State) -> {noreply, State}. @@ -79,3 +128,12 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. + +%%%========================================================================= +%%% internal functions +%%%========================================================================= + +flow_control(#state{socket = Socket, active_n = once}) -> + inet:setopts(Socket, [{active, once}]); +flow_control(_) -> + ok. diff --git a/src/eradius_client_sup.erl b/src/eradius_client_sup.erl index cd6b8339..7c40b2cd 100644 --- a/src/eradius_client_sup.erl +++ b/src/eradius_client_sup.erl @@ -1,12 +1,52 @@ - -module(eradius_client_sup). -behaviour(supervisor). --export([start/0, init/1]). +%% API +-export([start_link/0, new/2]). + +%% Supervisor callbacks +-export([init/1]). + +-define(SERVER, ?MODULE). + +%%%=================================================================== +%%% API functions +%%%=================================================================== + +-spec start_link() -> {ok, Pid :: pid()} | + {error, {already_started, Pid :: pid()}} | + {error, {shutdown, term()}} | + {error, term()} | + ignore. +start_link() -> + supervisor:start_link(?MODULE, []). -start() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). +new(Sup, SocketId) -> + supervisor:start_child(Sup, [SocketId]). +%%%=================================================================== +%%% Supervisor callbacks +%%%=================================================================== + +-spec init(Args :: term()) -> + {ok, {SupFlags :: supervisor:sup_flags(), + [ChildSpec :: supervisor:child_spec()]}} | + ignore. init([]) -> - {ok, {{one_for_one, 5, 10}, []}}. + SupFlags = #{strategy => simple_one_for_one, + intensity => 5, + period => 10}, + + Child = #{id => eradius_client_socket, + start => {eradius_client_socket, start_link, []}, + restart => transient, + shutdown => 5000, + type => worker, + modules => [eradius_client_socket]}, + + {ok, {SupFlags, [Child]}}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index 43d6c28f..ae4929b4 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -23,6 +23,8 @@ -include("test/eradius_test.hrl"). +-define(HUT_SOCKET, eradius_client_socket). + -define(BAD_SERVER_IP, {eradius_test_handler:localhost(ip), 1820, "secret"}). -define(BAD_SERVER_INITIAL_RETRIES, 2). -define(BAD_SERVER_TUPLE_INITIAL, {{eradius_test_handler:localhost(tuple), 1820}, @@ -45,6 +47,7 @@ ?BAD_SERVER_TUPLE_INITIAL, ?GOOD_SERVER_2_TUPLE]). +-spec all() -> [ct_suite:ct_test_def(), ...]. all() -> [ send_request, wanna_send, @@ -60,11 +63,10 @@ all() -> [ init_per_suite(Config) -> {ok, _} = application:ensure_all_started(eradius), - startSocketCounter(), + logger:set_primary_config(level, debug), Config. end_per_suite(_Config) -> - stopSocketCounter(), application:stop(eradius), ok. @@ -97,48 +99,15 @@ end_per_testcase(_Test, Config) -> %% STUFF -socketCounter(Count) -> - receive - add -> socketCounter(Count+1); - del -> socketCounter(Count-1); - {get, PID} -> PID ! {ok, Count}, socketCounter(Count); - stop -> done - end. - -startSocketCounter() -> - register(socketCounter, spawn(?MODULE, socketCounter, [0])). - -stopSocketCounter() -> - socketCounter ! stop, - unregister(socketCounter). - -addSocket() -> socketCounter ! add. -delSocket() -> socketCounter ! del. - getSocketCount() -> - socketCounter ! {get, self()}, - receive - {ok, Count} -> Count - end. + #{sup := Sup} = eradius_client:get_state(), + Counts = supervisor:count_children(Sup), + proplists:get_value(active, Counts). -testSocket(undefined) -> true; +testSocket(undefined) -> + true; testSocket(Pid) -> - Pid ! {status, self()}, - receive - {ok, active} -> false; - {ok, inactive} -> true - after - 50 -> true - end. - --record(state, { - socket_ip :: inet:ip_address(), - no_ports = 1 :: pos_integer(), - idcounters = maps:new() :: map(), - sockets = array:new() :: array:array(), - sup :: pid(), - subscribed_clients = [] :: [{{integer(),integer(),integer(),integer()}, integer()}] - }). + not is_process_alive(Pid). split(N, List) -> split2(N, [], List). @@ -147,14 +116,17 @@ split2(_, List1, []) -> {lists:reverse(List1), []}; split2(N, List1, [L|List2]) -> split2(N-1, [L|List1], List2). meckStart() -> - ok = meck:new(eradius_client_socket), - ok = meck:expect(eradius_client_socket, start, fun(X, Y, Z) -> eradius_client_socket_test:start(X, Y, Z) end), - ok = meck:expect(eradius_client_socket, init, fun(X) -> eradius_client_socket_test:init(X) end), - ok = meck:expect(eradius_client_socket, handle_call, fun(X, Y, Z) -> eradius_client_socket_test:handle_call(X, Y, Z) end), - ok = meck:expect(eradius_client_socket, handle_cast, fun(X, Y) -> eradius_client_socket_test:handle_cast(X, Y) end), - ok = meck:expect(eradius_client_socket, handle_info, fun(X, Y) -> eradius_client_socket_test:handle_info(X, Y) end), - ok = meck:expect(eradius_client_socket, terminate, fun(X, Y) -> eradius_client_socket_test:terminate(X, Y) end), - ok = meck:expect(eradius_client_socket, code_change, fun(X, Y, Z) -> eradius_client_socket_test:code_change(X, Y, Z) end). + ok = meck:new(eradius_client_socket, [passthrough]), + ok = meck:expect(eradius_client_socket, init, + fun(_) -> {ok, undefined} end), + ok = meck:expect(eradius_client_socket, handle_call, + fun(_Request, _From, State) -> {noreply, State} end), + ok = meck:expect(eradius_client_socket, handle_cast, + fun(close, State) -> {stop, normal, State}; + (_Request, State) -> {noreply, State} end), + ok = meck:expect(eradius_client_socket, handle_info, + fun(_Info, State) -> {noreply, State} end), + ok. meckStop() -> ok = meck:unload(eradius_client_socket). @@ -172,13 +144,13 @@ parse_ip(T = {_, _, _, _, _, _}) -> test(true, _Msg) -> true; test(false, Msg) -> - io:format(standard_error, "~s~n", [Msg]), + ct:pal("~s", [Msg]), false. -check(OldState, NewState = #state{no_ports = P}, null, A) -> check(OldState, NewState, P, A); -check(OldState, NewState = #state{socket_ip = A}, P, null) -> check(OldState, NewState, P, A); -check(#state{sockets = OS, no_ports = _OP, idcounters = _OC, socket_ip = OA}, - #state{sockets = NS, no_ports = NP, idcounters = NC, socket_ip = NA}, +check(OldState, NewState = #{no_ports := P}, null, A) -> check(OldState, NewState, P, A); +check(OldState, NewState = #{socket_ip := A}, P, null) -> check(OldState, NewState, P, A); +check(#{sockets := OS, no_ports := _OP, idcounters := _OC, socket_ip := OA}, + #{sockets := NS, no_ports := NP, idcounters := NC, socket_ip := NA}, P, A) -> {ok, PA} = parse_ip(A), test(PA == NA, "Adress not configured") and @@ -209,9 +181,9 @@ send_request(_Config) -> send(FUN, Ports, Address) -> meckStart(), - {ok, OldState} = gen_server:call(eradius_client, debug), + OldState = eradius_client:get_state(), FUN(), - {ok, NewState} = gen_server:call(eradius_client, debug), + NewState = eradius_client:get_state(), true = check(OldState, NewState, Ports, Address), meckStop(). @@ -224,11 +196,9 @@ wanna_send(_Config) -> send(FUN, null, null) end, lists:seq(1, 10)). -%% I've catched some data races with `delSocket()' and `getSocketCount()' when -%% `delSocket()' happens after `getSocketCount()' (because `delSocket()' is sent from another process). -%% I don't know a better decision than add some delay before `getSocketCount()' +%% socket shutdown is done asynchronous, the tests need to wait a bit for it to finish. reconf_address(_Config) -> - FUN = fun() -> gen_server:call(eradius_client, reconfigure), timer:sleep(100) end, + FUN = fun() -> eradius_client:reconfigure(), timer:sleep(100) end, application:set_env(eradius, client_ip, "7.13.23.42"), send(FUN, null, "7.13.23.42"). diff --git a/test/eradius_client_socket_test.erl b/test/eradius_client_socket_test.erl deleted file mode 100644 index c2a3bf12..00000000 --- a/test/eradius_client_socket_test.erl +++ /dev/null @@ -1,66 +0,0 @@ -%% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> - -%% Permission is hereby granted, free of charge, to any person obtaining a -%% copy of this software and associated documentation files (the "Software"), -%% to deal in the Software without restriction, including without limitation -%% the rights to use, copy, modify, merge, publish, distribute, sublicense, -%% and/or sell copies of the Software, and to permit persons to whom the -%% Software is furnished to do so, subject to the following conditions: - -%% The above copyright notice and this permission notice shall be included in -%% all copies or substantial portions of the Software. - -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -%% DEALINGS IN THE SOFTWARE. - --module(eradius_client_socket_test). - --behaviour(gen_server). - --export([start/3]). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). - --record(state, {client, socket, pending, mode, counter}). - -start(SocketIP, Client, PortIdx) -> - gen_server:start_link(?MODULE, [SocketIP, Client, PortIdx], []). - -init([_SocketIP, Client, PortIdx]) -> - Client ! {PortIdx, self()}, - eradius_client_SUITE:addSocket(), - {ok, #state{pending = maps:new(), mode = active, counter = 0}}. - -handle_call(_Request, _From, State) -> - {noreply, State}. - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info({SenderPid, send_request, {IP, Port}, ReqId, _EncRequest}, - State = #state{pending = Pending, counter = Counter}) -> - ReqKey = {IP, Port, ReqId}, - NPending = Pending#{ReqKey => SenderPid}, - {noreply, State#state{pending = NPending, counter = Counter+1}}; - -handle_info(close, State) -> - %~ {noreply, State#state{mode = inactive}}; - {stop, normal, State}; - -handle_info({status, Pid}, State = #state{mode = Mode}) -> - Pid ! {ok, Mode}, - {noreply, State}; - -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - eradius_client_SUITE:delSocket(). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - From eb062e31d54218831c0f640dfea513cd08d2b636 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Tue, 16 Apr 2024 15:07:20 +0200 Subject: [PATCH 04/38] [client] split socket manager from sending functionality Seperate concerns better. Socket managing is not in the mngr module, while the sending logic is in the client module. --- src/eradius.erl | 2 +- src/eradius_client.erl | 388 +++++------------------------ src/eradius_client_mngr.erl | 399 ++++++++++++++++++++++++++++++ src/eradius_client_socket.erl | 28 +-- src/eradius_client_socket_sup.erl | 52 ++++ src/eradius_client_sup.erl | 46 ++-- src/eradius_internal.hrl | 2 + src/eradius_proxy.erl | 10 +- src/eradius_server.erl | 18 +- src/eradius_sup.erl | 18 +- test/eradius_client_SUITE.erl | 28 +-- test/eradius_metrics_SUITE.erl | 2 +- 12 files changed, 603 insertions(+), 390 deletions(-) create mode 100644 src/eradius_client_mngr.erl create mode 100644 src/eradius_client_socket_sup.erl create mode 100644 src/eradius_internal.hrl diff --git a/src/eradius.erl b/src/eradius.erl index dbd1e473..7bc497e0 100644 --- a/src/eradius.erl +++ b/src/eradius.erl @@ -62,7 +62,7 @@ config_change(Added, Changed, Removed) -> Keys = [K || {K, _} <- Added ++ Changed] ++ Removed, (lists:member(logging, Keys) or lists:member(logfile, Keys)) andalso eradius_log:reconfigure(), - eradius_client:reconfigure(). + eradius_client_mngr:reconfigure(). do_config_change({tables, NewTables}) -> eradius_dict:load_tables(NewTables); diff --git a/src/eradius_client.erl b/src/eradius_client.erl index ed727204..b2ece522 100644 --- a/src/eradius_client.erl +++ b/src/eradius_client.erl @@ -1,3 +1,8 @@ +%% Copyright (c) 2002-2007, Martin Björklund and Torbjörn Törnkvist +%% Copyright (c) 2011, Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT +%% %% @doc This module contains a RADIUS client that can be used to send authentication and accounting requests. %% A counter is kept for every NAS in order to determine the next request id and sender port %% for each outgoing request. The implementation naively assumes that you won't send requests to a @@ -12,31 +17,22 @@ %% or the atom ``undefined'' (the default), which uses whatever address the OS selects. -module(eradius_client). --export([start_link/0, send_request/2, send_request/3, send_remote_request/3, send_remote_request/4]). - -%% internal --export([reconfigure/0, send_remote_request_loop/8, - restore_upstream_server/1, store_radius_server_from_pool/3, - init_server_status_metrics/0]). +%% API +-export([send_request/2, send_request/3, + send_remote_request/3, send_remote_request/4]). --behaviour(gen_server). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +%% internal API +-export([send_remote_request_loop/8]). -import(eradius_lib, [printable_peer/2]). --ifdef(TEST). --export([get_state/0]). --endif. - -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("kernel/include/logger.hrl"). +-include_lib("kernel/include/inet.hrl"). -include("eradius_dict.hrl"). -include("eradius_lib.hrl"). +-include("eradius_internal.hrl"). --define(SERVER, ?MODULE). --define(DEFAULT_RETRIES, 3). --define(DEFAULT_TIMEOUT, 5000). --define(RECONFIGURE_TIMEOUT, 15000). -define(GOOD_CMD(Req), (Req#radius_request.cmd == 'request' orelse Req#radius_request.cmd == 'accreq' orelse Req#radius_request.cmd == 'coareq' orelse @@ -52,13 +48,11 @@ -export_type([nas_address/0, options/0]). --include_lib("kernel/include/inet.hrl"). +-define(SERVER, ?MODULE). -%% ------------------------------------------------------------------------------------------ -%% -- API -%% @private -start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). +%%%========================================================================= +%%% API +%%%========================================================================= %% @equiv send_request(NAS, Request, []) -spec send_request(nas_address(), #radius_request{}) -> {ok, binary()} | {error, 'timeout' | 'socket_down'}. @@ -85,7 +79,7 @@ send_request({IP, Port, Secret}, Request, Options) when ?GOOD_CMD(Request) andal SendReqFn = fun () -> Peer = {ServerName, {IP, Port}}, update_client_requests(MetricsInfo), - {Socket, ReqId} = gen_server:call(?SERVER, {wanna_send, Peer, 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), @@ -98,7 +92,7 @@ send_request({IP, Port, Secret}, Request, Options) when ?GOOD_CMD(Request) andal [] -> SendReqFn(); UpstreamServers -> - case find_suitable_peer([{IP, Port, Secret, Options} | UpstreamServers]) of + case eradius_client_mngr:find_suitable_peer([{IP, Port, Secret, Options} | UpstreamServers]) of [] -> no_active_servers; {{IP, Port, Secret, Options}, _NewPool} -> @@ -126,7 +120,7 @@ send_remote_request(Node, {IP, Port, Secret}, Request, Options) when ?GOOD_CMD(R MetricsInfo = make_metrics_info(Options, {IP, Port}), update_client_requests(MetricsInfo), Peer = {ServerName, {IP, Port}}, - try gen_server:call({?SERVER, Node}, {wanna_send, Peer, MetricsInfo}) of + try eradius_client_mngr:wanna_send(Node, Peer) of {Socket, ReqId} -> Request1 = case eradius_node_mon:get_remote_version(Node) of {0, Minor} when Minor < 6 -> @@ -155,29 +149,40 @@ send_remote_request(Node, {IP, Port, Secret}, Request, Options) when ?GOOD_CMD(R send_remote_request(_Node, {_IP, _Port, _Secret}, _Request, _Options) -> error(badarg). -restore_upstream_server({ServerIP, Port, Retries, InitialRetries}) -> - ets:insert(?MODULE, {{ServerIP, Port}, Retries, InitialRetries}). - proceed_response(Request, {ok, Response, Secret, Authenticator}, _Peer = {_ServerName, {ServerIP, Port}}, TS1, MetricsInfo, Options) -> 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, "Message-Authenticator Attribute is invalid" = Reason} -> - update_client_response(bad_authenticator, MetricsInfo, Request), - ?LOG(error, "~s INF: Noreply for request ~p. Could not decode the response, reason: ~s", [printable_peer(ServerIP, Port), Request, Reason]), - noreply; - {bad_pdu, "Authenticator Attribute is invalid" = Reason} -> - update_client_response(bad_authenticator, MetricsInfo, Request), - ?LOG(error, "~s INF: Noreply for request ~p. Could not decode the response, reason: ~s", [printable_peer(ServerIP, Port), Request, Reason]), - noreply; - {bad_pdu, "unknown request type" = Reason} -> - update_client_response(unknown_req_type, MetricsInfo, Request), - ?LOG(error, "~s INF: Noreply for request ~p. Could not decode the response, reason: ~s", [printable_peer(ServerIP, Port), Request, Reason]), - noreply; {bad_pdu, Reason} -> - update_client_response(dropped, MetricsInfo, Request), - ?LOG(error, "~s INF: Noreply for request ~p. Could not decode the response, reason: ~s", [printable_peer(ServerIP, Port), Request, Reason]), - maybe_failover(Request, noreply, {ServerIP, Port}, Options); + eradius_client_mngr:request_failed(ServerIP, Port, Options), + update_server_status_metric(ServerIP, Port, false, Options), + + 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; + "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; + "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; + _ -> + 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) + end; Decoded -> update_server_status_metric(ServerIP, Port, true, Options), update_client_response(Decoded#radius_request.cmd, MetricsInfo, Request), @@ -187,39 +192,15 @@ proceed_response(Request, {ok, Response, Secret, Authenticator}, _Peer = {_Serve proceed_response(Request, Response, {_ServerName, {ServerIP, Port}}, TS1, MetricsInfo, Options) -> update_client_responses(MetricsInfo), update_client_request(Request#radius_request.cmd, MetricsInfo, erlang:monotonic_time() - TS1, Request), - maybe_failover(Request, Response, {ServerIP, Port}, Options). -maybe_failover(Request, Response, {ServerIP, Port}, Options) -> + eradius_client_mngr:request_failed(ServerIP, Port, Options), update_server_status_metric(ServerIP, Port, false, Options), - case proplists:get_value(failover, Options, []) of - [] -> - Response; - UpstreamServers -> - handle_failed_request(Request, {ServerIP, Port}, UpstreamServers, Response, Options) - end. -handle_failed_request(Request, {ServerIP, Port} = _FailedServer, UpstreamServers, Response, Options) -> - case ets:lookup(?MODULE, {ServerIP, Port}) of - [{{ServerIP, Port}, Retries, InitialRetries}] -> - FailedTries = proplists:get_value(retries, Options, ?DEFAULT_RETRIES), - %% Mark the given RADIUS server as 'non-active' if there were more tries - %% than possible - if FailedTries >= Retries -> - ets:delete(?MODULE, {ServerIP, Port}), - Timeout = application:get_env(eradius, unreachable_timeout, 60), - timer:apply_after(Timeout * 1000, ?MODULE, restore_upstream_server, - [{ServerIP, Port, InitialRetries, InitialRetries}]); - true -> - %% RADIUS client tried to send a request to the {ServierIP, Port} RADIUS - %% server. There were done FailedTries tries and all of them failed. - %% So decrease amount of tries for the given RADIUS server that - %% that will be used for next RADIUS requests towards this RADIUS server. - ets:update_counter(?MODULE, {ServerIP, Port}, -FailedTries) - end; - [] -> - ok - end, - case find_suitable_peer(UpstreamServers) of + maybe_failover(Request, Response, Options). + +maybe_failover(Request, Response, Options) -> + UpstreamServers = proplists:get_value(failover, Options, []), + case eradius_client_mngr:find_suitable_peer(UpstreamServers) of [] -> Response; {{NewPeerAddr, NewPeerPort, NewPeerSecret, PeerOpts}, NewPool} -> @@ -320,228 +301,9 @@ update_client_response(bad_authenticator, MetricsInfo, _) -> eradius_counter:inc update_client_response(unknown_req_type, MetricsInfo, _) -> eradius_counter:inc_counter(unknownTypes, MetricsInfo); update_client_response(_, _, _) -> ok. -%% @private -reconfigure() -> - catch gen_server:call(?SERVER, reconfigure, ?RECONFIGURE_TIMEOUT). - -%% ------------------------------------------------------------------------------------------ -%% -- socket process manager --record(state, { - socket_ip :: null | inet:ip_address(), - no_ports = 1 :: pos_integer(), - idcounters = maps:new() :: map(), - sockets = array:new() :: array:array(), - sup :: pid(), - clients = [] :: [{{integer(),integer(),integer(),integer()}, integer()}] - }). - -%% @private -init([]) -> - {ok, Sup} = eradius_client_sup:start_link(), - case configure(#state{socket_ip = null, sup = Sup}) of - {error, Error} -> {stop, Error}; - Else -> Else - end. - -%% @private -handle_call({wanna_send, Peer = {_PeerName, PeerSocket}, _MetricsInfo}, _From, State) -> - {PortIdx, ReqId, NewIdCounters} = next_port_and_req_id(PeerSocket, State#state.no_ports, State#state.idcounters), - {SocketProcess, NewSockets} = find_socket_process(PortIdx, State#state.sockets, State#state.socket_ip, State#state.sup), - IsCreated = lists:member(Peer, State#state.clients), - NewState = case IsCreated of - false -> - State#state{idcounters = NewIdCounters, sockets = NewSockets, clients = [Peer | State#state.clients]}; - true -> - State#state{idcounters = NewIdCounters, sockets = NewSockets} - end, - {reply, {SocketProcess, ReqId}, NewState}; - -%% @private -handle_call(reconfigure, _From, State) -> - case configure(State) of - {error, Error} -> {reply, Error, State}; - {ok, NState} -> {reply, ok, NState} - end; - -%% @private -handle_call(_OtherCall, _From, State) -> - {noreply, State}. - -%% @private -handle_cast(_Msg, State) -> {noreply, State}. - -%% @private -handle_info({PortIdx, Pid}, State = #state{sockets = Sockets}) -> - NSockets = update_socket_process(PortIdx, Sockets, Pid), - {noreply, State#state{sockets = NSockets}}; - -handle_info(_Info, State) -> - {noreply, State}. - -%% @private -terminate(_Reason, _State) -> ok. - -%% @private -code_change(_OldVsn, State, _Extra) -> {ok, State}. - -%% @private -configure(State) -> - case ets:info(?MODULE) of - undefined -> - prepare_pools(); - _ -> - %% if ets table is already exists - which could be in a case of - %% reconfigure, just re-create the table and fill it with newly - %% configured pools of RADIUS upstream servers - ets:delete(?MODULE), - prepare_pools() - end, - {ok, ClientPortCount} = application:get_env(eradius, client_ports), - {ok, ClientIP} = application:get_env(eradius, client_ip), - case parse_ip(ClientIP) of - {ok, Address} -> - configure_address(State, ClientPortCount, Address); - {error, _} -> - ?LOG(error, "Invalid RADIUS client IP (parsing failed): ~p", [ClientIP]), - {error, {bad_client_ip, ClientIP}} - end. - --ifdef(TEST). - -get_state() -> - State = sys:get_state(?SERVER), - Keys = record_info(fields, state), - Values = tl(tuple_to_list(State)), - maps:from_list(lists:zip(Keys, Values)). - --endif. - -%% private -prepare_pools() -> - ets:new(?MODULE, [ordered_set, public, named_table, {keypos, 1}, {write_concurrency,true}]), - lists:foreach(fun({_PoolName, Servers}) -> prepare_pool(Servers) end, application:get_env(eradius, servers_pool, [])), - lists:foreach(fun(Server) -> store_upstream_servers(Server) end, application:get_env(eradius, servers, [])), - init_server_status_metrics(). - -prepare_pool([]) -> ok; -prepare_pool([{Addr, Port, _, Opts} | Servers]) -> - Retries = proplists:get_value(retries, Opts, ?DEFAULT_RETRIES), - store_radius_server_from_pool(Addr, Port, Retries), - prepare_pool(Servers); -prepare_pool([{Addr, Port, _} | Servers]) -> - store_radius_server_from_pool(Addr, Port, ?DEFAULT_RETRIES), - prepare_pool(Servers). - -store_upstream_servers({Server, _}) -> - store_upstream_servers(Server); -store_upstream_servers({Server, _, _}) -> - store_upstream_servers(Server); -store_upstream_servers(Server) -> - HandlerDefinitions = application:get_env(eradius, Server, []), - UpdatePoolFn = fun (HandlerOpts) -> - {DefaultRoute, Routes, Retries} = eradius_proxy:get_routes_info(HandlerOpts), - eradius_proxy:put_default_route_to_pool(DefaultRoute, Retries), - eradius_proxy:put_routes_to_pool(Routes, Retries) - end, - lists:foreach(fun (HandlerDefinition) -> - case HandlerDefinition of - {{_, []}, _} -> ok; - {{_, _, []}, _} -> ok; - {{_, HandlerOpts}, _} -> UpdatePoolFn(HandlerOpts); - {{_, _, HandlerOpts}, _} -> UpdatePoolFn(HandlerOpts); - _HandlerDefinition -> ok - end - end, - HandlerDefinitions). - -%% private -store_radius_server_from_pool(Addr, Port, Retries) when is_tuple(Addr) and is_integer(Port) and is_integer(Retries) -> - ets:insert(?MODULE, {{Addr, Port}, Retries, Retries}); -store_radius_server_from_pool(Addr, Port, Retries) when is_list(Addr) and is_integer(Port) and is_integer(Retries) -> - try - IP = get_ip(Addr), - ets:insert(?MODULE, {{IP, Port}, Retries, Retries}) - catch error:badarg -> - ?LOG(error, "Can't resolve hostname - ~p", [Addr]), - % Throw again badarg as it was before to catch non-resolvable hostname during eradius start-up - % and exit early to not lost secondary pools. - error(badarg) - end; -store_radius_server_from_pool(Addr, Port, Retries) -> - ?LOG(error, "bad RADIUS upstream server specified in RADIUS servers pool configuration ~p", [{Addr, Port, Retries}]), - error(badarg). - -configure_address(State = #state{socket_ip = OAdd, sockets = Sockts}, NPorts, NAdd) -> - case OAdd of - null -> - {ok, State#state{socket_ip = NAdd, no_ports = NPorts}}; - NAdd -> - configure_ports(State, NPorts); - _ -> - ?LOG(info, "Reopening RADIUS client sockets (client_ip changed to ~s)", [inet:ntoa(NAdd)]), - array:map( - fun(_PortIdx, undefined) -> - ok; - (_PortIdx, Socket) -> - eradius_client_socket:close(Socket) - end, Sockts), - {ok, State#state{sockets = array:new(), socket_ip = NAdd, no_ports = NPorts}} - end. - -configure_ports(State = #state{no_ports = OPorts, sockets = Sockets}, NPorts) -> - if - OPorts =< NPorts -> - {ok, State#state{no_ports = NPorts}}; - true -> - Counters = fix_counters(NPorts, State#state.idcounters), - NSockets = close_sockets(NPorts, Sockets), - {ok, State#state{sockets = NSockets, no_ports = NPorts, idcounters = Counters}} - end. - -fix_counters(NPorts, Counters) -> - maps:map(fun(_Peer, Value = {NextPortIdx, _NextReqId}) when NextPortIdx < NPorts -> Value; - (_Peer, {_NextPortIdx, NextReqId}) -> {0, NextReqId} - end, Counters). - -close_sockets(NPorts, Sockets) -> - case array:size(Sockets) =< NPorts of - true -> - Sockets; - false -> - List = array:to_list(Sockets), - {_, Rest} = lists:split(NPorts, List), - lists:map( - fun(undefined) -> ok; - (Socket) -> eradius_client_socket:close(Socket) - end, Rest), - array:resize(NPorts, Sockets) - end. - -next_port_and_req_id(Peer, NumberOfPorts, Counters) -> - case Counters of - #{Peer := {NextPortIdx, ReqId}} when ReqId < 255 -> - NextReqId = (ReqId + 1); - #{Peer := {PortIdx, 255}} -> - NextPortIdx = (PortIdx + 1) rem (NumberOfPorts - 1), - NextReqId = 0; - _ -> - NextPortIdx = erlang:phash2(Peer, NumberOfPorts), - NextReqId = 0 - end, - NewCounters = Counters#{Peer => {NextPortIdx, NextReqId}}, - {NextPortIdx, NextReqId, NewCounters}. - -find_socket_process(PortIdx, Sockets, SocketIP, Sup) -> - case array:get(PortIdx, Sockets) of - undefined -> - {ok, Socket} = eradius_client_socket:new(Sup, SocketIP), - {Socket, array:set(PortIdx, Socket, Sockets)}; - Socket -> - {Socket, Sockets} - end. - -update_socket_process(PortIdx, Sockets, Pid) -> - array:set(PortIdx, Pid, Sockets). +%%%========================================================================= +%%% internal functions +%%%========================================================================= parse_ip(undefined) -> {ok, undefined}; @@ -549,22 +311,9 @@ parse_ip(Address) when is_list(Address) -> inet_parse:address(Address); parse_ip(T = {_, _, _, _}) -> {ok, T}; -parse_ip(T = {_, _, _, _, _, _}) -> +parse_ip(T = {_, _, _, _, _, _, _, _}) -> {ok, T}. -init_server_status_metrics() -> - case application:get_env(eradius, server_status_metrics_enabled, false) of - false -> - ok; - true -> - %% That will be called at eradius startup and we must be sure that prometheus - %% application already started if server status metrics supposed to be used - application:ensure_all_started(prometheus), - ets:foldl(fun ({{Addr, Port}, _, _}, _Acc) -> - eradius_counter:set_boolean_metric(server_status, [Addr, Port], false) - end, [], ?MODULE) - end. - make_metrics_info(Options, {ServerIP, ServerPort}) -> ServerName = proplists:get_value(server_name, Options, undefined), ClientName = proplists:get_value(client_name, Options, undefined), @@ -648,27 +397,6 @@ client_response_counter_account_match_spec_compile() -> MatchSpecCompile end. -find_suitable_peer([]) -> - []; -find_suitable_peer([{Host, Port, Secret, Opts} | Pool]) when is_list(Host) -> - try - IP = get_ip(Host), - find_suitable_peer([{IP, Port, Secret, Opts} | Pool]) - catch _:_ -> - ?LOG(error, "Can't resolve hostname - ~p", [Host]), - %% can't resolve ip by some reasons, just ignore it - find_suitable_peer(Pool) - end; -find_suitable_peer([{IP, Port, Secret, Opts} | Pool]) -> - case ets:lookup(?MODULE, {IP, Port}) of - [] -> - find_suitable_peer(Pool); - [{{IP, Port}, _Retries, _InitialRetries}] -> - {{IP, Port, Secret, Opts}, Pool} - end; -find_suitable_peer([{IP, Port, Secret} | Pool]) -> - find_suitable_peer([{IP, Port, Secret, []} | Pool]). - get_ip(Host) -> case inet:gethostbyname(Host) of {ok, #hostent{h_addrtype = inet, h_addr_list = [IP]}} -> diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl new file mode 100644 index 00000000..14a66d92 --- /dev/null +++ b/src/eradius_client_mngr.erl @@ -0,0 +1,399 @@ +%% Copyright (c) 2002-2007, Martin Björklund and Torbjörn Törnkvist +%% Copyright (c) 2011, Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT +%% +-module(eradius_client_mngr). + +-behaviour(gen_server). + +%% external API +-export([start_link/0, wanna_send/1, wanna_send/2, reconfigure/0, reconfigure/1]). + +%% internal API +-export([store_radius_server_from_pool/3, + request_failed/3, + restore_upstream_server/1, + find_suitable_peer/1]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). + +-ifdef(TEST). +-export([get_state/0, servers/0, servers/1, init_server_status_metrics/0]). +-endif. + +-include_lib("kernel/include/logger.hrl"). +-include_lib("kernel/include/inet.hrl"). +-include("eradius_internal.hrl"). + +-type client_opts() :: + #{family => inet | inet6, + ip => any | inet:ip_address(), + active_n => once | non_neg_integer(), + no_ports => non_neg_integer(), + recbuf => non_neg_integer(), + sndbuf => non_neg_integer(), + server_pool => [term()], + servers => [term()]}. +-type client_config() :: + #{family := inet | inet6, + ip := any | inet:ip_address(), + active_n := once | non_neg_integer(), + no_ports := non_neg_integer(), + recbuf := non_neg_integer(), + sndbuf := non_neg_integer(), + servers_pool => [term()], + servers => [term()]}. + +-export_type([client_config/0]). + +-record(state, { + config :: client_config(), + socket_id :: {Family :: inet | inet6, IP :: any | inet:ip_address()}, + no_ports = 1 :: pos_integer(), + idcounters = maps:new() :: map(), + sockets = array:new() :: array:array(), + clients = [] :: [{{integer(),integer(),integer(),integer()}, integer()}] + }). + +-define(SERVER, ?MODULE). + +-define(RECONFIGURE_TIMEOUT, 15000). + +%%%========================================================================= +%%% API +%%%========================================================================= + +start_link() -> + case client_config(default_client_opts()) of + {ok, Config} -> gen_server:start_link({local, ?SERVER}, ?MODULE, [Config], []); + {error, _} = Error -> Error + end. + +wanna_send(Peer) -> + gen_server:call(?SERVER, {wanna_send, Peer}). + +wanna_send(Node, Peer) -> + gen_server:call({?SERVER, Node}, {wanna_send, Peer}). + +%% @private +reconfigure() -> + %% backward compatibility wrapper + (catch reconfigure(#{})). + +%% @doc reconfigure the Radius client +reconfigure(Opts) -> + gen_server:call(?SERVER, {reconfigure, Opts}, ?RECONFIGURE_TIMEOUT). + +request_failed(ServerIP, Port, Options) -> + case ets:lookup(?MODULE, {ServerIP, Port}) of + [{{ServerIP, Port}, Retries, InitialRetries}] -> + FailedTries = proplists:get_value(retries, Options, ?DEFAULT_RETRIES), + %% Mark the given RADIUS server as 'non-active' if there were more tries + %% than possible + if FailedTries >= Retries -> + ets:delete(?MODULE, {ServerIP, Port}), + Timeout = application:get_env(eradius, unreachable_timeout, 60), + timer:apply_after(Timeout * 1000, ?MODULE, restore_upstream_server, + [{ServerIP, Port, InitialRetries, InitialRetries}]); + true -> + %% RADIUS client tried to send a request to the {ServierIP, Port} RADIUS + %% server. There were done FailedTries tries and all of them failed. + %% So decrease amount of tries for the given RADIUS server that + %% that will be used for next RADIUS requests towards this RADIUS server. + ets:update_counter(?MODULE, {ServerIP, Port}, -FailedTries) + end; + [] -> + ok + end. + +restore_upstream_server({ServerIP, Port, Retries, InitialRetries}) -> + ets:insert(?MODULE, {{ServerIP, Port}, Retries, InitialRetries}). + +find_suitable_peer(undefined) -> + []; +find_suitable_peer([]) -> + []; +find_suitable_peer([{Host, Port, Secret, Opts} | Pool]) when is_list(Host) -> + try + IP = get_ip(Host), + find_suitable_peer([{IP, Port, Secret, Opts} | Pool]) + catch _:_ -> + %% can't resolve ip by some reasons, just ignore it + find_suitable_peer(Pool) + end; +find_suitable_peer([{IP, Port, Secret, Opts} | Pool]) -> + case ets:lookup(?MODULE, {IP, Port}) of + [] -> + find_suitable_peer(Pool); + [{{IP, Port}, _Retries, _InitialRetries}] -> + {{IP, Port, Secret, Opts}, Pool} + end; +find_suitable_peer([{IP, Port, Secret} | Pool]) -> + find_suitable_peer([{IP, Port, Secret, []} | Pool]). + +-ifdef(TEST). + +get_state() -> + State = sys:get_state(?SERVER), + Keys = record_info(fields, state), + Values = tl(tuple_to_list(State)), + maps:from_list(lists:zip(Keys, Values)). + +servers() -> + ets:tab2list(?MODULE). + +servers(Key) -> + ets:lookup(?MODULE, Key). + +-endif. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +init([#{no_ports := NPorts} = Config]) -> + ets:new(?MODULE, [public, named_table, ordered_set, {keypos, 1}, {write_concurrency, true}]), + prepare_pools(Config), + + State = #state{ + config = Config, + socket_id = socket_id(Config), + no_ports = NPorts}, + {ok, State}. + +%% @private +handle_call({wanna_send, Peer = {_PeerName, PeerSocket}}, _From, + #state{config = Config, + no_ports = NoPorts, idcounters = IdCounters, + sockets = Sockets, clients = Clients} = State0) -> + {PortIdx, ReqId, NewIdCounters} = next_port_and_req_id(PeerSocket, NoPorts, IdCounters), + {SocketProcess, NewSockets} = find_socket_process(PortIdx, Sockets, Config), + State1 = State0#state{idcounters = NewIdCounters, sockets = NewSockets}, + State = + case lists:member(Peer, Clients) of + false -> State1#state{clients = [Peer | Clients]}; + true -> State1 + end, + {reply, {SocketProcess, ReqId}, State}; + +%% @private +handle_call({reconfigure, Opts}, _From, #state{config = OConfig} = State0) -> + case client_config(maps:merge(OConfig, Opts)) of + {ok, Config} -> + ets:delete_all_objects(?MODULE), + prepare_pools(Config), + + State = reconfigure_address(Config, State0#state{config = Config}), + {reply, ok, State}; + + {error, _} = Error -> + {reply, Error, State0} + end; + +%% @private +handle_call(_OtherCall, _From, State) -> + {noreply, State}. + +%% @private +handle_cast(_Msg, State) -> {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +%% @private +terminate(_Reason, _State) -> ok. + +%% @private +code_change(_OldVsn, State, _Extra) -> {ok, State}. + +%%%========================================================================= +%%% internal functions +%%%========================================================================= + +socket_id(#{family := Family, ip := IP}) -> + {Family, IP}. + +socket_id_str({_, IP}) when is_tuple(IP) -> + inet:ntoa(IP); +socket_id_str({_, IP}) when is_atom(IP) -> + atom_to_list(IP). + +get_ip(Host) -> + case inet:gethostbyname(Host) of + {ok, #hostent{h_addrtype = inet, h_addr_list = [IP]}} -> + IP; + {ok, #hostent{h_addrtype = inet, h_addr_list = [_ | _] = IPs}} -> + Index = rand:uniform(length(IPs)), + lists:nth(Index, IPs); + _ -> error(badarg) + end. + +%% @private +-spec default_client_opts() -> client_opts(). +default_client_opts() -> + #{ip => application:get_env(eradius, client_ip, any), + no_ports => application:get_env(eradius, client_ports, 10), + active_n => application:get_env(eradius, active_n, 100), + recbuf => application:get_env(eradius, recbuf, 8192), + sndbuf => application:get_env(eradius, sndbuf, 131072), + servers_pool => application:get_env(eradius, servers_pool, []), + servers => application:get_env(eradius, servers, []) + }. + + +-spec client_config(client_opts()) -> {ok, client_config()} | {error, _}. +client_config(#{ip := IP} = Opts) when is_atom(IP) -> + {ok, Opts#{family => inet6, ip := any}}; +client_config(#{ip := {_, _, _, _}} = Opts) -> + {ok, Opts#{family => inet}}; +client_config(#{ip := {_, _, _, _, _, _, _, _}} = Opts) -> + {ok, Opts#{family => inet6}}; +client_config(#{ip := Address} = Opts) when is_list(Address) -> + case inet_parse:address(Address) of + {ok, {_, _, _, _} = IP} -> + {ok, Opts#{family => inet, ip => IP}}; + {ok, {_, _, _, _, _, _, _, _} = IP} -> + {ok, Opts#{family => inet6, ip => IP}}; + _ -> + ?LOG(error, "Invalid RADIUS client IP (parsing failed): ~p", [Address]), + {error, {bad_client_ip, Address}} + end. + +%% private +prepare_pools(#{servers_pool := PoolList, servers := ServerList}) -> + lists:foreach(fun({_PoolName, Servers}) -> prepare_pool(Servers) end, PoolList), + lists:foreach(fun(Server) -> store_upstream_servers(Server) end, ServerList), + init_server_status_metrics(). + +prepare_pool([]) -> ok; +prepare_pool([{Addr, Port, _, Opts} | Servers]) -> + Retries = proplists:get_value(retries, Opts, ?DEFAULT_RETRIES), + store_radius_server_from_pool(Addr, Port, Retries), + prepare_pool(Servers); +prepare_pool([{Addr, Port, _} | Servers]) -> + store_radius_server_from_pool(Addr, Port, ?DEFAULT_RETRIES), + prepare_pool(Servers). + +store_upstream_servers({Server, _}) -> + store_upstream_servers(Server); +store_upstream_servers({Server, _, _}) -> + store_upstream_servers(Server); +store_upstream_servers(Server) -> + %% TBD: move proxy config into the proxy logic... + + HandlerDefinitions = application:get_env(eradius, Server, []), + UpdatePoolFn = fun (HandlerOpts) -> + {DefaultRoute, Routes, Retries} = eradius_proxy:get_routes_info(HandlerOpts), + eradius_proxy:put_default_route_to_pool(DefaultRoute, Retries), + eradius_proxy:put_routes_to_pool(Routes, Retries) + end, + lists:foreach(fun (HandlerDefinition) -> + case HandlerDefinition of + {{_, []}, _} -> ok; + {{_, _, []}, _} -> ok; + {{_, HandlerOpts}, _} -> UpdatePoolFn(HandlerOpts); + {{_, _, HandlerOpts}, _} -> UpdatePoolFn(HandlerOpts); + _HandlerDefinition -> ok + end + end, + HandlerDefinitions). + +%% private +store_radius_server_from_pool(Addr, Port, Retries) + when is_tuple(Addr), is_integer(Port), is_integer(Retries) -> + ets:insert(?MODULE, {{Addr, Port}, Retries, Retries}); +store_radius_server_from_pool(Addr, Port, Retries) + when is_list(Addr), is_integer(Port), is_integer(Retries) -> + IP = get_ip(Addr), + ets:insert(?MODULE, {{IP, Port}, Retries, Retries}); +store_radius_server_from_pool(Addr, Port, Retries) -> + ?LOG(error, "bad RADIUS upstream server specified in RADIUS servers pool configuration ~p", [{Addr, Port, Retries}]), + error(badarg). + +reconfigure_address(#{no_ports := NPorts} = Config, + #state{socket_id = OAdd, sockets = Sockts} = State) -> + NAdd = socket_id(Config), + case OAdd of + NAdd -> + reconfigure_ports(NPorts, State); + _ -> + ?LOG(info, "Reopening RADIUS client sockets (client_ip changed to ~s)", + [socket_id_str(NAdd)]), + array:map( + fun(_PortIdx, undefined) -> + ok; + (_PortIdx, Socket) -> + eradius_client_socket:close(Socket) + end, Sockts), + Counters = fix_counters(NPorts, State#state.idcounters), + State#state{sockets = array:new(), socket_id = NAdd, + no_ports = NPorts, idcounters = Counters} + end. + +reconfigure_ports(NPorts, #state{no_ports = OPorts, sockets = Sockets} = State) -> + if + OPorts =< NPorts -> + State#state{no_ports = NPorts}; + true -> + Counters = fix_counters(NPorts, State#state.idcounters), + NSockets = close_sockets(NPorts, Sockets), + State#state{sockets = NSockets, no_ports = NPorts, idcounters = Counters} + end. + +fix_counters(NPorts, Counters) -> + maps:map(fun(_Peer, Value = {NextPortIdx, _NextReqId}) when NextPortIdx < NPorts -> Value; + (_Peer, {_NextPortIdx, NextReqId}) -> {0, NextReqId} + end, Counters). + +close_sockets(NPorts, Sockets) -> + case array:size(Sockets) =< NPorts of + true -> + Sockets; + false -> + List = array:to_list(Sockets), + {_, Rest} = lists:split(NPorts, List), + lists:map( + fun(undefined) -> ok; + (Socket) -> eradius_client_socket:close(Socket) + end, Rest), + array:resize(NPorts, Sockets) + end. + +next_port_and_req_id(Peer, NumberOfPorts, Counters) -> + case Counters of + #{Peer := {NextPortIdx, ReqId}} when ReqId < 255 -> + NextReqId = (ReqId + 1); + #{Peer := {PortIdx, 255}} -> + NextPortIdx = (PortIdx + 1) rem (NumberOfPorts - 1), + NextReqId = 0; + _ -> + NextPortIdx = erlang:phash2(Peer, NumberOfPorts), + NextReqId = 0 + end, + NewCounters = Counters#{Peer => {NextPortIdx, NextReqId}}, + {NextPortIdx, NextReqId, NewCounters}. + +find_socket_process(PortIdx, Sockets, Config) -> + case array:get(PortIdx, Sockets) of + undefined -> + {ok, Socket} = eradius_client_socket:new(Config), + {Socket, array:set(PortIdx, Socket, Sockets)}; + Socket -> + {Socket, Sockets} + end. + +%% @private +init_server_status_metrics() -> + case application:get_env(eradius, server_status_metrics_enabled, false) of + false -> + ok; + true -> + %% That will be called at eradius startup and we must be sure that prometheus + %% application already started if server status metrics supposed to be used + application:ensure_all_started(prometheus), + ets:foldl(fun ({{Addr, Port}, _, _}, _Acc) -> + eradius_counter:set_boolean_metric(server_status, [Addr, Port], false) + end, [], ?MODULE) + end. diff --git a/src/eradius_client_socket.erl b/src/eradius_client_socket.erl index 7472853e..22fddf3d 100644 --- a/src/eradius_client_socket.erl +++ b/src/eradius_client_socket.erl @@ -8,7 +8,7 @@ -behaviour(gen_server). %% API --export([new/2, start_link/1, send_request/5, close/1]). +-export([new/1, start_link/1, send_request/5, close/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -19,11 +19,11 @@ %%% API %%%========================================================================= -new(Sup, SocketIP) -> - eradius_client_sup:new(Sup, SocketIP). +new(Config) -> + eradius_client_socket_sup:new(Config). -start_link(SocketIP) -> - gen_server:start_link(?MODULE, [SocketIP], []). +start_link(Config) -> + gen_server:start_link(?MODULE, [Config], []). send_request(Socket, Peer, ReqId, Request, Timeout) -> try @@ -44,17 +44,17 @@ close(Socket) -> %%% gen_server callbacks %%%=================================================================== -init([SocketIP]) -> - case SocketIP of - undefined -> +init([#{family := Family, ip := IP, active_n := ActiveN, + recbuf := RecBuf, sndbuf := SndBuf} = _Config]) -> + case IP of + any -> ExtraOptions = []; - SocketIP when is_tuple(SocketIP) -> - ExtraOptions = [{ip, SocketIP}] + _ when is_tuple(IP) -> + ExtraOptions = [{ip, IP}] end, - ActiveN = application:get_env(eradius, active_n, 100), - RecBuf = application:get_env(eradius, recbuf, 8192), - SndBuf = application:get_env(eradius, sndbuf, 131072), - Opts = [{active, ActiveN}, binary, {recbuf, RecBuf}, {sndbuf, SndBuf} | ExtraOptions], + + Opts = [{active, ActiveN}, binary, {recbuf, RecBuf}, {sndbuf, SndBuf}, + Family | ExtraOptions], {ok, Socket} = gen_udp:open(0, Opts), State = #state{ diff --git a/src/eradius_client_socket_sup.erl b/src/eradius_client_socket_sup.erl new file mode 100644 index 00000000..954156be --- /dev/null +++ b/src/eradius_client_socket_sup.erl @@ -0,0 +1,52 @@ +-module(eradius_client_socket_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0, new/1]). + +%% Supervisor callbacks +-export([init/1]). + +-define(SERVER, ?MODULE). + +%%%=================================================================== +%%% API functions +%%%=================================================================== + +-spec start_link() -> {ok, Pid :: pid()} | + {error, {already_started, Pid :: pid()}} | + {error, {shutdown, term()}} | + {error, term()} | + ignore. +start_link() -> + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + +new(Config) -> + supervisor:start_child(?SERVER, [Config]). + +%%%=================================================================== +%%% Supervisor callbacks +%%%=================================================================== + +-spec init(Args :: term()) -> + {ok, {SupFlags :: supervisor:sup_flags(), + [ChildSpec :: supervisor:child_spec()]}} | + ignore. +init([]) -> + SupFlags = #{strategy => simple_one_for_one, + intensity => 5, + period => 10}, + + Child = #{id => eradius_client_socket, + start => {eradius_client_socket, start_link, []}, + restart => transient, + shutdown => 5000, + type => worker, + modules => [eradius_client_socket]}, + + {ok, {SupFlags, [Child]}}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/eradius_client_sup.erl b/src/eradius_client_sup.erl index 7c40b2cd..6db9d435 100644 --- a/src/eradius_client_sup.erl +++ b/src/eradius_client_sup.erl @@ -3,7 +3,7 @@ -behaviour(supervisor). %% API --export([start_link/0, new/2]). +-export([start_link/1, new/2]). %% Supervisor callbacks -export([init/1]). @@ -14,16 +14,23 @@ %%% API functions %%%=================================================================== --spec start_link() -> {ok, Pid :: pid()} | +-spec start_link(Config :: eradius_client:client_config()) -> + {ok, Pid :: pid()} | {error, {already_started, Pid :: pid()}} | {error, {shutdown, term()}} | {error, term()} | ignore. -start_link() -> - supervisor:start_link(?MODULE, []). +start_link(Config) -> + supervisor:start_link(?MODULE, [Config]). -new(Sup, SocketId) -> - supervisor:start_child(Sup, [SocketId]). +new(Owner, SocketId) -> + Children = supervisor:which_children(Owner), + case lists:keyfind(eradius_client_socket_sup, 1, Children) of + {eradius_client_socket_sup, SupPid, _, _} when is_pid(SupPid) -> + supervisor:start_child(SupPid, [SocketId]); + _ -> + {error, dead} + end. %%%=================================================================== %%% Supervisor callbacks @@ -33,19 +40,26 @@ new(Sup, SocketId) -> {ok, {SupFlags :: supervisor:sup_flags(), [ChildSpec :: supervisor:child_spec()]}} | ignore. -init([]) -> - SupFlags = #{strategy => simple_one_for_one, +init([Opts]) -> + SupFlags = #{strategy => one_for_one, intensity => 5, period => 10}, + Client = + #{id => eradius_client, + start => {eradius_client, start_link, [self(), Opts]}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [eradius_client]}, + SocketSup = + #{id => eradius_client_socket_sup, + start => {eradius_client_socket_sup, start_link, []}, + restart => permanent, + shutdown => 5000, + type => supervisor, + modules => [eradius_client_socket_sup]}, - Child = #{id => eradius_client_socket, - start => {eradius_client_socket, start_link, []}, - restart => transient, - shutdown => 5000, - type => worker, - modules => [eradius_client_socket]}, - - {ok, {SupFlags, [Child]}}. + {ok, {SupFlags, [Client, SocketSup]}}. %%%=================================================================== %%% Internal functions diff --git a/src/eradius_internal.hrl b/src/eradius_internal.hrl new file mode 100644 index 00000000..cc814287 --- /dev/null +++ b/src/eradius_internal.hrl @@ -0,0 +1,2 @@ +-define(DEFAULT_RETRIES, 3). +-define(DEFAULT_TIMEOUT, 5000). diff --git a/src/eradius_proxy.erl b/src/eradius_proxy.erl index bdca5761..b3130dc9 100644 --- a/src/eradius_proxy.erl +++ b/src/eradius_proxy.erl @@ -290,9 +290,9 @@ get_routes_info(HandlerOpts) -> put_default_route_to_pool(false, _) -> ok; put_default_route_to_pool({default_route, {Host, Port, _Secret}}, Retries) -> - eradius_client:store_radius_server_from_pool(Host, Port, Retries); + eradius_client_mngr:store_radius_server_from_pool(Host, Port, Retries); put_default_route_to_pool({default_route, {Host, Port, _Secret}, _PoolName}, Retries) -> - eradius_client:store_radius_server_from_pool(Host, Port, Retries); + eradius_client_mngr:store_radius_server_from_pool(Host, Port, Retries); put_default_route_to_pool(_, _) -> ok. put_routes_to_pool(false, _Retries) -> ok; @@ -300,11 +300,11 @@ put_routes_to_pool({routes, Routes}, Retries) -> lists:foreach(fun (Route) -> case Route of {_RouteName, {Host, Port, _Secret}} -> - eradius_client:store_radius_server_from_pool(Host, Port, Retries); + eradius_client_mngr:store_radius_server_from_pool(Host, Port, Retries); {_RouteName, {Host, Port, _Secret}, _Pool} -> - eradius_client:store_radius_server_from_pool(Host, Port, Retries); + eradius_client_mngr:store_radius_server_from_pool(Host, Port, Retries); {Host, Port, _Secret, _Opts} -> - eradius_client:store_radius_server_from_pool(Host, Port, Retries); + eradius_client_mngr:store_radius_server_from_pool(Host, Port, Retries); _ -> ok end end, Routes). diff --git a/src/eradius_server.erl b/src/eradius_server.erl index c77bae2f..b5e50f86 100644 --- a/src/eradius_server.erl +++ b/src/eradius_server.erl @@ -307,17 +307,21 @@ handle_request({HandlerMod, HandlerArg}, NasProp = #nas_prop{secret = Secret, na maps:from_list(eradius_log:collect_meta(Sender, Request))), eradius_log:write_request(Sender, Request), apply_handler_mod(HandlerMod, HandlerArg, Request, NasProp); - {bad_pdu, "Message-Authenticator Attribute is invalid" = Reason} -> - ?LOG(error, "~s INF: Could not decode the request, reason: ~s", [printable_peer(ServerIP, Port), Reason]), + {bad_pdu, "Message-Authenticator Attribute is invalid"} -> + ?LOG(error, "~s INF: Message-Authenticator Attribute is invalid", + [printable_peer(ServerIP, Port)]), {discard, bad_authenticator}; - {bad_pdu, "Authenticator Attribute is invalid" = Reason} -> - ?LOG(error, "~s INF: Could not decode the request, reason: ~s", [printable_peer(ServerIP, Port), Reason]), + {bad_pdu, "Authenticator Attribute is invalid"} -> + ?LOG(error, "~s INF: Authenticator Attribute is invalid", + [printable_peer(ServerIP, Port)]), {discard, bad_authenticator}; - {bad_pdu, "unknown request type" = Reason} -> - ?LOG(error, "~s INF: Could not decode the request, reason: ~s", [printable_peer(ServerIP, Port), Reason]), + {bad_pdu, "unknown request type"} -> + ?LOG(error, "~s INF: unknown request type", + [printable_peer(ServerIP, Port)]), {discard, unknown_req_type}; {bad_pdu, Reason} -> - ?LOG(error, "~s INF: Could not decode the request, reason: ~s", [printable_peer(ServerIP, Port), Reason]), + ?LOG(error, "~s INF: Could not decode the request, reason: ~s", + [printable_peer(ServerIP, Port), Reason]), {discard, malformed} end. diff --git a/src/eradius_sup.erl b/src/eradius_sup.erl index 342af84f..86093a41 100644 --- a/src/eradius_sup.erl +++ b/src/eradius_sup.erl @@ -25,6 +25,20 @@ init([]) -> NodeMon = {node_mon, {eradius_node_mon, start_link, []}, permanent, brutal_kill, worker, [eradius_node_mon]}, RadiusLog = {radius_log, {eradius_log, start_link, []}, permanent, brutal_kill, worker, [eradius_log]}, ServerTopSup = {server_top_sup, {eradius_server_top_sup, start_link, []}, permanent, infinity, supervisor, [eradius_server_top_sup]}, - Client = {client, {eradius_client, start_link, []}, permanent, 500, worker, [eradius_client]}, + ClientMngr = + #{id => client_mngr, + start => {eradius_client_mngr, start_link, []}, + restart => permanent, + shutdown => 500, + type => worker, + modules => [eradius_client_mngr]}, + ClientSocketSup = + #{id => eradius_client_socket_sup, + start => {eradius_client_socket_sup, start_link, []}, + restart => permanent, + shutdown => 5000, + type => supervisor, + modules => [eradius_client_socket_sup]}, - {ok, {SupFlags, [DictServer, NodeMon, StatsServer, StatsCollect, RadiusLog, ServerTopSup, Client]}}. + {ok, {SupFlags, [DictServer, NodeMon, StatsServer, StatsCollect, RadiusLog, + ServerTopSup, ClientSocketSup, ClientMngr]}}. diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index ae4929b4..65305f19 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -100,8 +100,7 @@ end_per_testcase(_Test, Config) -> %% STUFF getSocketCount() -> - #{sup := Sup} = eradius_client:get_state(), - Counts = supervisor:count_children(Sup), + Counts = supervisor:count_children(eradius_client_socket_sup), proplists:get_value(active, Counts). testSocket(undefined) -> @@ -133,6 +132,8 @@ meckStop() -> parse_ip(undefined) -> {ok, undefined}; +parse_ip(any) -> + {ok, any}; parse_ip(Address) when is_list(Address) -> inet_parse:address(Address); parse_ip(T = {_, _, _, _}) -> @@ -148,9 +149,9 @@ test(false, Msg) -> false. check(OldState, NewState = #{no_ports := P}, null, A) -> check(OldState, NewState, P, A); -check(OldState, NewState = #{socket_ip := A}, P, null) -> check(OldState, NewState, P, A); -check(#{sockets := OS, no_ports := _OP, idcounters := _OC, socket_ip := OA}, - #{sockets := NS, no_ports := NP, idcounters := NC, socket_ip := NA}, +check(OldState, NewState = #{socket_id := {_, A}}, P, null) -> check(OldState, NewState, P, A); +check(#{sockets := OS, no_ports := _OP, idcounters := _OC, socket_id := {_, OA}}, + #{sockets := NS, no_ports := NP, idcounters := NC, socket_id := {_, NA}}, P, A) -> {ok, PA} = parse_ip(A), test(PA == NA, "Adress not configured") and @@ -181,9 +182,9 @@ send_request(_Config) -> send(FUN, Ports, Address) -> meckStart(), - OldState = eradius_client:get_state(), + OldState = eradius_client_mngr:get_state(), FUN(), - NewState = eradius_client:get_state(), + NewState = eradius_client_mngr:get_state(), true = check(OldState, NewState, Ports, Address), meckStop(). @@ -191,24 +192,23 @@ wanna_send(_Config) -> lists:map(fun(_) -> IP = {rand:uniform(100), rand:uniform(100), rand:uniform(100), rand:uniform(100)}, Port = rand:uniform(100), - MetricsInfo = {{undefined, undefined, undefined}, {undefined, undefined, undefined}}, - FUN = fun() -> gen_server:call(eradius_client, {wanna_send, {undefined, {IP, Port}}, MetricsInfo}) end, + FUN = fun() -> eradius_client_mngr:wanna_send({undefined, {IP, Port}}) end, send(FUN, null, null) end, lists:seq(1, 10)). %% socket shutdown is done asynchronous, the tests need to wait a bit for it to finish. reconf_address(_Config) -> - FUN = fun() -> eradius_client:reconfigure(), timer:sleep(100) end, + FUN = fun() -> eradius_client_mngr:reconfigure(), timer:sleep(100) end, application:set_env(eradius, client_ip, "7.13.23.42"), send(FUN, null, "7.13.23.42"). reconf_ports_30(_Config) -> - FUN = fun() -> gen_server:call(eradius_client, reconfigure), timer:sleep(100) end, + FUN = fun() -> eradius_client_mngr:reconfigure(), timer:sleep(100) end, application:set_env(eradius, client_ports, 30), send(FUN, 30, null). reconf_ports_10(_Config) -> - FUN = fun() -> gen_server:call(eradius_client, reconfigure), timer:sleep(100) end, + FUN = fun() -> eradius_client_mngr:reconfigure(), timer:sleep(100) end, application:set_env(eradius, client_ports, 10), send(FUN, 10, null). @@ -216,9 +216,9 @@ send_request_failover(_Config) -> ?equal(accept, eradius_test_handler:send_request_failover(?BAD_SERVER_IP)), {ok, Timeout} = application:get_env(eradius, unreachable_timeout), timer:sleep(Timeout * 1000), - ?equal([?BAD_SERVER_TUPLE], ets:lookup(eradius_client, ?BAD_SERVER_IP_ETS_KEY)), + ?equal([?BAD_SERVER_TUPLE], eradius_client_mngr:servers(?BAD_SERVER_IP_ETS_KEY)), ok. check_upstream_servers(_Config) -> - ?equal(?RADIUS_SERVERS, ets:tab2list(eradius_client)), + ?equal(?RADIUS_SERVERS, eradius_client_mngr:servers()), ok. diff --git a/test/eradius_metrics_SUITE.erl b/test/eradius_metrics_SUITE.erl index 60cc0fbb..d788d843 100644 --- a/test/eradius_metrics_SUITE.erl +++ b/test/eradius_metrics_SUITE.erl @@ -82,7 +82,7 @@ end_per_suite(_Config) -> ok. init_per_testcase(_, Config) -> - eradius_client:init_server_status_metrics(), + eradius_client_mngr:init_server_status_metrics(), Config. %% tests From 27ff2127762838a3694cf5453a8c1feede38fa50 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Tue, 16 Apr 2024 16:54:42 +0200 Subject: [PATCH 05/38] [client] move request timeout handling to socket process There was no logic in place to remove entries from the pending registry once the timeout hits. The client side timeout would only handle re-transmission, but not the cleanup. Move the timeout logic to the sender and let it also cleanup the pending registry. --- src/eradius_client.erl | 2 +- src/eradius_client_socket.erl | 94 +++++++++++++++++++++++------------ 2 files changed, 62 insertions(+), 34 deletions(-) diff --git a/src/eradius_client.erl b/src/eradius_client.erl index b2ece522..77a941b7 100644 --- a/src/eradius_client.erl +++ b/src/eradius_client.erl @@ -239,7 +239,7 @@ send_request_loop(Socket, Peer = {_ServerName, {IP, Port}}, ReqId, Authenticator end, case Result of - {response, ReqId, Response} -> + {ok, Response} -> {ok, Response, Secret, Authenticator}; {error, close} -> {error, socket_down}; diff --git a/src/eradius_client_socket.erl b/src/eradius_client_socket.erl index 22fddf3d..9278119e 100644 --- a/src/eradius_client_socket.erl +++ b/src/eradius_client_socket.erl @@ -13,7 +13,7 @@ %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {socket, active_n, pending, mode, counter}). +-record(state, {family, socket, active_n, pending, mode, counter}). %%%========================================================================= %%% API @@ -27,10 +27,8 @@ start_link(Config) -> send_request(Socket, Peer, ReqId, Request, Timeout) -> try - gen_server:call(Socket, {send_request, Peer, ReqId, Request}, Timeout) + gen_server:call(Socket, {send_request, Peer, ReqId, Request, Timeout}, infinity) catch - exit:{timeout, _} -> - {error, timeout}; exit:{noproc, _} -> {error, closed}; {nodedown, _} -> @@ -58,6 +56,7 @@ init([#{family := Family, ip := IP, active_n := ActiveN, {ok, Socket} = gen_udp:open(0, Opts), State = #state{ + family = Family, socket = Socket, active_n = ActiveN, pending = #{}, @@ -65,15 +64,19 @@ init([#{family := Family, ip := IP, active_n := ActiveN, }, {ok, State}. -handle_call({send_request, {IP, Port}, ReqId, Request}, From, - #state{socket = Socket, pending = Pending} = State) -> - case gen_udp:send(Socket, IP, Port, Request) of - ok -> - ReqKey = {IP, Port, ReqId}, - NPending = Pending#{ReqKey => From}, - {noreply, State#state{pending = NPending}}; - {error, Reason} -> - {reply, {error, Reason}, State} +handle_call({send_request, {IP, Port}, ReqId, Request, Timeout}, From, + #state{family = Family, socket = Socket} = State) -> + case send_ip(Family, IP) of + {ok, SendIP} -> + case gen_udp:send(Socket, SendIP, Port, Request) of + ok -> + ReqKey = {SendIP, Port, ReqId}, + {noreply, pending_request(ReqKey, From, Timeout, State)}; + {error, _} = Error -> + {reply, Error, State} + end; + {error, _} = Error -> + {reply, Error, State} end; handle_call(_Request, _From, State) -> @@ -92,27 +95,17 @@ handle_info({udp_passive, _Socket}, #state{socket = Socket, active_n = ActiveN} inet:setopts(Socket, [{active, ActiveN}]), {noreply, State}; -handle_info({udp, Socket, FromIP, FromPort, Request}, - State = #state{socket = Socket, pending = Pending, mode = Mode}) -> - case eradius_lib:decode_request_id(Request) of - {ReqId, Request} -> - case Pending of - #{{FromIP, FromPort, ReqId} := From} -> - gen_server:reply(From, {response, ReqId, Request}), - - flow_control(State), - NPending = maps:remove({FromIP, FromPort, ReqId}, Pending), - NState = State#state{pending = NPending}, - case Mode of - inactive when map_size(NPending) =:= 0 -> - {stop, normal, NState}; - _ -> - {noreply, NState} - end; +handle_info({udp, Socket, FromIP, FromPort, Response}, + State = #state{socket = Socket, mode = Mode}) -> + case eradius_lib:decode_request_id(Response) of + {ReqId, Response} -> + NState = request_done({FromIP, FromPort, ReqId}, {ok, Response}, State), + case Mode of + inactive when map_size(State#state.pending) =:= 0 -> + {stop, normal, NState}; _ -> - %% discard reply because we didn't expect it - flow_control(State), - {noreply, State} + flow_control(NState), + {noreply, NState} end; {bad_pdu, _} -> %% discard reply because it was malformed @@ -120,6 +113,17 @@ handle_info({udp, Socket, FromIP, FromPort, Request}, {noreply, State} end; +handle_info({timeout, TRef, ReqKey}, #state{pending = Pending} = State) -> + NState = + case Pending of + #{ReqKey := {From, TRef}} -> + gen_server:reply(From, {error, timeout}), + State#state{pending = maps:remove(ReqKey, Pending)}; + _ -> + State + end, + {noreply, NState}; + handle_info(_Info, State) -> {noreply, State}. @@ -137,3 +141,27 @@ flow_control(#state{socket = Socket, active_n = once}) -> inet:setopts(Socket, [{active, once}]); flow_control(_) -> ok. + +pending_request(ReqKey, From, Timeout, + #state{pending = Pending} = State) -> + TRef = erlang:start_timer(Timeout, self(), ReqKey), + State#state{pending = Pending#{ReqKey => {From, TRef}}}. + +request_done(ReqKey, Reply, #state{pending = Pending} = State) -> + case Pending of + #{ReqKey := {From, TRef}} -> + gen_server:reply(From, Reply), + erlang:cancel_timer(TRef), + State#state{pending = maps:remove(ReqKey, Pending)}; + _ -> + State + end. + +send_ip(inet, {_, _, _, _} = IP) -> + {ok, IP}; +send_ip(inet6, {_, _, _, _} = IP) -> + {ok, inet:ipv4_mapped_ipv6_address(IP)}; +send_ip(inet6, {_, _, _, _,_, _, _, _} = IP) -> + {ok, IP}; +send_ip(_, _) -> + {error, eafnosupport}. From e3c5753b08687b9b2693b6a331fa2358d11faba3 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 17 Apr 2024 12:54:25 +0200 Subject: [PATCH 06/38] [client] add more socket options to the client --- src/eradius_client_socket.erl | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/eradius_client_socket.erl b/src/eradius_client_socket.erl index 9278119e..764f0f98 100644 --- a/src/eradius_client_socket.erl +++ b/src/eradius_client_socket.erl @@ -42,17 +42,8 @@ close(Socket) -> %%% gen_server callbacks %%%=================================================================== -init([#{family := Family, ip := IP, active_n := ActiveN, - recbuf := RecBuf, sndbuf := SndBuf} = _Config]) -> - case IP of - any -> - ExtraOptions = []; - _ when is_tuple(IP) -> - ExtraOptions = [{ip, IP}] - end, - - Opts = [{active, ActiveN}, binary, {recbuf, RecBuf}, {sndbuf, SndBuf}, - Family | ExtraOptions], +init([#{family := Family, active_n := ActiveN} = Config]) -> + Opts = inet_opts(Config, [{active, ActiveN}, binary, Family]), {ok, Socket} = gen_udp:open(0, Opts), State = #state{ @@ -165,3 +156,15 @@ send_ip(inet6, {_, _, _, _,_, _, _, _} = IP) -> {ok, IP}; send_ip(_, _) -> {error, eafnosupport}. + +inet_opts(Config, Opts0) -> + Opts = + maps:to_list( + maps:with([recbuf, sndbuf, ip, + ipv6_v6only, netns, bind_to_device, read_packets], Config)) ++ Opts0, + case Config of + #{inet_backend := Backend} when Backend =:= inet; Backend =:= socket -> + [{inet_backend, Backend} | Opts]; + _ -> + Opts + end. From a1c999d48100ea9e5efc1977eda1ebf1bdefdfb1 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 17 Apr 2024 12:55:21 +0200 Subject: [PATCH 07/38] [ct] prepare for IPv6 support Fully prepare client suite for full IPv6 support and add some minor tweaks to the others to move them closer to IPv6 support. --- test/eradius_client_SUITE.erl | 202 +++++++++++++++++++++------------ test/eradius_config_SUITE.erl | 14 +-- test/eradius_logtest.erl | 24 ++-- test/eradius_metrics_SUITE.erl | 36 +++--- test/eradius_proxy_SUITE.erl | 30 ++--- test/eradius_test_handler.erl | 62 +++++----- test/eradius_test_lib.erl | 53 +++++++++ 7 files changed, 269 insertions(+), 152 deletions(-) create mode 100644 test/eradius_test_lib.erl diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index 65305f19..14628d76 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -1,65 +1,81 @@ -%% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> - -%% Permission is hereby granted, free of charge, to any person obtaining a -%% copy of this software and associated documentation files (the "Software"), -%% to deal in the Software without restriction, including without limitation -%% the rights to use, copy, modify, merge, publish, distribute, sublicense, -%% and/or sell copies of the Software, and to permit persons to whom the -%% Software is furnished to do so, subject to the following conditions: - -%% The above copyright notice and this permission notice shall be included in -%% all copies or substantial portions of the Software. - -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -%% DEALINGS IN THE SOFTWARE. - +%% Copyright (c) 2010-2017, Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT +%% -module(eradius_client_SUITE). --compile(export_all). + +-compile([export_all, nowarn_export_all]). + +-behaviour(ct_suite). -include("test/eradius_test.hrl"). +%%%=================================================================== +%%% Defines +%%%=================================================================== + -define(HUT_SOCKET, eradius_client_socket). --define(BAD_SERVER_IP, {eradius_test_handler:localhost(ip), 1820, "secret"}). +-define(BAD_SERVER_IP(Family), + {eradius_test_lib:localhost(Family, ip), 1820, "secret"}). -define(BAD_SERVER_INITIAL_RETRIES, 2). --define(BAD_SERVER_TUPLE_INITIAL, {{eradius_test_handler:localhost(tuple), 1820}, - ?BAD_SERVER_INITIAL_RETRIES, - ?BAD_SERVER_INITIAL_RETRIES}). --define(BAD_SERVER_TUPLE, {{eradius_test_handler:localhost(tuple), 1820}, - ?BAD_SERVER_INITIAL_RETRIES - 1, - ?BAD_SERVER_INITIAL_RETRIES}). --define(BAD_SERVER_IP_ETS_KEY, {eradius_test_handler:localhost(tuple), 1820}). +-define(BAD_SERVER_TUPLE_INITIAL(Family), + {{eradius_test_lib:localhost(Family, tuple), 1820}, + ?BAD_SERVER_INITIAL_RETRIES, + ?BAD_SERVER_INITIAL_RETRIES}). +-define(BAD_SERVER_TUPLE(Family), + {{eradius_test_lib:localhost(Family, tuple), 1820}, + ?BAD_SERVER_INITIAL_RETRIES - 1, + ?BAD_SERVER_INITIAL_RETRIES}). +-define(BAD_SERVER_IP_ETS_KEY(Family), + {eradius_test_lib:localhost(Family, tuple), 1820}). -define(GOOD_SERVER_INITIAL_RETRIES, 3). --define(GOOD_SERVER_TUPLE, {{eradius_test_handler:localhost(tuple), 1812}, - ?GOOD_SERVER_INITIAL_RETRIES, - ?GOOD_SERVER_INITIAL_RETRIES}). --define(GOOD_SERVER_2_TUPLE, {{{127, 0, 0, 2}, 1813}, - ?GOOD_SERVER_INITIAL_RETRIES, - ?GOOD_SERVER_INITIAL_RETRIES}). - --define(RADIUS_SERVERS, [?GOOD_SERVER_TUPLE, - ?BAD_SERVER_TUPLE_INITIAL, - ?GOOD_SERVER_2_TUPLE]). +-define(GOOD_SERVER_TUPLE(Family), + {{eradius_test_lib:localhost(Family, tuple), 1812}, + ?GOOD_SERVER_INITIAL_RETRIES, + ?GOOD_SERVER_INITIAL_RETRIES}). +-define(GOOD_SERVER_2_TUPLE(Family), + {{eradius_test_lib:badhost(Family), 1813}, + ?GOOD_SERVER_INITIAL_RETRIES, + ?GOOD_SERVER_INITIAL_RETRIES}). + +-define(RADIUS_SERVERS(Family), + [?GOOD_SERVER_TUPLE(Family), + ?BAD_SERVER_TUPLE_INITIAL(Family), + ?GOOD_SERVER_2_TUPLE(Family)]). + +%%%=================================================================== +%%% Setup +%%%=================================================================== -spec all() -> [ct_suite:ct_test_def(), ...]. -all() -> [ - send_request, - wanna_send, - reconf_address, - wanna_send, - reconf_ports_30, - wanna_send, - reconf_ports_10, - wanna_send, - send_request_failover, - check_upstream_servers - ]. +all() -> + [{group, ipv4}, + {group, ipv4_mapped_ipv6}, + {group, ipv6}]. + +common() -> + [send_request, + wanna_send, + reconf_address, + wanna_send, + reconf_ports_30, + wanna_send, + reconf_ports_10, + wanna_send, + send_request_failover, + check_upstream_servers + ]. + +-spec groups() -> [ct_suite:ct_group_def(), ...]. +groups() -> + SocketGroups = [{group, inet}, {group, socket}], + [{inet, [], common()}, + {socket, [], common()}, + {ipv4, [], SocketGroups}, + {ipv4_mapped_ipv6, [], SocketGroups}, + {ipv6, [], SocketGroups}]. init_per_suite(Config) -> {ok, _} = application:ensure_all_started(eradius), @@ -70,17 +86,48 @@ end_per_suite(_Config) -> application:stop(eradius), ok. +init_per_group(inet, Config) -> + [{inet_backend, inet} | Config]; +init_per_group(socket, Config) -> + [{inet_backend, socket} | Config]; +init_per_group(ipv6 = Group, Config) -> + {skip, "no IPv6 server support (yet)"}; + %% case eradius_test_lib:has_ipv6_test_config() of + %% true -> + %% [{family, Group} | Config]; + %% _ -> + %% {skip, "IPv6 test IPs not configured"} + %% end; +init_per_group(ipv4_mapped_ipv6 = Group, Config) -> + case eradius_test_lib:has_ipv6_test_config() of + true -> + [{family, Group} | Config]; + _ -> + {skip, "IPv6 test IPs not configured"} + end; +init_per_group(ipv4 = Group, Config) -> + [{family, Group} | Config]. + +end_per_group(_Group, _Config) -> + application:stop(eradius), + ok. + +start_handler(Config) -> + Backend = proplists:get_value(inet_backend, Config, inet), + Family = proplists:get_value(family, Config), + eradius_test_handler:start(Backend, Family). + init_per_testcase(send_request, Config) -> application:stop(eradius), - eradius_test_handler:start(), + start_handler(Config), Config; init_per_testcase(send_request_failover, Config) -> application:stop(eradius), - eradius_test_handler:start(), + start_handler(Config), Config; init_per_testcase(check_upstream_servers, Config) -> application:stop(eradius), - eradius_test_handler:start(), + start_handler(Config), Config; init_per_testcase(_Test, Config) -> Config. @@ -157,6 +204,7 @@ check(#{sockets := OS, no_ports := _OP, idcounters := _OC, socket_id := {_, OA}} test(PA == NA, "Adress not configured") and case NA of OA -> + ct:pal("NP: ~p, NC: ~p", [NP, NC]), {_, Rest} = split(NP, array:to_list(OS)), test(P == NP,"Ports not configured") and test(maps:fold( fun(_Peer, {NextPortIdx, _NextReqId}, Akk) -> @@ -173,11 +221,16 @@ check(#{sockets := OS, no_ports := _OP, idcounters := _OC, socket_id := {_, OA}} %% TESTS -send_request(_Config) -> - ?equal(accept, eradius_test_handler:send_request(eradius_test_handler:localhost(tuple))), - ?equal(accept, eradius_test_handler:send_request(eradius_test_handler:localhost(ip))), - ?equal(accept, eradius_test_handler:send_request(eradius_test_handler:localhost(string))), - ?equal(accept, eradius_test_handler:send_request(eradius_test_handler:localhost(binary))), +send_request(Config) -> + Family = proplists:get_value(family, Config), + ?equal(accept, + eradius_test_handler:send_request(eradius_test_lib:localhost(Family, tuple))), + ?equal(accept, + eradius_test_handler:send_request(eradius_test_lib:localhost(Family, ip))), + ?equal(accept, + eradius_test_handler:send_request(eradius_test_lib:localhost(Family, string))), + ?equal(accept, + eradius_test_handler:send_request(eradius_test_lib:localhost(Family, binary))), ok. send(FUN, Ports, Address) -> @@ -198,27 +251,36 @@ wanna_send(_Config) -> %% socket shutdown is done asynchronous, the tests need to wait a bit for it to finish. reconf_address(_Config) -> - FUN = fun() -> eradius_client_mngr:reconfigure(), timer:sleep(100) end, - application:set_env(eradius, client_ip, "7.13.23.42"), + FUN = fun() -> + eradius_client_mngr:reconfigure(#{ip => "7.13.23.42"}), + timer:sleep(100) + end, send(FUN, null, "7.13.23.42"). reconf_ports_30(_Config) -> - FUN = fun() -> eradius_client_mngr:reconfigure(), timer:sleep(100) end, - application:set_env(eradius, client_ports, 30), + FUN = fun() -> + eradius_client_mngr:reconfigure(#{no_ports => 30}), + timer:sleep(100) + end, send(FUN, 30, null). reconf_ports_10(_Config) -> - FUN = fun() -> eradius_client_mngr:reconfigure(), timer:sleep(100) end, - application:set_env(eradius, client_ports, 10), + FUN = fun() -> + eradius_client_mngr:reconfigure(#{no_ports => 10}), + timer:sleep(100) + end, send(FUN, 10, null). -send_request_failover(_Config) -> - ?equal(accept, eradius_test_handler:send_request_failover(?BAD_SERVER_IP)), +send_request_failover(Config) -> + Family = proplists:get_value(family, Config), + ?equal(accept, eradius_test_handler:send_request_failover(?BAD_SERVER_IP(Family))), {ok, Timeout} = application:get_env(eradius, unreachable_timeout), timer:sleep(Timeout * 1000), - ?equal([?BAD_SERVER_TUPLE], eradius_client_mngr:servers(?BAD_SERVER_IP_ETS_KEY)), + ?equal([?BAD_SERVER_TUPLE(Family)], + eradius_client_mngr:servers(?BAD_SERVER_IP_ETS_KEY(Family))), ok. -check_upstream_servers(_Config) -> - ?equal(?RADIUS_SERVERS, eradius_client_mngr:servers()), +check_upstream_servers(Config) -> + Family = proplists:get_value(family, Config), + ?equal(lists:keysort(1, ?RADIUS_SERVERS(Family)), eradius_client_mngr:servers()), ok. diff --git a/test/eradius_config_SUITE.erl b/test/eradius_config_SUITE.erl index 2f35e1e6..07dd2cba 100644 --- a/test/eradius_config_SUITE.erl +++ b/test/eradius_config_SUITE.erl @@ -40,7 +40,7 @@ config_1(_Config) -> Conf = [{session_nodes, ['node1@host1', 'node2@host2']}, {radius_callback, ?MODULE}, {servers, [ - {root, {eradius_test_handler:localhost(ip), [1812, 1813]}} + {root, {eradius_test_lib:localhost(ip), [1812, 1813]}} ]}, {root, [ { {"NAS1", [arg1, arg2]}, @@ -49,7 +49,7 @@ config_1(_Config) -> [{{10, 18, 14, 3}, <<"secret2">>, [{nas_id, <<"name">>}]}]} ]}], ok = apply_conf(Conf), - LocalHost = eradius_test_handler:localhost(tuple), + LocalHost = eradius_test_lib:localhost(tuple), ?match({ok, {?MODULE,[arg1,arg2]}, #nas_prop{ server_ip = LocalHost, @@ -83,7 +83,7 @@ config_2(_Config) -> ] }, {servers, [ - {root, {eradius_test_handler:localhost(ip), [1812, 1813]}} + {root, {eradius_test_lib:localhost(ip), [1812, 1813]}} ]}, {root, [ { {handler1, "NAS1", [arg1, arg2]}, @@ -93,7 +93,7 @@ config_2(_Config) -> [ {"10.18.14.2", <<"secret2">>, [{group, "NodeGroup2"}]} ] } ]}], ok = apply_conf(Conf), - LocalHost = eradius_test_handler:localhost(tuple), + LocalHost = eradius_test_lib:localhost(tuple), ?match({ok, {handler1,[arg1,arg2]}, #nas_prop{ server_ip = LocalHost, @@ -146,7 +146,7 @@ test_validate_server(_Config) -> ok. config_nas_removing(_Config) -> - Conf = [{servers, [ {root, {eradius_test_handler:localhost(ip), [1812, 1813]}} ]}, + Conf = [{servers, [ {root, {eradius_test_lib:localhost(ip), [1812, 1813]}} ]}, {root, [ ]}], ok = apply_conf(Conf), ?match([], ets:tab2list(eradius_nas_tab)), @@ -159,14 +159,14 @@ config_with_ranges(_Config) -> ] }, {servers, [ - {root, {eradius_test_handler:localhost(ip), [1812, 1813]}} + {root, {eradius_test_lib:localhost(ip), [1812, 1813]}} ]}, {root, [ { {handler, "NAS", []}, [ {"10.18.14.2/30", <<"secret2">>, [{group, "NodeGroup"}]} ] } ]}], ok = apply_conf(Conf), - LocalHost = eradius_test_handler:localhost(tuple), + LocalHost = eradius_test_lib:localhost(tuple), ?match({ok, {handler,[]}, #nas_prop{ server_ip = LocalHost, diff --git a/test/eradius_logtest.erl b/test/eradius_logtest.erl index aa410aad..125422ab 100644 --- a/test/eradius_logtest.erl +++ b/test/eradius_logtest.erl @@ -41,25 +41,25 @@ start() -> application:load(eradius), - ProxyConfig = [{default_route, {eradius_test_handler:localhost(tuple), 1813, ?SECRET}}, + ProxyConfig = [{default_route, {eradius_test_lib:localhost(tuple), 1813, ?SECRET}}, {options, [{type, realm}, {strip, true}, {separator, "@"}]}, - {routes, [{"test", {eradius_test_handler:localhost(tuple), 1815, ?SECRET3}} + {routes, [{"test", {eradius_test_lib:localhost(tuple), 1815, ?SECRET3}} ]} ], Config = [{radius_callback, eradius_logtest}, - {servers, [{root, {eradius_test_handler:localhost(ip), [1812, 1813]}}, - {test, {eradius_test_handler:localhost(ip), [1815]}}, - {proxy, {eradius_test_handler:localhost(ip), [11812, 11813]}} + {servers, [{root, {eradius_test_lib:localhost(ip), [1812, 1813]}}, + {test, {eradius_test_lib:localhost(ip), [1815]}}, + {proxy, {eradius_test_lib:localhost(ip), [11812, 11813]}} ]}, {session_nodes, [node()]}, {root, [ { {eradius_logtest, "root", [] }, [{"127.0.0.1/24", ?SECRET, [{nas_id, <<"Test_Nas_Id">>}]}] } ]}, {test, [ - { {eradius_logtest, "test", [] }, [{eradius_test_handler:localhost(ip), ?SECRET3, [{nas_id, <<"Test_Nas_Id_test">>}]}] } + { {eradius_logtest, "test", [] }, [{eradius_test_lib:localhost(ip), ?SECRET3, [{nas_id, <<"Test_Nas_Id_test">>}]}] } ]}, {proxy, [ - { {eradius_proxy, "proxy", ProxyConfig }, [{eradius_test_handler:localhost(ip), ?SECRET2, [{nas_id, <<"Test_Nas_proxy">>}]}] } + { {eradius_proxy, "proxy", ProxyConfig }, [{eradius_test_lib:localhost(ip), ?SECRET2, [{nas_id, <<"Test_Nas_proxy">>}]}] } ]} ], [application:set_env(eradius, Key, Value) || {Key, Value} <- Config], @@ -97,20 +97,20 @@ test_client() -> test_client(Command) -> eradius_dict:load_tables([dictionary, dictionary_3gpp]), Request = eradius_lib:set_attributes(#radius_request{cmd = Command, msg_hmac = true}, attrs("user")), - send_request(eradius_test_handler:localhost(tuple), 1813, ?SECRET, Request). + send_request(eradius_test_lib:localhost(tuple), 1813, ?SECRET, Request). test_proxy() -> test_proxy(request). test_proxy(Command) -> eradius_dict:load_tables([dictionary, dictionary_3gpp]), - send_request(eradius_test_handler:localhost(tuple), 11813, ?SECRET2, #radius_request{cmd = Command}), + send_request(eradius_test_lib:localhost(tuple), 11813, ?SECRET2, #radius_request{cmd = Command}), Request = eradius_lib:set_attributes(#radius_request{cmd = Command, msg_hmac = true}, attrs("proxy_test")), - send_request(eradius_test_handler:localhost(tuple), 11813, ?SECRET2, Request), + send_request(eradius_test_lib:localhost(tuple), 11813, ?SECRET2, Request), Request2 = eradius_lib:set_attributes(#radius_request{cmd = Command, msg_hmac = true}, attrs("user@test")), - send_request(eradius_test_handler:localhost(tuple), 11813, ?SECRET2, Request2), + send_request(eradius_test_lib:localhost(tuple), 11813, ?SECRET2, Request2), Request3 = eradius_lib:set_attributes(#radius_request{cmd = Command, msg_hmac = true}, attrs("user@domain@test")), - send_request(eradius_test_handler:localhost(tuple), 11813, ?SECRET2, Request3). + send_request(eradius_test_lib:localhost(tuple), 11813, ?SECRET2, Request3). send_request(Ip, Port, Secret, Request) -> case eradius_client:send_request({Ip, Port, Secret}, Request) of diff --git a/test/eradius_metrics_SUITE.erl b/test/eradius_metrics_SUITE.erl index d788d843..a94dff65 100644 --- a/test/eradius_metrics_SUITE.erl +++ b/test/eradius_metrics_SUITE.erl @@ -31,7 +31,7 @@ -define(ATTRS_BAD, [{?NAS_Identifier, "bad"}]). -define(ATTRS_ERROR, [{?NAS_Identifier, "error"}]). -define(ATTRS_AS_RECORD, [{#attribute{id = ?RStatus_Type}, ?RStatus_Type_Start}]). --define(LOCALHOST, eradius_test_handler:localhost(atom)). +-define(LOCALHOST, eradius_test_lib:localhost(atom)). %% test callbacks all() -> [good_requests, bad_requests, error_requests, request_with_attrs_as_record]. @@ -39,15 +39,15 @@ all() -> [good_requests, bad_requests, error_requests, request_with_attrs_as_rec init_per_suite(Config) -> application:load(eradius), EradiusConfig = [{radius_callback, ?MODULE}, - {servers, [{good, {eradius_test_handler:localhost(ip), [1812]}}, %% for 'positive' responses, e.g. access accepts - {bad, {eradius_test_handler:localhost(ip), [1813]}}, %% for 'negative' responses, e.g. coa naks - {error, {eradius_test_handler:localhost(ip), [1814]}} %% here things go wrong, e.g. duplicate requests + {servers, [{good, {eradius_test_lib:localhost(ip), [1812]}}, %% for 'positive' responses, e.g. access accepts + {bad, {eradius_test_lib:localhost(ip), [1813]}}, %% for 'negative' responses, e.g. coa naks + {error, {eradius_test_lib:localhost(ip), [1814]}} %% here things go wrong, e.g. duplicate requests ]}, {session_nodes, [node()]}, {servers_pool, - [{test_pool, [{eradius_test_handler:localhost(tuple), 1814, ?SECRET}, - {eradius_test_handler:localhost(tuple), 1813, ?SECRET}, - {eradius_test_handler:localhost(tuple), 1812, ?SECRET}]}] + [{test_pool, [{eradius_test_lib:localhost(tuple), 1814, ?SECRET}, + {eradius_test_lib:localhost(tuple), 1813, ?SECRET}, + {eradius_test_lib:localhost(tuple), 1812, ?SECRET}]}] }, {good, [ { {"good", [] }, [{"127.0.0.2", ?SECRET, [{nas_id, <<"good_nas">>}]}] } @@ -107,12 +107,12 @@ error_requests(_Config) -> check_single_request(error, request, access, access_accept). request_with_attrs_as_record(_Config) -> - ok = send_request(accreq, eradius_test_handler:localhost(tuple), 1812, ?ATTRS_AS_RECORD, [{server_name, good}, {client_name, test_records}]), + ok = send_request(accreq, eradius_test_lib:localhost(tuple), 1812, ?ATTRS_AS_RECORD, [{server_name, good}, {client_name, test_records}]), ok = check_metric(accreq, client_accounting_requests_total, [{server_name, good}, {client_name, test_records}, {acct_type, start}], 1). %% helpers check_single_request(good, EradiusRequestType, _RequestType, _ResponseType) -> - ok = send_request(EradiusRequestType, eradius_test_handler:localhost(tuple), 1812, ?ATTRS_GOOD, [{server_name, good}, {client_name, test}]), + ok = send_request(EradiusRequestType, eradius_test_lib:localhost(tuple), 1812, ?ATTRS_GOOD, [{server_name, good}, {client_name, test}]), ok = check_metric(client_access_requests_total, [{server_name, good}], 1), ok = check_metric_multi(EradiusRequestType, client_accounting_requests_total, [{server_name, good}], 1), ok = check_metric_multi({bad_type, EradiusRequestType}, client_accounting_requests_total, [{server_name, good}, {acct_type, bad_type}], 0), @@ -122,18 +122,18 @@ check_single_request(good, EradiusRequestType, _RequestType, _ResponseType) -> ok = check_metric(client_accept_responses_total, [{server_name, good}], 1), ok = check_metric(accept_responses_total, [{server_name, good}], 1), ok = check_metric(access_requests_total, [{server_name, good}], 1), - ok = check_metric(server_status, true, [eradius_test_handler:localhost(tuple), 1812]); + ok = check_metric(server_status, true, [eradius_test_lib:localhost(tuple), 1812]); check_single_request(bad, EradiusRequestType, _RequestType, _ResponseType) -> - ok = send_request(EradiusRequestType, eradius_test_handler:localhost(tuple), 1813, ?ATTRS_BAD, [{server_name, bad}, {client_name, test}]), + ok = send_request(EradiusRequestType, eradius_test_lib:localhost(tuple), 1813, ?ATTRS_BAD, [{server_name, bad}, {client_name, test}]), ok = check_metric(client_access_requests_total, [{server_name, bad}], 1), ok = check_metric(client_reject_responses_total, [{server_name, bad}], 1), ok = check_metric(access_requests_total, [{server_name, bad}], 1), ok = check_metric(reject_responses_total, [{server_name, bad}], 1), - ok = check_metric(server_status, true, [eradius_test_handler:localhost(tuple), 1813]); + ok = check_metric(server_status, true, [eradius_test_lib:localhost(tuple), 1813]); check_single_request(error, EradiusRequestType, _RequestType, _ResponseType) -> - ok = send_request(EradiusRequestType, eradius_test_handler:localhost(tuple), 1814, ?ATTRS_ERROR, + ok = send_request(EradiusRequestType, eradius_test_lib:localhost(tuple), 1814, ?ATTRS_ERROR, [{server_name, error}, {client_name, test}, {timeout, 1000}, - {failover, [{eradius_test_handler:localhost(tuple), 1812, ?SECRET}]}]), + {failover, [{eradius_test_lib:localhost(tuple), 1812, ?SECRET}]}]), ok = check_metric(client_access_requests_total, [{server_name, error}], 1), ok = check_metric(client_retransmissions_total, [{server_name, error}], 1), ok = check_metric(access_requests_total, [{server_name, error}], 1), @@ -141,10 +141,10 @@ check_single_request(error, EradiusRequestType, _RequestType, _ResponseType) -> ok = check_metric(duplicated_requests_total, [{server_name, error}], 1), ok = check_metric(client_requests_total, [{server_name, error}], 1), ok = check_metric(requests_total, [{server_name, error}], 2), - ok = check_metric(server_status, false, [eradius_test_handler:localhost(tuple), 1812]), - ok = check_metric(server_status, false, [eradius_test_handler:localhost(tuple), 1813]), - ok = check_metric(server_status, true, [eradius_test_handler:localhost(tuple), 1814]), - ok = check_metric(server_status, undefined, [eradius_test_handler:localhost(tuple), 1815]). + ok = check_metric(server_status, false, [eradius_test_lib:localhost(tuple), 1812]), + ok = check_metric(server_status, false, [eradius_test_lib:localhost(tuple), 1813]), + ok = check_metric(server_status, true, [eradius_test_lib:localhost(tuple), 1814]), + ok = check_metric(server_status, undefined, [eradius_test_lib:localhost(tuple), 1815]). check_total_requests(good, N) -> diff --git a/test/eradius_proxy_SUITE.erl b/test/eradius_proxy_SUITE.erl index b76ee2f1..85c6a2e2 100644 --- a/test/eradius_proxy_SUITE.erl +++ b/test/eradius_proxy_SUITE.erl @@ -34,10 +34,10 @@ all() -> [ ]. resolve_routes_test(_) -> - DefaultRoute = {eradius_test_handler:localhost(tuple), 1813, <<"secret">>}, - Prod = {eradius_test_handler:localhost(tuple), 1812, <<"prod">>}, - Test = {eradius_test_handler:localhost(tuple), 11813, <<"test">>}, - Dev = {eradius_test_handler:localhost(tuple), 11814, <<"dev">>}, + DefaultRoute = {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}, + Prod = {eradius_test_lib:localhost(tuple), 1812, <<"prod">>}, + Test = {eradius_test_lib:localhost(tuple), 11813, <<"test">>}, + Dev = {eradius_test_lib:localhost(tuple), 11814, <<"dev">>}, {ok, R1} = re:compile("prod"), {ok, R2} = re:compile("test"), {ok, R3} = re:compile("^dev_.*"), @@ -66,41 +66,41 @@ resolve_routes_test(_) -> ok. validate_arguments_test(_) -> - GoodConfig = [{default_route, {eradius_test_handler:localhost(tuple), 1813, <<"secret">>}}, + GoodConfig = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}}, {options, [{type, realm}, {strip, true}, {separator, "@"}]}, - {routes, [{"test_1", {eradius_test_handler:localhost(tuple), 1815, <<"secret1">>}, [{pool, test_pool}]}, + {routes, [{"test_1", {eradius_test_lib:localhost(tuple), 1815, <<"secret1">>}, [{pool, test_pool}]}, {"test_2", {<<"localhost">>, 1816, <<"secret2">>}} ]} ], - GoodOldConfig = [{default_route, {eradius_test_handler:localhost(tuple), 1813, <<"secret">>}, test_pool}, + GoodOldConfig = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}, test_pool}, {options, [{type, realm}, {strip, true}, {separator, "@"}]}, - {routes, [{"test_1", {eradius_test_handler:localhost(tuple), 1815, <<"secret1">>}, [{pool, test_pool}]}, + {routes, [{"test_1", {eradius_test_lib:localhost(tuple), 1815, <<"secret1">>}, [{pool, test_pool}]}, {"test_2", {<<"localhost">>, 1816, <<"secret2">>}} ]} ], - BadConfig = [{default_route, {eradius_test_handler:localhost(tuple), 1813, <<"secret">>}}, + BadConfig = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}}, {options, [{type, abc}]} ], - BadConfig1 = [{default_route, {eradius_test_handler:localhost(tuple), 0, <<"secret">>}}], + BadConfig1 = [{default_route, {eradius_test_lib:localhost(tuple), 0, <<"secret">>}}], BadConfig2 = [{default_route, {abc, 123, <<"secret">>}}], - BadConfig3 = [{default_route, {eradius_test_handler:localhost(tuple), 1813, <<"secret">>}}, + BadConfig3 = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}}, {options, [{type, realm}, {strip, true}, {separator, "@"}]}, {routes, [{"test_1", {wrong_ip, 1815, <<"secret1">>}}, {"test_2", {<<"localhost">>, 1816, <<"secret2">>}} ]}], - BadConfig4 = [{default_route, {eradius_test_handler:localhost(tuple), 1813, <<"secret">>}}, + BadConfig4 = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}}, {options, [{type, realm}, {strip, true}, {separator, "@"}, {timeout, "wrong"}]}, {routes, [{"test", {wrong_ip, 1815, <<"secret1">>}}, {"test_2", {<<"localhost">>, 1816, <<"secret2">>}} ]}], - BadConfig5 = [{default_route, {eradius_test_handler:localhost(tuple), 1813, <<"secret">>}}, + BadConfig5 = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}}, {options, [{type, realm}, {strip, true}, {separator, "@"}, {retries, "wrong"}]}, {routes, [{"test", {wrong_ip, 1815, <<"secret1">>}}, {"test_2", {"localhost", 1816, <<"secret2">>}} ]}], - BadConfig6 = [{default_route, {eradius_test_handler:localhost(tuple), 1813, <<"secret">>, [{pool, "wrong_pool"}]}}], - BadConfig7 = [{default_route, {eradius_test_handler:localhost(tuple), 1813, <<"secret">>}}, + BadConfig6 = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>, [{pool, "wrong_pool"}]}}], + BadConfig7 = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}}, {routes, [{"test", {wrong_ip, 1815, <<"secret1">>}, [{pool, "wrong_pool"}]}]}], {Result, ConfigData} = eradius_proxy:validate_arguments(GoodConfig), ?equal(true, Result), diff --git a/test/eradius_test_handler.erl b/test/eradius_test_handler.erl index 2d7e6e41..753f4db3 100644 --- a/test/eradius_test_handler.erl +++ b/test/eradius_test_handler.erl @@ -2,26 +2,47 @@ -behaviour(eradius_server). --export([start/0, stop/0, send_request/1, send_request_failover/1, radius_request/3]). --export([localhost/1]). +-export([start/0, start/2, stop/0, send_request/1, send_request_failover/1, radius_request/3]). -include("include/eradius_lib.hrl"). start() -> + start(inet, ipv4). + +start(Backend, Family) -> application:load(eradius), application:set_env(eradius, radius_callback, ?MODULE), - application:set_env(eradius, client_ip, localhost(tuple)), + %% application:set_env(eradius, client_ip, eradius_test_lib:localhost(tuple)), application:set_env(eradius, session_nodes, local), - application:set_env(eradius, one, [{{"ONE", []}, [{localhost(ip), "secret"}]}]), - application:set_env(eradius, two, [{{"TWO", [{default_route, {{127, 0, 0, 2}, 1813, <<"secret">>}}]}, - [{localhost(ip), "secret"}]}]), - application:set_env(eradius, servers, [{one, {localhost(ip), [1812]}}, - {two, {localhost(ip), [1813]}}]), + application:set_env(eradius, one, + [{{"ONE", []}, [{eradius_test_lib:localhost(ip), "secret"}]}]), + application:set_env(eradius, two, + [{{"TWO", [{default_route, {{127, 0, 0, 2}, 1813, <<"secret">>}}]}, + [{eradius_test_lib:localhost(ip), "secret"}]}]), + application:set_env(eradius, servers, + [{one, {eradius_test_lib:localhost(ip), [1812]}}, + {two, {eradius_test_lib:localhost(ip), [1813]}}]), application:set_env(eradius, unreachable_timeout, 2), - application:set_env(eradius, servers_pool, [{test_pool, [{localhost(tuple), 1812, "secret"}, - %% fake upstream server for fail-over - {localhost(string), 1820, "secret", [{timeout, 1000}, {retries, 2}]}]}]), + application:set_env(eradius, servers_pool, + [{test_pool, + [{eradius_test_lib:localhost(tuple), 1812, "secret"}, + %% fake upstream server for fail-over + {eradius_test_lib:localhost(string), 1820, "secret", [{timeout, 1000}, {retries, 2}]}]}]), application:ensure_all_started(eradius), + + ClientConfig = + #{inet_backend => Backend, + family => eradius_test_lib:inet_family(Family), + ip => eradius_test_lib:localhost(Family, tuple), + servers => [{one, {eradius_test_lib:localhost(Family, ip), [1812]}}, + {two, {eradius_test_lib:localhost(Family, ip), [1813]}}], + servers_pool => + [{test_pool, [{eradius_test_lib:localhost(Family, tuple), 1812, "secret"}, + %% fake upstream server for fail-over + {eradius_test_lib:localhost(Family, string), 1820, "secret", + [{timeout, 1000}, {retries, 2}]}]}] + }, + eradius_client_mngr:reconfigure(ClientConfig), eradius:modules_ready([?MODULE]). stop() -> @@ -45,22 +66,3 @@ send_request_failover(Server) -> radius_request(#radius_request{cmd = request}, _Nasprop, _Args) -> {reply, #radius_request{cmd = accept}}. - -%% travis is stupid, it includes localhost twice with -%% different IPs in /etc/hosts. This will cause a list -%% of IP to be returned from inet:gethostbyname and that -%% triggers the load balancing in eradius_client. -localhost(string) -> - case os:getenv("TRAVIS") of - false -> "localhost"; - _ -> "ip4-loopback" - end; -localhost(binary) -> - list_to_binary(localhost(string)); -localhost(tuple) -> - {ok, IP} = inet:getaddr(localhost(string), inet), - IP; -localhost(ip) -> - inet:ntoa(localhost(tuple)); -localhost(atom) -> - list_to_atom(localhost(ip)). diff --git a/test/eradius_test_lib.erl b/test/eradius_test_lib.erl new file mode 100644 index 00000000..bde63471 --- /dev/null +++ b/test/eradius_test_lib.erl @@ -0,0 +1,53 @@ +%% Copyright (c) 2010-2017, Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT +%% +-module(eradius_test_lib). + +-compile([export_all, nowarn_export_all]). + +%%%=================================================================== +%%% Helper functions +%%%=================================================================== + +has_ipv6_test_config() -> + try + {ok, IfList} = inet:getifaddrs(), + Lo = proplists:get_value("lo", IfList), + V6 = [X || {addr, X = {0,0,0,0,0,0,0,1}} <- Lo], + ct:pal("V6: ~p", [V6]), + length(V6) > 0 + catch + _:_ -> + false + end. + +inet_family(ipv4) -> inet; +inet_family(ipv6) -> inet6; +inet_family(ipv4_mapped_ipv6) -> inet6. + +badhost(Family) + when Family =:= ipv4; Family =:= ipv4_mapped_ipv6 -> + {127, 0, 0, 2}; +badhost(ipv6) -> + {0, 0, 0, 0, 0, 0, 0, 2}. + +localhost(Type) -> + localhost(ipv4, Type). + +localhost(Family, string) when Family =:= ipv4; Family =:= ipv4_mapped_ipv6 -> + "ip4-loopback"; +localhost(ipv6, string) -> + "ip6-loopback"; +localhost(Family, binary) -> + list_to_binary(localhost(Family, string)); +localhost(Family, tuple) when Family =:= ipv4; Family =:= ipv4_mapped_ipv6 -> + {ok, IP} = inet:getaddr(localhost(ipv4, string), inet), + IP; +localhost(ipv6, tuple) -> + {ok, IP} = inet:getaddr(localhost(ipv6, string), inet6), + IP; +localhost(Family, ip) -> + inet:ntoa(localhost(Family, tuple)); +localhost(Family, atom) -> + list_to_atom(localhost(Family, ip)). From 0f35d259106c244a7f1bc8b3c91efa6be076b808 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 18 Apr 2024 14:07:56 +0200 Subject: [PATCH 08/38] rewrite eradius v3 rewrite of eradius, major changes: * clients and servers are now started and configured through APIs, not app env settings any more * IPv6 support * supports multiple server and client instances * metrics are optional and callback based (allows the easy use of other metrics frameworks) * distributed handlers are no longer support, use erpc to replicate in use case specific code if needed. * removed proxy support (use freeradius or similar instead) --- .github/workflows/hex.yaml | 2 +- .github/workflows/main.yml | 16 + README.md | 334 +------- .../eradius_prometheus_collector/rebar.config | 3 - .../src/eradius_prometheus_collector.app.src | 12 - .../src/eradius_prometheus_collector.erl | 336 -------- dicts_compiler.erl | 2 +- include/eradius_lib.hrl | 31 +- rebar.config | 36 +- src/eradius.app.src | 60 +- src/eradius.erl | 70 +- src/eradius_auth.erl | 62 +- src/eradius_client.erl | 450 +++-------- src/eradius_client_mngr.erl | 516 +++++++----- src/eradius_client_socket.erl | 43 +- src/eradius_client_socket_sup.erl | 15 +- src/eradius_client_sup.erl | 41 +- src/eradius_client_top_sup.erl | 60 ++ src/eradius_config.erl | 401 ---------- src/eradius_counter.erl | 301 ------- src/eradius_counter_aggregator.erl | 162 ---- src/eradius_dict.erl | 56 +- src/eradius_eap_packet.erl | 27 +- src/eradius_lib.erl | 557 +------------ src/eradius_log.erl | 206 ++--- src/eradius_metrics_prometheus.erl | 575 ++++++++++++++ src/eradius_node_mon.erl | 186 ----- src/eradius_proxy.erl | 334 -------- src/eradius_req.erl | 732 +++++++++++++++++ src/eradius_server.erl | 746 ++++++++---------- src/eradius_server_mon.erl | 169 ---- src/eradius_server_sup.erl | 23 +- src/eradius_server_top_sup.erl | 27 - src/eradius_sup.erl | 34 +- test/eradius_client_SUITE.erl | 120 +-- test/eradius_config_SUITE.erl | 257 ------ test/eradius_lib_SUITE.erl | 79 +- test/eradius_logtest.erl | 134 ---- test/eradius_metrics_SUITE.erl | 292 ++++--- test/eradius_proxy_SUITE.erl | 166 ---- test/eradius_test_handler.erl | 101 +-- test/eradius_test_lib.erl | 36 +- 42 files changed, 2886 insertions(+), 4924 deletions(-) delete mode 100644 applications/eradius_prometheus_collector/rebar.config delete mode 100644 applications/eradius_prometheus_collector/src/eradius_prometheus_collector.app.src delete mode 100644 applications/eradius_prometheus_collector/src/eradius_prometheus_collector.erl create mode 100644 src/eradius_client_top_sup.erl delete mode 100644 src/eradius_config.erl delete mode 100644 src/eradius_counter.erl delete mode 100644 src/eradius_counter_aggregator.erl create mode 100644 src/eradius_metrics_prometheus.erl delete mode 100644 src/eradius_node_mon.erl delete mode 100644 src/eradius_proxy.erl create mode 100644 src/eradius_req.erl delete mode 100644 src/eradius_server_mon.erl delete mode 100644 src/eradius_server_top_sup.erl delete mode 100644 test/eradius_config_SUITE.erl delete mode 100644 test/eradius_logtest.erl delete mode 100644 test/eradius_proxy_SUITE.erl diff --git a/.github/workflows/hex.yaml b/.github/workflows/hex.yaml index 04363db1..36ae4285 100644 --- a/.github/workflows/hex.yaml +++ b/.github/workflows/hex.yaml @@ -25,5 +25,5 @@ jobs: env: HEX_API_KEY: ${{ secrets.HEX_API_KEY }} run: | - rebar3 edoc + rebar3 ex_doc rebar3 hex publish -r hexpm --yes diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 00b5c0d0..6ae07da8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,13 +12,29 @@ jobs: container: image: erlang:${{ matrix.otp }}-alpine steps: + - name: Prepare + run: | + apk update + apk --no-cache upgrade + apk --no-cache add zstd - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Build run: rebar3 compile - name: Run tests run: | + export ERL_AFLAGS="+pc unicode -enable-feature all" rebar3 do xref, ct + rebar3 as dialyzer do dialyzer + - name: Tar Test Output + if: ${{ always() }} + run: tar -cf - _build/test/logs/ | zstd -15 -o ct-logs-${{ matrix.otp }}.tar.zst + - name: Archive Test Output + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: test-output-${{ matrix.otp }} + path: ct-logs-${{ matrix.otp }}.tar.xz slack: needs: test diff --git a/README.md b/README.md index 330246cb..50266f35 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,19 @@ [![Build Status][gh badge]][gh] [![Erlang Versions][erlang version badge]][gh] +eradius is a library to add [RADIUS] client and server funtionality to Erlang applications. + +v3 of eradius is in many places a full rewrite of the v2 versions and is not API compatible +with older versions. + +Previous versions provided generic, standalone [RADIUS] servers and proxies. This generic +functionality is better handled by existing, feature rich, stand alone RADIUS servers. + +This versions aims a providing support for implementing a versatile, simple to use +RADIUS client and a flexible library for implementing sue case specific RADIUS server +functionality. + +v2 was based on the original Jungerl code, some piece of it might have survived. This fork of `eradius` is a radical deviation from the original Jungerl code. It contains a generic [RADIUS](https://en.wikipedia.org/wiki/RADIUS) client, support for several authentication mechanisms and dynamic configuration @@ -15,328 +28,46 @@ several authentication mechanisms and dynamic configuration * [Erlang Version Support](#erlang-version-support) * [Building eradius](#building-eradius) * [Using eradius](#using-eradius) -* [Run sample server](#run-sample-server) * [Metrics](#metrics) -* [RADIUS server configuration](#radius-server-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 4](#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) -* [Eradius counter aggregator](#eradius-counter-aggregator) * [Tables](#tables) # Erlang Version Support All minor version of the current major release and the highest minor version of the previous major release will be supported. -At the moment this means OTP `21.3`, OTP `22.x`, OTP `23.x` and OTP `24.x` are supported. OTP versions before `21.0` -do not work due the use of logger. When in doubt check the `otp_release` section in -[main.yml](.github/workflows/main.yml) for tested versions. +At the time of writing, OTP `25.3`, `26.0` `26.1` and `26.2` are supported. +When in doubt check the `otp_release` section in [main.yml](.github/workflows/main.yml) for +tested versions. -# Building eradius +> #### NOTE {: .tip} +> +> OTP-25 does not support the `-feature(maybe_expr, enable).` compiler directive, it is therefore necessary to pass the `-enable-feature all` option to the compiler. -```sh -$ rebar3 compile -``` +## Planned incompatiblities -# Using eradius +### v3.1 will require OTP-27 -Eradius requires a certain degree of configuration which is described in the -app.src file. Minimal examples of server callbacks can be found in the tests. +The current planning is to switch to documentation support introduced with OTP-27 for the v3.1 +release. That release will therefore drop support for pre OTP-27 releases. -# Run sample server +# Building eradius ```sh -$ cd sample -$ rebar3 shell --config example.config --apps eradius_server_sample +$ rebar3 compile ``` -Then run a simple benchmark: +# Using eradius -```erlang -1> eradius_server_sample:test(). -... -13:40:43.979 [info] 127.0.0.1:59254 [8]: Access-Accept -13:40:43.979 [info] 127.0.0.1:59254 [6]: Access-Accept -13:40:43.980 [info] 127.0.0.1:59254 [3]: Access-Accept -13:40:43.980 [info] 127.0.0.1:59254 [0]: Access-Accept -4333.788381 req/sec. -ok -``` +Eradius client are started and configured through their APIs. See `m:eradius_server` and +`m:eradius_client` for the APIs and settings. # Metrics -Eradius exposes following metrics via exometer: - * counter and handle time for requests - * counter for responses (this includes acks, naks, accepts etc.) - -The measurements are available for client, server and also for the specific -NAS callbacks. Further they are exposed in a 'total' fashion but also itemized -by request/response type (e.g. access request, accounting response etc.). - -It is possible to expose measurements compliant with [RFC 2619](https://tools.ietf.org/html/rfc2619) and [RFC 2621](https://tools.ietf.org/html/rfc2621) using -the build in metrics. - -The handle time metrics are generated internally using histograms. These histograms -all have a time span of 60s. The precise metrics are defined in [include/eradius_metrics.hrl](include/eradius_metrics.hrl). +A sample metrics callback module is provided that exposes metrics through prometheus.erl that +are compatible with the metrics that where included in previous versions. See more in [METRICS.md](METRICS.md). -# RADIUS server configuration - -> :warning: **Notes** :warning: -> * Square brackets ([]) denote an array that consists of n comma-separated objects. -> * Curly brackets ({}) denote a tuple that consists of a defined number of objects. - -Servers in this configuration are endpoints consisting of an IPv4 address and one or more ports. -`servers` is a list `[]` of said endpoints: -``` -servers == { servers, [<Server>] } -``` -Each server is tuple ({}): -``` -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 configured per server, in extra configuration, with a symbolic name or directly in server -``` -{SymbolicNameLimit, RateConfigList} -RateConfigList == [<RateOption>] -RateOption == { limit | max_size | max_time, integer() | undefined } -``` - -Each server is assigned a list of handlers. This list defines the NASes that are allowed to send RADIUS requests to a server and -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. -NasId == String describing the Source. -HandlerArgs == List of arguments givent the handler module. -Source == {<IP>, <Secret>} | {<IP>, <Secret>, [<SourceOption>]} -SourceOption == {group, <GroupName>} | {nas_id, <NasId> } - -IP == IPv4 source address. -Secret == Binary. Passphrase, the NAS authenticates with. -GroupName: -RADIUS requests received by a server are forwarded to lists of nodes. -The lists are assigned to handlers, so the RADIUS requests of every handler can be forwarded to different nodes, if necessary. -The lists are referenced by a GroupName. If only one group is defined, the GroupName can be omitted. -In this case, all handlers forward their requests to the same list of nodes. -Session nodes == {session_nodes, ['node@host', ...]} | {session_nodes, [{<GroupName>, ['node@host', ...]}]} -``` - -## eradius configuration example 1 - -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']}, - {radius_callback, tposs_pcrf_radius}, - {servers, [ - {root, {"127.0.0.1", [1812, 1813]}} - ]}, - {root, [ - { - {"NAS1", [handler_arg1, handler_arg2]}, - [ {"10.18.14.2", <<"secret1">>} ] - }, - { - {"NAS2", [handler_arg1, handler_arg2]}, - [ {"10.18.14.3", <<"secret2">>, [{nas_id, <<"name">>}]} ] - } - ]} -]}] -``` - -## eradius configuration example 2 - -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]}} - ]}, - {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 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}, - {sndbuf, 131072}, - {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 - {client_ports, 256}, - %% how long the binary response is kept before re-sending it - {resend_timeout, 500}, - %% List of RADIUS dictionaries - {tables, [dictionary]}, - %% List of nodes where RADIUS requests possibly will be forwarded by a RADIUS server - {session_nodes, local}, - %% A RADIUS requests handler callback module - {radius_callback, eradius_server_sample}, - %% NAS specified for `root` RADIUS server - {root, [ - {{"root", []}, [{"127.0.0.1", "secret"}]} - ]}, - %% NAS specified for `acct` RADIUS server - {acct, [ - {{eradius_proxy, "radius_acct", [{default_route, {{127, 0, 0, 2}, 1813, <<"secret">>}, [{pool, pool_name}, {timeout, 5000}, {retries, 3}]]}, - [{"127.0.0.1", "secret"}]} - ]}, - %% List of RADIUS servers - {servers, [ - {root, {"127.0.0.1", [1812]}}, - {acct, {"127.0.0.1", [1813]}} - ]}, - {counter_aggregator, false}, - %% List of histogram buckets for RADIUS servers metrics - {histogram_buckets, [10, 30, 50, 75, 100, 1000, 2000]}, - %% Simple file-based logging of RADIUS requests and metadata - {logging, true}, - %% Path to log file - {logfile, "./radius.log"}, - %% List of upstream RADIUS servers pools - {servers_pool, [ - {pool_name, [ - {{127, 0, 0, 2}, 1812, <<"secret">>, [{retries, 3}]}, - {{127, 0, 0, 3}, 1812, <<"secret">>} - ]} - ]}, - {server_status_metrics_enabled, false}, - {counter_aggregator, false}, - %% Size of RADIUS receive buffer - {recbuf, 8192}, - %% The minimum size of the send buffer to use for the RADIUS - {sndbuf, 131072} -]}]. -``` - -# Support of failover for client - -Added support for fail-over. -Set of secondary RADIUS servers could be passed to the RADIUS client API `eradius_client:send_request/3` via options or to RADIUS proxy via configuration. - -If the response wasn't received after a number of requests specified by `retries` RADIUS client options - such RADIUS servers will be marked as non-active and RADIUS requests will not be sent for such non-active RADIUS servers, while configurable timeout (`eradius.unreachable_timeout`) is not expired. - -Secondary RADIUS servers could be specified via RADIUS proxy configuration, with the new configuration option - pool name. - -## Failover configuration - -Configuration example of failover where the `pool_name` is `atom` specifies name of a pool of secondary RADIUS servers. - -```erlang -[{eradius, [ - %%% ... - {default_route, {{127, 0, 0, 1}, 1812, <<"secret">>}, [{pool, pool_name}]} - %%% ... -]}] -``` -All pools are configured via: - -```erlang -[{eradius, [ - %%% ... - {servers_pool, [ - {pool_name, [ - {{127, 0, 0, 2}, 1812, <<"secret">>, [{retries, 3}, {timeout, 3000}]}, - {{127, 0, 0, 3}, 1812, <<"secret">>} - ]} - ]} - %%% ... -]}] -``` - -## 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. - -# Eradius counter aggregator -The `eradius_counter_aggregator` would go over all nodes in an Erlang cluster and aggregate the counter values from all nodes. -Configuration value of `counter_aggregator` can be `true` or `false` where `true` - is enable, `false` - is disable counter aggregator. -By default the `counter_aggregator` is disabled and have default value `false`. -Configuration example: -```erlang -[{eradius, [ - %%% ... - {counter_aggregator, true} - %%% ... -]}] -``` - # Tables A list of RADIUS dictionaries to be loaded at startup. The atoms in this list are resolved to files in @@ -348,10 +79,11 @@ Example: [dictionary, dictionary_cisco, dictionary_travelping] ``` -<!-- Badges --> +<!-- Badges and Links--> [hexpm]: https://hex.pm/packages/eradius [hexpm version]: https://img.shields.io/hexpm/v/eradius.svg?style=flat-square [hexpm downloads]: https://img.shields.io/hexpm/dt/eradius.svg?style=flat-square [gh]: https://github.com/travelping/eradius/actions/workflows/main.yml [gh badge]: https://img.shields.io/github/workflow/status/travelping/eradius/CI?style=flat-square -[erlang version badge]: https://img.shields.io/badge/erlang-22.0%20to%2024.0.1-blue.svg?style=flat-square +[erlang version badge]: https://img.shields.io/badge/erlang-25.3%20to%2026.2-blue.svg?style=flat-square +[RADIUS]: https://en.wikipedia.org/wiki/RADIUS diff --git a/applications/eradius_prometheus_collector/rebar.config b/applications/eradius_prometheus_collector/rebar.config deleted file mode 100644 index d07757cd..00000000 --- a/applications/eradius_prometheus_collector/rebar.config +++ /dev/null @@ -1,3 +0,0 @@ -{deps, [ - {prometheus, "4.10.0"} - ]}. diff --git a/applications/eradius_prometheus_collector/src/eradius_prometheus_collector.app.src b/applications/eradius_prometheus_collector/src/eradius_prometheus_collector.app.src deleted file mode 100644 index 9c1762e5..00000000 --- a/applications/eradius_prometheus_collector/src/eradius_prometheus_collector.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application, eradius_prometheus_collector, - [{description, "An OTP library"}, - {vsn, "0.1.0"}, - {registered, []}, - {applications, - [kernel, - stdlib - ]}, - {env,[]}, - {modules, [eradius_prometheus_collector]}, - {licenses, ["MIT"]} - ]}. diff --git a/applications/eradius_prometheus_collector/src/eradius_prometheus_collector.erl b/applications/eradius_prometheus_collector/src/eradius_prometheus_collector.erl deleted file mode 100644 index fd2b1546..00000000 --- a/applications/eradius_prometheus_collector/src/eradius_prometheus_collector.erl +++ /dev/null @@ -1,336 +0,0 @@ --module(eradius_prometheus_collector). - --behaviour(prometheus_collector). - --include_lib("eradius/include/eradius_lib.hrl"). --include_lib("prometheus/include/prometheus.hrl"). - --export([deregister_cleanup/1, collect_mf/2, collect_metrics/2, fetch_counter/2, fetch_counter/3, fetch_histogram/2]). --export([augment_counters/1]). - --import(prometheus_model_helpers, [create_mf/5, gauge_metric/2, counter_metric/1]). - --define(METRIC_NAME_PREFIX, "eradius_"). - --define(METRICS, [ - {uptime_milliseconds, gauge, "RADIUS server uptime"}, - {since_last_reset_milliseconds, gauge, "RADIUS last server reset time"}, - - {requests_total, counter, "Amount of requests received by the RADIUS server"}, - {replies_total, counter, "Amount of responses"}, - - {access_requests_total, counter, "Amount of Access requests received by the RADIUS server"}, - {accounting_requests_total, counter, "Amount of Accounting requests received by RADIUS server"}, - {coa_requests_total, counter, "Amount of CoA requests received by the RADIUS server"}, - {disconnect_requests_total, counter, "Amount of Disconnect requests received by the RADIUS server"}, - {accept_responses_total, counter, "Amount of Access-Accept responses"}, - {reject_responses_total, counter, "Amount of Access-Reject responses"}, - {access_challenge_total, counter, "Amount of Access-Challenge responses"}, - {accounting_responses_total, counter, "Amount of Accounting responses"}, - {coa_acks_total, counter, "Amount of CoA ACK responses"}, - {coa_nacks_total, counter, "Amount of CoA Nack responses"}, - {disconnect_acks_total, counter, "Amount of Disconnect-Ack responses"}, - {disconnect_nacks_total, counter, "Amount of Disconnect-Nack responses"}, - {malformed_requests_total, counter, "Amount of malformed requests on RADIUS server"}, - {invalid_requests_total, counter, "Amount of invalid requests on RADIUS server"}, - {retransmissions_total, counter, "Amount of retrasmissions done by NAS"}, - {duplicated_requests_total, counter, "Amount of duplicated requests"}, - {pending_requests_total, gauge, "Amount of pending requests"}, - {packets_dropped_total, counter, "Amount of dropped packets"}, - {unknown_type_request_total, counter, "Amount of RADIUS requests with unknown type"}, - {bad_authenticator_request_total, counter, "Amount of RADIUS requests with bad authenticator"}, - - {client_requests_total, counter, "Amount of requests sent by a client"}, - {client_replies_total, counter, "Amount of replies received by a client"}, - {client_access_requests_total, counter, "Amount of Access requests sent by a client"}, - {client_accounting_requests_total, counter, "Amount of Accounting requests sent by a client"}, - {client_coa_requests_total, counter, "Amount of CoA requests sent by a client"}, - {client_disconnect_requests_total, counter, "Amount of Disconnect requests sent by client"}, - {client_retransmissions_total, counter, "Amount of retransmissions done by a cliet"}, - {client_timeouts_total, counter, "Amount of timeout errors triggered on a client"}, - {client_accept_responses_total, counter, "Amount of Accept responses received by a client"}, - {client_reject_responses_total, counter, "Amount of Reject responses received by a client"}, - {client_access_challenge_total, counter, "Amount of Access-Challenge responses"}, - {client_accounting_responses_total, counter, "Amount of Accounting responses received by a client"}, - {client_coa_nacks_total, counter, "Amount of CoA Nack received by a client"}, - {client_coa_acks_total, counter, "Amount of CoA Ack received by a client"}, - {client_disconnect_acks_total, counter, "Amount of Disconnect Acks received by a client"}, - {client_disconnect_nacks_total, counter, "Amount of Disconnect Nacks received by a client"}, - {client_packets_dropped_total, counter, "Amount of dropped packets"}, - {client_unknown_type_request_total, counter, "Amount of RADIUS requests with unknown type"}, - {client_bad_authenticator_request_total, counter, "Amount of RADIUS requests with bad authenticator"}, - {client_pending_requests_total, gauge, "Amount of pending requests on client site"} - ]). - --define(ACCT_TYPES, [start, stop, update]). - -collect_mf(_Registry, Callback) -> - {Stats, NasCntFields, ClientCntFields} = get_stats(), - [mf(Callback, Metric, {Stats, NasCntFields, ClientCntFields}) || Metric <- ?METRICS], - ok. - -mf(Callback, {Name, PromMetricType, Help}, Data) -> - Callback(create_mf(?METRIC_NAME(Name), Help, PromMetricType, ?MODULE, - {PromMetricType, fun (_) -> Name end, Data})), - ok. - -collect_metrics(_, {PromMetricType, Fun, Stats}) -> - build_metric(Fun(Stats), PromMetricType, Stats). - -fetch_histogram(Name, Labels) -> - try - lists:flatten(lists:map(fun ({LabelsFromStat, Buckets, DurationUnit}) -> - case compare_labels(Labels, LabelsFromStat) of - true -> - {Buckets1, Values} = lists:unzip(Buckets), - Values1 = augment_counters(Values), - Buckets2 = lists:zip(Buckets1, Values1), - {Buckets2, LabelsFromStat, DurationUnit}; - _ -> [] - end - end, prometheus_histogram:values(default, Name))) - catch _:_ -> [] end. - -fetch_counter(Name, Labels) -> - {Stats, NasCntFields, ClientCntFields} = get_stats(), - fetch_counter(Name, {Stats, NasCntFields, ClientCntFields}, Labels). - -fetch_counter(uptime_milliseconds, Stat, Labels) -> - {{ServerMetrics, _}, _, _} = Stat, - lists:flatten(lists:map(fun (#server_counter{} = Cnt) -> - fetch_server_value(Labels, Cnt, fun () -> erlang:system_time(milli_seconds) - Cnt#server_counter.startTime end) - end, ServerMetrics)); -fetch_counter(since_last_reset_milliseconds, Stat, Labels) -> - {{ServerMetrics, _}, _, _} = Stat, - lists:flatten(lists:map(fun (#server_counter{} = Cnt) -> - fetch_server_value(Labels, Cnt, fun () -> erlang:system_time(milli_seconds) - Cnt#server_counter.resetTime end) - end, ServerMetrics)); -fetch_counter(Name, Stat, Labels) -> - case get_metric_info(Name, Stat) of - {_, undefined, _} -> - []; - {Metrics, MetricIdx, RadiusMetricType} -> - lists:flatten(lists:map(fun (Cnt) -> - case get_labels_and_val(MetricIdx, {Cnt, RadiusMetricType}, {Name, Labels}) of - {Value, LabelsFromStat} -> - case compare_labels(Labels, LabelsFromStat) of - true -> {Value, LabelsFromStat}; - _ -> [] - end; - List -> - lists:map(fun ({Value, LabelsFromStat}) -> - case compare_labels(Labels, LabelsFromStat) of - true -> {Value, LabelsFromStat}; - _ -> [] - end - end, List) - end - end, Metrics)) - end. - -%% from prometheus.erl as prometheus_histogram:values/1 returns -%% non-cumulative values -augment_counters([Start | Counters]) -> - augment_counters(Counters, [Start], Start). - -augment_counters([], LAcc, _CAcc) -> - LAcc; -augment_counters([Counter | Counters], LAcc, CAcc) -> - augment_counters(Counters, LAcc ++ [CAcc + Counter], CAcc + Counter). - -build_metric(uptime_milliseconds, Type, Stat) -> - {{ServerMetrics, _}, _, _} = Stat, - lists:map(fun (#server_counter{} = Cnt) -> - build_server_metric_value(Type, Cnt, fun () -> erlang:system_time(milli_seconds) - Cnt#server_counter.startTime end) - end, ServerMetrics); -build_metric(since_last_reset_milliseconds, Type, Stat) -> - {{ServerMetrics, _}, _, _} = Stat, - lists:map(fun (#server_counter{} = Cnt) -> - build_server_metric_value(Type, Cnt, fun () -> erlang:system_time(milli_seconds) - Cnt#server_counter.resetTime end) - end, ServerMetrics); -build_metric(MetricName, Type, Stat) - when MetricName =:= client_accounting_requests_total; - MetricName =:= client_accounting_responses_total; - MetricName =:= accounting_requests_total; - MetricName =:= accounting_responses_total -> - lists:flatten(lists:map(fun (AcctType) -> - case get_metric_info(MetricName, Stat) of - {_, undefined, _} -> - []; - {Metrics, MetricIdx, RadiusMetricType} -> - lists:map(fun (Cnt) -> - {Value, Labels} = get_labels_and_val(MetricIdx, {Cnt, RadiusMetricType}, {MetricName, [{acct_type, AcctType}]}), - metric(Type, Value, Labels) - end, Metrics) - end - end, [start, stop, update])); -build_metric(MetricName, Type, Stat) -> - case get_metric_info(MetricName, Stat) of - {_, undefined, _} -> - []; - {Metrics, MetricIdx, RadiusMetricType} -> - lists:flatten(lists:map(fun (Cnt) -> - {Value, Labels} = get_labels_and_val(MetricIdx, {Cnt, RadiusMetricType}, {}), - metric(Type, Value, Labels) - end, Metrics)) - end. - -metric(_, [], []) -> undefined; -metric(counter, Value, Labels) -> counter_metric({Labels, Value}); -metric(gauge, Value, Labels) -> gauge_metric(Labels, Value). - -deregister_cleanup(_) -> ok. - -%% helper to make mapping between prometheus metrics names and eradius_counter fields -%% @private -map_record_field(requests_total) -> {requests, server}; -map_record_field(replies_total) -> {replies, server}; -map_record_field(access_requests_total) -> {accessRequests, server}; -map_record_field(accounting_requests_total) -> {accountRequestsStart, server}; -map_record_field(coa_requests_total) -> {coaRequests, server}; -map_record_field(disconnect_requests_total) -> {discRequests, server}; -map_record_field(accept_responses_total) -> {accessAccepts, server}; -map_record_field(access_challenge_total) -> {accessChallenges, server}; -map_record_field(reject_responses_total) -> {accessRejects, server}; -map_record_field(accounting_responses_total) -> {accountResponsesStart, server}; -map_record_field(coa_acks_total) -> {coaAcks, server}; -map_record_field(coa_nacks_total) -> {coa_nacks_total, server}; -map_record_field(disconnect_acks_total) -> {discAcks, server}; -map_record_field(disconnect_nacks_total) -> {discNaks, server}; -map_record_field(malformed_requests_total) -> {malformedRequests, server}; -map_record_field(invalid_requests_total) -> {invalidRequests, server}; -map_record_field(retransmissions_total) -> {retransmissions, server}; -map_record_field(duplicated_requests_total) -> {dupRequests, server}; -map_record_field(pending_requests_total) -> {pending, server}; -map_record_field(packets_dropped_total) -> {packetsDropped, server}; -map_record_field(unknown_type_request_total) -> {unknownTypes, server}; -map_record_field(bad_authenticator_request_total) -> {badAuthenticators, server}; -map_record_field(client_requests_total) -> {requests, client}; -map_record_field(client_replies_total) -> {replies, client}; -map_record_field(client_access_requests_total) -> {accessRequests, client}; -map_record_field(client_accept_responses_total) -> {accessAccepts, client}; -map_record_field(client_access_challenge_total) -> {accessChallenges, client}; -map_record_field(client_reject_responses_total) -> {accessRejects, client}; -map_record_field(client_accounting_requests_total) -> {accountRequestsStart, client}; -map_record_field(client_accounting_responses_total) -> {accountResponsesStart, client}; -map_record_field(client_coa_requests_total) -> {coaRequests, client}; -map_record_field(client_coa_nacks_total) -> {coaNaks, client}; -map_record_field(client_coa_acks_total) -> {coaAcks, client}; -map_record_field(client_disconnect_requests_total) -> {discRequests, client}; -map_record_field(client_disconnect_nacks_total) -> {discNaks, client}; -map_record_field(client_disconnect_acks_total) -> {discAcks, client}; -map_record_field(client_retransmissions_total) -> {retransmissions, client}; -map_record_field(client_pending_requests_total) -> {pending, client}; -map_record_field(client_timeouts_total) -> {timeouts, client}; -map_record_field(client_packets_dropped_total) -> {packetsDropped, client}; -map_record_field(client_unknown_type_request_total) -> {unknownTypes, client}; -map_record_field(client_bad_authenticator_request_total) -> {badAuthenticators, client}; -map_record_field(_) -> undefined. - -%% Helper to fetch stats from eradius_counter -%% @private -get_stats() -> - Stats = eradius_counter:read(), - NasCntFields = lists:zip(record_info(fields, nas_counter), lists:seq(1, length(record_info(fields, nas_counter)))), - ClientCntFields = lists:zip(record_info(fields, client_counter), lists:seq(1, length(record_info(fields, client_counter)))), - {Stats, NasCntFields, ClientCntFields}. - -%% Helper to get value for the given metric from the #server_counter{} -%% @private -fetch_server_value(Labels, #server_counter{} = Cnt, FnVal) -> - {ServerIP, ServerPort} = Cnt#server_counter.key, - LabelsFromStat = [{server_name, Cnt#server_counter.server_name}, - {server_ip, inet:ntoa(ServerIP)}, - {server_port, ServerPort}], - case compare_labels(Labels, LabelsFromStat) of - true -> {FnVal(), LabelsFromStat}; - _ -> [] - end. - -%% Helper to build prometheus metric for the given metric from the #server_counter{} -%% @private -build_server_metric_value(Type, #server_counter{} = Cnt, FnVal) -> - {ServerIP, ServerPort} = Cnt#server_counter.key, - metric(Type, FnVal(), [{server_name, Cnt#server_counter.server_name}, - {server_ip, inet:ntoa(ServerIP)}, - {server_port, ServerPort}]). - -%% Helper to compare Labels from a query and labels from eradius_counter stat -%% @private -compare_labels(_, []) -> false; -compare_labels(LabelsFromQuery, LabelsFromStat) -> - lists:all(fun ({K, V}) -> V == proplists:get_value(K, LabelsFromStat, undefined) end, LabelsFromQuery). - -%% Helper to fetch a metric information from eradius_counter stats by the given metric name -%% @private -get_metric_info(Name, Stat) -> - {{_, {_, Metrics}}, NasFields, ClientFields} = Stat, - {Metric, RadiusMetricType} = map_record_field(Name), - case RadiusMetricType of - client -> {Metrics, proplists:get_value(Metric, ClientFields), RadiusMetricType}; - server -> {Metrics, proplists:get_value(Metric, NasFields), RadiusMetricType} - end. - -%% Helper to get a value of a server/nas metric by the given #nas_counter{} or #client_counter{} index -%% @private -get_labels_and_val(_, {#nas_counter{} = Cnt, server}, {Name, Labels}) - when Name =:= accounting_requests_total; - Name =:= accounting_responses_total -> - Type = proplists:get_value(acct_type, Labels), - {{ServerIP, ServerPort}, NasIP, NasId} = Cnt#nas_counter.key, - case get_value(Name, Type, Cnt) of - undefined -> - lists:map(fun (AcctType) -> - ResLabels = get_labels(Cnt, ServerIP, ServerPort, NasId, NasIP), - {get_value(Name, AcctType, Cnt), [{acct_type, AcctType} | ResLabels]} - end, ?ACCT_TYPES); - Value -> - ResLabels = get_labels(Cnt, ServerIP, ServerPort, NasId, NasIP), - {Value, [{acct_type, Type} | ResLabels]} - end; -get_labels_and_val(MetricIdx, {#nas_counter{} = Cnt, server}, _) -> - {{ServerIP, ServerPort}, NasIP, NasId} = Cnt#nas_counter.key, - {element(MetricIdx + 1, Cnt), get_labels(Cnt, ServerIP, ServerPort, NasId, NasIP)}; -get_labels_and_val(_, {#client_counter{} = Cnt, client}, {Name, Labels}) - when Name =:= client_accounting_requests_total; - Name =:= client_accounting_responses_total -> - Type = proplists:get_value(acct_type, Labels), - {{ClientName, ClientIP, _ClientPort}, {_, ServerIP, ServerPort}} = Cnt#client_counter.key, - case get_value(Name, Type, Cnt) of - undefined -> - lists:map(fun (AcctType) -> - ResLabels = get_labels(Cnt, ServerIP, ServerPort, ClientName, ClientIP), - {get_value(Name, AcctType, Cnt), [{acct_type, AcctType} | ResLabels]} - end, ?ACCT_TYPES); - Value -> - ResLabels = get_labels(Cnt, ServerIP, ServerPort, ClientName, ClientIP), - {Value, [{acct_type, Type} | ResLabels]} - end; -get_labels_and_val(MetricIdx, {#client_counter{} = Cnt, client}, _) -> - {{ClientName, ClientIP, _ClientPort}, {_, ServerIP, ServerPort}} = Cnt#client_counter.key, - {element(MetricIdx + 1, Cnt), get_labels(Cnt, ServerIP, ServerPort, ClientName, ClientIP)}; -get_labels_and_val(_, _, _) -> - {[], []}. - -%% @private -get_labels(#client_counter{server_name = ServerName}, ServerIP, ServerPort, ClientName, ClientIP) -> - [{server_name, ServerName}, {server_ip, ServerIP}, - {server_port, ServerPort}, {client_name, ClientName}, {client_ip, ClientIP}]; -get_labels(#nas_counter{server_name = ServerName}, ServerIP, ServerPort, NasId, NasIP) -> - [{server_name, ServerName}, {server_ip, inet:ntoa(ServerIP)}, - {server_port, ServerPort}, {nas_id, NasId}, {nas_ip, inet:ntoa(NasIP)}]. - -%% @private -get_value(accounting_requests_total, start, #nas_counter{accountRequestsStart = Value}) -> Value; -get_value(accounting_requests_total, stop, #nas_counter{accountRequestsStop = Value}) -> Value; -get_value(accounting_requests_total, update, #nas_counter{accountRequestsUpdate = Value}) -> Value; -get_value(accounting_responses_total, start, #nas_counter{accountResponsesStart = Value}) -> Value; -get_value(accounting_responses_total, stop, #nas_counter{accountResponsesStop = Value}) -> Value; -get_value(accounting_responses_total, update, #nas_counter{accountResponsesUpdate = Value}) -> Value; -get_value(client_accounting_requests_total, start, #client_counter{accountRequestsStart = Value}) -> Value; -get_value(client_accounting_requests_total, stop, #client_counter{accountRequestsStop = Value}) -> Value; -get_value(client_accounting_requests_total, update, #client_counter{accountRequestsUpdate = Value}) -> Value; -get_value(client_accounting_responses_total, start, #client_counter{accountResponsesStart = Value}) -> Value; -get_value(client_accounting_responses_total, stop, #client_counter{accountResponsesStop = Value}) -> Value; -get_value(client_accounting_responses_total, update, #client_counter{accountResponsesUpdate = Value}) -> Value; -get_value(_, _, _) -> undefined. diff --git a/dicts_compiler.erl b/dicts_compiler.erl index 2aad5743..6e4eef27 100755 --- a/dicts_compiler.erl +++ b/dicts_compiler.erl @@ -24,7 +24,7 @@ compile() -> %% sort dictionaries in alphabetical order to be sure that %% basic `priv/dictionaries/dictionary` builds first %% because it contains some attributes which may be needed - % for vendor's dictionaries + %% for vendor's dictionaries Dictionaries = lists:sort(Dictionaries0), Targets = [{Dictionary, out_files(Dictionary)} || Dictionary <- Dictionaries], compile_each(Targets) diff --git a/include/eradius_lib.hrl b/include/eradius_lib.hrl index e5236be4..e44fc53c 100644 --- a/include/eradius_lib.hrl +++ b/include/eradius_lib.hrl @@ -1,6 +1,6 @@ -define(BYTE, integer-unit:8). % Nice syntactic sugar... --type server() :: {inet:ip_address(), eradius_server:port_number()}. +-type server() :: {inet:ip_address(), inet:port_number()}. -type handler() :: {module(), term()}. -type server_name() :: atom(). -type atom_name() :: atom(). @@ -125,32 +125,3 @@ invalidRequests = 0 :: non_neg_integer(), discardNoHandler = 0 :: non_neg_integer() }). - --record(nas_prop, { - server_ip :: inet:ip_address(), - server_port :: eradius_server:port_number(), - nas_id :: term(), - nas_ip :: inet:ip_address(), - nas_port :: eradius_server:port_number(), - metrics_info :: {atom_address(), atom_address()}, - secret :: eradius_lib:secret(), - handler_nodes = local :: list(atom()) | local - }). - --record(radius_request, { - reqid = 0 :: byte(), - cmd = request :: eradius_lib:command(), - attrs = [] :: eradius_lib:attribute_list(), - secret :: eradius_lib:secret(), - authenticator :: eradius_lib:authenticator(), - msg_hmac = false :: boolean(), - eap_msg = <<>> :: binary() - }). - - --record(decoder_state, { - request_authenticator :: 'undefined' | binary(), - attrs = [] :: eradius_lib:attribute_list(), - hmac_pos :: 'undefined' | non_neg_integer(), - eap_msg = [] :: [binary()] - }). diff --git a/rebar.config b/rebar.config index be584e49..9a819778 100644 --- a/rebar.config +++ b/rebar.config @@ -1,15 +1,16 @@ %%-*-Erlang-*- {erl_opts, [debug_info]}. -{minimum_otp_vsn, "22"}. +{minimum_otp_vsn, "25.3"}. {pre_hooks, [{compile, "escript dicts_compiler.erl compile"}, {clean, "escript dicts_compiler.erl clean"}]}. {profiles, [ + {dialyzer, [{deps, [{prometheus, "4.11.0"}]}]}, {test, [ {erl_opts, [nowarn_export_all]}, - {project_app_dirs, ["applications/*", "src/*", "."]}, - {deps, [{meck, "0.9.0"}]} + {deps, [{meck, "0.9.2"}, + {prometheus, "4.11.0"}]} ]} ]}. @@ -18,12 +19,37 @@ %% xref checks to run {xref_checks, [undefined_function_calls, undefined_functions, locals_not_used, deprecated_function_calls, - deprecated_functions]}. + deprecated_functions, exports_not_used]}. {xref_ignores, [{prometheus_histogram, declare, 1}, {prometheus_histogram, observe, 3}, + {prometheus_counter, declare, 1}, + {prometheus_counter, inc, 3}, + {prometheus_gauge, declare, 1}, + {prometheus_gauge, inc, 3}, {prometheus_boolean, declare, 1}, {prometheus_boolean, set, 3}]}. %% == Plugins == -{plugins, [rebar3_hex]}. +{plugins, [rebar3_hex, rebar3_ex_doc]}. + +%% == Dialyzer == + +{dialyzer, + [%%{warnings, [unmatched_returns, underspecs]}, + {plt_extra_apps, [prometheus]} + ]}. + +%% == ExDoc == + +{ex_doc, [ + {extras, ["README.md", "METRICS.md", "LICENSE"]}, + {main, "README.md"}, + {source_url, "https://github.com/travelping/eradius"} +]}. + +%% == Hex == + +{hex, [ + {doc, #{provider => ex_doc}} +]}. diff --git a/src/eradius.app.src b/src/eradius.app.src index 3af15b11..deffd7dd 100644 --- a/src/eradius.app.src +++ b/src/eradius.app.src @@ -1,31 +1,31 @@ %% vim: ft=erlang -{application, eradius, [ - {description, "Erlang RADIUS server"}, - {vsn, semver}, - {registered, [eradius_dict, eradius_sup, eradius_server_top_sup, eradius_server_sup, eradius_server_mon]}, - {applications, [kernel, stdlib, crypto]}, - {mod, {eradius, []}}, - {env, [ - {servers, []}, - {tables, [dictionary]}, - {client_ip, undefined}, - {client_ports, 100}, - {resend_timeout, 30000}, - {logging, false}, - {counter_aggregator, false}, - {server_status_metrics_enabled, false}, - {logfile, "./radius.log"}, - {recbuf, 8192}, - {sndbuf, 131072} - ]}, - {maintainers, ["Andreas Schultz", "Vladimir Tarasenko", "Yury Gargay"]}, - {licenses, ["MIT"]}, - {links, [{"Github", "https://github.com/travelping/eradius"}]}, - %% List copied from rebar3_hex.hrl ?DEFAULT_FILES, adding "Makefile" - {files, ["applications", "src", "c_src", "include/eradius_*.hrl", "rebar.config.script" - ,"priv/dictionaries", "rebar.config", "rebar.lock" - ,"README*", "readme*" - ,"LICENSE*", "license*" - ,"NOTICE" - ,"dicts_compiler.erl"]} - ]}. +{application, eradius, + [{description, "Erlang RADIUS server"}, + {vsn, semver}, + {registered, [eradius_dict, eradius_sup, eradius_server_sup]}, + {applications, [kernel, stdlib, crypto]}, + {mod, {eradius, []}}, + {env, [ + {servers, []}, + {tables, [dictionary]}, + {client_ip, undefined}, + {client_ports, 100}, + {resend_timeout, 30000}, + {logging, false}, + {counter_aggregator, false}, + {server_status_metrics_enabled, false}, + {logfile, "./radius.log"}, + {recbuf, 8192}, + {sndbuf, 131072} + ]}, + {maintainers, ["Andreas Schultz", "Vladimir Tarasenko", "Yury Gargay"]}, + {licenses, ["MIT"]}, + {links, [{"Github", "https://github.com/travelping/eradius"}]}, + %% List copied from rebar3_hex.hrl ?DEFAULT_FILES, adding "Makefile" + {files, ["applications", "src", "c_src", "include/eradius_*.hrl", "rebar.config.script" + ,"priv/dictionaries", "rebar.config", "rebar.lock" + ,"README*", "readme*" + ,"LICENSE*", "license*" + ,"NOTICE" + ,"dicts_compiler.erl"]} + ]}. diff --git a/src/eradius.erl b/src/eradius.erl index 7bc497e0..13c6a4e0 100644 --- a/src/eradius.erl +++ b/src/eradius.erl @@ -1,51 +1,49 @@ %% @doc Main module of the eradius application. -module(eradius). --export([load_tables/1, load_tables/2, - modules_ready/1, modules_ready/2, - statistics/1]). -behaviour(application). --export([start/2, stop/1, config_change/3]). + +%% API +-export([load_tables/1, load_tables/2, + start_server/3, start_server/4]). +-ignore_xref([load_tables/1, load_tables/2, + start_server/3, start_server/4]). + +%% application callbacks +-export([start/2, stop/1]). %% internal use -include("eradius_lib.hrl"). +%%%========================================================================= +%%% API +%%%========================================================================= + %% @doc Load RADIUS dictionaries from the default directory. -spec load_tables(list(eradius_dict:table_name())) -> ok | {error, {consult, eradius_dict:table_name()}}. load_tables(Tables) -> eradius_dict:load_tables(Tables). %% @doc Load RADIUS dictionaries from a certain directory. --spec load_tables(file:filename(), list(eradius_dict:table_name())) -> ok | {error, {consult, eradius_dict:table_name()}}. +-spec load_tables(Dir :: file:filename(), Tables :: [Table :: eradius_dict:table_name()]) -> + ok | {error, {consult, Table :: eradius_dict:table_name()}}. load_tables(Dir, Tables) -> eradius_dict:load_tables(Dir, Tables). -%% @equiv modules_ready(self(), Modules) -modules_ready(Modules) -> - eradius_node_mon:modules_ready(self(), Modules). - -%% @doc Announce request handler module availability. -%% Applications need to call this function (usually from their application master) -%% in order to make their modules (which should implement the {@link eradius_server} behaviour) -%% available for processing. The modules will be revoked when the given Pid goes down. -modules_ready(Pid, Modules) -> - eradius_node_mon:modules_ready(Pid, Modules). +start_server(IP, Port, #{handler := {_, _}, clients := #{}} = Opts) + when (IP =:= any orelse is_tuple(IP)) andalso + is_integer(Port) andalso Port >= 0 andalso Port < 65536 -> + eradius_server:start_instance(IP, Port, Opts). -%% @doc manipulate server statistics -%% * reset: reset all counters to zero -%% * pull: read counters and reset to zero -%% * read: read counters -statistics(reset) -> - eradius_counter_aggregator:reset(); -statistics(pull) -> - eradius_counter_aggregator:pull(); -statistics(read) -> - eradius_counter_aggregator:read(). +start_server(ServerName, IP, Port, #{handler := {_, _}, clients := #{}} = Opts) + when (IP =:= any orelse is_tuple(IP)) andalso + is_integer(Port) andalso Port >= 0 andalso Port < 65536 -> + eradius_server:start_instance(ServerName, IP, Port, Opts). - -%% ---------------------------------------------------------------------------------------------------- -%% -- application callbacks +%%%=================================================================== +%%% application callbacks +%%%=================================================================== %% @private start(_StartType, _StartArgs) -> @@ -54,19 +52,3 @@ start(_StartType, _StartArgs) -> %% @private stop(_State) -> ok. - -%% @private -config_change(Added, Changed, Removed) -> - lists:foreach(fun do_config_change/1, Added), - lists:foreach(fun do_config_change/1, Changed), - Keys = [K || {K, _} <- Added ++ Changed] ++ Removed, - (lists:member(logging, Keys) or lists:member(logfile, Keys)) - andalso eradius_log:reconfigure(), - eradius_client_mngr:reconfigure(). - -do_config_change({tables, NewTables}) -> - eradius_dict:load_tables(NewTables); -do_config_change({servers, _}) -> - eradius_server_mon:reconfigure(); -do_config_change({_, _}) -> - ok. diff --git a/src/eradius_auth.erl b/src/eradius_auth.erl index bc3f7e63..2ec7eb7b 100644 --- a/src/eradius_auth.erl +++ b/src/eradius_auth.erl @@ -1,16 +1,33 @@ +%% Copyright (c) 2002-2007, Martin Björklund and Torbjörn Törnkvist +%% Copyright (c) 2011, Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT +%% + %% @doc user authentication helper functions -module(eradius_auth). + -export([check_password/2]). -export([pap/2, chap/3, ms_chap/3, ms_chap_v2/4]). -export([des_key_from_hash/1, nt_password_hash/1, challenge_response/2, ascii_to_unicode/1]). +-ignore_xref([check_password/2]). +-ignore_xref([pap/2, chap/3, ms_chap/3, ms_chap_v2/4]). +-ignore_xref([des_key_from_hash/1, nt_password_hash/1, challenge_response/2, + ascii_to_unicode/1]). + -ifdef(TEST). -export([nt_hash/1, v2_generate_nt_response/4, mppe_get_master_key/2, mppe_generate_session_keys/3, mppe_get_asymetric_send_start_key/2, v2_generate_authenticator_response/5]). +-ignore_xref([nt_hash/1, v2_generate_nt_response/4, mppe_get_master_key/2, + mppe_generate_session_keys/3, mppe_get_asymetric_send_start_key/2, + v2_generate_authenticator_response/5]). -endif. +-dialyzer({nowarn_function, [mppe_generate_session_keys/3]}). + -include("eradius_lib.hrl"). -include("eradius_dict.hrl"). -include("dictionary.hrl"). @@ -21,9 +38,10 @@ %% @doc check the request password using all available authentication mechanisms. %% Tries CHAP, then MS-CHAP, then MS-CHAPv2, finally PAP. --spec check_password(binary(), #radius_request{}) -> - false | {boolean(), eradius_lib:attribute_list()}. -check_password(Password, Req = #radius_request{authenticator = Authenticator, attrs = AVPs}) -> +-spec check_password(binary(), eradius_req:req()) -> + false | {boolean(), eradius_req:attribute_list()}. +check_password(Password, #{authenticator := Authenticator, is_valid := true} = Req) -> + {AVPs, _} = eradius_req:attrs(Req), case lookup_auth_attrs(AVPs) of {false, Chap_pass, false, false, false, false} -> {chap(Password, Chap_pass, Authenticator), []}; @@ -32,12 +50,12 @@ check_password(Password, Req = #radius_request{authenticator = Authenticator, at {false, _, _, Challenge, Response, false} when Challenge =/= false, Response =/= false -> ms_chap(Password, Challenge, Response); {false, _, _, Challenge, false, Response} when Challenge =/= false, Response =/= false -> - Username = eradius_lib:get_attr(Req, ?User_Name), + Username = eradius_req:attr(?User_Name, Req), ms_chap_v2(Username, Password, Challenge, Response); {ReqPassword, _, _, _, _, _} when ReqPassword =/= false -> {pap(Password, ReqPassword), []}; {_, _, _, _, _, _} -> - {false, []} + false end. %% composite lookup function, retrieve User_Password, CHAP_Password and CHAP_Challenge at once @@ -59,7 +77,8 @@ lookup_auth_attrs(Attrs, [{#attribute{id = ?MS_CHAP2_Response}, Val}|T]) -> lookup_auth_attrs(Attrs, [{{_,_} = Id, Val}|T]) -> %% fallback for undecoded AVPs - lookup_auth_attrs(Attrs, [{#attribute{id = Id}, Val}|T]); + %% init name field to make dialyzer happy + lookup_auth_attrs(Attrs, [{#attribute{id = Id, name = ""}, Val}|T]); lookup_auth_attrs(Attrs, [_|T]) -> lookup_auth_attrs(Attrs, T); lookup_auth_attrs(Attrs, []) -> @@ -181,29 +200,34 @@ lm_password_hash(Password) -> << (des_hash(Key1))/binary, (des_hash(Key2))/binary >>. %% @doc MS-CHAP authentication --spec ms_chap(binary(), binary(), binary()) -> false | {true, eradius_lib:attribute_list()}. -ms_chap(Passwd, Challenge, <<_Ident:1/binary, Flags:1/integer-unit:8, LMResponse:24/binary, NTResponse:24/binary>>) -> +-spec ms_chap(binary(), binary(), binary()) -> {boolean(), eradius_req:attribute_list()}. +ms_chap(Passwd, Challenge, + <<_Ident:1/binary, Flags:1/integer-unit:8, LMResponse:24/binary, NTResponse:24/binary>>) -> LmPasswdHash = lm_password_hash(Passwd), NtPasswdHash = nt_password_hash(Passwd), - Resp1 = if - Flags == 1; NTResponse =/= << 0:24/unit:8 >> -> + Resp1 = case Flags of + 1 when NTResponse =/= << 0:24/unit:8 >> -> NTResponse =:= challenge_response(Challenge, NtPasswdHash); - true -> + _ -> false end, - Resp2 = if - Resp1 == false -> + Resp2 = case Resp1 of + false -> LMResponse =:= challenge_response(Challenge, LmPasswdHash); - Resp1 -> + _ -> Resp1 end, - - Resp2 andalso {true, ms_chap_attrs(LmPasswdHash, NtPasswdHash)}. + case Resp2 of + false -> {false, []}; + true -> {true, ms_chap_attrs(LmPasswdHash, NtPasswdHash)} + end. %% @doc MS-CHAP-V2 authentication --spec ms_chap_v2(binary(), binary(), binary(), binary()) -> false | {true, eradius_lib:attribute_list()}. -ms_chap_v2(UserName, Passwd, AuthenticatorChallenge, <<Ident:1/binary, _Flags:1/binary, PeerChallenge:16/binary, _Reserved:8/binary, Response/binary>>) -> +-spec ms_chap_v2(binary(), binary(), binary(), binary()) -> + {boolean(), eradius_req:attribute_list()}. +ms_chap_v2(UserName, Passwd, AuthenticatorChallenge, + <<Ident:1/binary, _Flags:1/binary, PeerChallenge:16/binary, _Reserved:8/binary, Response/binary>>) -> PasswdHash = nt_password_hash(Passwd), ExpectedResponse = v2_generate_nt_response(AuthenticatorChallenge, PeerChallenge, UserName, PasswdHash), @@ -211,7 +235,7 @@ ms_chap_v2(UserName, Passwd, AuthenticatorChallenge, <<Ident:1/binary, _Flags:1/ ExpectedResponse == Response -> {true, ms_chap_v2_attrs(UserName, PasswdHash, AuthenticatorChallenge, Ident, PeerChallenge, Response)}; true -> - false + {false, []} end. %% @doc calculate MS-CHAP response attributes diff --git a/src/eradius_client.erl b/src/eradius_client.erl index 77a941b7..5eb9cc30 100644 --- a/src/eradius_client.erl +++ b/src/eradius_client.erl @@ -4,9 +4,8 @@ %% SPDX-License-Identifier: MIT %% %% @doc This module contains a RADIUS client that can be used to send authentication and accounting requests. -%% A counter is kept for every NAS in order to determine the next request id and sender port -%% for each outgoing request. The implementation naively assumes that you won't send requests to a -%% distinct number of NASs over the lifetime of the VM, which is why the counters are not garbage-collected. +%% A counter is kept for every client instance in order to determine the next request id and sender port +%% for each outgoing request. %% %% The client uses OS-assigned ports. The maximum number of open ports can be specified through the %% ``client_ports'' application environment variable, it defaults to ``20''. The number of ports should not @@ -14,18 +13,16 @@ %% %% The IP address used to send requests is read <emph>once</emph> (at startup) from the ``client_ip'' %% parameter. Changing it currently requires a restart. It can be given as a string or ip address tuple, -%% or the atom ``undefined'' (the default), which uses whatever address the OS selects. +%% or the atom ``any'' (the default), which uses whatever address the OS selects. -module(eradius_client). %% API --export([send_request/2, send_request/3, - send_remote_request/3, send_remote_request/4]). - -%% internal API --export([send_remote_request_loop/8]). +-export([send_request/3, send_request/4]). +-ignore_xref([send_request/3, send_request/4]). -import(eradius_lib, [printable_peer/2]). + -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("kernel/include/logger.hrl"). -include_lib("kernel/include/inet.hrl"). @@ -33,376 +30,113 @@ -include("eradius_lib.hrl"). -include("eradius_internal.hrl"). --define(GOOD_CMD(Req), (Req#radius_request.cmd == 'request' orelse - Req#radius_request.cmd == 'accreq' orelse - Req#radius_request.cmd == 'coareq' orelse - Req#radius_request.cmd == 'discreq')). +-define(GOOD_CMD(Cmd), (Cmd == 'request' orelse + Cmd == 'accreq' orelse + Cmd == 'coareq' orelse + 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 options() :: #{retries => pos_integer(), + timeout => timeout(), + failover => [eradius_client_mngr:server_name()] + }. +%% Options for a RADIUS request to override configure +%% server defaults and to add failover alternatives --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'}. -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()) -> - {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) -> - 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, Options} | UpstreamServers]) of - [] -> - no_active_servers; - {{IP, Port, Secret, Options}, _NewPool} -> - SendReqFn(); - {{NewPeerAddr, NewPeerPort, NewPeerSecret, PeerOpts}, NewPool} -> - %% current server is not in list of active servers, so use another one - send_request({NewPeerAddr, NewPeerPort, NewPeerSecret}, Request, [{failover, NewPool} | PeerOpts]) - end - end; -send_request({_IP, _Port, _Secret}, _Request, _Options) -> - error(badarg). - -%% @equiv send_remote_request(Node, NAS, Request, []) --spec send_remote_request(node(), nas_address(), #radius_request{}) -> {ok, binary()} | {error, 'timeout' | 'node_down' | 'socket_down'}. -send_remote_request(Node, NAS, Request) -> - send_remote_request(Node, NAS, Request, []). +%% @equiv send_request(ServerRef, ServerName, Req, []) +-spec send_request(gen_server:server_ref(), + eradius_client_mngr:server_name() | [eradius_client_mngr:server_name()], + eradius_req:req()) -> + {{ok, eradius_req:req()} | {error, 'timeout' | 'socket_down'}, eradius_req:req()}. +send_request(ServerRef, ServerName, #{cmd := _, payload := _} = Req) -> + send_request(ServerRef, ServerName, Req, #{}). -%% @doc Send a radius request to the given NAS through a socket on the specified node. +%% @doc Send a radius request to the given server or server pool. %% If no answer is received within the specified timeout, the request will be sent again. -%% The request will not be sent again if the remote node is unreachable. --spec send_remote_request(node(), nas_address(), #radius_request{}, options()) -> {ok, binary()} | {error, 'timeout' | 'node_down' | 'socket_down'}. -send_remote_request(Node, {IP, Port, Secret}, Request, Options) when ?GOOD_CMD(Request) -> - TS1 = erlang:monotonic_time(), - ServerName = proplists:get_value(server_name, Options, undefined), - MetricsInfo = make_metrics_info(Options, {IP, Port}), - update_client_requests(MetricsInfo), - Peer = {ServerName, {IP, Port}}, - try eradius_client_mngr:wanna_send(Node, Peer) of - {Socket, ReqId} -> - Request1 = case eradius_node_mon:get_remote_version(Node) of - {0, Minor} when Minor < 6 -> - {_, EncRequest} = eradius_lib:encode_request(Request#radius_request{reqid = ReqId, secret = Secret}), - EncRequest; - _ -> - Request#radius_request{reqid = ReqId, secret = Secret} - end, - Retries = proplists:get_value(retries, Options, ?DEFAULT_RETRIES), - Timeout = proplists:get_value(timeout, Options, ?DEFAULT_TIMEOUT), - SenderPid = spawn(Node, ?MODULE, send_remote_request_loop, - [self(), Socket, ReqId, Peer, Request1, Retries, Timeout, MetricsInfo]), - SenderMonitor = monitor(process, SenderPid), - Response = receive - {SenderPid, Result} -> - erlang:demonitor(SenderMonitor, [flush]), - Result; - {'DOWN', SenderMonitor, process, SenderPid, _Reason} -> - {error, socket_down} - end, - proceed_response(Request, Response, Peer, TS1, MetricsInfo, Options) - catch - exit:{{nodedown, Node}, _} -> - {error, node_down} - end; -send_remote_request(_Node, {_IP, _Port, _Secret}, _Request, _Options) -> - error(badarg). - -proceed_response(Request, {ok, Response, Secret, Authenticator}, _Peer = {_ServerName, {ServerIP, Port}}, TS1, MetricsInfo, Options) -> - 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), - - 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; - "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; - "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; - _ -> - 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) - end; - Decoded -> - update_server_status_metric(ServerIP, Port, true, Options), - update_client_response(Decoded#radius_request.cmd, MetricsInfo, Request), - {ok, Response, Authenticator} - end; - -proceed_response(Request, Response, {_ServerName, {ServerIP, Port}}, TS1, MetricsInfo, Options) -> - 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), +-spec send_request(gen_server:server_ref(), + eradius_client_mngr:server_name() | [eradius_client_mngr:server_pool()], + eradius_req:req(), options()) -> + {{ok, eradius_req:req()} | {error, 'timeout' | 'socket_down'}, eradius_req:req()}. +send_request(ServerRef, ServerName, #{cmd := Cmd} = Req, Opts) + when ?GOOD_CMD(Cmd), is_map(Opts), is_list(ServerName) -> + do_send_request(ServerRef, ServerName, [], Req, Opts); +send_request(ServerRef, ServerName, Req, Opts) when not is_list(ServerName) -> + send_request(ServerRef, [ServerName], Req, Opts). + +do_send_request(_ServerRef, [], _Tried, _Req, _Opts) -> + {error, no_active_servers}; +do_send_request(ServerRef, Peers, Tried, Req0, Opts0) -> + case eradius_client_mngr:wanna_send(ServerRef, Peers, Tried) of + {ok, {Socket, ReqId, ServerName, Server, ReqInfo}} -> + #{secret := Secret} = Server, + + ServerOpts0 = maps:with([retries, timeout], Server), + ServerOpts = maps:merge(?DEFAULT_REQUEST_OPTS, ServerOpts0), + Opts = maps:merge(ServerOpts, Opts0), + + Req1 = maps:merge(Req0, ReqInfo), + Req2 = eradius_req:record_metric(request, #{}, Req1), + + {Response, Req} = + send_request_loop( + Socket, ReqId, Req2#{req_id => ReqId, secret => Secret}, Opts), + proceed_response(ServerRef, [ServerName | Tried], Req, Response, ServerName, Opts); - maybe_failover(Request, Response, Options). - -maybe_failover(Request, Response, Options) -> - UpstreamServers = proplists:get_value(failover, Options, []), - case eradius_client_mngr:find_suitable_peer(UpstreamServers) of - [] -> - Response; - {{NewPeerAddr, NewPeerPort, NewPeerSecret, PeerOpts}, NewPool} -> - send_request({NewPeerAddr, NewPeerPort, NewPeerSecret}, Request, [{failover, NewPool} | PeerOpts]) + {error, _} = Error -> + maybe_failover(ServerRef, Tried, Req0, Error, Opts0) end. -%% @private -%% send_remote_request_loop/8 -send_remote_request_loop(ReplyPid, Socket, ReqId, Peer, EncRequest, Retries, Timeout, MetricsInfo) -> - ReplyPid ! {self(), send_request_loop(Socket, ReqId, Peer, EncRequest, Retries, Timeout, MetricsInfo)}. - -%% send_remote_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) -> - {Authenticator, EncRequest} = eradius_lib:encode_request(Request), - send_request_loop(Socket, Peer, ReqId, Authenticator, EncRequest, - Timeout, Retries, MetricsInfo, Request#radius_request.secret, Request). - -%% send_remote_request_loop/10 -send_request_loop(_Socket, _Peer, _ReqId, _Authenticator, _EncRequest, - Timeout, 0, MetricsInfo, _Secret, 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) -> +proceed_response(_ServerRef, _Tried, Req, {ok, Resp0}, _ServerName, _Opts) -> + Resp = eradius_req:record_metric(reply, #{request => Req}, Resp0), + {{ok, Resp}, Req}; + +proceed_response(ServerRef, Tried, Req0, {error, Error} = Response, ServerName, Opts) -> + Req = eradius_req:record_metric(discard, #{reason => Error, request => Req0}, Req0), + eradius_client_mngr:request_failed(ServerRef, ServerName), + maybe_failover(ServerRef, Tried, Req, Response, Opts). + +maybe_failover(ServerRef, Tried, Req, _Response, #{failover := [_|_] = FailOver} = Opts) -> + do_send_request(ServerRef, FailOver, Tried, Req, Opts#{failover := []}); +maybe_failover(_, _, Req, Response, _) -> + {Response, Req}. + +%% send_request_loop/4 +send_request_loop(Socket, ReqId, Req0, Opts) -> + {Packet, Req} = eradius_req:packet(Req0), + send_request_loop(Socket, ReqId, Packet, Opts, Req). + +%% send_request_loop/8 +send_request_loop(_Socket, _ReqId, _Packet, #{retries := 0}, Req) -> + {{error, timeout}, Req}; +send_request_loop(Socket, ReqId, Packet, + #{timeout := Timeout, retries := RetryN} = Opts, + #{server_addr := PeerAddress} = Req) -> Result = try - update_client_request(pending, MetricsInfo, 1, Request), - eradius_client_socket:send_request(Socket, {IP, Port}, ReqId, EncRequest, Timeout) + %% update_client_request(pending, 1), + eradius_client_socket:send_request(Socket, PeerAddress, ReqId, Packet, Timeout) after - update_client_request(pending, MetricsInfo, -1, Request) + %% update_client_request(pending, -1) + ok end, case Result of - {ok, Response} -> - {ok, Response, Secret, Authenticator}; + {ok, Header, Body} -> + {{ok, eradius_req:response(Header, Body, Req)}, Req}; {error, close} -> - {error, socket_down}; + {{error, socket_down}, Req}; {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); + ReqN = eradius_req:record_metric(retransmission, #{}, Req), + send_request_loop(Socket, ReqId, Packet, + Opts#{retries := RetryN - 1}, ReqN); {error, _} = Error -> - Error - end. - -%% @private -update_client_requests(MetricsInfo) -> - eradius_counter:inc_counter(requests, MetricsInfo). - -%% @private -update_client_request(pending, MetricsInfo, Pending, _) -> - if Pending =< 0 -> eradius_counter:dec_counter(pending, MetricsInfo); - true -> eradius_counter:inc_counter(pending, MetricsInfo) - end; -update_client_request(Cmd, MetricsInfo, Ms, Request) -> - eradius_counter:observe(eradius_client_request_duration_milliseconds, MetricsInfo, Ms, "Execution time of a RADIUS request"), - update_client_request_by_type(Cmd, MetricsInfo, Ms, Request). - -%% @private -update_client_request_by_type(request, MetricsInfo, Ms, _) -> - eradius_counter:observe(eradius_client_access_request_duration_milliseconds, MetricsInfo, Ms, "Access-Request execution time"), - eradius_counter:inc_counter(accessRequests, MetricsInfo); -update_client_request_by_type(accreq, MetricsInfo, Ms, Request) -> - eradius_counter:observe(eradius_client_accounting_request_duration_milliseconds, MetricsInfo, Ms, "Accounting-Request execution time"), - inc_request_counter_accounting(MetricsInfo, Request); -update_client_request_by_type(coareq, MetricsInfo, Ms, _) -> - eradius_counter:observe(eradius_client_coa_request_duration_milliseconds, MetricsInfo, Ms, "Coa request execution time"), - eradius_counter:inc_counter(coaRequests, MetricsInfo); -update_client_request_by_type(discreq, MetricsInfo, Ms, _) -> - eradius_counter:observe(eradius_client_disconnect_request_duration_milliseconds, MetricsInfo, Ms, "Disconnect execution time"), - eradius_counter:inc_counter(discRequests, MetricsInfo); -update_client_request_by_type(retransmission, MetricsInfo, _Ms, _) -> - eradius_counter:inc_counter(retransmissions, MetricsInfo); -update_client_request_by_type(timeout, MetricsInfo, _Ms, _) -> - eradius_counter:inc_counter(timeouts, MetricsInfo); -update_client_request_by_type(_, _, _, _) -> ok. - -%% @private -update_client_responses(MetricsInfo) -> eradius_counter:inc_counter(replies, MetricsInfo). - -%% @private -update_client_response(accept, MetricsInfo, _) -> eradius_counter:inc_counter(accessAccepts, MetricsInfo); -update_client_response(reject, MetricsInfo, _) -> eradius_counter:inc_counter(accessRejects, MetricsInfo); -update_client_response(challenge, MetricsInfo, _) -> eradius_counter:inc_counter(accessChallenges, MetricsInfo); -update_client_response(accresp, MetricsInfo, Request) -> inc_responses_counter_accounting(MetricsInfo, Request); -update_client_response(coanak, MetricsInfo, _) -> eradius_counter:inc_counter(coaNaks, MetricsInfo); -update_client_response(coaack, MetricsInfo, _) -> eradius_counter:inc_counter(coaAcks, MetricsInfo); -update_client_response(discnak, MetricsInfo, _) -> eradius_counter:inc_counter(discNaks, MetricsInfo); -update_client_response(discack, MetricsInfo, _) -> eradius_counter:inc_counter(discAcks, MetricsInfo); -update_client_response(dropped, MetricsInfo, _) -> eradius_counter:inc_counter(packetsDropped, MetricsInfo); -update_client_response(bad_authenticator, MetricsInfo, _) -> eradius_counter:inc_counter(badAuthenticators, MetricsInfo); -update_client_response(unknown_req_type, MetricsInfo, _) -> eradius_counter:inc_counter(unknownTypes, MetricsInfo); -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}}), - ServerAddrInfo = eradius_lib:make_addr_info({ServerName, {ServerIP, ServerPort}}), - {ClientAddrInfo, ServerAddrInfo}. - -inc_request_counter_accounting(MetricsInfo, #radius_request{attrs = Attrs}) -> - Requests = ets:match_spec_run(Attrs, client_request_counter_account_match_spec_compile()), - [eradius_counter:inc_counter(Type, MetricsInfo) || Type <- Requests], - ok; -inc_request_counter_accounting(_, _) -> - ok. - -inc_responses_counter_accounting(MetricsInfo, #radius_request{attrs = Attrs}) -> - Responses = ets:match_spec_run(Attrs, client_response_counter_account_match_spec_compile()), - [eradius_counter:inc_counter(Type, MetricsInfo) || Type <- Responses], - ok; -inc_responses_counter_accounting(_, _) -> - ok. - -update_server_status_metric(IP, Port, false, _Options) -> - eradius_counter:set_boolean_metric(server_status, [IP, Port], false); -update_server_status_metric(IP, Port, true, Options) -> - UpstreamServers = proplists:get_value(failover, Options, []), - %% set all servesr from pool as inactive - if is_list(UpstreamServers) -> - lists:foreach( - fun (Server) -> - case Server of - {ServerIP, ServerPort, _} -> - eradius_counter:set_boolean_metric(server_status, [ServerIP, ServerPort], false); - {ServerIP, ServerPort, _, _} -> - eradius_counter:set_boolean_metric(server_status, [ServerIP, ServerPort], false); - _ -> - ok - end - - end, UpstreamServers); - true -> - ok - end, - %% set current service as active - eradius_counter:set_boolean_metric(server_status, [IP, Port], true). - -client_request_counter_account_match_spec_compile() -> - case persistent_term:get({?MODULE, ?FUNCTION_NAME}, undefined) of - undefined -> - MatchSpecCompile = - ets:match_spec_compile( - ets:fun2ms( - fun ({?RStatus_Type, ?RStatus_Type_Start}) -> accountRequestsStart; - ({?RStatus_Type, ?RStatus_Type_Stop}) -> accountRequestsStop; - ({?RStatus_Type, ?RStatus_Type_Update}) -> accountRequestsUpdate; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Start}) -> accountRequestsStart; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Stop}) -> accountRequestsStop; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Update}) -> accountRequestsUpdate end)), - persistent_term:put({?MODULE, ?FUNCTION_NAME}, MatchSpecCompile), - MatchSpecCompile; - MatchSpecCompile -> - MatchSpecCompile - end. - -client_response_counter_account_match_spec_compile() -> - case persistent_term:get({?MODULE, ?FUNCTION_NAME}, undefined) of - undefined -> - MatchSpecCompile = - ets:match_spec_compile( - ets:fun2ms( - fun ({?RStatus_Type, ?RStatus_Type_Start}) -> accountResponsesStart; - ({?RStatus_Type, ?RStatus_Type_Stop}) -> accountResponsesStop; - ({?RStatus_Type, ?RStatus_Type_Update}) -> accountResponsesUpdate; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Start}) -> accountResponsesStart; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Stop}) -> accountResponsesStop; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Update}) -> accountResponsesUpdate end)), - persistent_term:put({?MODULE, ?FUNCTION_NAME}, MatchSpecCompile), - MatchSpecCompile; - MatchSpecCompile -> - MatchSpecCompile - end. - -get_ip(Host) -> - case inet:gethostbyname(Host) of - {ok, #hostent{h_addrtype = inet, h_addr_list = [IP]}} -> - IP; - {ok, #hostent{h_addrtype = inet, h_addr_list = [_ | _] = IPs}} -> - Index = rand:uniform(length(IPs)), - lists:nth(Index, IPs); - _ -> error(badarg) + {Error, Req} end. diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index 14a66d92..b1490baa 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -3,149 +3,198 @@ %% %% SPDX-License-Identifier: MIT %% +%% @doc This module contains the management logic for the RADIUS client instances. +%% A counter is kept for every client instance in order to determine the next request id and sender port +%% for each outgoing request. +%% +%% The client uses OS-assigned ports. The maximum number of open ports can be specified through the +%% ``client_ports'' option, it defaults to ``20''. The number of ports should not +%% be set too low. If ``N'' ports are opened, the maximum number of concurrent requests is ``N * 256''. +%% +%% The IP address used to send requests is configured through the ``ip'' option. +%% Changing it currently requires a restart. It can be given as a string or ip address tuple, +%% or the atom ``any'' (the default), which uses whatever address the OS selects. -module(eradius_client_mngr). +-feature(maybe_expr, enable). -behaviour(gen_server). %% external API --export([start_link/0, wanna_send/1, wanna_send/2, reconfigure/0, reconfigure/1]). +-export([start_client/1, start_client/2]). %% internal API --export([store_radius_server_from_pool/3, - request_failed/3, - restore_upstream_server/1, - find_suitable_peer/1]). +-export([start_link/2, start_link/3]). +-export([wanna_send/3, reconfigure/2]). +-export([request_failed/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -ifdef(TEST). --export([get_state/0, servers/0, servers/1, init_server_status_metrics/0]). +-export([get_state/1, servers/1, server/2, get_socket_count/1]). +-ignore_xref([get_state/1, servers/1, server/2, get_socket_count/1]). -endif. +-ignore_xref([start_client/1, start_client/2]). +-ignore_xref([start_link/2, start_link/3]). +-ignore_xref([reconfigure/2]). + -include_lib("kernel/include/logger.hrl"). -include_lib("kernel/include/inet.hrl"). -include("eradius_internal.hrl"). +-type server_name() :: atom() | binary(). +%% Name of RADIUS server (or client). + +-type server_opts() :: #{ip := inet:ip_address(), + port := inet:port_number(), + secret := binary, + retries => non_neg_integer(), + timeout => non_neg_integer()}. +%% Options to describe a RADIUS server. + +-type server() :: #{ip := inet:ip_address(), + port := inet:port_number(), + secret := binary, + retries := non_neg_integer(), + timeout := non_neg_integer(), + failed := non_neg_integer()}. +%% Options to describe a RADIUS server. +%% Conceptually the same as `t:server_opts/0', except that may fields are mandatory. + +-type server_pool() :: [server_name()]. +%% List of server names that form a pool. + +-type servers() :: #{server_name() := server() | server_pool()}. +%% Map of server and pool definition. Key is the name of the entry. + -type client_opts() :: - #{family => inet | inet6, + #{name => server_name(), + servers := #{server_name() := server_opts() | server_pool()}, + family => inet | inet6, ip => any | inet:ip_address(), active_n => once | non_neg_integer(), no_ports => non_neg_integer(), recbuf => non_neg_integer(), sndbuf => non_neg_integer(), - server_pool => [term()], - servers => [term()]}. + metrics_callback => eradius_req:metrics_callback() + }. +%% Options to configure the RADIUS client. + -type client_config() :: - #{family := inet | inet6, + #{name := server_name(), + servers := servers(), + family := inet | inet6, ip := any | inet:ip_address(), active_n := once | non_neg_integer(), no_ports := non_neg_integer(), recbuf := non_neg_integer(), sndbuf := non_neg_integer(), - servers_pool => [term()], - servers => [term()]}. + metrics_callback := 'undefined' | eradius_req:metrics_callback() + }. +%% Options to configure the RADIUS client. +%% Conceptually the same as `t:client_opts/0', except that may fields are mandatory. --export_type([client_config/0]). +-export_type([server_name/0, server_pool/0, servers/0, client_opts/0]). -record(state, { + owner :: pid(), config :: client_config(), + client_name :: server_name(), + client_addr :: any | inet:ip_address(), + servers :: servers(), socket_id :: {Family :: inet | inet6, IP :: any | inet:ip_address()}, no_ports = 1 :: pos_integer(), idcounters = maps:new() :: map(), sockets = array:new() :: array:array(), - clients = [] :: [{{integer(),integer(),integer(),integer()}, integer()}] + metrics_callback :: undefined | eradius_req:metrics_callback() }). --define(SERVER, ?MODULE). - -define(RECONFIGURE_TIMEOUT, 15000). +-define(DEFAULT_MAX_RETRIES, 20). +-define(DEFAULT_DOWN_TIME, 1000). %%%========================================================================= %%% API %%%========================================================================= -start_link() -> - case client_config(default_client_opts()) of - {ok, Config} -> gen_server:start_link({local, ?SERVER}, ?MODULE, [Config], []); - {error, _} = Error -> Error +%% @doc Start a new RADIUS client that is managed by the eradius applications supervisor tree. +-spec start_client(client_opts()) -> + {ok, pid()} | {error, supervisor:startchild_err()}. +start_client(Opts) -> + eradius_client_top_sup:start_client([Opts]). + +%% @doc Start a new, named RADIUS client that is managed by the eradius applications supervisor tree. +-spec start_client(server_name(), client_opts()) -> + {ok, pid()} | {error, supervisor:startchild_err()}. +start_client(ServerName, Opts) -> + maybe + ok ?= check_already_started(ServerName), + eradius_client_top_sup:start_client([ServerName, Opts]) end. -wanna_send(Peer) -> - gen_server:call(?SERVER, {wanna_send, Peer}). +%% @private +-spec start_link(pid(), client_opts()) -> + {ok, pid()} | {error, supervisor:startchild_err()}. +start_link(Owner, Opts) -> + maybe + {ok, Config} ?= client_config(maps:merge(default_client_opts(), Opts)), + gen_server:start_link(?MODULE, [Owner, Config], []) + end. -wanna_send(Node, Peer) -> - gen_server:call({?SERVER, Node}, {wanna_send, Peer}). +%% @private +-spec start_link(pid(), server_name(), client_opts()) -> + {ok, pid()} | {error, supervisor:startchild_err()}. +start_link(Owner, ServerName, Opts) -> + maybe + ok ?= check_already_started(ServerName), + {ok, Config} ?= client_config(maps:merge(default_client_opts(), Opts)), + gen_server:start_link(ServerName, ?MODULE, [Owner, Config], []) + end. %% @private -reconfigure() -> - %% backward compatibility wrapper - (catch reconfigure(#{})). +wanna_send(Server, Peer, Tried) -> + gen_server:call(Server, {wanna_send, Peer, Tried}). -%% @doc reconfigure the Radius client -reconfigure(Opts) -> - gen_server:call(?SERVER, {reconfigure, Opts}, ?RECONFIGURE_TIMEOUT). - -request_failed(ServerIP, Port, Options) -> - case ets:lookup(?MODULE, {ServerIP, Port}) of - [{{ServerIP, Port}, Retries, InitialRetries}] -> - FailedTries = proplists:get_value(retries, Options, ?DEFAULT_RETRIES), - %% Mark the given RADIUS server as 'non-active' if there were more tries - %% than possible - if FailedTries >= Retries -> - ets:delete(?MODULE, {ServerIP, Port}), - Timeout = application:get_env(eradius, unreachable_timeout, 60), - timer:apply_after(Timeout * 1000, ?MODULE, restore_upstream_server, - [{ServerIP, Port, InitialRetries, InitialRetries}]); - true -> - %% RADIUS client tried to send a request to the {ServierIP, Port} RADIUS - %% server. There were done FailedTries tries and all of them failed. - %% So decrease amount of tries for the given RADIUS server that - %% that will be used for next RADIUS requests towards this RADIUS server. - ets:update_counter(?MODULE, {ServerIP, Port}, -FailedTries) - end; - [] -> - ok - end. +%% @private +request_failed(Server, Peer) -> + gen_server:call(Server, {failed, Peer}). -restore_upstream_server({ServerIP, Port, Retries, InitialRetries}) -> - ets:insert(?MODULE, {{ServerIP, Port}, Retries, InitialRetries}). - -find_suitable_peer(undefined) -> - []; -find_suitable_peer([]) -> - []; -find_suitable_peer([{Host, Port, Secret, Opts} | Pool]) when is_list(Host) -> - try - IP = get_ip(Host), - find_suitable_peer([{IP, Port, Secret, Opts} | Pool]) - catch _:_ -> - %% can't resolve ip by some reasons, just ignore it - find_suitable_peer(Pool) - end; -find_suitable_peer([{IP, Port, Secret, Opts} | Pool]) -> - case ets:lookup(?MODULE, {IP, Port}) of - [] -> - find_suitable_peer(Pool); - [{{IP, Port}, _Retries, _InitialRetries}] -> - {{IP, Port, Secret, Opts}, Pool} - end; -find_suitable_peer([{IP, Port, Secret} | Pool]) -> - find_suitable_peer([{IP, Port, Secret, []} | Pool]). +%% @doc reconfigure the Radius client +reconfigure(ServerRef, Opts) -> + gen_server:call(ServerRef, {reconfigure, Opts}, ?RECONFIGURE_TIMEOUT). -ifdef(TEST). -get_state() -> - State = sys:get_state(?SERVER), +get_state(ServerRef) -> + State = sys:get_state(ServerRef), Keys = record_info(fields, state), Values = tl(tuple_to_list(State)), maps:from_list(lists:zip(Keys, Values)). -servers() -> - ets:tab2list(?MODULE). - -servers(Key) -> - ets:lookup(?MODULE, Key). +get_socket_count(ServerRef) -> + #state{owner = Owner} = sys:get_state(ServerRef), + {ok, SockSup} = eradius_client_sup:socket_supervisor(Owner), + Counts = supervisor:count_children(SockSup), + proplists:get_value(active, Counts). + +servers(ServerRef) -> + #state{servers = Servers} = sys:get_state(ServerRef), + maps:fold( + fun(_, #{ip := IP, port := Port, retries := Retries, failed := Failed} = _, M) + when Failed < Retries -> + [{{IP, Port}, Retries, Failed} | M]; + (_, _, M) -> M + end, [], Servers). + +server(ServerRef, Key) -> + #state{servers = Servers} = sys:get_state(ServerRef), + case Servers of + #{Key := #{ip := IP, port := Port, retries := Retries, failed := Failed}} -> + {{IP, Port}, Retries, Failed}; + _ -> + undefined + end. -endif. @@ -153,38 +202,70 @@ servers(Key) -> %%% gen_server callbacks %%%=================================================================== -init([#{no_ports := NPorts} = Config]) -> - ets:new(?MODULE, [public, named_table, ordered_set, {keypos, 1}, {write_concurrency, true}]), - prepare_pools(Config), - +%% @private +init([Owner, #{name := ClientName, servers := Servers, + ip := IP, no_ports := NPorts, + metrics_callback := MetricsCallback} = Config]) -> + process_flag(trap_exit, true), + ?LOG(info, "Starting RADIUS client"), State = #state{ + client_name = ClientName, + client_addr = IP, + owner = Owner, config = Config, + servers = Servers, socket_id = socket_id(Config), - no_ports = NPorts}, + no_ports = NPorts, + metrics_callback = MetricsCallback + }, {ok, State}. %% @private -handle_call({wanna_send, Peer = {_PeerName, PeerSocket}}, _From, - #state{config = Config, - no_ports = NoPorts, idcounters = IdCounters, - sockets = Sockets, clients = Clients} = State0) -> - {PortIdx, ReqId, NewIdCounters} = next_port_and_req_id(PeerSocket, NoPorts, IdCounters), - {SocketProcess, NewSockets} = find_socket_process(PortIdx, Sockets, Config), - State1 = State0#state{idcounters = NewIdCounters, sockets = NewSockets}, - State = - case lists:member(Peer, Clients) of - false -> State1#state{clients = [Peer | Clients]}; - true -> State1 +handle_call({wanna_send, Candidates, Tried}, _From, + #state{ + client_name = ClientName, + client_addr = ClientAddr, + servers = Servers, + no_ports = NoPorts, idcounters = IdCounters, + sockets = Sockets, + metrics_callback = MetricsCallback} = State0) -> + case select_server(Candidates, Tried, Servers) of + {ok, {ServerName, #{ip := IP, port := Port} = Server}} -> + ServerAddr = {IP, Port}, + {PortIdx, ReqId, NewIdCounters} = + next_port_and_req_id(ServerAddr, NoPorts, IdCounters), + {SocketProcess, NewSockets} = find_socket_process(PortIdx, Sockets, State0), + State = State0#state{idcounters = NewIdCounters, sockets = NewSockets}, + ReqInfo = + #{server => ServerName, server_addr => ServerAddr, + client => ClientName, client_addr => ClientAddr, + metrics_callback => MetricsCallback}, + Reply = {ok, {SocketProcess, ReqId, ServerName, Server, ReqInfo}}, + {reply, Reply, State}; + {error, _} = Error -> + {reply, Error, State0} + end; + +handle_call({failed, Peer}, _From, #state{servers = Servers0} = State0) -> + Servers = + case Servers0 of + #{Peer := #{retries := Retries, failed := Failed} = Server} + when Failed < Retries -> + Servers0#{Peer := Server#{failed := Failed + 1}}; + #{Peer := #{retries := Retries, failed := Failed} = Server} + when Failed =:= Retries -> + erlang:start_timer(?DEFAULT_DOWN_TIME, self(), {reset, Peer}), + Servers0#{Peer := Server#{failed := Failed + 1}}; + _ -> + Servers0 end, - {reply, {SocketProcess, ReqId}, State}; + State = State0#state{servers = Servers}, + {reply, ok, State}; %% @private handle_call({reconfigure, Opts}, _From, #state{config = OConfig} = State0) -> case client_config(maps:merge(OConfig, Opts)) of {ok, Config} -> - ets:delete_all_objects(?MODULE), - prepare_pools(Config), - State = reconfigure_address(Config, State0#state{config = Config}), {reply, ok, State}; @@ -199,11 +280,25 @@ handle_call(_OtherCall, _From, State) -> %% @private handle_cast(_Msg, State) -> {noreply, State}. +%% @private +handle_info({timeout, _, {reset, Peer}}, #state{servers = Servers0} = State0) -> + Servers = + case Servers0 of + #{Peer := Server} -> + Servers0#{Peer := Server#{failed := 0}}; + _ -> + Servers0 + end, + State = State0#state{servers = Servers}, + {noreply, State}; + handle_info(_Info, State) -> - {noreply, State}. + {noreply, State}. %% @private -terminate(_Reason, _State) -> ok. +terminate(Reason, _State) -> + ?LOG(info, "RADIUS client stopped with ~p", [Reason]), + ok. %% @private code_change(_OldVsn, State, _Extra) -> {ok, State}. @@ -212,6 +307,20 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %%% internal functions %%%========================================================================= +check_already_started(Name) -> + case where(Name) of + Pid when is_pid(Pid) -> + {error, {already_started, Pid}}; + undefined -> + ok + end. + +where({global, Name}) -> global:whereis_name(Name); +where({via, Module, Name}) -> Module:whereis_name(Name); +where({local, Name}) -> whereis(Name); +where(ServerName) -> + error(badarg, [ServerName]). + socket_id(#{family := Family, ip := IP}) -> {Family, IP}. @@ -220,97 +329,117 @@ socket_id_str({_, IP}) when is_tuple(IP) -> socket_id_str({_, IP}) when is_atom(IP) -> atom_to_list(IP). -get_ip(Host) -> - case inet:gethostbyname(Host) of - {ok, #hostent{h_addrtype = inet, h_addr_list = [IP]}} -> - IP; - {ok, #hostent{h_addrtype = inet, h_addr_list = [_ | _] = IPs}} -> - Index = rand:uniform(length(IPs)), - lists:nth(Index, IPs); - _ -> error(badarg) - end. - %% @private --spec default_client_opts() -> client_opts(). default_client_opts() -> - #{ip => application:get_env(eradius, client_ip, any), - no_ports => application:get_env(eradius, client_ports, 10), - active_n => application:get_env(eradius, active_n, 100), - recbuf => application:get_env(eradius, recbuf, 8192), - sndbuf => application:get_env(eradius, sndbuf, 131072), - servers_pool => application:get_env(eradius, servers_pool, []), - servers => application:get_env(eradius, servers, []) + #{family => inet6, + ip => any, + no_ports => 10, + active_n => 100, + recbuf => 8192, + sndbuf => 131072, + metrics_callback => undefined }. +socket_ip(inet, {_, _, _, _} = IP) -> + IP; +socket_ip(inet6, {_, _, _, _} = IP) -> + inet:ipv4_mapped_ipv6_address(IP); +socket_ip(inet6, {_, _, _, _,_, _, _, _} = IP) -> + IP. + +select_server(Candidates, Tried, Servers) -> + case select_servers(Candidates, Servers, []) -- Tried of + [] -> + {error, no_active_servers}; + PL -> + N = rand:uniform(length(PL)), + ServerName = lists:nth(N, PL), + {ok, {ServerName, maps:get(ServerName, Servers)}} + end. + +select_servers([], _Servers, Selected) -> + Selected; +select_servers([Candidate|More], Servers, Selected) -> + case Servers of + #{Candidate := [_|_] = Pool} -> + select_servers(More, Servers, select_servers(Pool, Servers, Selected)); + #{Candidate := #{retries := Retries, failed := Failed}} + when Failed < Retries -> + select_servers(More, Servers, [Candidate | Selected]); + _ -> + select_servers(More, Servers, Selected) + end. -spec client_config(client_opts()) -> {ok, client_config()} | {error, _}. -client_config(#{ip := IP} = Opts) when is_atom(IP) -> - {ok, Opts#{family => inet6, ip := any}}; -client_config(#{ip := {_, _, _, _}} = Opts) -> - {ok, Opts#{family => inet}}; -client_config(#{ip := {_, _, _, _, _, _, _, _}} = Opts) -> - {ok, Opts#{family => inet6}}; -client_config(#{ip := Address} = Opts) when is_list(Address) -> +client_config_ip(#{ip := IP} = Opts) when is_atom(IP) -> + {ok, Opts#{ip := any}}; +client_config_ip(#{family := Family, ip := IP} = Opts) when is_tuple(IP) -> + {ok, Opts#{ip := socket_ip(Family, IP)}}; +client_config_ip(#{ip := Address} = Opts) when is_list(Address) -> case inet_parse:address(Address) of - {ok, {_, _, _, _} = IP} -> - {ok, Opts#{family => inet, ip => IP}}; - {ok, {_, _, _, _, _, _, _, _} = IP} -> - {ok, Opts#{family => inet6, ip => IP}}; + {ok, IP} -> + client_config_ip(Opts#{ip => IP}); _ -> ?LOG(error, "Invalid RADIUS client IP (parsing failed): ~p", [Address]), {error, {bad_client_ip, Address}} end. -%% private -prepare_pools(#{servers_pool := PoolList, servers := ServerList}) -> - lists:foreach(fun({_PoolName, Servers}) -> prepare_pool(Servers) end, PoolList), - lists:foreach(fun(Server) -> store_upstream_servers(Server) end, ServerList), - init_server_status_metrics(). - -prepare_pool([]) -> ok; -prepare_pool([{Addr, Port, _, Opts} | Servers]) -> - Retries = proplists:get_value(retries, Opts, ?DEFAULT_RETRIES), - store_radius_server_from_pool(Addr, Port, Retries), - prepare_pool(Servers); -prepare_pool([{Addr, Port, _} | Servers]) -> - store_radius_server_from_pool(Addr, Port, ?DEFAULT_RETRIES), - prepare_pool(Servers). - -store_upstream_servers({Server, _}) -> - store_upstream_servers(Server); -store_upstream_servers({Server, _, _}) -> - store_upstream_servers(Server); -store_upstream_servers(Server) -> - %% TBD: move proxy config into the proxy logic... - - HandlerDefinitions = application:get_env(eradius, Server, []), - UpdatePoolFn = fun (HandlerOpts) -> - {DefaultRoute, Routes, Retries} = eradius_proxy:get_routes_info(HandlerOpts), - eradius_proxy:put_default_route_to_pool(DefaultRoute, Retries), - eradius_proxy:put_routes_to_pool(Routes, Retries) - end, - lists:foreach(fun (HandlerDefinition) -> - case HandlerDefinition of - {{_, []}, _} -> ok; - {{_, _, []}, _} -> ok; - {{_, HandlerOpts}, _} -> UpdatePoolFn(HandlerOpts); - {{_, _, HandlerOpts}, _} -> UpdatePoolFn(HandlerOpts); - _HandlerDefinition -> ok - end - end, - HandlerDefinitions). - -%% private -store_radius_server_from_pool(Addr, Port, Retries) - when is_tuple(Addr), is_integer(Port), is_integer(Retries) -> - ets:insert(?MODULE, {{Addr, Port}, Retries, Retries}); -store_radius_server_from_pool(Addr, Port, Retries) - when is_list(Addr), is_integer(Port), is_integer(Retries) -> - IP = get_ip(Addr), - ets:insert(?MODULE, {{IP, Port}, Retries, Retries}); -store_radius_server_from_pool(Addr, Port, Retries) -> - ?LOG(error, "bad RADIUS upstream server specified in RADIUS servers pool configuration ~p", [{Addr, Port, Retries}]), - error(badarg). +client_config_servers(none, _, Servers) -> + {ok, Servers}; +client_config_servers({ServerName, #{ip := IP, port := _, secret := _} = SIn, Next}, + #{family := Family} = Opts, Servers) -> + Server = SIn#{ip := socket_ip(Family, IP), + retries => maps:get(retries, SIn, ?DEFAULT_MAX_RETRIES), + failed => 0}, + client_config_servers(maps:next(Next), Opts, Servers#{ServerName => Server}); +client_config_servers({ServerPoolName, [_|_] = Pool, Next}, + #{servers := CfgServers} = Opts, Servers) -> + HasAll = lists:all(fun(SrvId) -> is_map_key(SrvId, CfgServers) end, Pool), + case HasAll of + true -> client_config_servers(maps:next(Next), Opts, Servers#{ServerPoolName => Pool}); + false -> {error, {server_definition_missing, Pool}} + end; +client_config_servers({ServerName, _, _}, _, _) -> + {error, {mandatory_opts_missing, ServerName}}. + +client_config_servers(#{servers := Servers} = Opts) -> + maybe + {ok, NewServers} ?= + client_config_servers(maps:next(maps:iterator(Servers)), Opts, #{}), + {ok, Opts#{servers := NewServers}} + end. + +client_config_name(#{name := _} = Opts) -> + {ok, Opts}; +client_config_name(#{netdev := NetDev} = Opts) -> + client_config_name([$%, NetDev], Opts); +client_config_name(#{netns := NetNS} = Opts) -> + client_config_name([$@, NetNS], Opts); +client_config_name(Opts) -> + client_config_name([], Opts). + +client_config_name(Tag, #{family := inet6, ip := IP, ipv6_v6only := true} = Opts) + when IP =:= any; IP =:= {0, 0, 0, 0, 0, 0, 0, 0} -> + client_config_name("*", Tag, Opts); +client_config_name(Tag, #{family := inet6, ip := any} = Opts) -> + client_config_name("[::]", Tag, Opts); +client_config_name(Tag, #{family := inet, ip := any} = Opts) -> + client_config_name("[0.0.0.0]", Tag, Opts); +client_config_name(Tag, #{family := inet6, ip := IP} = Opts) -> + client_config_name([$[, inet:ntoa(IP), $]], Tag, Opts); +client_config_name(Tag, #{family := inet, ip := IP} = Opts) -> + client_config_name(inet:ntoa(IP), Tag, Opts). + +client_config_name(IP, Tag, Opts) -> + {ok, Opts#{name => iolist_to_binary([IP, Tag])}}. + +client_config(Opts0) -> + maybe + {ok, Opts1} ?= client_config_ip(Opts0), + {ok, Opts2} ?= client_config_servers(Opts1), + {ok, Opts} ?= client_config_name(Opts2), + {ok, Opts#{metrics_callback => maps:get(metrics_callback, Opts0, undefined)}} + end. reconfigure_address(#{no_ports := NPorts} = Config, #state{socket_id = OAdd, sockets = Sockts} = State) -> @@ -375,25 +504,12 @@ next_port_and_req_id(Peer, NumberOfPorts, Counters) -> NewCounters = Counters#{Peer => {NextPortIdx, NextReqId}}, {NextPortIdx, NextReqId, NewCounters}. -find_socket_process(PortIdx, Sockets, Config) -> +find_socket_process(PortIdx, Sockets, #state{owner = Owner, config = Config}) -> case array:get(PortIdx, Sockets) of undefined -> - {ok, Socket} = eradius_client_socket:new(Config), + {ok, Supervisor} = eradius_client_sup:socket_supervisor(Owner), + {ok, Socket} = eradius_client_socket:new(Supervisor, Config), {Socket, array:set(PortIdx, Socket, Sockets)}; Socket -> {Socket, Sockets} end. - -%% @private -init_server_status_metrics() -> - case application:get_env(eradius, server_status_metrics_enabled, false) of - false -> - ok; - true -> - %% That will be called at eradius startup and we must be sure that prometheus - %% application already started if server status metrics supposed to be used - application:ensure_all_started(prometheus), - ets:foldl(fun ({{Addr, Port}, _, _}, _Acc) -> - eradius_counter:set_boolean_metric(server_status, [Addr, Port], false) - end, [], ?MODULE) - end. diff --git a/src/eradius_client_socket.erl b/src/eradius_client_socket.erl index 764f0f98..1515744f 100644 --- a/src/eradius_client_socket.erl +++ b/src/eradius_client_socket.erl @@ -3,24 +3,27 @@ %% %% SPDX-License-Identifier: MIT %% +%% @private -module(eradius_client_socket). -behaviour(gen_server). %% API --export([new/1, start_link/1, send_request/5, close/1]). +-export([new/2, start_link/1, send_request/5, close/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-ignore_xref([start_link/1]). + -record(state, {family, socket, active_n, pending, mode, counter}). %%%========================================================================= %%% API %%%========================================================================= -new(Config) -> - eradius_client_socket_sup:new(Config). +new(Supervisor, Config) -> + eradius_client_socket_sup:new(Supervisor, Config). start_link(Config) -> gen_server:start_link(?MODULE, [Config], []). @@ -88,21 +91,17 @@ handle_info({udp_passive, _Socket}, #state{socket = Socket, active_n = ActiveN} handle_info({udp, Socket, FromIP, FromPort, Response}, State = #state{socket = Socket, mode = Mode}) -> - case eradius_lib:decode_request_id(Response) of - {ReqId, Response} -> - NState = request_done({FromIP, FromPort, ReqId}, {ok, Response}, State), - case Mode of - inactive when map_size(State#state.pending) =:= 0 -> - {stop, normal, NState}; - _ -> - flow_control(NState), - {noreply, NState} - end; - {bad_pdu, _} -> - %% discard reply because it was malformed - flow_control(State), - {noreply, State} - end; + flow_control(State), + NState = + case Response of + <<Header:20/binary, Body/binary>> -> + <<_, ReqId:8, _/binary>> = Header, + request_done({FromIP, FromPort, ReqId}, {ok, Header, Body}, State); + _ -> + %% discard reply because it was malformed + State + end, + noreply_or_stop(NState); handle_info({timeout, TRef, ReqKey}, #state{pending = Pending} = State) -> NState = @@ -113,7 +112,7 @@ handle_info({timeout, TRef, ReqKey}, #state{pending = Pending} = State) -> _ -> State end, - {noreply, NState}; + noreply_or_stop(NState); handle_info(_Info, State) -> {noreply, State}. @@ -133,6 +132,12 @@ flow_control(#state{socket = Socket, active_n = once}) -> flow_control(_) -> ok. +noreply_or_stop(#state{pending = Pending, mode = inactive} = State) + when map_size(Pending) =:= 0 -> + {stop, normal, State}; +noreply_or_stop(State) -> + {noreply, State}. + pending_request(ReqKey, From, Timeout, #state{pending = Pending} = State) -> TRef = erlang:start_timer(Timeout, self(), ReqKey), diff --git a/src/eradius_client_socket_sup.erl b/src/eradius_client_socket_sup.erl index 954156be..db2d5957 100644 --- a/src/eradius_client_socket_sup.erl +++ b/src/eradius_client_socket_sup.erl @@ -1,13 +1,20 @@ +%% Copyright (c) 2024 Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT +%% +%% @private -module(eradius_client_socket_sup). -behaviour(supervisor). %% API --export([start_link/0, new/1]). +-export([start_link/0, new/2]). %% Supervisor callbacks -export([init/1]). +-ignore_xref([start_link/0]). + -define(SERVER, ?MODULE). %%%=================================================================== @@ -20,10 +27,10 @@ {error, term()} | ignore. start_link() -> - supervisor:start_link({local, ?SERVER}, ?MODULE, []). + supervisor:start_link(?MODULE, []). -new(Config) -> - supervisor:start_child(?SERVER, [Config]). +new(Supervisor, Config) -> + supervisor:start_child(Supervisor, [Config]). %%%=================================================================== %%% Supervisor callbacks diff --git a/src/eradius_client_sup.erl b/src/eradius_client_sup.erl index 6db9d435..e8d63ea0 100644 --- a/src/eradius_client_sup.erl +++ b/src/eradius_client_sup.erl @@ -1,33 +1,40 @@ +%% Copyright (c) 2024 Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT +%% +%% @private -module(eradius_client_sup). -behaviour(supervisor). %% API --export([start_link/1, new/2]). +-export([start_link/1, socket_supervisor/1]). %% Supervisor callbacks -export([init/1]). +-ignore_xref([start_link/1]). + -define(SERVER, ?MODULE). %%%=================================================================== %%% API functions %%%=================================================================== --spec start_link(Config :: eradius_client:client_config()) -> +-spec start_link(Config :: eradius_client_mngr:client_opts()) -> {ok, Pid :: pid()} | {error, {already_started, Pid :: pid()}} | {error, {shutdown, term()}} | {error, term()} | ignore. -start_link(Config) -> - supervisor:start_link(?MODULE, [Config]). +start_link(Opts) -> + supervisor:start_link(?MODULE, Opts). -new(Owner, SocketId) -> +socket_supervisor(Owner) -> Children = supervisor:which_children(Owner), case lists:keyfind(eradius_client_socket_sup, 1, Children) of - {eradius_client_socket_sup, SupPid, _, _} when is_pid(SupPid) -> - supervisor:start_child(SupPid, [SocketId]); + {eradius_client_socket_sup, Pid, _, _} when is_pid(Pid) -> + {ok, Pid}; _ -> {error, dead} end. @@ -40,26 +47,26 @@ new(Owner, SocketId) -> {ok, {SupFlags :: supervisor:sup_flags(), [ChildSpec :: supervisor:child_spec()]}} | ignore. -init([Opts]) -> +init(Opts) -> SupFlags = #{strategy => one_for_one, intensity => 5, period => 10}, - Client = - #{id => eradius_client, - start => {eradius_client, start_link, [self(), Opts]}, - restart => permanent, - shutdown => 5000, - type => worker, - modules => [eradius_client]}, - SocketSup = + ClientSocketSup = #{id => eradius_client_socket_sup, start => {eradius_client_socket_sup, start_link, []}, restart => permanent, shutdown => 5000, type => supervisor, modules => [eradius_client_socket_sup]}, + ClientMngr = + #{id => eradius_client_mngr, + start => {eradius_client_mngr, start_link, [self() | Opts]}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [eradius_client_mngr]}, - {ok, {SupFlags, [Client, SocketSup]}}. + {ok, {SupFlags, [ClientSocketSup, ClientMngr]}}. %%%=================================================================== %%% Internal functions diff --git a/src/eradius_client_top_sup.erl b/src/eradius_client_top_sup.erl new file mode 100644 index 00000000..fff1e951 --- /dev/null +++ b/src/eradius_client_top_sup.erl @@ -0,0 +1,60 @@ +%% Copyright (c) 2024 Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT +%% +%% @private +-module(eradius_client_top_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0, start_client/1]). + +%% Supervisor callbacks +-export([init/1]). + +-ignore_xref([start_link/0]). + +-define(SERVER, ?MODULE). + +%%%=================================================================== +%%% API functions +%%%=================================================================== + +-spec start_link() -> {ok, Pid :: pid()} | + {error, {already_started, Pid :: pid()}} | + {error, {shutdown, term()}} | + {error, term()} | + ignore. +start_link() -> + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + +start_client(Opts) -> + supervisor:start_child(?SERVER, [Opts]). + +%%%=================================================================== +%%% Supervisor callbacks +%%%=================================================================== + +-spec init(Args :: term()) -> + {ok, {SupFlags :: supervisor:sup_flags(), + [ChildSpec :: supervisor:child_spec()]}} | + ignore. +init([]) -> + SupFlags = #{strategy => simple_one_for_one, + intensity => 1, + period => 5}, + + ClientSup = + #{id => eradius_client_sup, + start => {eradius_client_sup, start_link, []}, + restart => permanent, + shutdown => 5000, + type => supervisor, + modules => [eradius_client_sup]}, + + {ok, {SupFlags, [ClientSup]}}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/src/eradius_config.erl b/src/eradius_config.erl deleted file mode 100644 index b0ea1e2b..00000000 --- a/src/eradius_config.erl +++ /dev/null @@ -1,401 +0,0 @@ --module(eradius_config). - % Eradius API's: --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, - validate_options/1, validate_socket_options/1, validate_server/1]). --export([generate_ip_list/2]). - -%% ------------------------------------------------------------------------------------------ -%% -- config validation --define(pos_int(X), is_integer(X), X >= 0). --define(ip4_address_num(X), ?pos_int(X), X < 256). --define(ip4_address(T), ?ip4_address_num(element(1, T)), ?ip4_address_num(element(2, T)), - ?ip4_address_num(element(3, T)), ?ip4_address_num(element(4, T))). --define(valid_atom(Value), Value =/= invalid). --define(valid(X), is_tuple(X), ?valid_atom(element(1, X))). --define(is_io(IO), is_list(IO) orelse is_binary(IO)). --define(invalid(ErrorMsg, ErrorValue), {invalid, io_lib:format(ErrorMsg, ErrorValue)}). - -validate_new_config() -> - validate_new_config(get_app_env(servers), get_app_env(session_nodes)). - -validate_new_config({invalid, _} = Invalid, _Nodes) -> Invalid; -validate_new_config(_Servers, {invalid, _} = Invalid) -> Invalid; -validate_new_config(Servers, Nodes) -> - validate_new_config_start(Servers, check_root(Nodes)). - -validate_new_config_start(_Servers, {invalid, _} = Invalid) -> Invalid; -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, {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, Opts}, Values} end, ListOfPorts) - end. - -validate_new_nas_list(NasLists, ServerConfig) -> - map_helper(fun(NasList) -> validate_behavior_naslist(NasList, ServerConfig) end, NasLists, flatten). - -validate_behavior_naslist({Behavior, ListOfNases}, {_IP, _ListOfPorts, Nodes}) -> - validate_behavior_nases(validate_behavior(Behavior), validate_naslist(ListOfNases, Nodes)). - -validate_behavior_nases({invalid, _} = Invalid, _) -> Invalid; -validate_behavior_nases(_, {invalid, _} = Invalid) -> Invalid; -validate_behavior_nases(Behavior, Nases) -> - build_nas_behavior_list(Behavior, Nases). - -validate_behavior({Nas, Args}) -> - validate_behavior({get_app_env(radius_callback), Nas, Args}); -validate_behavior({{invalid, _} = Invalid, _Nas, _Args}) -> - Invalid; -validate_behavior({Module, Nas, _Args} = Value) when is_atom(Module) andalso ?is_io(Nas) -> - code:is_loaded(Module) =:= false andalso code:load_file(Module), - case erlang:function_exported(Module, validate_arguments, 1) of - true -> validate_arguments(Value); - false -> Value - end; -validate_behavior({Module, _, _}) when is_atom(Module) -> - ?invalid("bad NAS Id in Behavior specification: ~p", [Module]); -validate_behavior({Module, _, _}) -> - ?invalid("bad module in Behavior specification: ~p", [Module]); -validate_behavior(Term) -> - ?invalid("bad Term in Behavior specification: ~p", [Term]). - -validate_arguments({Module, Nas, Args} = Value) -> - case Module:validate_arguments(Args) of - true -> Value; - {true, NewArgs} -> {Module, Nas, NewArgs}; - false -> ?invalid("~p: bad configuration", [Module]); - Error -> ?invalid("~p: bad configuration: ~p", [Module, Error]) - end. - -validate_naslist(ListOfNases, Nodes) -> map_helper(fun(Nas) -> validate_nas(Nas, Nodes) end, ListOfNases, yes). - -validate_nas({IP, Secret}, Nodes) -> - validate_nas({IP, Secret, []}, Nodes); -validate_nas({IP, Secret, Options}, Nodes) -> - validate_nas({proplists:get_value(nas_id, Options), IP, Secret, proplists:get_value(group, Options)}, Nodes); -validate_nas({NasId, IP, Secret, undefined}, {root, Nodes}) -> - validate_nas(NasId, IP, Secret, root, Nodes); -validate_nas({NasId, IP, Secret, GroupName}, Nodes) when is_list(Nodes) -> - validate_nas(NasId, IP, Secret, GroupName, proplists:get_value(GroupName, Nodes)); -validate_nas(Term, _) -> - ?invalid("bad term in NAS specification: ~p", [Term]). - -validate_nas(_NasId, {invalid, _} = Invalid, _Secret, _Name, _Nodes) -> Invalid; - -validate_nas(NasId, IP, Secret, Name, undefined) -> - validate_nas(NasId, IP, Secret, Name, validate_handler_nodes(Name)); -validate_nas(_NasId, IP, _Secret, Name, {invalid, _}) -> - ?invalid("group ~p for nas ~p is undefined", [Name, IP]); -validate_nas(NasId, IP, Secret, _Name, Nodes) when ?is_io(Secret) andalso (?is_io(NasId) orelse NasId == undefined) -> - case is_list(IP) andalso string:tokens(IP, "/") of - [IP0, Mask] -> - [{NasId, validate_ip(IP1), validate_secret(Secret), Nodes} || IP1 <- generate_ip_list(validate_ip(IP0), Mask)]; - _ -> {NasId, validate_ip(IP), validate_secret(Secret), Nodes} - end; -validate_nas(NasId, _IP, Secret, _Name, _) when ?is_io(Secret) -> - ?invalid("bad nas id name: ~p", [NasId]); -validate_nas(_NasId, _IP, Secret, _Name, _) -> - ?invalid("bad RADIUS secret: ~p", [Secret]). - -%% -------------------------------------------------------------------------------------------------- -%% -- direct validation function - -validate_ip(IP) when is_list(IP) -> - ok_error_helper(inet_parse:address(IP), {"bad IP address: ~p", [IP]}); -validate_ip(IP) when ?ip4_address(IP) -> - IP; -validate_ip(X) -> - ?invalid("bad IP address: ~p", [X]). - -validate_ports(Ports) -> map_helper(fun validate_port/1, Ports). -validate_port(Port) when is_list(Port) -> validate_port(catch list_to_integer(Port)); -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 - {invalid, _} = Invalid -> - Invalid; - Value -> - {Name, Value} - end - end, AllNodes); -check_root(Nodes) -> - case validate_handler_nodes(Nodes) of - {invalid, _} = Invalid -> - Invalid; - Values -> - {root, Values} - end. - -%% -------------------------------------------------------------------------------------------------- -%% -- build right format function - -build_nas_behavior_list({Module, Nas, Args}, ListOfNases) -> - lists:map(fun({undefined, IP, Secret, Nodes}) -> - {build_nasname(Nas, IP), IP, Secret, Nodes, Module, Args}; - ({NasName, IP, Secret, Nodes}) -> - {NasName, IP, Secret, Nodes, Module, Args} - end, ListOfNases). - -build_nasname(Nas, IP) -> - NasBinary = tob(Nas), - IPString = inet_parse:ntoa(IP), - <<NasBinary/binary, "_", (list_to_binary(IPString))/binary>>. - -tob(Integer) when is_integer(Integer) -> tob(integer_to_list(Integer)); -tob(List) when is_list(List) -> list_to_binary(List); -tob(Binary) -> Binary. - --type valid_nas() :: {inet:ip_address(), binary(), list(atom()), module(), term()}. --type valid_server() :: {eradius_server_mon:server(), list(valid_nas())}. --type valid_config() :: list(valid_server()). - --spec validate_config(list(term())) -> valid_config() | {invalid, io_lib:chars()}. -validate_config(Config) -> - case Config of - [Server | _] -> - case Server of - {List, SecondList} when is_list(List) and is_list(SecondList) -> - validate_server_config(dedup_keys(Config)); - %% Check format of new command - {_Name, ServerConf} when is_tuple(ServerConf) -> - validate_new_config() - end; - [] -> - validate_server_config(dedup_keys(Config)) - end. - --spec validate_server_config(list(term())) -> valid_config() | {invalid, io_lib:chars()}. -validate_server_config([]) -> - []; -validate_server_config([{Server, NasList} | ConfigRest]) -> - case validate_server(Server) of - {invalid, _} = E -> - E; - ValidServer -> - case validate_nas_list(NasList) of - {invalid, _} = E -> - E; - ValidNasList -> - case validate_server_config(ConfigRest) of - E = {invalid, _} -> - E; - ValidConfigRest -> - [{ValidServer, ValidNasList} | ValidConfigRest] - end - end - end; -validate_server_config([InvalidTerm | _ConfigRest]) -> ?invalid("bad term in server list: ~p", [InvalidTerm]). - -validate_server({IP, Port}) when is_list(Port) -> - case (catch list_to_integer(Port)) of - {'EXIT', _} -> - {invalid, io_lib:format("bad port number: ~p", [Port])}; - Num when ?pos_int(Num) -> - validate_server({IP, Num}); - Num -> - {invalid, io_lib:format("port number out of range: ~p", [Num])} - end; -validate_server({IP, Port}) when is_list(IP), ?pos_int(Port) -> - case inet_parse:ipv4_address(IP) of - {ok, Address} -> - {Address, Port}; - {error, einval} -> - {invalid, io_lib:format("bad IP address: ~p", [IP])} - end; -validate_server({IP, Port}) when ?ip4_address(IP), ?pos_int(Port) -> - {IP, Port}; -validate_server(String) when is_list(String) -> - %% TODO: IPv6 address support - case string:tokens(String, ":") of - [IP, Port] -> - validate_server({IP, Port}); - _ -> - {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])}. - -validate_nas_list([]) -> - []; -validate_nas_list([{NasAddress, Secret, HandlerNodes, Module, Args} | NasListRest]) when is_list(NasAddress) -> - case inet_parse:ipv4_address(NasAddress) of - {ok, ValidAddress} -> - validate_nas_list([{ValidAddress, Secret, HandlerNodes, Module, Args} | NasListRest]); - {error, einval} -> - {invalid, io_lib:format("bad IP address in NAS specification: ~p", [NasAddress])} - end; -validate_nas_list([{NasAddress, Secret, HandlerNodes, Module, Args} | NasListRest]) when ?ip4_address(NasAddress) -> - case validate_secret(Secret) of - E = {invalid, _} -> - E; - ValidSecret -> - case validate_handler_nodes(HandlerNodes) of - E = {invalid, _} -> - E; - ValidHandlerNodes -> - case Module of - _ when is_atom(Module) -> - case validate_nas_list(NasListRest) of - E = {invalid, _} -> - E; - ValidNasListRest -> - [{build_nasname("", NasAddress), NasAddress, ValidSecret, ValidHandlerNodes, Module, Args} | ValidNasListRest] - end; - _Else -> - {invalid, io_lib:format("bad module in NAS specifification: ~p", [Module])} - end - end - end; -validate_nas_list([{InvalidAddress, _, _, _, _} | _NasListRest]) -> - {invalid, io_lib:format("bad IP address in NAS specification: ~p", [InvalidAddress])}; -validate_nas_list([OtherTerm | _NasListRest]) -> - {invalid, io_lib:format("bad term in NAS specification: ~p", [OtherTerm])}. - -validate_secret(Secret) when is_list(Secret) -> - unicode:characters_to_binary(Secret); -validate_secret(Secret) when is_binary(Secret) -> - Secret; -validate_secret(OtherTerm) -> - {invalid, io_lib:format("bad RADIUS secret: ~p", [OtherTerm])}. - -validate_handler_nodes(local) -> - local; -validate_handler_nodes("local") -> - local; -validate_handler_nodes([]) -> - {invalid, "empty node list"}; -validate_handler_nodes(NodeL) when is_list(NodeL) -> - validate_node_list(NodeL); -validate_handler_nodes(OtherTerm) -> - {invalid, io_lib:format("bad node list: ~p", [OtherTerm])}. - -validate_node_list([]) -> - []; -validate_node_list([Node | Rest]) when is_atom(Node) -> - case validate_node_list(Rest) of - E = {invalid, _} -> - E; - ValidRest -> - [Node | ValidRest] - end; -validate_node_list([OtherTerm | _]) -> - {invalid, io_lib:format("bad term in node list: ~p", [OtherTerm])}. - -dedup_keys(Proplist) -> - dedup_keys1(lists:keysort(1, Proplist)). - -dedup_keys1(Proplist) -> - lists:foldr(fun ({K, V1}, [{K, V2} | R]) -> - [{K, plmerge(V1, V2)} | R]; - ({K, V}, R) -> - [{K, V} | R] - end, [], Proplist). - -plmerge(List1, List2) -> - M1 = [{K, V} || {K, V} <- List1, not proplists:is_defined(K, List2)], - lists:keysort(1, M1 ++ List2). - -%% -------------------------------------------------------------------------------------------------- -%% -- helpers - -get_app_env(Env) -> - get_app_env(eradius, Env). -get_app_env(App, Env) -> - case application:get_env(App, Env) of - {ok, Value} -> - Value; - _ -> - ?invalid("config parameter: ~p is undefined for application ~p", [Env, App]) - end. - -map_helper(Fun, Values) -> - map_helper(Fun, Values, no). -map_helper(Fun, Values, Type) -> - map_helper(Fun, Values, Type, []). -map_helper(_Fun, [], _Type, Values) -> lists:reverse(Values); -map_helper(Fun, [Head | Tail], Type, Values) -> - case Fun(Head) of - {invalid, _} = Error -> - Error; - Result when Type =/= no andalso is_list(Result) -> - map_helper(Fun, Tail, Type, Result ++ Values); - Result -> - map_helper(Fun, Tail, Type, [Result | Values]) - end. - -ok_error_helper({error, _Error}, {Msg, Value}) when is_list(Msg) -> ?invalid(Msg, Value); -ok_error_helper({error, _Error}, ErrorMsg) when is_list(ErrorMsg) -> ErrorMsg; -ok_error_helper({ok, Value}, _ErrorMessage) -> Value; -ok_error_helper(Value, _ErrorMessage) -> Value. - -generate_ip_list(IP, Mask) when is_list(Mask) -> - generate_ip_list(IP, catch list_to_integer(Mask)); -generate_ip_list({A, B, C, D}, Mask) when Mask >=0, Mask =< 32 -> - <<Address:32/integer>> = <<A, B, C, D>>, - Wildcard = 16#ffffffff bsr Mask, - <<Netmask:32/unsigned-integer>> = << (bnot Wildcard):32 >>, - generate_ip(Address band Netmask, Address bor Wildcard); -generate_ip_list(_, Mask) -> ?invalid("invalid mask ~p", [Mask]). - -generate_ip(E, E) -> - <<A:8, B:8, C:8, D:8>> = <<E:32/integer>>, - [{A, B, C, D}]; -generate_ip(S, E) -> - <<A:8, B:8, C:8, D:8>> = <<S:32/integer>>, - [{A, B, C, D} | generate_ip(S+1, E)]. diff --git a/src/eradius_counter.erl b/src/eradius_counter.erl deleted file mode 100644 index 9d245e59..00000000 --- a/src/eradius_counter.erl +++ /dev/null @@ -1,301 +0,0 @@ -%% @doc -%% This module implements the statitics counter for RADIUS servers and clients - --module(eradius_counter). --export([init_counter/1, init_counter/2, inc_counter/2, dec_counter/2, reset_counter/1, reset_counter/2, - inc_request_counter/2, inc_reply_counter/2, observe/4, observe/5, - set_boolean_metric/3]). - --behaviour(gen_server). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --export([start_link/0, reset/0, pull/0, read/0, aggregate/1]). --export([collect/2]). - --include("eradius_lib.hrl"). - --record(state, { - reset :: erlang:timestamp() - }). - --type srv_counters() :: [#server_counter{}]. --type nas_counters() :: {erlang:timestamp(), [#nas_counter{}]}. --type stats() :: {srv_counters(), nas_counters()}. - -%% ------------------------------------------------------------------------------------------ -%% API -%% @doc initialize a counter structure -init_counter({ServerIP, ServerPort, ServerName}) when is_integer(ServerPort) -> - #server_counter{key = {ServerIP, ServerPort}, startTime = eradius_lib:timestamp(), resetTime = eradius_lib:timestamp(), server_name = ServerName}; -init_counter({{ClientName, ClientIP, ClientPort}, {ServerName, ServerIp, ServerPort}}) -> - #client_counter{key = {{ClientName, ClientIP, ClientPort}, {ServerName, ServerIp, ServerPort}}, server_name = ServerName}. -init_counter(#nas_prop{server_ip = ServerIP, server_port = ServerPort, nas_ip = NasIP, nas_id = NasId}, ServerName) -> - #nas_counter{key = {{ServerIP, ServerPort}, NasIP, NasId}, server_name = ServerName}. - -%% @doc reset counters -reset_counter(#server_counter{startTime = Up}) -> #server_counter{startTime = Up, resetTime = eradius_lib:timestamp()}. -reset_counter(Nas = #nas_prop{}, ServerName) -> - init_counter(Nas, ServerName). - -%% @doc increment requests counters -inc_request_counter(Counter, Nas) -> - inc_counter(Counter, Nas). - -%% @doc increment reply counters -inc_reply_counter(Counter, Nas) -> - inc_counter(Counter, Nas). - -%% @doc increment a specific counter value -inc_counter(invalidRequests, Counters = #server_counter{invalidRequests = Value}) -> - Counters#server_counter{invalidRequests = Value + 1}; -inc_counter(discardNoHandler, Counters = #server_counter{discardNoHandler = Value}) -> - Counters#server_counter{discardNoHandler = Value + 1}; -inc_counter(Counter, Nas = #nas_prop{}) -> - gen_server:cast(?MODULE, {inc_counter, Counter, Nas}); -inc_counter(Counter, {{ClientName, ClientIP, ClientPort}, {ServerName, ServerIp, ServerPort}}) -> - gen_server:cast(?MODULE, {inc_counter, Counter, {{ClientName, ClientIP, ClientPort}, {ServerName, ServerIp, ServerPort}}}). - -dec_counter(Counter, Nas = #nas_prop{}) -> - gen_server:cast(?MODULE, {dec_counter, Counter, Nas}); -dec_counter(Counter, {{ClientName, ClientIP, ClientPort}, {ServerName, ServerIp, ServerPort}}) -> - gen_server:cast(?MODULE, {dec_counter, Counter, {{ClientName, ClientIP, ClientPort}, {ServerName, ServerIp, ServerPort}}}). - -%% @doc reset all counters to zero -reset() -> - gen_server:call(?MODULE, reset). - -%% @doc read counters and reset to zero --spec pull() -> stats(). -pull() -> - gen_server:call(?MODULE, pull). - -%% @doc read counters --spec read() -> stats(). -read() -> - gen_server:call(?MODULE, read). - -%% @doc calculate the per server sum of all counters of a per NAS list of counters --spec aggregate(stats()) -> stats(). -aggregate({Servers, {ResetTS, Nass}}) -> - NSums = lists:foldl(fun(Nas = #nas_counter{key = {ServerId, _}}, Acc) -> - orddict:update(ServerId, fun(Value) -> add_counter(Value, Nas) end, Nas#nas_counter{key = ServerId}, Acc) - end, - orddict:new(), Nass), - NSum1 = [Value || {_Key, Value} <- orddict:to_list(NSums)], - {Servers, {ResetTS, NSum1}}. - -%% @doc Set Value for the given prometheus boolean metric by the given Name with -%% the given values -set_boolean_metric(Name, Labels, Value) -> - case code:is_loaded(prometheus) of - {file, _} -> - try - prometheus_boolean:set(Name, Labels, Value) - catch _:_ -> - prometheus_boolean:declare([{name, server_status}, {labels, [server_ip, server_port]}, - {help, "Status of an upstream RADIUS Server"}]), - prometheus_boolean:set(Name, Labels, Value) - end; - _ -> - ok - end. - -%% @doc Update the given histogram metric value -%% NOTE: We use prometheus_histogram collector here instead of eradius_counter ets table because -%% it is much easy to use histograms in this way. As we don't need to manage buckets and do -%% the other histogram things in eradius, but prometheus.erl will do it for us -observe(Name, {{ClientName, ClientIP, _}, {ServerName, ServerIP, ServerPort}} = MetricsInfo, Value, Help) -> - case code:is_loaded(prometheus) of - {file, _} -> - try - prometheus_histogram:observe(Name, [ServerIP, ServerPort, ServerName, ClientName, ClientIP], Value) - catch _:_ -> - Buckets = application:get_env(eradius, histogram_buckets, [10, 30, 50, 75, 100, 1000, 2000]), - prometheus_histogram:declare([{name, Name}, {labels, [server_ip, server_port, server_name, client_name, client_ip]}, - {duration_unit, milliseconds}, - {buckets, Buckets}, {help, Help}]), - observe(Name, MetricsInfo, Value, Help) - end; - _ -> - ok - end. -observe(Name, #nas_prop{server_ip = ServerIP, server_port = ServerPort, nas_ip = NasIP, nas_id = NasId} = Nas, Value, ServerName, Help) -> - case code:is_loaded(prometheus) of - {file, _} -> - try - prometheus_histogram:observe(Name, [inet:ntoa(ServerIP), ServerPort, ServerName, inet:ntoa(NasIP), NasId], Value) - catch _:_ -> - Buckets = application:get_env(eradius, histogram_buckets, [10, 30, 50, 75, 100, 1000, 2000]), - prometheus_histogram:declare([{name, Name}, {labels, [server_ip, server_port, server_name, nas_ip, nas_id]}, - {duration_unit, milliseconds}, - {buckets, Buckets}, {help, Help}]), - observe(Name, Nas, Value, ServerName, Help) - end; - _ -> - ok - end. - -%% helper to be called from the aggregator to fetch this nodes values -%% @private -collect(Ref, Process) -> - lists:foreach(fun(Node) -> gen_server:cast({?MODULE, Node}, {collect, Ref, Process}) end, - eradius_node_mon:get_module_nodes(?MODULE)). - -%% @private --spec start_link() -> {ok, pid()} | {error, term()}. -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -%% ------------------------------------------------------------------------------------------ -%% -- gen_server Callbacks -%% @private -init([]) -> - ets:new(?MODULE, [ordered_set, protected, named_table, {keypos, #nas_counter.key}, {write_concurrency,true}]), - eradius:modules_ready([?MODULE]), - {ok, #state{reset = eradius_lib:timestamp()}}. - -%% @private -handle_call(pull, _From, State) -> - NassAndClients = read_stats(State), - Servers = server_stats(pull), - ets:delete_all_objects(?MODULE), - {reply, {Servers, NassAndClients}, State#state{reset = eradius_lib:timestamp()}}; -handle_call(read, _From, State) -> - NassAndClients = read_stats(State), - Servers = server_stats(read), - {reply, {Servers, NassAndClients}, State}; -handle_call(reset, _From, State) -> - server_stats(reset), - ets:delete_all_objects(?MODULE), - {reply, ok, State#state{reset = eradius_lib:timestamp()}}. - -%% @private -handle_cast({inc_counter, Counter, Key = {{_ClientName, _ClientIP, _ClientPort}, {_ServerName, _ServerIp, _ServerPort}}}, State) -> - ets:update_counter(?MODULE, Key, {counter_idx(Counter, client), 1}, init_counter(Key)), - {noreply, State}; - -handle_cast({inc_counter, Counter, Nas = #nas_prop{server_ip = ServerIP, server_port = ServerPort, nas_ip = NasIP, nas_id = NasId}}, State) -> - Key = {{ServerIP, ServerPort}, NasIP, NasId}, - {{ServerName, _, _}, _} = Nas#nas_prop.metrics_info, - ets:update_counter(?MODULE, Key, {counter_idx(Counter, nas), 1}, init_counter(Nas, ServerName)), - {noreply, State}; - -handle_cast({dec_counter, Counter, Key = {{_ClientName, _ClientIP, _ClientPort}, {_ServerName, _ServerIp, _ServerPort}}}, State) -> - ets:update_counter(?MODULE, Key, {counter_idx(Counter, client), -1}, init_counter(Key)), - {noreply, State}; - -handle_cast({dec_counter, Counter, Nas = #nas_prop{server_ip = ServerIP, server_port = ServerPort, nas_ip = NasIP, nas_id = NasId}}, State) -> - Key = {{ServerIP, ServerPort}, NasIP, NasId}, - {{ServerName, _, _}, _} = Nas#nas_prop.metrics_info, - ets:update_counter(?MODULE, Key, {counter_idx(Counter, nas), -1}, init_counter(Nas, ServerName)), - {noreply, State}; - -handle_cast({collect, Ref, Process}, State) -> - Process ! {collect, Ref, ets:tab2list(?MODULE)}, - ets:delete_all_objects(?MODULE), - {noreply, State#state{reset = eradius_lib:timestamp()}}; - -handle_cast(_Msg, State) -> - {noreply, State}. - -%% -- unused callbacks -%% @private -handle_info(_Info, State) -> {noreply, State}. -%% @private -code_change(_OldVsn, State, _Extra) -> {ok, State}. -%% @private -terminate(_Reason, _State) -> ok. - -%% ------------------------------------------------------------------------------------------ -%% -- helper functions -%% @private - -read_stats(State) -> - {State#state.reset, ets:tab2list(?MODULE)}. - -server_stats(Func) -> - lists:foldl(fun(S, Acc) -> [eradius_server:stats(S, Func)|Acc] end, [], eradius_server_sup:all()). - -%% @private -counter_idx(requests, nas) -> #nas_counter.requests; -counter_idx(replies, nas) -> #nas_counter.replies; -counter_idx(dupRequests, nas) -> #nas_counter.dupRequests; -counter_idx(malformedRequests, nas) -> #nas_counter.malformedRequests; -counter_idx(accessRequests, nas) -> #nas_counter.accessRequests; -counter_idx(accessAccepts, nas) -> #nas_counter.accessAccepts; -counter_idx(accessRejects, nas) -> #nas_counter.accessRejects; -counter_idx(accessChallenges, nas) -> #nas_counter.accessChallenges; -counter_idx(badAuthenticators, nas) -> #nas_counter.badAuthenticators; -counter_idx(packetsDropped, nas) -> #nas_counter.packetsDropped; -counter_idx(unknownTypes, nas) -> #nas_counter.unknownTypes; -counter_idx(handlerFailure, nas) -> #nas_counter.handlerFailure; -counter_idx(coaRequests, nas) -> #nas_counter.coaRequests; -counter_idx(coaAcks, nas) -> #nas_counter.coaAcks; -counter_idx(coaNaks, nas) -> #nas_counter.coaNaks; -counter_idx(discRequests, nas) -> #nas_counter.discRequests; -counter_idx(discAcks, nas) -> #nas_counter.discAcks; -counter_idx(discNaks, nas) -> #nas_counter.discNaks; -counter_idx(retransmissions, nas) -> #nas_counter.retransmissions; -counter_idx(pending, nas) -> #nas_counter.pending; -counter_idx(accountRequestsStart, nas) -> #nas_counter.accountRequestsStart; -counter_idx(accountRequestsStop, nas) -> #nas_counter.accountRequestsStop; -counter_idx(accountRequestsUpdate, nas) -> #nas_counter.accountRequestsUpdate; -counter_idx(accountResponsesStart, nas) -> #nas_counter.accountResponsesStart; -counter_idx(accountResponsesStop, nas) -> #nas_counter.accountResponsesStop; -counter_idx(accountResponsesUpdate, nas) -> #nas_counter.accountResponsesUpdate; - -counter_idx(requests, client) -> #client_counter.requests; -counter_idx(replies, client) -> #client_counter.replies; -counter_idx(accessRequests, client) -> #client_counter.accessRequests; -counter_idx(coaRequests, client) -> #client_counter.coaRequests; -counter_idx(discRequests, client) -> #client_counter.discRequests; -counter_idx(retransmissions, client) -> #client_counter.retransmissions; -counter_idx(accessAccepts, client) -> #client_counter.accessAccepts; -counter_idx(accessRejects, client) -> #client_counter.accessRejects; -counter_idx(accessChallenges, client) -> #client_counter.accessChallenges; -counter_idx(coaNaks, client) -> #client_counter.coaNaks; -counter_idx(coaAcks, client) -> #client_counter.coaAcks; -counter_idx(discNaks, client) -> #client_counter.discNaks; -counter_idx(discAcks, client) -> #client_counter.discAcks; -counter_idx(badAuthenticators, client) -> #client_counter.badAuthenticators; -counter_idx(packetsDropped, client) -> #client_counter.packetsDropped; -counter_idx(unknownTypes, client) -> #client_counter.unknownTypes; -counter_idx(pending, client) -> #client_counter.pending; -counter_idx(timeouts, client) -> #client_counter.timeouts; -counter_idx(accountRequestsStart, client) -> #client_counter.accountRequestsStart; -counter_idx(accountRequestsStop, client) -> #client_counter.accountRequestsStop; -counter_idx(accountRequestsUpdate, client) -> #client_counter.accountRequestsUpdate; -counter_idx(accountResponsesStart, client) -> #client_counter.accountResponsesStart; -counter_idx(accountResponsesStop, client) -> #client_counter.accountResponsesStop; -counter_idx(accountResponsesUpdate, client) -> #client_counter.accountResponsesUpdate. - -add_counter(Cnt1 = #nas_counter{}, Cnt2 = #nas_counter{}) -> - #nas_counter{ - key = Cnt1#nas_counter.key, - requests = Cnt1#nas_counter.requests + Cnt2#nas_counter.requests, - replies = Cnt1#nas_counter.replies + Cnt2#nas_counter.replies, - dupRequests = Cnt1#nas_counter.dupRequests + Cnt2#nas_counter.dupRequests, - malformedRequests = Cnt1#nas_counter.malformedRequests + Cnt2#nas_counter.malformedRequests, - accessRequests = Cnt1#nas_counter.accessRequests + Cnt2#nas_counter.accessRequests, - accessAccepts = Cnt1#nas_counter.accessAccepts + Cnt2#nas_counter.accessAccepts, - accessRejects = Cnt1#nas_counter.accessRejects + Cnt2#nas_counter.accessRejects, - accessChallenges = Cnt1#nas_counter.accessChallenges + Cnt2#nas_counter.accessChallenges, - accountRequestsStart = Cnt1#nas_counter.accountRequestsStart + Cnt2#nas_counter.accountRequestsStart, - accountRequestsStop = Cnt1#nas_counter.accountRequestsStop + Cnt2#nas_counter.accountRequestsStop, - accountRequestsUpdate = Cnt1#nas_counter.accountRequestsUpdate + Cnt2#nas_counter.accountRequestsUpdate, - accountResponsesStart = Cnt1#nas_counter.accountResponsesStart + Cnt2#nas_counter.accountResponsesStart, - accountResponsesStop = Cnt1#nas_counter.accountResponsesStop + Cnt2#nas_counter.accountResponsesStop, - accountResponsesUpdate = Cnt1#nas_counter.accountResponsesUpdate + Cnt2#nas_counter.accountResponsesUpdate, - noRecords = Cnt1#nas_counter.noRecords + Cnt2#nas_counter.noRecords, - badAuthenticators = Cnt1#nas_counter.badAuthenticators + Cnt2#nas_counter.badAuthenticators, - packetsDropped = Cnt1#nas_counter.packetsDropped + Cnt2#nas_counter.packetsDropped, - unknownTypes = Cnt1#nas_counter.unknownTypes + Cnt2#nas_counter.unknownTypes, - handlerFailure = Cnt1#nas_counter.handlerFailure + Cnt2#nas_counter.handlerFailure, - coaRequests = Cnt1#nas_counter.coaRequests + Cnt2#nas_counter.coaRequests, - coaAcks = Cnt1#nas_counter.coaAcks + Cnt2#nas_counter.coaAcks, - coaNaks = Cnt1#nas_counter.coaNaks + Cnt2#nas_counter.coaNaks, - discRequests = Cnt1#nas_counter.discRequests + Cnt2#nas_counter.discRequests, - discAcks = Cnt1#nas_counter.discAcks + Cnt2#nas_counter.discAcks, - discNaks = Cnt1#nas_counter.discNaks + Cnt2#nas_counter.discNaks, - retransmissions = Cnt1#nas_counter.retransmissions + Cnt2#nas_counter.retransmissions, - pending = Cnt1#nas_counter.pending + Cnt2#nas_counter.pending - }. diff --git a/src/eradius_counter_aggregator.erl b/src/eradius_counter_aggregator.erl deleted file mode 100644 index b137b370..00000000 --- a/src/eradius_counter_aggregator.erl +++ /dev/null @@ -1,162 +0,0 @@ --module(eradius_counter_aggregator). - --behaviour(gen_server). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --export([start_link/0, reset/0, pull/0, read/0]). - --include("eradius_lib.hrl"). - --define(INIT_HB, 1000). --define(INTERVAL_HB, 5000). - --record(state, { - me :: reference(), - reset :: erlang:timestamp() - }). - -%% @doc reset all counters to zero -reset() -> - gen_server:call(?MODULE, reset). -%% @doc read counters and reset to zero --spec pull() -> eradius_counter:stats(). -pull() -> - gen_server:call(?MODULE, pull). -%% @doc read counters --spec read() -> eradius_counter:stats(). -read() -> - gen_server:call(?MODULE, read). - -%% @private --spec start_link() -> {ok, pid()} | {error, term()}. -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). - -%% ------------------------------------------------------------------------------------------ -%% -- gen_server Callbacks -%% @private -init([]) -> - ets:new(?MODULE, [ordered_set, protected, named_table, {keypos, #nas_counter.key}, {write_concurrency,true}]), - eradius:modules_ready([?MODULE]), - EnableAggregator = application:get_env(eradius, counter_aggregator, false), - if EnableAggregator == true -> - erlang:send_after(?INIT_HB, self(), heartbeat); - true -> - ok - end, - {ok, #state{me = make_ref(), reset = eradius_lib:timestamp()}}. - -%% @private -handle_call(pull, _From, State) -> - Nass = read_stats(State), - Servers = server_stats(pull), - ets:delete_all_objects(?MODULE), - {reply, {Servers, Nass}, State#state{reset = eradius_lib:timestamp()}}; -handle_call(read, _From, State) -> - Nass = read_stats(State), - Servers = server_stats(read), - {reply, {Servers, Nass}, State}; -handle_call(reset, _From, State) -> - server_stats(reset), - ets:delete_all_objects(?MODULE), - {reply, ok, State#state{reset = eradius_lib:timestamp()}}. - -%% @private -handle_info(heartbeat, State) -> - eradius_counter:collect(State#state.me, self()), - erlang:send_after(?INTERVAL_HB, self(), heartbeat), - {noreply, State}; -handle_info({collect, Ref, Stats}, State = #state{me = Ref}) -> - lists:foreach(fun update_stats/1, Stats), - {noreply, State}; -handle_info({collect, Ref, Stats}, State) -> - io:format("invalid stats answer: ~p~n", [{collect, Ref, Stats}]), - {noreply, State}. - -%% -- unused callbacks -%% @private -handle_cast(_Msg, State) -> {noreply, State}. -%% @private -code_change(_OldVsn, State, _Extra) -> {ok, State}. -%% @private -terminate(_Reason, _State) -> ok. - -%% ------------------------------------------------------------------------------------------ -%% -- helper functions -%% @private - -read_stats(State) -> - {State#state.reset, ets:tab2list(?MODULE)}. - -server_stats(Func) -> - lists:foldl(fun(S, Acc) -> [eradius_server:stats(S, Func)|Acc] end, [], eradius_server_sup:all()). - -update_stats(Rec = #nas_counter{key = Key}) -> - Cnt0 = case ets:lookup(?MODULE, Key) of - [] -> #nas_counter{key = Key}; - [Cnt] -> Cnt - end, - ets:insert(?MODULE, add_counter(Cnt0, Rec)); -update_stats(Rec = #client_counter{key = Key}) -> - Cnt0 = case ets:lookup(?MODULE, Key) of - [] -> #client_counter{key = Key}; - [Cnt] -> Cnt - end, - ets:insert(?MODULE, add_counter(Cnt0, Rec)). - -add_counter(Cnt1 = #nas_counter{}, Cnt2 = #nas_counter{}) -> - #nas_counter{ - key = Cnt1#nas_counter.key, - requests = Cnt1#nas_counter.requests + Cnt2#nas_counter.requests, - replies = Cnt1#nas_counter.replies + Cnt2#nas_counter.replies, - dupRequests = Cnt1#nas_counter.dupRequests + Cnt2#nas_counter.dupRequests, - malformedRequests = Cnt1#nas_counter.malformedRequests + Cnt2#nas_counter.malformedRequests, - accessRequests = Cnt1#nas_counter.accessRequests + Cnt2#nas_counter.accessRequests, - accessAccepts = Cnt1#nas_counter.accessAccepts + Cnt2#nas_counter.accessAccepts, - accessRejects = Cnt1#nas_counter.accessRejects + Cnt2#nas_counter.accessRejects, - accessChallenges = Cnt1#nas_counter.accessChallenges + Cnt2#nas_counter.accessChallenges, - accountRequestsStart = Cnt1#nas_counter.accountRequestsStart + Cnt2#nas_counter.accountRequestsStart, - accountRequestsStop = Cnt1#nas_counter.accountRequestsStop + Cnt2#nas_counter.accountRequestsStop, - accountRequestsUpdate = Cnt1#nas_counter.accountRequestsUpdate + Cnt2#nas_counter.accountRequestsUpdate, - accountResponsesStart = Cnt1#nas_counter.accountResponsesStart + Cnt2#nas_counter.accountResponsesStart, - accountResponsesStop = Cnt1#nas_counter.accountResponsesStop + Cnt2#nas_counter.accountResponsesStop, - accountResponsesUpdate = Cnt1#nas_counter.accountResponsesUpdate + Cnt2#nas_counter.accountResponsesUpdate, - noRecords = Cnt1#nas_counter.noRecords + Cnt2#nas_counter.noRecords, - badAuthenticators = Cnt1#nas_counter.badAuthenticators + Cnt2#nas_counter.badAuthenticators, - packetsDropped = Cnt1#nas_counter.packetsDropped + Cnt2#nas_counter.packetsDropped, - unknownTypes = Cnt1#nas_counter.unknownTypes + Cnt2#nas_counter.unknownTypes, - handlerFailure = Cnt1#nas_counter.handlerFailure + Cnt2#nas_counter.handlerFailure, - coaRequests = Cnt1#nas_counter.coaRequests + Cnt2#nas_counter.coaRequests, - coaAcks = Cnt1#nas_counter.coaAcks + Cnt2#nas_counter.coaAcks, - coaNaks = Cnt1#nas_counter.coaNaks + Cnt2#nas_counter.coaNaks, - discRequests = Cnt1#nas_counter.discRequests + Cnt2#nas_counter.discRequests, - discAcks = Cnt1#nas_counter.discAcks + Cnt2#nas_counter.discAcks, - discNaks = Cnt1#nas_counter.discNaks + Cnt2#nas_counter.discNaks - }; -add_counter(Cnt1 = #client_counter{}, Cnt2 = #client_counter{}) -> - #client_counter{ - key = Cnt1#client_counter.key, - requests = Cnt1#client_counter.requests + Cnt2#client_counter.requests, - replies = Cnt1#client_counter.replies + Cnt2#client_counter.replies, - accessRequests = Cnt1#client_counter.accessRequests + Cnt2#client_counter.accessRequests, - accessAccepts = Cnt1#client_counter.accessAccepts + Cnt2#client_counter.accessAccepts, - accessRejects = Cnt1#client_counter.accessRejects + Cnt2#client_counter.accessRejects, - accessChallenges = Cnt1#client_counter.accessChallenges + Cnt2#client_counter.accessChallenges, - accountRequestsStart = Cnt1#client_counter.accountRequestsStart + Cnt2#client_counter.accountRequestsStart, - accountRequestsStop = Cnt1#client_counter.accountRequestsStop + Cnt2#client_counter.accountRequestsStop, - accountRequestsUpdate = Cnt1#client_counter.accountRequestsUpdate + Cnt2#client_counter.accountRequestsUpdate, - accountResponsesStart = Cnt1#client_counter.accountResponsesStart + Cnt2#client_counter.accountResponsesStart, - accountResponsesStop = Cnt1#client_counter.accountResponsesStop + Cnt2#client_counter.accountResponsesStop, - accountResponsesUpdate = Cnt1#client_counter.accountResponsesUpdate + Cnt2#client_counter.accountResponsesUpdate, - badAuthenticators = Cnt1#client_counter.badAuthenticators + Cnt2#client_counter.badAuthenticators, - packetsDropped = Cnt1#client_counter.packetsDropped + Cnt2#client_counter.packetsDropped, - unknownTypes = Cnt1#client_counter.unknownTypes + Cnt2#client_counter.unknownTypes, - coaRequests = Cnt1#client_counter.coaRequests + Cnt2#client_counter.coaRequests, - coaAcks = Cnt1#client_counter.coaAcks + Cnt2#client_counter.coaAcks, - coaNaks = Cnt1#client_counter.coaNaks + Cnt2#client_counter.coaNaks, - discRequests = Cnt1#client_counter.discRequests + Cnt2#client_counter.discRequests, - discAcks = Cnt1#client_counter.discAcks + Cnt2#client_counter.discAcks, - discNaks = Cnt1#client_counter.discNaks + Cnt2#client_counter.discNaks, - retransmissions = Cnt1#client_counter.retransmissions + Cnt2#client_counter.retransmissions, - timeouts = Cnt1#client_counter.timeouts + Cnt2#client_counter.timeouts, - pending = Cnt1#client_counter.pending + Cnt2#client_counter.pending - }. diff --git a/src/eradius_dict.erl b/src/eradius_dict.erl index 3fca6f30..00847dc4 100644 --- a/src/eradius_dict.erl +++ b/src/eradius_dict.erl @@ -1,11 +1,18 @@ -%% @private +%% Copyright (c) 2002-2007, Martin Björklund and Torbjörn Törnkvist +%% Copyright (c) 2011, Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT +%% %% @doc Dictionary server -module(eradius_dict). --export([start_link/0, lookup/2, load_tables/1, load_tables/2, unload_tables/1, unload_tables/2]). --export_type([attribute/0, attr_value/0, table_name/0, attribute_id/0, attribute_type/0, - attribute_prim_type/0, attribute_encryption/0, vendor_id/0, value_id/0]). -behaviour(gen_server). + +%% API +-export([start_link/0, lookup/2, load_tables/1, load_tables/2, unload_tables/1, unload_tables/2]). +-ignore_xref([start_link/0, unload_tables/1, unload_tables/2]). + +%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include_lib("kernel/include/logger.hrl"). @@ -17,8 +24,9 @@ -type attribute_id() :: pos_integer() | {vendor_id(), pos_integer()}. -type attribute_encryption() :: 'no' | 'scramble' | 'salt_crypt' | 'ascend'. -type attribute_type() :: attribute_prim_type() | {tagged, attribute_prim_type()}. --type attribute_prim_type() :: 'string' | 'integer' | 'integer64' | 'ipaddr' | 'ipv6addr' - | 'ipv6prefix' | 'date' | 'abinary' | 'binary' | 'octets'. +-type attribute_prim_type() :: 'string' | 'integer' | 'integer24' | 'integer64' | + 'ipaddr' | 'ipv6addr' | 'ipv6prefix' | + 'date' | 'abinary' | 'binary' | 'octets'. -type value_id() :: {attribute_id(), pos_integer()}. -type vendor_id() :: pos_integer(). @@ -26,15 +34,23 @@ -type attribute() :: #attribute{} | attribute_id(). -type attr_value() :: term(). +-export_type([attribute/0, attr_value/0, table_name/0, attribute_id/0, attribute_type/0, + attribute_prim_type/0, attribute_encryption/0, vendor_id/0, value_id/0]). + + -record(state, {}). -%% ------------------------------------------------------------------------------------------ -%% -- API +%%%========================================================================= +%%% API +%%%========================================================================= + +%% @private -spec start_link() -> {ok, pid()} | {error, term()}. start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). --spec lookup(attribute | vendor | value, attribute_id() | value_id() | vendor_id()) -> false | #attribute{} | #value{} | #vendor{}. +-spec lookup(attribute | vendor | value, attribute_id() | value_id() | vendor_id()) -> + false | #attribute{} | #value{} | #vendor{}. lookup(Type, Id) -> dict_lookup(Type, Id). @@ -54,13 +70,17 @@ unload_tables(Tables) when is_list(Tables) -> unload_tables(Dir, Tables) when is_list(Tables) -> gen_server:call(?SERVER, {unload_tables, Dir, Tables}, infinity). -%% ------------------------------------------------------------------------------------------ -%% -- gen_server callbacks +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +%% @private init([]) -> {ok, InitialLoadTables} = application:get_env(eradius, tables), do_load_tables(code:priv_dir(eradius), InitialLoadTables), {ok, #state{}}. +%% @private handle_call({load_tables, Dir, Tables}, _From, State) -> {reply, do_load_tables(Dir, Tables), State}; @@ -68,13 +88,23 @@ handle_call({unload_tables, Dir, Tables}, _From, State) -> {reply, do_unload_tables(Dir, Tables), State}. %% unused callbacks + +%% @private handle_cast(_Msg, State) -> {noreply, State}. + +%% @private handle_info(_Info, State) -> {noreply, State}. + +%% @private terminate(_Reason, _State) -> ok. + +%% @private code_change(_OldVsn, _NewVsn, _State) -> {ok, state}. -%% ------------------------------------------------------------------------------------------ -%% -- gen_server callbacks +%%%========================================================================= +%%% internal functions +%%%========================================================================= + mapfile(A) when is_atom(A) -> mapfile(atom_to_list(A)); mapfile(A) when is_list(A) -> A ++ ".map". diff --git a/src/eradius_eap_packet.erl b/src/eradius_eap_packet.erl index 0f55cbd4..85176d61 100644 --- a/src/eradius_eap_packet.erl +++ b/src/eradius_eap_packet.erl @@ -8,32 +8,38 @@ %% -- gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-ignore_xref([start/0, lookup_type/1, register_type/2, unregister_type/1]). +-ignore_xref([decode/1, encode/3, decode_eap_type/2, encode_eap_type/1]). + -behaviour(gen_server). --define(NAME, ?MODULE). +-define(SERVER, ?MODULE). %% ------------------------------------------------------------------------------------------ %% -- API start() -> - gen_server:start({local, ?NAME}, ?MODULE, []). + gen_server:start({local, ?SERVER}, ?MODULE, [], []). %% @doc lookup the handler module for an extended EAP type lookup_type(Type) -> - ets:lookup(?NAME, Type). + case ets:lookup(?SERVER, Type) of + [{Type, Module}] -> {ok, Module}; + _ -> false + end. %% @doc register the handler module for an extended EAP type register_type(Type, Module) -> - gen_server:call(?NAME, {register, Type, Module}). + gen_server:call(?SERVER, {register, Type, Module}). %% @doc unregister the handler module for an extended EAP type unregister_type(Type) -> - gen_server:call(?NAME, {unregister, Type}). + gen_server:call(?SERVER, {unregister, Type}). %% ------------------------------------------------------------------------------------------ %% -- gen_server callbacks %% @private init([]) -> - Table = ets:new(?NAME, [ordered_set, protected, named_table, {read_concurrency, true}]), + Table = ets:new(?SERVER, [ordered_set, protected, named_table, {read_concurrency, true}]), {ok, Table}. %% @private @@ -71,7 +77,7 @@ decode(<<Code:8, Id:8, Len:16, Rest/binary>>) -> encode(Code, Id, Msg) -> Data = encode_payload(Code, Msg), Len = size(Data) + 4, - <<Code:8, Id:8, Len:16, Data/binary>>. + <<(code(Code)):8, Id:8, Len:16, Data/binary>>. do_decode_payload(Code, Id, Data) -> try @@ -131,7 +137,7 @@ decode_eap_type({0, 3}, Data) -> decode_eap_type(Type, Data) -> case lookup_type(Type) of - [Module] -> + {ok, Module} -> Module:decode_eap_type(Type, Data); _ -> {Type, Data} end. @@ -173,7 +179,7 @@ encode_eap_type({otp, Data}) %% 6 Generic Token Card (GTC) encode_eap_type({gtc, Data}) when is_binary(Data) -> - <<6:8, Data>>; + <<6:8, Data/binary>>; %% 254 Expanded Types encode_eap_type({{Vendor, Type}, Data}) @@ -192,7 +198,7 @@ encode_eap_type({nak_ext, Data}) encode_eap_type(Msg) when is_tuple(Msg) -> - [Module] = lookup_type(element(1, Msg)), + {ok, Module} = lookup_type(element(1, Msg)), Module:encode_eap_type(Msg). code(1) -> request; @@ -206,4 +212,3 @@ code(success) -> 3; code(failure) -> 4; code(_) -> error. - diff --git a/src/eradius_lib.erl b/src/eradius_lib.erl index 650cbde3..9840b341 100644 --- a/src/eradius_lib.erl +++ b/src/eradius_lib.erl @@ -1,501 +1,24 @@ +%% Copyright (c) 2002-2007, Martin Björklund and Torbjörn Törnkvist +%% Copyright (c) 2011, Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT +%% +%% @private -module(eradius_lib). --export([del_attr/2, get_attr/2, encode_request/1, encode_reply/1, decode_request/2, decode_request/3, decode_request_id/1]). --export([random_authenticator/0, zero_authenticator/0, pad_to/2, set_attr/3, get_attributes/1, set_attributes/2]). --export([timestamp/0, timestamp/1, printable_peer/2, make_addr_info/1]). --export_type([command/0, secret/0, authenticator/0, attribute_list/0]). + +-export([pad_to/2]). +-export([printable_peer/1, printable_peer/2]). %% -compile(bin_opt_info). --ifdef(TEST). --export([encode_value/2, decode_value/2, scramble/3, ascend/3]). --export([salt_encrypt/4, salt_decrypt/3, encode_attribute/3, decode_attribute/5]). --endif. -include("eradius_lib.hrl"). -include("eradius_dict.hrl"). --type command() :: 'request' | 'accept' | 'challenge' | 'reject' | 'accreq' | 'accresp' | 'coareq' | 'coaack' | 'coanak' | 'discreq' | 'discack' | 'discnak'. --type secret() :: binary(). --type authenticator() :: <<_:128>>. --type salt() :: binary(). --type attribute_list() :: list({eradius_dict:attribute(), term()}). - --define(IS_ATTR(Key, Attr), ?IS_KEY(Key, element(1, Attr))). -define(IS_KEY(Key, Attr), ((is_record(Attr, attribute) andalso (element(2, Attr) == Key)) orelse (Attr == Key)) ). %% ------------------------------------------------------------------------------------------ %% -- Request Accessors --spec random_authenticator() -> authenticator(). -random_authenticator() -> crypto:strong_rand_bytes(16). - --spec zero_authenticator() -> authenticator(). -zero_authenticator() -> <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>>. - --spec set_attributes(#radius_request{}, attribute_list()) -> #radius_request{}. -set_attributes(Req = #radius_request{attrs = Attrs}, NewAttrs) -> - Req#radius_request{attrs = NewAttrs ++ Attrs}. - --spec get_attributes(#radius_request{}) -> attribute_list(). -get_attributes(#radius_request{attrs = Attrs}) -> - Attrs. - --spec set_attr(#radius_request{}, eradius_dict:attribute_id(), eradius_dict:attr_value()) -> #radius_request{}. -set_attr(Req = #radius_request{attrs = Attrs}, Id, Val) -> - Req#radius_request{attrs = [{Id, Val} | Attrs]}. - --spec get_attr(#radius_request{}, eradius_dict:attribute_id()) -> eradius_dict:attr_value() | undefined. -get_attr(#radius_request{attrs = Attrs}, Id) -> - get_attr_loop(Id, Attrs). - -del_attr(Req = #radius_request{attrs = Attrs}, Id) -> - Req#radius_request{attrs = lists:reverse(lists:foldl(fun(Attr, Acc) when ?IS_ATTR(Id, Attr) -> Acc; - (Attr, Acc) -> [Attr | Acc] - end, [], Attrs))}. - -get_attr_loop(Key, [{Id, Val}|_T]) when ?IS_KEY(Key, Id) -> Val; -get_attr_loop(Key, [_|T]) -> get_attr_loop(Key, T); -get_attr_loop(_, []) -> undefined. - -%% ------------------------------------------------------------------------------------------ -%% -- Wire Encoding - -%% @doc Convert a RADIUS request to the wire format. -%% The Message-Authenticator MUST be used in Access-Request that include an EAP-Message attribute [RFC 3579]. --spec encode_request(#radius_request{}) -> {binary(), binary()}. -encode_request(Req = #radius_request{reqid = ReqID, cmd = Command, attrs = Attributes}) when (Command == request) -> - Authenticator = random_authenticator(), - Req1 = Req#radius_request{authenticator = Authenticator}, - EncReq1 = encode_attributes(Req1, Attributes), - EncReq2 = encode_eap_message(Req1, EncReq1), - {Body, BodySize} = encode_message_authenticator(Req1, EncReq2), - {Authenticator, <<(encode_command(Command)):8, ReqID:8, (BodySize + 20):16, Authenticator:16/binary, Body/binary>>}; -encode_request(Req = #radius_request{reqid = ReqID, cmd = Command, attrs = Attributes}) -> - {Body, BodySize} = encode_attributes(Req, Attributes), - Head = <<(encode_command(Command)):8, ReqID:8, (BodySize + 20):16>>, - Authenticator = crypto:hash(md5, [Head, zero_authenticator(), Body, Req#radius_request.secret]), - {Authenticator, <<Head/binary, Authenticator:16/binary, Body/binary>>}. - -%% @doc Convert a RADIUS reply to the wire format. -%% This function performs the same task as {@link encode_request/2}, -%% except that it includes the authenticator substitution required for replies. -%% The Message-Authenticator MUST be used in Access-Accept, Access-Reject or Access-Chalange -%% replies that includes an EAP-Message attribute [RFC 3579]. --spec encode_reply(#radius_request{}) -> binary(). -encode_reply(Req = #radius_request{reqid = ReqID, cmd = Command, authenticator = RequestAuthenticator, attrs = Attributes}) -> - EncReq1 = encode_attributes(Req, Attributes), - EncReq2 = encode_eap_message(Req, EncReq1), - {Body, BodySize} = encode_message_authenticator(Req, EncReq2), - Head = <<(encode_command(Command)):8, ReqID:8, (BodySize + 20):16>>, - ReplyAuthenticator = crypto:hash(md5, [Head, <<RequestAuthenticator:16/binary>>, Body, Req#radius_request.secret]), - <<Head/binary, ReplyAuthenticator:16/binary, Body/binary>>. - --spec encode_command(command()) -> byte(). -encode_command(request) -> ?RAccess_Request; -encode_command(accept) -> ?RAccess_Accept; -encode_command(challenge) -> ?RAccess_Challenge; -encode_command(reject) -> ?RAccess_Reject; -encode_command(accreq) -> ?RAccounting_Request; -encode_command(accresp) -> ?RAccounting_Response; -encode_command(coareq) -> ?RCoa_Request; -encode_command(coaack) -> ?RCoa_Ack; -encode_command(coanak) -> ?RCoa_Nak; -encode_command(discreq) -> ?RDisconnect_Request; -encode_command(discack) -> ?RDisconnect_Ack; -encode_command(discnak) -> ?RDisconnect_Nak. - --spec encode_message_authenticator(#radius_request{}, {binary(), non_neg_integer()}) -> {binary(), non_neg_integer()}. -encode_message_authenticator(_Req = #radius_request{msg_hmac = false}, Request) -> - Request; -encode_message_authenticator(Req = #radius_request{reqid = ReqID, cmd = Command, authenticator = Authenticator, msg_hmac = true}, {Body, BodySize}) -> - Head = <<(encode_command(Command)):8, ReqID:8, (BodySize + 20 + 2 +16):16>>, - ReqAuth = <<Authenticator:16/binary>>, - HMAC = message_authenticator(Req#radius_request.secret, [Head, ReqAuth, Body, <<?RMessage_Authenticator,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>>]), - {<<Body/binary, ?RMessage_Authenticator, 18, HMAC/binary>>, BodySize + 2 + 16}. - -chunk(Bin, Length) -> - case Bin of - <<First:Length/bytes, Rest/binary>> -> {First, Rest}; - _ -> {Bin, <<>>} - end. - -encode_eap_attribute({<<>>, _}, EncReq) -> - EncReq; -encode_eap_attribute({Value, Rest}, {Body, BodySize}) -> - EncAttr = <<?REAP_Message, (byte_size(Value) + 2):8, Value/binary>>, - EncReq = {<<Body/binary, EncAttr/binary>>, BodySize + byte_size(EncAttr)}, - encode_eap_attribute(chunk(Rest, 253), EncReq). - --spec encode_eap_message(#radius_request{}, {binary(), non_neg_integer()}) -> {binary(), non_neg_integer()}. -encode_eap_message(#radius_request{eap_msg = EAP}, EncReq) - when is_binary(EAP); size(EAP) > 0 -> - encode_eap_attribute(chunk(EAP, 253), EncReq); -encode_eap_message(#radius_request{eap_msg = <<>>}, EncReq) -> - EncReq. - --spec encode_attributes(#radius_request{}, attribute_list()) -> {binary(), non_neg_integer()}. -encode_attributes(Req, Attributes) -> - F = fun ({A = #attribute{}, Val}, {Body, BodySize}) -> - EncAttr = encode_attribute(Req, A, Val), - {<<Body/binary, EncAttr/binary>>, BodySize + byte_size(EncAttr)}; - ({ID, Val}, {Body, BodySize}) -> - case eradius_dict:lookup(attribute, ID) of - AttrRec = #attribute{} -> - EncAttr = encode_attribute(Req, AttrRec, Val), - {<<Body/binary, EncAttr/binary>>, BodySize + byte_size(EncAttr)}; - _ -> - {Body, BodySize} - end - end, - lists:foldl(F, {<<>>, 0}, Attributes). - --spec encode_attribute(#radius_request{}, #attribute{}, term()) -> binary(). -encode_attribute(_Req, _Attr = #attribute{id = ?RMessage_Authenticator}, _) -> - %% message authenticator is handled through the msg_hmac flag - <<>>; -encode_attribute(_Req, _Attr = #attribute{id = ?REAP_Message}, _) -> - %% EAP-Message attributes are handled through the eap_msg field - <<>>; -encode_attribute(Req, Attr = #attribute{id = {Vendor, ID}}, Value) -> - EncValue = encode_attribute(Req, Attr#attribute{id = ID}, Value), - if byte_size(EncValue) + 6 > 255 -> - error(badarg, [{Vendor, ID}, Value]); - true -> ok - end, - <<?RVendor_Specific:8, (byte_size(EncValue) + 6):8, Vendor:32, EncValue/binary>>; -encode_attribute(Req, #attribute{type = {tagged, Type}, id = ID, enc = Enc}, Value) -> - case Value of - {Tag, UntaggedValue} when Tag >= 1, Tag =< 16#1F -> ok; - UntaggedValue -> Tag = 0 - end, - EncValue = encrypt_value(Req, encode_value(Type, UntaggedValue), Enc), - if byte_size(EncValue) + 3 > 255 -> - error(badarg, [ID, Value]); - true -> ok - end, - <<ID, (byte_size(EncValue) + 3):8, Tag:8, EncValue/binary>>; -encode_attribute(Req, #attribute{type = Type, id = ID, enc = Enc}, Value)-> - EncValue = encrypt_value(Req, encode_value(Type, Value), Enc), - if byte_size(EncValue) + 2 > 255 -> - error(badarg, [ID, Value]); - true -> ok - end, - <<ID, (byte_size(EncValue) + 2):8, EncValue/binary>>. - --spec encrypt_value(#radius_request{}, binary(), eradius_dict:attribute_encryption()) -> binary(). -encrypt_value(Req, Val, scramble) -> scramble(Req#radius_request.secret, Req#radius_request.authenticator, Val); -encrypt_value(Req, Val, salt_crypt) -> salt_encrypt(generate_salt(), Req#radius_request.secret, Req#radius_request.authenticator, Val); -encrypt_value(Req, Val, ascend) -> ascend(Req#radius_request.secret, Req#radius_request.authenticator, Val); -encrypt_value(_Req, Val, no) -> Val. - --spec encode_value(eradius_dict:attribute_prim_type(), term()) -> binary(). -encode_value(_, V) when is_binary(V) -> - V; -encode_value(binary, V) -> - V; -encode_value(integer, V) -> - <<V:32>>; -encode_value(integer24, V) -> - <<V:24>>; -encode_value(integer64, V) -> - <<V:64>>; -encode_value(ipaddr, {A,B,C,D}) -> - <<A:8, B:8, C:8, D:8>>; -encode_value(ipv6addr, {A,B,C,D,E,F,G,H}) -> - <<A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>; -encode_value(ipv6prefix, {{A,B,C,D,E,F,G,H}, PLen}) -> - L = (PLen + 7) div 8, - <<IP:L/bytes, _R/binary>> = <<A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>, - <<0, PLen, IP/binary>>; -encode_value(string, V) when is_list(V) -> - unicode:characters_to_binary(V); -encode_value(octets, V) when is_list(V) -> - iolist_to_binary(V); -encode_value(octets, V) when is_integer(V) -> - <<V:32>>; -encode_value(date, V) when is_list(V) -> - unicode:characters_to_binary(V); -encode_value(date, Date = {{_,_,_},{_,_,_}}) -> - EpochSecs = calendar:datetime_to_gregorian_seconds(Date) - calendar:datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}}), - <<EpochSecs:32>>. - -%% ------------------------------------------------------------------------------------------ -%% -- Wire Decoding - --spec decode_request_id(binary()) -> {0..255, binary()} | {bad_pdu, list()}. -decode_request_id(Req = <<_Cmd:8, ReqId:8, _Rest/binary>>) -> {ReqId, Req}; -decode_request_id(_Req) -> {bad_pdu, "invalid request id"}. - --spec decode_request(binary(), secret()) -> #radius_request{} | {bad_pdu, list()}. -decode_request(Packet, Secret) -> - decode_request(Packet, Secret, undefined). - --spec decode_request(binary(), secret(), authenticator()) -> #radius_request{} | {bad_pdu, list()}. -decode_request(Packet, Secret, Authenticator) -> - case (catch decode_request0(Packet, Secret, Authenticator)) of - {'EXIT', _} -> {bad_pdu, "decode packet error"}; - Else -> Else - end. - --spec decode_request0(binary(), secret(), authenticator() | 'undefined') -> #radius_request{}. -decode_request0(<<Cmd:8, ReqId:8, Len:16, PacketAuthenticator:16/binary, Body0/binary>>, Secret, RequestAuthenticator) -> - ActualBodySize = byte_size(Body0), - GivenBodySize = Len - 20, - Body = if - ActualBodySize > GivenBodySize -> - throw({bad_pdu, "false packet size"}); - ActualBodySize == GivenBodySize -> - Body0; - true -> - binary:part(Body0, 0, GivenBodySize) - end, - Command = decode_command(Cmd), - PartialRequest = #radius_request{cmd = Command, reqid = ReqId, authenticator = PacketAuthenticator, secret = Secret, msg_hmac = false}, - DecodedState = decode_attributes(PartialRequest, RequestAuthenticator, Body), - Request = PartialRequest#radius_request{attrs = lists:reverse(DecodedState#decoder_state.attrs), - eap_msg = list_to_binary(lists:reverse(DecodedState#decoder_state.eap_msg))}, - validate_authenticator(Command, <<Cmd:8, ReqId:8, Len:16>>, RequestAuthenticator, PacketAuthenticator, Body, Secret), - if - is_integer(DecodedState#decoder_state.hmac_pos) -> - validate_packet_authenticator(Cmd, ReqId, Len, Body, DecodedState#decoder_state.hmac_pos, Secret, PacketAuthenticator, RequestAuthenticator), - Request#radius_request{msg_hmac = true}; - true -> Request - end. - --spec validate_packet_authenticator(non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer(), binary(), binary(), authenticator(), authenticator() | 'undefined') -> ok. -validate_packet_authenticator(Cmd, ReqId, Len, Body, Pos, Secret, PacketAuthenticator, undefined) -> - validate_packet_authenticator(Cmd, ReqId, Len, PacketAuthenticator, Body, Pos, Secret); -validate_packet_authenticator(Cmd, ReqId, Len, Body, Pos, Secret, _PacketAuthenticator, RequestAuthenticator) -> - validate_packet_authenticator(Cmd, ReqId, Len, RequestAuthenticator, Body, Pos, Secret). - --spec validate_packet_authenticator(non_neg_integer(), non_neg_integer(), non_neg_integer(), authenticator(), non_neg_integer(), binary(), binary()) -> ok. -validate_packet_authenticator(Cmd, ReqId, Len, Auth, Body, Pos, Secret) -> - case Body of - <<Before:Pos/bytes, Value:16/bytes, After/binary>> -> - case message_authenticator(Secret, [<<Cmd:8, ReqId:8, Len:16>>, Auth, Before, zero_authenticator(), After]) of - Value -> - ok; - _ -> - throw({bad_pdu, "Message-Authenticator Attribute is invalid"}) - end; - _ -> - throw({bad_pdu, "Message-Authenticator Attribute is malformed"}) - end. - -validate_authenticator(accreq, Head, _RequestAuthenticator, PacketAuthenticator, Body, Secret) -> - compare_authenticator(crypto:hash(md5, [Head, zero_authenticator(), Body, Secret]), PacketAuthenticator); -validate_authenticator(Cmd, Head, RequestAuthenticator, PacketAuthenticator, Body, Secret) - when - (Cmd =:= accept) orelse - (Cmd =:= reject) orelse - (Cmd =:= accresp) orelse - (Cmd =:= coaack) orelse - (Cmd =:= coanak) orelse - (Cmd =:= discack) orelse - (Cmd =:= discnak) orelse - (Cmd =:= challenge) -> - compare_authenticator(crypto:hash(md5, [Head, RequestAuthenticator, Body, Secret]), PacketAuthenticator); -validate_authenticator(_Cmd, _Head, _RequestAuthenticator, _PacketAuthenticator, - _Body, _Secret) -> - true. - -compare_authenticator(Authenticator, Authenticator) -> - true; -compare_authenticator(_RequestAuthenticator, _PacketAuthenticator) -> - throw({bad_pdu, "Authenticator Attribute is invalid"}). - --spec decode_command(byte()) -> command(). -decode_command(?RAccess_Request) -> request; -decode_command(?RAccess_Accept) -> accept; -decode_command(?RAccess_Reject) -> reject; -decode_command(?RAccess_Challenge) -> challenge; -decode_command(?RAccounting_Request) -> accreq; -decode_command(?RAccounting_Response) -> accresp; -decode_command(?RCoa_Request) -> coareq; -decode_command(?RCoa_Ack) -> coaack; -decode_command(?RCoa_Nak) -> coanak; -decode_command(?RDisconnect_Request) -> discreq; -decode_command(?RDisconnect_Ack) -> discack; -decode_command(?RDisconnect_Nak) -> discnak; -decode_command(_) -> error({bad_pdu, "unknown request type"}). - -append_attr(Attr, State) -> - State#decoder_state{attrs = [Attr | State#decoder_state.attrs]}. - --spec decode_attributes(#radius_request{}, binary(), binary()) -> #decoder_state{}. -decode_attributes(Req, RequestAuthenticator, As) -> - decode_attributes(Req, As, 0, #decoder_state{request_authenticator = RequestAuthenticator}). - --spec decode_attributes(#radius_request{}, binary(), non_neg_integer(), #decoder_state{}) -> #decoder_state{}. -decode_attributes(_Req, <<>>, _Pos, State) -> - State; -decode_attributes(Req, <<Type:8, ChunkLength:8, ChunkRest/binary>>, Pos, State) -> - ValueLength = ChunkLength - 2, - <<Value:ValueLength/binary, PacketRest/binary>> = ChunkRest, - NewState = case eradius_dict:lookup(attribute, Type) of - AttrRec = #attribute{} -> - decode_attribute(Value, Req, AttrRec, Pos + 2, State); - _ -> - append_attr({Type, Value}, State) - end, - decode_attributes(Req, PacketRest, Pos + ChunkLength, NewState). - -%% gotcha: the function returns a LIST of attribute-value pairs because -%% a vendor-specific attribute blob might contain more than one attribute. --spec decode_attribute(binary(), #radius_request{}, #attribute{}, non_neg_integer(), #decoder_state{}) -> #decoder_state{}. -decode_attribute(<<VendorID:32/integer, ValueBin/binary>>, Req, #attribute{id = ?RVendor_Specific}, Pos, State) -> - decode_vendor_specific_attribute(Req, VendorID, ValueBin, Pos + 4, State); -decode_attribute(<<Value/binary>>, _Req, Attr = #attribute{id = ?REAP_Message}, _Pos, State) -> - NewState = State#decoder_state{eap_msg = [Value | State#decoder_state.eap_msg]}, - append_attr({Attr, Value}, NewState); -decode_attribute(<<EncValue/binary>>, Req, Attr = #attribute{id = ?RMessage_Authenticator, type = Type, enc = Encryption}, Pos, State) -> - append_attr({Attr, decode_value(decrypt_value(Req, State, EncValue, Encryption), Type)}, State#decoder_state{hmac_pos = Pos}); -decode_attribute(<<EncValue/binary>>, Req, Attr = #attribute{type = Type, enc = Encryption}, _Pos, State) when is_atom(Type) -> - append_attr({Attr, decode_value(decrypt_value(Req, State, EncValue, Encryption), Type)}, State); -decode_attribute(WholeBin = <<Tag:8, Bin/binary>>, Req, Attr = #attribute{type = {tagged, Type}}, _Pos, State) -> - case {decode_tag_value(Tag), Attr#attribute.enc} of - {0, no} -> - %% decode including tag byte if tag is out of range - append_attr({Attr, {0, decode_value(WholeBin, Type)}}, State); - {TagV, no} -> - append_attr({Attr, {TagV, decode_value(Bin, Type)}}, State); - {TagV, Encryption} -> - %% for encrypted attributes, tag byte is never part of the value - append_attr({Attr, {TagV, decode_value(decrypt_value(Req, State, Bin, Encryption), Type)}}, State) - end. - --compile({inline, decode_tag_value/1}). -decode_tag_value(Tag) when (Tag >= 1) and (Tag =< 16#1F) -> Tag; -decode_tag_value(_OtherTag) -> 0. - --spec decode_value(binary(), eradius_dict:attribute_prim_type()) -> term(). -decode_value(<<Bin/binary>>, Type) -> - case Type of - octets -> - Bin; - binary -> - Bin; - abinary -> - Bin; - string -> - Bin; - integer -> - decode_integer(Bin); - integer24 -> - decode_integer(Bin); - integer64 -> - decode_integer(Bin); - date -> - case decode_integer(Bin) of - Int when is_integer(Int) -> - calendar:now_to_universal_time({Int div 1000000, Int rem 1000000, 0}); - _ -> - Bin - end; - ipaddr -> - <<B,C,D,E>> = Bin, - {B,C,D,E}; - ipv6addr -> - <<B:16,C:16,D:16,E:16,F:16,G:16,H:16,I:16>> = Bin, - {B,C,D,E,F,G,H,I}; - ipv6prefix -> - <<0,PLen,P/binary>> = Bin, - <<B:16,C:16,D:16,E:16,F:16,G:16,H:16,I:16>> = pad_to(16, P), - {{B,C,D,E,F,G,H,I}, PLen} - end. - --compile({inline, decode_integer/1}). -decode_integer(Bin) -> - ISize = bit_size(Bin), - case Bin of - <<Int:ISize/integer>> -> Int; - _ -> Bin - end. - --spec decrypt_value(#radius_request{}, #decoder_state{}, binary(), - eradius_dict:attribute_encryption()) -> eradius_dict:attr_value(). -decrypt_value(#radius_request{secret = Secret, authenticator = Authenticator}, - _, <<Val/binary>>, scramble) -> - scramble(Secret, Authenticator, Val); -decrypt_value(#radius_request{secret = Secret}, - #decoder_state{request_authenticator = RequestAuthenticator}, - <<Val/binary>>, salt_crypt) - when is_binary(RequestAuthenticator) -> - salt_decrypt(Secret, RequestAuthenticator, Val); -decrypt_value(#radius_request{secret = Secret, authenticator = Authenticator}, - _, <<Val/binary>>, ascend) -> - ascend(Secret, Authenticator, Val); -decrypt_value(_Req, _State, <<Val/binary>>, _Type) -> - Val. - --spec decode_vendor_specific_attribute(#radius_request{}, non_neg_integer(), binary(), non_neg_integer(), #decoder_state{}) -> #decoder_state{}. -decode_vendor_specific_attribute(_Req, _VendorID, <<>>, _Pos, State) -> - State; -decode_vendor_specific_attribute(Req, VendorID, <<Type:8, ChunkLength:8, ChunkRest/binary>>, Pos, State) -> - ValueLength = ChunkLength - 2, - <<Value:ValueLength/binary, PacketRest/binary>> = ChunkRest, - VendorAttrKey = {VendorID, Type}, - NewState = case eradius_dict:lookup(attribute, VendorAttrKey) of - Attr = #attribute{} -> - decode_attribute(Value, Req, Attr, Pos + 2, State); - _ -> - append_attr({VendorAttrKey, Value}, State) - end, - decode_vendor_specific_attribute(Req, VendorID, PacketRest, Pos + ChunkLength, NewState). - -%% ------------------------------------------------------------------------------------------ -%% -- Attribute Encryption --spec scramble(secret(), authenticator(), binary()) -> binary(). -scramble(SharedSecret, RequestAuthenticator, <<PlainText/binary>>) -> - B = crypto:hash(md5, [SharedSecret, RequestAuthenticator]), - do_scramble(SharedSecret, B, pad_to(16, PlainText), << >>). - -do_scramble(SharedSecret, B, <<PlainText:16/binary, Remaining/binary>>, CipherText) -> - NewCipherText = crypto:exor(PlainText, B), - Bnext = crypto:hash(md5, [SharedSecret, NewCipherText]), - do_scramble(SharedSecret, Bnext, Remaining, <<CipherText/binary, NewCipherText/binary>>); - -do_scramble(_SharedSecret, _B, << >>, CipherText) -> - CipherText. - --spec generate_salt() -> salt(). -generate_salt() -> - <<Salt1, Salt2>> = crypto:strong_rand_bytes(2), - <<(Salt1 bor 16#80), Salt2>>. - --spec salt_encrypt(salt(), secret(), authenticator(), binary()) -> binary(). -salt_encrypt(Salt, SharedSecret, RequestAuthenticator, PlainText) -> - CipherText = do_salt_crypt(encrypt, Salt, SharedSecret, RequestAuthenticator, (pad_to(16, << (byte_size(PlainText)):8, PlainText/binary >>))), - <<Salt/binary, CipherText/binary>>. - --spec salt_decrypt(secret(), authenticator(), binary()) -> binary(). -salt_decrypt(SharedSecret, RequestAuthenticator, <<Salt:2/binary, CipherText/binary>>) -> - << Length:8/integer, PlainText/binary >> = do_salt_crypt(decrypt, Salt, SharedSecret, RequestAuthenticator, CipherText), - if - Length < byte_size(PlainText) -> - binary:part(PlainText, 0, Length); - true -> - PlainText - end. - -do_salt_crypt(Op, Salt, SharedSecret, RequestAuthenticator, <<CipherText/binary>>) -> - B = crypto:hash(md5, [SharedSecret, RequestAuthenticator, Salt]), - salt_crypt(Op, SharedSecret, B, CipherText, << >>). - -salt_crypt(Op, SharedSecret, B, <<PlainText:16/binary, Remaining/binary>>, CipherText) -> - NewCipherText = crypto:exor(PlainText, B), - Bnext = case Op of - decrypt -> crypto:hash(md5, [SharedSecret, PlainText]); - encrypt -> crypto:hash(md5, [SharedSecret, NewCipherText]) - end, - salt_crypt(Op, SharedSecret, Bnext, Remaining, <<CipherText/binary, NewCipherText/binary>>); - -salt_crypt(_Op, _SharedSecret, _B, << >>, CipherText) -> - CipherText. - --spec ascend(secret(), authenticator(), binary()) -> binary(). -ascend(SharedSecret, RequestAuthenticator, <<PlainText/binary>>) -> - Digest = crypto:hash(md5, [RequestAuthenticator, SharedSecret]), - crypto:exor(Digest, pad_to(16, PlainText)). %% @doc pad binary to specific length %% See <a href="http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html"> @@ -508,50 +31,18 @@ pad_to(Width, Binary) -> N -> <<Binary/binary, 0:(N*8)>> end. --spec timestamp() -> erlang:timestamp(). -timestamp() -> - erlang:system_time(milli_seconds). - -timestamp(Units) -> - erlang:system_time(Units). - --spec make_addr_info({term(), {inet:ip_address(), integer()}}) -> atom_address(). -make_addr_info({undefined, {IP, Port}}) -> - {socket_to_atom(IP, Port), ip_to_atom(IP), port_to_atom(Port)}; -make_addr_info({Name, {IP, Port}}) -> - {to_atom(Name), ip_to_atom(IP), port_to_atom(Port)}. - -to_atom(Value) when is_atom(Value) -> Value; -to_atom(Value) when is_binary(Value) -> binary_to_atom(Value, latin1); -to_atom(Value) when is_list(Value) -> list_to_atom(Value). - -socket_to_atom(IP, undefined) -> - ip_to_atom(IP); -socket_to_atom(IP, Port) when is_tuple(IP) -> - list_to_atom(inet:ntoa(IP) ++ ":" ++ integer_to_list(Port)); -socket_to_atom(IP, Port) when is_binary(IP) -> - binary_to_atom(erlang:iolist_to_binary([IP, <<":">>, Port]), latin1); -socket_to_atom(IP, Port) when is_atom(IP) -> - binary_to_atom(erlang:iolist_to_binary([atom_to_binary(IP, latin1), <<":">>, Port]), latin1). - -ip_to_atom(IP) when is_atom(IP) -> IP; -ip_to_atom(IP) -> list_to_atom(inet:ntoa(IP)). - -port_to_atom(undefined) -> undefined; -port_to_atom(Port) when is_atom(Port) -> Port; -port_to_atom(Port) -> list_to_atom(integer_to_list(Port)). - --spec printable_peer(inet:ip4_address(),eradius_server:port_number()) -> io_lib:chars(). -printable_peer({IA,IB,IC,ID}, Port) -> - io_lib:format("~b.~b.~b.~b:~b",[IA,IB,IC,ID,Port]). - -%% @doc calculate the MD5 message authenticator --if(?OTP_RELEASE >= 23). -%% crypto API changes in OTP >= 23 -message_authenticator(Secret, Msg) -> - crypto:mac(hmac, md5, Secret, Msg). --else. -message_authenticator(Secret, Msg) -> - crypto:hmac(md5, Secret, Msg). - --endif. +-spec printable_peer(server_name() | {inet:ip_address() | any, inet:port_number()}) -> io_lib:chars(). +printable_peer(Atom) when is_atom(Atom) -> + [atom_to_list(Atom)]; +printable_peer(Binary) when is_binary(Binary) -> + [Binary]; +printable_peer({IP, Port}) -> + printable_peer(IP, Port). + +-spec printable_peer(inet:ip_address() | any, inet:port_number()) -> io_lib:chars(). +printable_peer(any, Port) -> + ["any:", integer_to_list(Port)]; +printable_peer({_, _, _, _} = IP, Port) -> + [inet:ntoa(IP), $:, integer_to_list(Port)]; +printable_peer({_, _, _, _, _, _, _, _} = IP, Port) -> + [$[, inet:ntoa(IP), $], $:, integer_to_list(Port)]. diff --git a/src/eradius_log.erl b/src/eradius_log.erl index f2974fc4..21b19c11 100644 --- a/src/eradius_log.erl +++ b/src/eradius_log.erl @@ -1,149 +1,99 @@ -%% Copyright (c) 2010-2011 by Travelping GmbH <info@travelping.com> - -%% Permission is hereby granted, free of charge, to any person obtaining a -%% copy of this software and associated documentation files (the "Software"), -%% to deal in the Software without restriction, including without limitation -%% the rights to use, copy, modify, merge, publish, distribute, sublicense, -%% and/or sell copies of the Software, and to permit persons to whom the -%% Software is furnished to do so, subject to the following conditions: - -%% The above copyright notice and this permission notice shall be included in -%% all copies or substantial portions of the Software. - -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -%% DEALINGS IN THE SOFTWARE. - -%% @private +%% Copyright (c) 2010, 2011, Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT +%% -module(eradius_log). --behaviour(gen_server). - %% API --export([start_link/0, write_request/2, collect_meta/2, collect_message/2, reconfigure/0]). +-export([update_logger_process_metadata/1, line/1]). +-export([collect_meta/1, format_req/1]). -export([bin_to_hexstr/1, format_cmd/1]). -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-ignore_xref([?MODULE]). -include_lib("kernel/include/logger.hrl"). -include("eradius_lib.hrl"). -include("eradius_dict.hrl"). -include("dictionary.hrl"). --type sender() :: {inet:ip_address(), eradius_server:port_number(), eradius_server:req_id()}. - --define(SERVER, ?MODULE). - %%%=================================================================== %%% API %%%=================================================================== --spec start_link() -> {ok, pid()} | {error, Reason :: term}. -start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). - --spec write_request(sender(), #radius_request{}) -> ok. -write_request(Sender, Request = #radius_request{}) -> - case application:get_env(eradius, logging) of - {ok, true} -> - Time = calendar:universal_time(), - gen_server:cast(?SERVER, {write_request, Time, Sender, Request}); - _ -> - ok - end. - --spec collect_meta(sender(),#radius_request{}) -> [{term(),term()}]. -collect_meta({_NASIP, _NASPort, ReqID}, Request) -> - Request_Type = binary_to_list(format_cmd(Request#radius_request.cmd)), - Request_ID = integer_to_list(ReqID), - Attrs = Request#radius_request.attrs, - [{request_type, Request_Type},{request_id, Request_ID}|[collect_attr(Key, Val) || {Key, Val} <- Attrs]]. - --spec collect_message(sender(),#radius_request{}) -> iolist(). -collect_message({NASIP, NASPort, ReqID}, Request) -> - StatusType = format_acct_status_type(Request), - io_lib:format("~s:~p [~p]: ~s ~s",[inet:ntoa(NASIP), NASPort, ReqID, format_cmd(Request#radius_request.cmd), StatusType]). - --spec reconfigure() -> ok. -reconfigure() -> - gen_server:call(?SERVER, reconfigure). - -%%%=================================================================== -%%% gen_server callbacks -%%%=================================================================== -init(_) -> {ok, init_logger()}. - -handle_call(reconfigure, _From, State) -> - file:close(State), - {reply, ok, init_logger()}; - -%% for tests -handle_call(get_state, _From, State) -> - {reply, State, State}; - -handle_call(_Request, _From, State) -> - {reply, ok, State}. - -handle_cast({write_request, _Time, _Sender, _Request}, logger_disabled = State) -> - {noreply, State}; - -handle_cast({write_request, Time, Sender, Request}, State) -> - try - Msg = format_message(Time, Sender, Request), - ok = io:put_chars(State, Msg), - {noreply, State} - catch - _:Error -> - ?LOG(error, "Failed to log RADIUS request: error: ~p, request: ~p, sender: ~p, " - "logging will be disabled", [Error, Request, Sender]), - {noreply, logger_disabled} - end. -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, Fd) -> - file:close(Fd), - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. +%% @doc copy RADIUS AVPs into `m:logger' metadata. +%% +%% Helper function for use in a RADIUS server handler that will +%% copy all RADIUS attributes into `m:logger' metadata. +%% +%% <blockquote><h4 class="warning">WARNING</h4> +%% The function will format all attribute values to strings, +%% the resulting processing load can be significant. +%% </blockquote> +-spec update_logger_process_metadata(eradius_req:req()) -> ok. +update_logger_process_metadata(Req) -> + Metadata = maps:from_list(collect_meta(Req)), + logger:update_process_metadata(Metadata). + +%% @doc Serialize `t:eradius_req:req/0' object into a proplist. +%% +%% Helper function for use in a RADIUS server handler that will +%% serialize a RADIUS `t:eradius_req:req/0' object into a Key/Value lists. +%% +%% All values will be converted to human readable strings. +-spec collect_meta(eradius_req:req()) -> [{term(), term()}]. +collect_meta(#{cmd := Cmd, req_id := ReqId, attrs := Attrs}) -> + RequestType = binary_to_list(format_cmd(Cmd)), + RequestId = integer_to_list(ReqId), + [{request_type, RequestType}, + {request_id, RequestId}| + [collect_attr(Key, Val) || {Key, Val} <- Attrs]]. + +%% @doc Format `t:eradius_req:req/0' object into a RADIUS short log entry +%% +%% The short log format is not part of any RADIUS RFC, but has been +%% used by many RADIUS server implementations. +%% +%% The format is: `<Client-IP>:<Client-Port> [<Request-Id>]: <Command> [AcctStatusType]' +-spec line(eradius_req:req()) -> iolist(). +line(#{cmd := Cmd, req_id := ReqId, server_addr := {IP, Port}} = Req) -> + StatusType = format_acct_status_type(Req), + io_lib:format("~s:~p [~p]: ~s ~s", [inet:ntoa(IP), Port, ReqId, format_cmd(Cmd), StatusType]). + +%% @doc Format `t:eradius_req:req/0' object into a RADIUS log entry +%% +%% The long log format is not part of any RADIUS RFC, but has been +%% used by many RADIUS server implementations in the past. +%% +%% The format is: +%% ``` +%% <TimeStamp> <Client-IP>:<Client-Port> [<Request-Id>] <Command> +%% [<Key> = <Value>]+ +%% ''' +-spec format_req(eradius_req:req()) -> binary(). +format_req(Req) -> + Time = + case Req of + #{arrival_time := ATime} -> ATime + erlang:time_offset(); + _ -> erlang:system_time() + end, + format_message(Time, Req). %%%=================================================================== %%% Internal functions %%%=================================================================== -%% -- init -init_logger() -> - case application:get_env(eradius, logging) of - {ok, true} -> init_logfile(); - _ -> logger_disabled - end. - -init_logfile() -> - {ok, LogFile} = application:get_env(eradius, logfile), - ok = filelib:ensure_dir(LogFile), - case file:open(LogFile, [append]) of - {ok, Fd} -> Fd; - Error -> - ?LOG(error, "Failed to open file ~p (~p)", [LogFile, Error]), - logger_disabled - end. %% -- formatting -format_message(Time, Sender, Request) -> +format_message(Time, #{cmd := Cmd} = Req) -> BinTStamp = radius_date(Time), - BinSender = format_sender(Sender), - BinCommand = format_cmd(Request#radius_request.cmd), - BinPacket = format_packet(Request), + BinSender = format_sender(Req), + BinCommand = format_cmd(Cmd), + BinPacket = format_packet(Req), <<BinTStamp/binary, " ", BinSender/binary, " ", BinCommand/binary, "\n", BinPacket/binary, "\n">>. -format_sender({NASIP, NASPort, ReqID}) -> - <<(format_ip(NASIP))/binary, $:, (i2b(NASPort))/binary, " [", (i2b(ReqID))/binary, $]>>. +format_sender(#{req_id := ReqId, server_addr := {IP, Port}}) -> + <<(format_ip(IP))/binary, $:, (i2b(Port))/binary, " [", (i2b(ReqId))/binary, $]>>. +%% @private format_cmd(request) -> <<"Access-Request">>; format_cmd(accept) -> <<"Access-Accept">>; format_cmd(reject) -> <<"Access-Reject">>; @@ -160,8 +110,7 @@ format_cmd(discnak) -> <<"Disconnect-Nak">>. format_ip(IP) -> list_to_binary(inet_parse:ntoa(IP)). -format_packet(Request) -> - Attrs = Request#radius_request.attrs, +format_packet(#{attrs := Attrs} = _Req) -> << <<(print_attr(Key, Val))/binary>> || {Key, Val} <- Attrs >>. print_attr(Key = #attribute{name = Attr, type = Type}, InVal) -> @@ -242,8 +191,10 @@ collectable_attr_value(_Attr, <<Val/binary>>) -> collectable_attr_value(_Attr, Val) -> io_lib:format("~p", [Val]). -radius_date({{YYYY,MM,DD},{Hour,Min,Sec}}) -> - DayNumber = calendar:day_of_the_week(YYYY, MM, DD), +radius_date(Time) -> + {{YYYY, MM, DD} = Date, {Hour, Min, Sec}} = + calendar:system_time_to_universal_time(Time, native), + DayNumber = calendar:day_of_the_week(Date), list_to_binary( io_lib:format("~s ~3.s ~2.2.0w ~2.2.0w:~2.2.0w:~2.2.0w ~4.4.0w", [day(DayNumber), month(MM), DD, Hour, Min, Sec, YYYY])). @@ -292,12 +243,13 @@ hexchar(X) when X >= 0, X < 10 -> hexchar(X) when X >= 10, X < 16 -> X + ($A - 10). +%% @private -compile({inline, bin_to_hexstr/1}). bin_to_hexstr(Bin) -> << << (hexchar(X)) >> || <<X:4>> <= Bin >>. -format_acct_status_type(Request) -> - StatusType = eradius_lib:get_attr(Request, ?Acct_Status_Type), +format_acct_status_type(Req) -> + StatusType = eradius_req:attr(?Acct_Status_Type, Req), case StatusType of undefined -> ""; diff --git a/src/eradius_metrics_prometheus.erl b/src/eradius_metrics_prometheus.erl new file mode 100644 index 00000000..a1dee4a7 --- /dev/null +++ b/src/eradius_metrics_prometheus.erl @@ -0,0 +1,575 @@ +%% Copyright (c) 2024, Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT +%% +%% @doc Provides metrics callbacks for recording metrics with prometheus.erl +-module(eradius_metrics_prometheus). + +-export([init/1, reset/0]). +-export([client_metrics_callback/3, server_metrics_callback/3]). + +-ignore_xref([init/1, reset/0]). +-ignore_xref([client_metrics_callback/3, server_metrics_callback/3]). + +-include_lib("kernel/include/logger.hrl"). +-include("dictionary.hrl"). +-include("eradius_lib.hrl"). +-include("eradius_dict.hrl"). + +-define(DEFAULT_BUCKETS, [10, 30, 50, 75, 100, 1000, 2000]). +-define(CONFIG, #{histogram_buckets => ?DEFAULT_BUCKETS, + client_metrics => true, + server_metrics => true}). +-define(TS_CLIENT_KEY, '_prometheus_metrics_client_ts'). +-define(TS_SERVER_KEY, '_prometheus_metrics_server_ts'). + +%%%========================================================================= +%%% Setup +%%%========================================================================= + +%% @doc Initialize the prometheus metrics +-spec init(#{histogram_buckets => [pos_integer()], + client_metrics => boolean(), + server_metrics => boolean()}) -> ok. +init(Opts) -> + Config = maps:merge(?CONFIG, Opts), + + init_client_metrics(Config), + init_server_metrics(Config), + ok. + +reset() -> + ok. + +init_client_metrics(#{histogram_buckets := Buckets, client_metrics := true}) -> + %% + %% Client Side Metrics + %% + + %% Server Status + prometheus_boolean:declare( + [{name, eradius_server_status}, + {labels, [server_ip, server_port]}, + {help, "Status of an upstream RADIUS Server"}]), + + ClientLabels = [server_ip, server_port, server_name, client_ip, client_name], + prometheus_counter:declare( + [{name, eradius_client_requests_total}, + {labels, ClientLabels}, + {help, "Amount of requests sent by a client"}]), + prometheus_counter:declare( + [{name, eradius_client_replies_total}, + {labels, ClientLabels}, + {help, "Amount of replies received by a client"}]), + prometheus_counter:declare( + [{name, eradius_client_access_requests_total}, + {labels, ClientLabels}, + {help, "Amount of Access requests sent by a client"}]), + prometheus_counter:declare( + [{name, eradius_client_accounting_requests_total}, + {labels, ClientLabels ++ [acct_type]}, + {help, "Amount of Accounting requests sent by a client"}]), + prometheus_counter:declare( + [{name, eradius_client_coa_requests_total}, + {labels, ClientLabels}, + {help, "Amount of CoA requests sent by a client"}]), + prometheus_counter:declare( + [{name, eradius_client_disconnect_requests_total}, + {labels, ClientLabels}, + {help, "Amount of Disconnect requests sent by client"}]), + prometheus_counter:declare( + [{name, eradius_client_retransmissions_total}, + {labels, ClientLabels}, + {help, "Amount of retransmissions done by a cliet"}]), + prometheus_counter:declare( + [{name, eradius_client_timeouts_total}, + {labels, ClientLabels}, + {help, "Amount of timeout errors triggered on a client"}]), + prometheus_counter:declare( + [{name, eradius_client_accept_responses_total}, + {labels, ClientLabels}, + {help, "Amount of Accept responses received by a client"}]), + prometheus_counter:declare( + [{name, eradius_client_reject_responses_total}, + {labels, ClientLabels}, + {help, "Amount of Reject responses received by a client"}]), + prometheus_counter:declare( + [{name, eradius_client_access_challenge_total}, + {labels, ClientLabels}, + {help, "Amount of Access-Challenge responses"}]), + prometheus_counter:declare( + [{name, eradius_client_accounting_responses_total}, + {labels, ClientLabels ++ [acct_type]}, + {help, "Amount of Accounting responses received by a client"}]), + prometheus_counter:declare( + [{name, eradius_client_coa_nacks_total}, + {labels, ClientLabels}, + {help, "Amount of CoA Nack received by a client"}]), + prometheus_counter:declare( + [{name, eradius_client_coa_acks_total}, + {labels, ClientLabels}, + {help, "Amount of CoA Ack received by a client"}]), + prometheus_counter:declare( + [{name, eradius_client_disconnect_acks_total}, + {labels, ClientLabels}, + {help, "Amount of Disconnect Acks received by a client"}]), + prometheus_counter:declare( + [{name, eradius_client_disconnect_nacks_total}, + {labels, ClientLabels}, + {help, "Amount of Disconnect Nacks received by a client"}]), + prometheus_counter:declare( + [{name, eradius_client_packets_dropped_total}, + {labels, ClientLabels}, + {help, "Amount of dropped packets"}]), + prometheus_counter:declare( + [{name, eradius_client_unknown_type_request_total}, + {labels, ClientLabels}, + {help, "Amount of RADIUS requests with unknown type"}]), + prometheus_counter:declare( + [{name, eradius_client_bad_authenticator_request_total}, + {labels, ClientLabels}, + {help, "Amount of RADIUS requests with bad authenticator"}]), + prometheus_gauge:declare( + [{name, eradius_client_pending_requests_total}, + {labels, ClientLabels}, + {help, "Amount of pending requests on client side"}]), + + %% Histograms + prometheus_histogram:declare( + [{name, eradius_client_request_duration_milliseconds}, + {labels, ClientLabels}, + {duration_unit, milliseconds}, + {buckets, Buckets}, + {help, "Execution time of a RADIUS request"}]), + prometheus_histogram:declare( + [{name, eradius_client_access_request_duration_milliseconds}, + {labels, ClientLabels}, + {duration_unit, milliseconds}, + {buckets, Buckets}, + {help, "Access-Request execution time"}]), + prometheus_histogram:declare( + [{name, eradius_client_accounting_request_duration_milliseconds}, + {labels, ClientLabels ++ [acct_type]}, + {duration_unit, milliseconds}, + {buckets, Buckets}, + {help, "Accounting-Request execution time"}]), + prometheus_histogram:declare( + [{name, eradius_client_coa_request_duration_milliseconds}, + {labels, ClientLabels}, + {duration_unit, milliseconds}, + {buckets, Buckets}, + {help, "CoA request execution time"}]), + prometheus_histogram:declare( + [{name, eradius_client_disconnect_request_duration_milliseconds}, + {labels, ClientLabels}, + {duration_unit, milliseconds}, + {buckets, Buckets}, + {help, "Disconnect execution time"}]), + ok; +init_client_metrics(_Config) -> + ok. + +init_server_metrics(#{histogram_buckets := Buckets, server_metrics := true}) -> + %% + %% Server Side Metrics + %% + + %% this need a collector... + %% {uptime_milliseconds, gauge, "RADIUS server uptime"}, + %% {since_last_reset_milliseconds, gauge, "RADIUS last server reset time"}, + + ServerLabels = [server_ip, server_port, server_name, nas_ip, nas_id], + prometheus_counter:declare( + [{name, eradius_requests_total}, + {labels, ServerLabels}, + {help, "Amount of requests received by the RADIUS server"}]), + prometheus_counter:declare( + [{name, eradius_replies_total}, + {labels, ServerLabels}, + {help, "Amount of responses"}]), + prometheus_counter:declare( + [{name, eradius_access_requests_total}, + {labels, ServerLabels}, + {help, "Amount of Access requests received by the RADIUS server"}]), + prometheus_counter:declare( + [{name, eradius_accounting_requests_total}, + {labels, ServerLabels ++ [acct_type]}, + {help, "Amount of Accounting requests received by RADIUS server"}]), + prometheus_counter:declare( + [{name, eradius_coa_requests_total}, + {labels, ServerLabels}, + {help, "Amount of CoA requests received by the RADIUS server"}]), + prometheus_counter:declare( + [{name, eradius_disconnect_requests_total}, + {labels, ServerLabels}, + {help, "Amount of Disconnect requests received by the RADIUS server"}]), + prometheus_counter:declare( + [{name, eradius_accept_responses_total}, + {labels, ServerLabels}, + {help, "Amount of Access-Accept responses"}]), + prometheus_counter:declare( + [{name, eradius_reject_responses_total}, + {labels, ServerLabels}, + {help, "Amount of Access-Reject responses"}]), + prometheus_counter:declare( + [{name, eradius_access_challenge_total}, + {labels, ServerLabels}, + {help, "Amount of Access-Challenge responses"}]), + prometheus_counter:declare( + [{name, eradius_accounting_responses_total}, + {labels, ServerLabels ++ [acct_type]}, + {help, "Amount of Accounting responses"}]), + prometheus_counter:declare( + [{name, eradius_coa_acks_total}, + {labels, ServerLabels}, + {help, "Amount of CoA ACK responses"}]), + prometheus_counter:declare( + [{name, eradius_coa_nacks_total}, + {labels, ServerLabels}, + {help, "Amount of CoA Nack responses"}]), + prometheus_counter:declare( + [{name, eradius_disconnect_acks_total}, + {labels, ServerLabels}, + {help, "Amount of Disconnect-Ack responses"}]), + prometheus_counter:declare( + [{name, eradius_disconnect_nacks_total}, + {labels, ServerLabels}, + {help, "Amount of Disconnect-Nack responses"}]), + prometheus_counter:declare( + [{name, eradius_malformed_requests_total}, + {labels, ServerLabels}, + {help, "Amount of malformed requests on RADIUS server"}]), + prometheus_counter:declare( + [{name, eradius_invalid_requests_total}, + {labels, ServerLabels}, + {help, "Amount of invalid requests on RADIUS server"}]), + prometheus_counter:declare( + [{name, eradius_retransmissions_total}, + {labels, ServerLabels}, + {help, "Amount of retrasmissions done by NAS"}]), + prometheus_counter:declare( + [{name, eradius_duplicated_requests_total}, + {labels, ServerLabels}, + {help, "Amount of duplicated requests"}]), + prometheus_gauge:declare( + [{name, eradius_pending_requests_total}, + {labels, ServerLabels}, + {help, "Amount of pending requests"}]), + prometheus_counter:declare( + [{name, eradius_packets_dropped_total}, + {labels, ServerLabels}, + {help, "Amount of dropped packets"}]), + prometheus_counter:declare( + [{name, eradius_unknown_type_request_total}, + {labels, ServerLabels}, + {help, "Amount of RADIUS requests with unknown type"}]), + prometheus_counter:declare( + [{name, eradius_bad_authenticator_request_total}, + {labels, ServerLabels}, + {help, "Amount of RADIUS requests with bad authenticator"}]), + + %% Histograms + prometheus_histogram:declare( + [{name, eradius_request_duration_milliseconds}, + {labels, ServerLabels}, + {duration_unit, milliseconds}, + {buckets, Buckets}, + {help, "RADIUS request execution time"}]), + prometheus_histogram:declare( + [{name, eradius_access_request_duration_milliseconds}, + {labels, ServerLabels}, + {duration_unit, milliseconds}, + {buckets, Buckets}, + {help, "Access-Request execution time"}]), + prometheus_histogram:declare( + [{name, eradius_accounting_request_duration_milliseconds}, + {labels, ServerLabels ++ [acct_type]}, + {duration_unit, milliseconds}, + {buckets, Buckets}, + {help, "Accounting-Request execution time"}]), + prometheus_histogram:declare( + [{name, eradius_coa_request_duration_milliseconds}, + {labels, ServerLabels}, + {duration_unit, milliseconds}, + {buckets, Buckets}, + {help, "Coa-Request execution time"}]), + prometheus_histogram:declare( + [{name, eradius_disconnect_request_duration_milliseconds}, + {labels, ServerLabels}, + {duration_unit, milliseconds}, + {buckets, Buckets}, + {help, "Disconnect-Request execution time"}]), + ok; +init_server_metrics(_Config) -> + ok. + +%%%========================================================================= +%%% Metrics Handler +%%%========================================================================= + +%% @doc Function for use as `t:eradius_req:metrics_callback/0' for a `t:eradius_req:req/0' +%% object in a RADIUS client to record prometheus metrics +-spec client_metrics_callback(Event :: eradius_req:metrics_event(), + MetaData :: term(), + Req :: eradius_req:req()) -> eradius_req:req(). +client_metrics_callback(Event, MetaData, + #{server := Server, server_addr := {ServerIP, ServerPort}, + client := Client, client_addr := ClientIP + } = Req) -> + ?LOG(debug, "Client-Metric:~nEvent: ~p~nMetaData: ~p~nReq: ~p~n", + [Event, MetaData, Req]), + + Labels = [ServerIP, ServerPort, Server, ClientIP, Client], + case Event of + request -> + client_request_metrics(MetaData, Labels, Req); + retransmission -> + prometheus_counter:inc(eradius_client_retransmissions_total, Labels, 1), + Req; + reply -> + client_reply_metrics(MetaData, Labels, Req); + _ -> + Req + end; +client_metrics_callback(Event, MetaData, Req) -> + ?LOG(error, "BROKEN Client-Metric:~nEvent: ~p~nMetaData: ~p~nReq: ~p~n", + [Event, MetaData, Req]), + Req. + +client_request_metrics(_MetaData, Labels, #{cmd := Cmd} = Req) -> + prometheus_counter:inc(eradius_client_requests_total, Labels, 1), + case Cmd of + request -> + prometheus_counter:inc(eradius_client_access_requests_total, Labels, 1); + accreq -> + AcctStatusType = acct_status_type(Req), + prometheus_counter:inc( + eradius_client_accounting_requests_total, Labels ++ [AcctStatusType], 1); + coareq -> + prometheus_counter:inc(eradius_client_coa_requests_total, Labels, 1); + discreq -> + prometheus_counter:inc(eradius_client_disconnect_requests_total, Labels, 1); + _ -> + %% WTF? how can a client generate a request with an unknown type? + %% should probably be _response_, keep it for compatibility + prometheus_counter:inc(eradius_client_unknown_type_request_total, Labels, 1) + end, + Req#{?TS_CLIENT_KEY => erlang:monotonic_time()}. + +client_request_duration(#{request := #{cmd := ReqCmd}}, Labels, + #{?TS_CLIENT_KEY := TS} = Req) -> + Duration = erlang:monotonic_time() - TS, + prometheus_histogram:observe( + eradius_request_duration_milliseconds, Labels, Duration), + + case ReqCmd of + request -> + prometheus_histogram:observe( + eradius_client_access_request_duration_milliseconds, Labels, Duration); + accreq -> + AcctStatusType = acct_status_type(Req), + prometheus_histogram:observe( + eradius_client_accounting_request_duration_milliseconds, + Labels ++ [AcctStatusType], Duration); + coareq -> + prometheus_histogram:observe( + eradius_client_coa_request_duration_milliseconds, Labels, Duration); + discreq -> + prometheus_histogram:observe( + eradius_client_disconnect_request_duration_milliseconds, Labels, Duration); + _ -> + ok + end, + Req. + +client_reply_metrics(MetaData, Labels, + #{cmd := Cmd, server_addr := {ServerIP, ServerPort}} = Req) -> + prometheus_boolean:set(eradius_server_status, [ServerIP, ServerPort], true), + case Cmd of + accept -> + prometheus_counter:inc(eradius_client_accept_responses_total, Labels, 1); + reject -> + prometheus_counter:inc(eradius_client_reject_responses_total, Labels, 1); + challenge -> + prometheus_counter:inc(eradius_client_access_challenge_total, Labels, 1); + accresp -> + AcctStatusType = acct_status_type(Req), + prometheus_counter:inc( + eradius_client_accounting_responses_total, Labels ++ [AcctStatusType], 1); + coaack -> + prometheus_counter:inc(eradius_client_coa_acks_total, Labels, 1); + coanak -> + prometheus_counter:inc(eradius_client_coa_nacks_total, Labels, 1); + discack -> + prometheus_counter:inc(eradius_client_disconnect_acks_total, Labels, 1); + discnak -> + prometheus_counter:inc(eradius_client_disconnect_nacks_total, Labels, 1); + _ -> + %% should probably be _response_, keep it for compatibility + prometheus_counter:inc(eradius_client_unknown_type_request_total, Labels, 1) + end, + client_request_duration(MetaData, Labels, Req). + +%% @doc Function for use as `t:eradius_req:metrics_callback/0' for a `t:eradius_req:req/0' +%% object in a RADIUS server to record prometheus metrics +-spec server_metrics_callback(Event :: eradius_req:metrics_event(), + MetaData :: term(), + Req :: eradius_req:req()) -> eradius_req:req(). +server_metrics_callback(Event, MetaData, + #{server := Server, server_addr := {ServerIP, ServerPort}, + client := Client, client_addr := ClientIP + } = Req) -> + ?LOG(debug, "Server-Metric:~nEvent: ~p~nMetaData: ~p~nReq: ~p~n", + [Event, MetaData, Req]), + + Labels = [ServerIP, ServerPort, Server, ClientIP, Client], + case Event of + request -> + server_request_metrics(MetaData, Labels, Req); + retransmission -> + prometheus_counter:inc(eradius_requests_total, Labels, 1), + prometheus_counter:inc(eradius_retransmissions_total, Labels, 1), + Req; + discard -> + server_discard_metrics(MetaData, Labels, Req); + reply -> + server_reply_metrics(MetaData, Labels, Req); + _ -> + ?LOG(error, "Unexpected Server-Metric:~nEvent: ~p~nMetaData: ~p~nReq: ~p~n", + [Event, MetaData, Req]), + Req + end; +server_metrics_callback(invalid_request, #{server := Server} = _MetaData, _) -> + prometheus_counter:inc(eradius_invalid_requests_total, [Server], 1), + ok; +server_metrics_callback(Event, MetaData, Req) -> + ?LOG(error, "Unexpected Server-Metric:~nEvent: ~p~nMetaData: ~p~nReq: ~p~n", + [Event, MetaData, Req]), + Req. + +server_request_metrics(_MetaData, Labels, #{cmd := Cmd} = Req) -> + prometheus_counter:inc(eradius_requests_total, Labels, 1), + prometheus_gauge:inc(eradius_pending_requests_total, Labels, 1), + case Cmd of + request -> + prometheus_counter:inc(eradius_access_requests_total, Labels, 1); + accreq -> + AcctStatusType = acct_status_type(Req), + prometheus_counter:inc( + eradius_accounting_requests_total, Labels ++ [AcctStatusType], 1); + coareq -> + prometheus_counter:inc(eradius_coa_requests_total, Labels, 1); + discreq -> + prometheus_counter:inc(eradius_disconnect_requests_total, Labels, 1); + _ -> + prometheus_counter:inc(eradius_unknown_type_request_total, Labels, 1) + end, + Req#{?TS_SERVER_KEY => erlang:monotonic_time()}. + +server_discard_metrics(MetaData, Labels, Req) -> + prometheus_gauge:inc(eradius_pending_requests_total, Labels, -1), + prometheus_counter:inc(eradius_requests_total, Labels, 1), + prometheus_counter:inc(eradius_packets_dropped_total, Labels, 1), + case MetaData of + #{reason := duplicate} -> + prometheus_counter:inc(eradius_duplicated_requests_total, Labels, 1); + #{reason := bad_authenticator} -> + prometheus_counter:inc(eradius_bad_authenticator_request_total, Labels, 1); + #{reason := unknown_req_type} -> + prometheus_counter:inc(eradius_unknown_type_request_total, Labels, 1); + #{reason := malformed} -> + prometheus_counter:inc(eradius_malformed_requests_total, Labels, 1); + _ -> + ?LOG(error, "Unexpected Server-Metric:~nEvent: ~p~nMetaData: ~p~nReq: ~p~n", + [discard, MetaData, Req]), + ok + end, + Req. + +server_request_duration(#{request := #{cmd := ReqCmd}}, Labels, + #{?TS_SERVER_KEY := TS} = Req) -> + Duration = erlang:monotonic_time() - TS, + prometheus_histogram:observe( + eradius_request_duration_milliseconds, Labels, Duration), + + case ReqCmd of + request -> + prometheus_histogram:observe( + eradius_access_request_duration_milliseconds, Labels, Duration); + accreq -> + AcctStatusType = acct_status_type(Req), + prometheus_histogram:observe( + eradius_accounting_request_duration_milliseconds, + Labels ++ [AcctStatusType], Duration); + coareq -> + prometheus_histogram:observe( + eradius_coa_request_duration_milliseconds, Labels, Duration); + discreq -> + prometheus_histogram:observe( + eradius_disconnect_request_duration_milliseconds, Labels, Duration); + _ -> + ok + end, + Req. + +server_reply_metrics(MetaData, Labels, #{cmd := Cmd} = Req) -> + prometheus_counter:inc(eradius_replies_total, Labels, 1), + prometheus_gauge:inc(eradius_pending_requests_total, Labels, -1), + case Cmd of + accept -> + prometheus_counter:inc(eradius_accept_responses_total, Labels, 1); + reject -> + prometheus_counter:inc(eradius_reject_responses_total, Labels, 1); + challenge -> + prometheus_counter:inc(eradius_access_challenge_total, Labels, 1); + accresp -> + AcctStatusType = acct_status_type(Req), + prometheus_counter:inc( + eradius_accounting_responses_total, Labels ++ [AcctStatusType], 1); + coaack -> + prometheus_counter:inc(eradius_coa_acks_total, Labels, 1); + coanak -> + prometheus_counter:inc(eradius_coa_nacks_total, Labels, 1); + discack -> + prometheus_counter:inc(eradius_disconnect_acks_total, Labels, 1); + discnak -> + prometheus_counter:inc(eradius_disconnect_nacks_total, Labels, 1); + _ -> + ok + end, + server_request_duration(MetaData, Labels, Req). + +acct_status_type(#{attrs := Attrs}) when is_list(Attrs) -> + acct_status_type_list(Attrs); +acct_status_type(#{body := Body}) when is_binary(Body) -> + acct_status_type_scan(Body); +acct_status_type(_) -> + invalid. + +acct_status_type_list([]) -> + invalid; +acct_status_type_list([{?Acct_Status_Type, Type}|_]) -> + acct_status_type_label(Type); +acct_status_type_list([{#attribute{id = ?Acct_Status_Type}, Type}|_]) -> + acct_status_type_label(Type); +acct_status_type_list([_|Next]) -> + acct_status_type_list(Next). + +acct_status_type_scan(<<?Acct_Status_Type, 6, Type:32, _/binary>>) -> + acct_status_type_label(Type); +acct_status_type_scan(<<_, Len, Rest/binary>>) -> + case Rest of + <<_:(Len-2)/bytes, Next/binary>> -> + acct_status_type_scan(Next); + _ -> + invalid + end; +acct_status_type_scan(_) -> + invalid. + +acct_status_type_label(?RStatus_Type_Start) -> start; +acct_status_type_label(?RStatus_Type_Stop) -> stop; +acct_status_type_label(?RStatus_Type_Update) -> update; +acct_status_type_label(?RStatus_Type_On) -> on; +acct_status_type_label(?RStatus_Type_Off) -> off; +acct_status_type_label(Type) -> integer_to_list(Type). diff --git a/src/eradius_node_mon.erl b/src/eradius_node_mon.erl deleted file mode 100644 index f352ab40..00000000 --- a/src/eradius_node_mon.erl +++ /dev/null @@ -1,186 +0,0 @@ -%% @private -%% @doc A server that keeps track of handler nodes. -%% Handler nodes should call {@link eradius:modules_ready/2} from their application master -%% as soon as they are ready, which makes them available for request processing. -%% The node_mon server monitors the application master and removes it from -%% request processing when it goes down. --module(eradius_node_mon). --export([start_link/0, modules_ready/2, set_nodes/1, get_module_nodes/1, get_remote_version/1]). - --behaviour(gen_server). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). - --include_lib("kernel/include/logger.hrl"). - --define(NODE_TAB, eradius_node_mon). --define(NODE_INFO_TAB, eradius_node_info). --define(PING_INTERVAL, 3000). % 3 sec --define(PING_TIMEOUT, 300). % 0.3 sec --define(SERVER, ?MODULE). - -%% ------------------------------------------------------------------------------------------ -%% -- API -start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). - --spec modules_ready(pid(), list(module())) -> ok. -modules_ready(ApplicationMaster, Modules) when is_pid(ApplicationMaster), is_list(Modules) -> - gen_server:cast(?SERVER, {modules_ready, ApplicationMaster, Modules}). - --spec set_nodes(list(node())) -> ok. -set_nodes(Nodes) -> - gen_server:call(?SERVER, {set_nodes, Nodes}). - --spec get_module_nodes(module()) -> [node()]. -get_module_nodes(Module) -> - try - ets:lookup_element(?NODE_TAB, Module, 2) - catch - error:badarg -> - [] - end. - --spec get_remote_version(node()) -> {integer(), integer()} | undefined. -get_remote_version(Node) -> - try - ets:lookup_element(?NODE_INFO_TAB, Node, 2) - catch - error:badarg -> - undefined - end. - -%% ------------------------------------------------------------------------------------------ -%% -- gen_server callbacks --record(state, { - live_registrar_nodes = sets:new() :: sets:set(), - dead_registrar_nodes = sets:new() :: sets:set(), - app_masters = maps:new() :: map(), - ping_timer :: reference() - }). - -init([]) -> - ets:new(?NODE_TAB, [bag, named_table, protected, {read_concurrency, true}]), - ets:new(?NODE_INFO_TAB, [set, named_table, protected, {read_concurrency, true}]), - PingTimer = erlang:send_after(?PING_INTERVAL, self(), ping_dead_nodes), - {ok, #state{ping_timer = PingTimer}}. - -handle_call(remote_get_regs_v1, From, State) -> - check_eradius_version(From), - Registrations = maps:to_list(State#state.app_masters), - {reply, {ok, Registrations}, State}; -handle_call({set_nodes, Nodes}, _From, State) -> - NewState = State#state{live_registrar_nodes = sets:new(), - dead_registrar_nodes = sets:from_list(Nodes)}, - self() ! ping_dead_nodes, - {reply, ok, NewState}. - -handle_cast({remote_modules_ready_v1, ApplicationMaster, Modules}, State) -> - NewState = State#state{app_masters = register_locally({ApplicationMaster, Modules}, State#state.app_masters)}, - {noreply, NewState}; -handle_cast({modules_ready, ApplicationMaster, Modules}, State) -> - NewState = State#state{app_masters = register_locally({ApplicationMaster, Modules}, State#state.app_masters)}, - lists:foreach(fun (Node) -> - check_eradius_version(Node), - gen_server:cast({?SERVER, Node}, {remote_modules_ready_v1, ApplicationMaster, Modules}) - end, nodes()), - {noreply, NewState}. - -handle_info({'DOWN', _MRef, process, {?SERVER, Node}, _Reason}, State = #state{live_registrar_nodes = LiveRegistrars}) -> - case sets:is_element(Node, LiveRegistrars) of - false -> - %% ignore the 'DOWN', it's from a node we don't really want to monitor anymore - %% and that shouldn't get into dead_registrar_nodes - {noreply, State}; - true -> - {noreply, State#state{live_registrar_nodes = sets:del_element(Node, LiveRegistrars), - dead_registrar_nodes = sets:add_element(Node, State#state.dead_registrar_nodes)}} - end; -handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #state{app_masters = AppMasters}) when is_pid(Pid) -> - case maps:find(Pid, AppMasters) of - error -> - {noreply, State}; - {ok, Modules} -> - ServerNode = node(Pid), - lists:foreach(fun (Mod) -> ets:delete_object(?NODE_TAB, {Mod, ServerNode}) end, Modules), - NewState = State#state{app_masters = maps:remove(Pid, AppMasters)}, - {noreply, NewState} - end; -handle_info(ping_dead_nodes, State = #state{app_masters = AppMasters, live_registrar_nodes = LiveRegistrars}) -> - erlang:cancel_timer(State#state.ping_timer), - {NewLive, NewDead, NewAppMasters} = - sets:fold(fun (Node, {Live, Dead, AppMastersAcc}) -> - case (catch gen_server:call({?SERVER, Node}, remote_get_regs_v1, ?PING_TIMEOUT)) of - {ok, Registrations} -> - NewAppMastersAcc = lists:foldl(fun register_locally/2, AppMastersAcc, Registrations), - erlang:monitor(process, {?SERVER, Node}), - {sets:add_element(Node, Live), Dead, NewAppMastersAcc}; - {'EXIT', _Reason} -> - {Live, sets:add_element(Node, Dead), AppMastersAcc} - end - end, {LiveRegistrars, sets:new(), AppMasters}, State#state.dead_registrar_nodes), - NewPingTimer = erlang:send_after(?PING_INTERVAL, self(), ping_dead_nodes), - NewState = State#state{live_registrar_nodes = NewLive, - dead_registrar_nodes = NewDead, - app_masters = NewAppMasters, - ping_timer = NewPingTimer}, - {noreply, NewState}; -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> ok. -code_change(_OldVsn, State, _Extra) -> {ok, State}. - -%% ------------------------------------------------------------------------------------------ -%% -- helpers --spec dict_prepend(term(), list(term()), map()) -> map(). -dict_prepend(Key, List, Map) -> - update_with(Key, fun (Old) -> List ++ Old end, List, Map). - -%% NOTE: -%% copy-pasted from maps.erl to have backward compatability with OTP 18 -%% it can be rewmoved if minimal version of OTP will be set to 19. -update_with(Key,Fun,Init,Map) when is_function(Fun,1), is_map(Map) -> - case maps:find(Key,Map) of - {ok,Val} -> maps:update(Key,Fun(Val),Map); - error -> maps:put(Key,Init,Map) - end; -update_with(Key,Fun,Init,Map) -> - erlang:error(error_type(Map),[Key,Fun,Init,Map]). - --define(IS_ITERATOR(I), is_tuple(I) andalso tuple_size(I) == 3; I == none; is_integer(hd(I)) andalso is_map(tl(I))). -error_type(M) when is_map(M); ?IS_ITERATOR(M) -> badarg; -error_type(V) -> {badmap, V}. - -register_locally({ApplicationMaster, Modules}, AppMasters) -> - case maps:is_key(ApplicationMaster, AppMasters) of - true -> - ok; %% already monitored - false -> - monitor(process, ApplicationMaster) - end, - ServerNode = node(ApplicationMaster), - ets:insert(?NODE_TAB, [{Mod, ServerNode} || Mod <- Modules]), - dict_prepend(ApplicationMaster, Modules, AppMasters). - -check_eradius_version({Pid, _}) when is_pid(Pid) -> - check_eradius_version(Pid); -check_eradius_version(Pid) when is_pid(Pid) -> - check_eradius_version(node(Pid)); -check_eradius_version(Node) -> - case rpc:call(Node, application, get_key, [eradius, vsn]) of - {ok, Vsn} -> - try interpret_vsn(Vsn) of - Version -> - ets:insert(?NODE_INFO_TAB, {Node, Version}) - catch - _:_ -> - ?LOG(warning, "unknown eradius version format ~p on node ~p", [Vsn, Node]) - end; - _ -> - ?LOG(warning, "eradius version do not known on node ~p", [Node]) - end. - -interpret_vsn(Vsn) -> - BinVsn = list_to_binary(Vsn), - [MajorVsn, MinorVsn | _] = binary:split(BinVsn, <<".">>, [global]), - {binary_to_integer(MajorVsn), binary_to_integer(MinorVsn)}. diff --git a/src/eradius_proxy.erl b/src/eradius_proxy.erl deleted file mode 100644 index b3130dc9..00000000 --- a/src/eradius_proxy.erl +++ /dev/null @@ -1,334 +0,0 @@ -%% @doc -%% This module implements a RADIUS proxy. -%% -%% It accepts following configuration: -%% -%% ``` -%% [{default_route, {{127, 0, 0, 1}, 1813, <<"secret">>}, [{pool, pool_name}, {retries, 5}, {timeout, 5000}], -%% {options, [{type, realm}, {strip, true}, {separator, "@"}]}, -%% {routes, [{"^test-[0-9].", {{127, 0, 0, 1}, 1815, <<"secret1">>}, [{pool, pool_name}, {retries, 5}, {timeout, 5000}]}]}] -%% ''' -%% -%% Or for backward compatibility: -%% -%% ``` -%% [{default_route, {{127, 0, 0, 1}, 1813, <<"secret">>}, pool_name}, -%% {options, [{type, realm}, {strip, true}, {separator, "@"}]}, -%% {routes, [{"^test-[0-9].", {{127, 0, 0, 1}, 1815, <<"secret1">>}, [{pool, pool_name}, {retries, 5}, {timeout, 5000}]}]}] -%% ''' -%% -%% Where the `pool_name` is the name of the pool that must be specified -%% in the `servers_pool` configuration and will be used as a pointer to -%% the list of secondary RADIUS servers for fail-over scenarios. -%% -%% ``` -%% {servers_pool, [{pool_name, [ -%% {{127, 0, 0, 1}, 1815, <<"secret">>, [{retries, 3}, {timeout, 5000}]}, -%% {{127, 0, 0, 1}, 1816, <<"secret">>}]}]} -%% ''' -%% -%% == WARNING == -%% -%% Define `routes' carefully. The `test' here in example above, is -%% a regular expression that may cause to problemts with performance. --module(eradius_proxy). - --behaviour(eradius_server). --export([radius_request/3, validate_arguments/1, get_routes_info/1, - put_default_route_to_pool/2, put_routes_to_pool/2]). - --ifdef(TEST). --export([resolve_routes/4, validate_options/1, new_request/3, - get_key/4, strip/4]). --endif. - --include_lib("kernel/include/logger.hrl"). --include("eradius_lib.hrl"). --include("dictionary.hrl"). - --define(DEFAULT_TYPE, realm). --define(DEFAULT_STRIP, false). --define(DEFAULT_SEPARATOR, "@"). --define(DEFAULT_TIMEOUT, 5000). --define(DEFAULT_RETRIES, 1). --define(DEFAULT_CLIENT_RETRIES, 3). - --define(DEFAULT_OPTIONS, [{type, ?DEFAULT_TYPE}, - {strip, ?DEFAULT_STRIP}, - {separator, ?DEFAULT_SEPARATOR}, - {timeout, ?DEFAULT_TIMEOUT}, - {retries, ?DEFAULT_RETRIES}]). - --type pool_name() :: atom(). --type route() :: eradius_client:nas_address() | - {eradius_client:nas_address(), RouteOptions :: [tuple()] | pool_name()}. --type routes() :: [{Name :: string(), eradius_client:nas_address()}] | - [{Name :: string(), eradius_client:nas_address(), PoolName :: atom()}]. --type undefined_route() :: {undefined, 0, []}. - -radius_request(Request, _NasProp, Args) -> - DefaultRoute = get_proxy_opt(default_route, Args, {undefined, 0, []}), - Routes = get_proxy_opt(routes, Args, []), - Options = proplists:get_value(options, Args, ?DEFAULT_OPTIONS), - Username = eradius_lib:get_attr(Request, ?User_Name), - {NewUsername, Route} = resolve_routes(Username, DefaultRoute, Routes, Options), - SendOpts = get_send_options(Route, Options), - send_to_server(new_request(Request, Username, NewUsername), Route, SendOpts). - -validate_arguments(Args) -> - DefaultRoute = get_proxy_opt(default_route, Args, {undefined, 0, []}), - Options = proplists:get_value(options, Args, ?DEFAULT_OPTIONS), - Routes = get_proxy_opt(routes, Args, undefined), - case {validate_route(DefaultRoute), validate_options(Options), compile_routes(Routes)} of - {false, _, _} -> default_route; - {_, false, _} -> options; - {_, _, false} -> routes; - {_, _, NewRoutes} -> - {true, [{default_route, DefaultRoute}, {options, Options}, {routes, NewRoutes}]} - end. - -compile_routes(undefined) -> []; -compile_routes(Routes) -> - RoutesOpts = lists:map(fun (Route) -> - {Name, Relay, RouteOptions} = route(Route), - case re:compile(Name) of - {ok, R} -> - case validate_route({Relay, RouteOptions}) of - false -> - false; - _ -> {R, Relay, RouteOptions} - end; - {error, {Error, Position}} -> - throw("Error during regexp compilation - " ++ Error ++ " at position " ++ integer_to_list(Position)) - end - end, Routes), - RelaysRegexps = lists:any(fun(Route) -> Route == false end, RoutesOpts), - if RelaysRegexps == false -> - RoutesOpts; - true -> - false - end. - - % @private --spec send_to_server(Request :: #radius_request{}, - Route :: undefined_route() | route(), - Options :: eradius_client:options()) -> - {reply, Reply :: #radius_request{}} | term(). -send_to_server(_Request, {undefined, 0, []}, _) -> - {error, no_route}; - -send_to_server(#radius_request{reqid = ReqID} = Request, {{Server, Port, Secret}, RelayOpts}, Options) -> - UpstreamServers = get_failover_servers(RelayOpts), - case eradius_client:send_request({Server, Port, Secret}, Request, [{failover, UpstreamServers} | Options]) of - {ok, Result, Auth} -> - decode_request(Result, ReqID, Secret, Auth); - no_active_servers -> - % If all RADIUS servers are marked as inactive for now just use - % just skip fail-over mechanism and use default given Peer - send_to_server(Request, {Server, Port, Secret}, Options); - Error -> - ?LOG(error, "~p: error during send_request (~p)", [?MODULE, Error]), - Error - end; -send_to_server(#radius_request{reqid = ReqID} = Request, {Server, Port, Secret}, Options) -> - case eradius_client:send_request({Server, Port, Secret}, Request, Options) of - {ok, Result, Auth} -> decode_request(Result, ReqID, Secret, Auth); - Error -> - ?LOG(error, "~p: error during send_request (~p)", [?MODULE, Error]), - Error - end. - - % @private -decode_request(Result, ReqID, Secret, Auth) -> - case eradius_lib:decode_request(Result, Secret, Auth) of - Reply = #radius_request{} -> - {reply, Reply#radius_request{reqid = ReqID}}; - Error -> - ?LOG(error, "~p: request is incorrect (~p)", [?MODULE, Error]), - Error - end. - - % @private --spec validate_route(Route :: route()) -> boolean(). -validate_route({{Host, Port, Secret}, RouteOpts}) -> - validate_route_options(RouteOpts) and validate_route({Host, Port, Secret}); -validate_route({_Host, Port, _Secret}) when not is_integer(Port); Port =< 0; Port > 65535 -> false; -validate_route({_Host, _Port, Secret}) when not is_list(Secret), not is_binary(Secret) -> false; -validate_route({Host, _Port, _Secret}) when is_list(Host) -> true; -validate_route({Host, Port, Secret}) when is_tuple(Host) -> - case inet_parse:ntoa(Host) of - {error, _} -> false; - Address -> validate_route({Address, Port, Secret}) - end; -validate_route({Host, _Port, _Secret}) when is_binary(Host) -> true; -validate_route(_) -> false. - - % @private --spec validate_route_options(Options :: [proplists:property()] | pool_name()) -> boolean(). -validate_route_options(PoolName) when is_atom(PoolName) -> - true; -validate_route_options([]) -> - true; -validate_route_options(Options) -> - Keys = proplists:get_keys(Options), - lists:all(fun(Key) -> validate_route_option(Key, proplists:get_value(Key, Options)) end, Keys). - - % @private --spec validate_route_option(Key :: atom(), Value :: term()) -> boolean(). -validate_route_option(timeout, Value) when is_integer(Value) -> - true; -validate_route_option(retries, Value) when is_integer(Value) -> - true; -validate_route_option(pool, Value) when is_atom(Value) -> - true; -validate_route_option(_, _) -> - false. - - % @private --spec validate_options(Options :: [proplists:property()]) -> boolean(). -validate_options(Options) -> - Keys = proplists:get_keys(Options), - lists:all(fun(Key) -> validate_option(Key, proplists:get_value(Key, Options)) end, Keys). - - % @private --spec validate_option(Key :: atom(), Value :: term()) -> boolean(). -validate_option(type, Value) when Value =:= realm; Value =:= prefix -> true; -validate_option(type, _Value) -> false; -validate_option(strip, Value) when is_boolean(Value) -> true; -validate_option(strip, _Value) -> false; -validate_option(separator, Value) when is_list(Value) -> true; -validate_option(timeout, Value) when is_integer(Value) -> true; -validate_option(retries, Value) when is_integer(Value) -> true; -validate_option(_, _) -> false. - - - % @private --spec new_request(Request :: #radius_request{}, - Username :: undefined | binary(), - NewUsername :: string()) -> - NewRequest :: #radius_request{}. -new_request(Request, Username, Username) -> Request; -new_request(Request, _Username, NewUsername) -> - eradius_lib:set_attr(eradius_lib:del_attr(Request, ?User_Name), - ?User_Name, NewUsername). - - % @private --spec resolve_routes(Username :: undefined | binary(), - DefaultRoute :: undefined_route() | route(), - Routes :: routes(), Options :: [proplists:property()]) -> - {NewUsername :: string(), Route :: route()}. -resolve_routes( undefined, DefaultRoute, _Routes, _Options) -> - {undefined, DefaultRoute}; -resolve_routes(Username, DefaultRoute, Routes, Options) -> - Type = proplists:get_value(type, Options, ?DEFAULT_TYPE), - Strip = proplists:get_value(strip, Options, ?DEFAULT_STRIP), - Separator = proplists:get_value(separator, Options, ?DEFAULT_SEPARATOR), - case get_key(Username, Type, Strip, Separator) of - {not_found, NewUsername} -> - {NewUsername, DefaultRoute}; - {Key, NewUsername} -> - {NewUsername, find_suitable_relay(Key, Routes, DefaultRoute)} - end. - -find_suitable_relay(_Key, [], DefaultRoute) -> DefaultRoute; -find_suitable_relay(Key, [{Regexp, Relay} | Routes], DefaultRoute) -> - case re:run(Key, Regexp, [{capture, none}]) of - nomatch -> find_suitable_relay(Key, Routes, DefaultRoute); - _ -> Relay - end; -find_suitable_relay(Key, [{Regexp, Relay, RelayOpts} | Routes], DefaultRoute) -> - case re:run(Key, Regexp, [{capture, none}]) of - nomatch -> find_suitable_relay(Key, Routes, DefaultRoute); - _ -> {Relay, RelayOpts} - end. - - % @private --spec get_key(Username :: binary() | string() | [], Type :: atom(), Strip :: boolean(), Separator :: list()) -> - {Key :: not_found | string(), NewUsername :: string()}. -get_key([], _, _, _) -> {not_found, []}; -get_key(Username, Type, Strip, Separator) when is_binary(Username) -> - get_key(binary_to_list(Username), Type, Strip, Separator); -get_key(Username, realm, Strip, Separator) -> - Realm = lists:last(string:tokens(Username, Separator)), - {Realm, strip(Username, realm, Strip, Separator)}; -get_key(Username, prefix, Strip, Separator) -> - Prefix = hd(string:tokens(Username, Separator)), - {Prefix, strip(Username, prefix, Strip, Separator)}; -get_key(Username, _, _, _) -> {not_found, Username}. - - % @private --spec strip(Username :: string(), Type :: atom(), Strip :: boolean(), Separator :: list()) -> - NewUsername :: string(). -strip(Username, _, false, _) -> Username; -strip(Username, realm, true, Separator) -> - case string:tokens(Username, Separator) of - [Username] -> Username; - [_ | _] = List -> - [_ | Tail] = lists:reverse(List), - string:join(lists:reverse(Tail), Separator) - end; -strip(Username, prefix, true, Separator) -> - case string:tokens(Username, Separator) of - [Username] -> Username; - [_ | Tail] -> string:join(Tail, Separator) - end. - -route({RouteName, RouteRelay}) -> {RouteName, RouteRelay, []}; -route({_RouteName, _RouteRelay, _RoutOptions} = Route) -> Route. - -get_routes_info(HandlerOpts) -> - DefaultRoute = lists:keyfind(default_route, 1, HandlerOpts), - Routes = lists:keyfind(routes, 1, HandlerOpts), - Options = lists:keyfind(options, 1, HandlerOpts), - Retries = case Options of - false -> - ?DEFAULT_CLIENT_RETRIES; - {options, Opts} -> - proplists:get_value(retries, Opts, ?DEFAULT_CLIENT_RETRIES) - end, - {DefaultRoute, Routes, Retries}. - -put_default_route_to_pool(false, _) -> ok; -put_default_route_to_pool({default_route, {Host, Port, _Secret}}, Retries) -> - eradius_client_mngr:store_radius_server_from_pool(Host, Port, Retries); -put_default_route_to_pool({default_route, {Host, Port, _Secret}, _PoolName}, Retries) -> - eradius_client_mngr:store_radius_server_from_pool(Host, Port, Retries); -put_default_route_to_pool(_, _) -> ok. - -put_routes_to_pool(false, _Retries) -> ok; -put_routes_to_pool({routes, Routes}, Retries) -> - lists:foreach(fun (Route) -> - case Route of - {_RouteName, {Host, Port, _Secret}} -> - eradius_client_mngr:store_radius_server_from_pool(Host, Port, Retries); - {_RouteName, {Host, Port, _Secret}, _Pool} -> - eradius_client_mngr:store_radius_server_from_pool(Host, Port, Retries); - {Host, Port, _Secret, _Opts} -> - eradius_client_mngr:store_radius_server_from_pool(Host, Port, Retries); - _ -> ok - end - end, Routes). - -get_proxy_opt(_, [], Default) -> Default; -get_proxy_opt(OptName, [{OptName, AddrOrRoutes} | _], _) -> AddrOrRoutes; -get_proxy_opt(OptName, [{OptName, Addr, Opts} | _], _) -> {Addr, Opts}; -get_proxy_opt(OptName, [_ | Args], Default) -> get_proxy_opt(OptName, Args, Default). - -get_send_options({_Relay, RelayOpts}, Options) when is_list(RelayOpts) -> - Retries = proplists:get_value(retries, Options, ?DEFAULT_RETRIES), - Timeout = proplists:get_value(timeout, Options, ?DEFAULT_TIMEOUT), - RelayTimeout = proplists:get_value(timeout, RelayOpts, Timeout), - RelayRetries = proplists:get_value(retries, RelayOpts, Retries), - [{retries, RelayRetries}, {timeout, RelayTimeout}]; -get_send_options(_Route, Options) -> - Retries = proplists:get_value(retries, Options, ?DEFAULT_RETRIES), - Timeout = proplists:get_value(timeout, Options, ?DEFAULT_TIMEOUT), - [{retries, Retries}, {timeout, Timeout}]. - -get_failover_servers(RelayOpts) when is_list(RelayOpts) -> - Pools = application:get_env(eradius, servers_pool, []), - Pool = proplists:get_value(pool, RelayOpts, undefined), - proplists:get_value(Pool, Pools, []); -get_failover_servers(Pool) -> - Pools = application:get_env(eradius, servers_pool, []), - proplists:get_value(Pool, Pools, []). diff --git a/src/eradius_req.erl b/src/eradius_req.erl new file mode 100644 index 00000000..94cfb1ef --- /dev/null +++ b/src/eradius_req.erl @@ -0,0 +1,732 @@ +%% Copyright (c) 2024, Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT + +-module(eradius_req). + +-export([is_valid/1, + req_id/1, + cmd/1, + authenticator/1, + request_authenticator/1, + msg_hmac/1, + eap_msg/1, + packet/1, + attrs/1, + attr/2]). +-export([new/1, new/2, + request/4, + response/3, + set_secret/2, + set_body/2, + set_attrs/2, + add_attr/3, + set_attr/3, + set_msg_hmac/2, + set_eap_msg/2, + set_metrics_callback/2]). +-export([record_metric/3, metrics_callback/3]). + +-ignore_xref([is_valid/1, + req_id/1, + cmd/1, + authenticator/1, + request_authenticator/1, + msg_hmac/1, + eap_msg/1, + packet/1, + attrs/1, + attr/2]). +-ignore_xref([new/1, new/2, + request/4, + response/3, + set_secret/2, + set_body/2, + set_attrs/2, + add_attr/3, + set_attr/3, + set_msg_hmac/2, + set_eap_msg/2, + set_metrics_callback/2]). + +-ifdef(TEST). +-export([encode_value/2, decode_value/2, scramble/3, ascend/3]). +-export([salt_encrypt/4, salt_decrypt/3, encode_attribute/3, decode_attribute/4]). +-ignore_xref([encode_value/2, decode_value/2, scramble/3, ascend/3]). +-ignore_xref([salt_encrypt/4, salt_decrypt/3, encode_attribute/3, decode_attribute/4]). +-endif. + +-include("eradius_lib.hrl"). +-include("eradius_dict.hrl"). + +-type command() :: 'request' | 'accept' | 'challenge' | 'reject' | 'accreq' | 'accresp' | + 'coareq' | 'coaack' | 'coanak' | 'discreq' | 'discack' | 'discnak'. +-type secret() :: binary(). +-type authenticator() :: <<_:128>>. +-type salt() :: binary(). +-type attribute_list() :: [{eradius_dict:attribute(), term()}]. +-export_type([command/0, secret/0, authenticator/0, salt/0, attribute_list/0]). + +-type metrics_event() :: 'request' | 'reply' | + 'retransmission' | 'discard' | + 'invalid_request'. +-type metrics_callback() :: + fun((Event :: metrics_event(), MetaData :: term(), Req :: req()) -> req()). +-export_type([metrics_event/0, metrics_callback/0]). + +-type req() :: + #{ + %% public fields + is_valid := undefined | boolean(), + cmd := command(), + secret => secret(), + req_id => byte(), + authenticator => authenticator(), + request_authenticator => authenticator(), + + %% public, server only + client => binary(), + client_addr => {IP :: inet:ip_address(), inet:port_number()}, + + server => eradius_server:server_name(), + server_addr => {IP :: inet:ip_address(), inet:port_number()}, + + %% private fields + arrival_time => integer(), + socket => gen_udp:socket(), + + head => binary(), + body := undefined | binary(), + attrs := undefined | attribute_list(), + eap_msg := undefined | binary(), + msg_hmac := undefined | boolean() | integer(), + + metrics_callback := undefined | metrics_callback(), + + _ => _ + }. + +-export_type([req/0]). + +%%%========================================================================= +%%% API +%%%========================================================================= + +%% @doc Return validation state of the request. +%% +%% - `true' for a requests if has been encoded to binary form, +%% +%% - `true' for a response if has been decoded from binary form +%% and the authenticator has been validate, +%% +%% - `true' for a response if has been decoded from binary form +%% and the authenticator failed to validate, +%% +%% - `undefined' otherwise +%% @end +-spec is_valid(req()) -> true | false | undefined. +is_valid(#{is_valid := IsValid}) -> IsValid. + +-spec req_id(req()) -> byte() | undefined. +req_id(#{req_id := ReqId}) -> ReqId; +req_id(_) -> undefined. + +-spec cmd(req()) -> command(). +cmd(#{cmd := Cmd}) -> Cmd. + +-spec authenticator(req()) -> authenticator() | undefined. +authenticator(#{authenticator := Authenticator}) -> Authenticator; +authenticator(_) -> undefined. + +-spec request_authenticator(req()) -> authenticator() | undefined. +request_authenticator(#{authenticator := Authenticator}) -> Authenticator; +request_authenticator(_) -> undefined. + +-spec msg_hmac(req()) -> boolean() | undefined. +msg_hmac(#{msg_hmac := MsgHMAC}) -> MsgHMAC; +msg_hmac(_) -> undefined. + +-spec eap_msg(req()) -> binary() | undefined. +eap_msg(#{eap_msg := EAPmsg}) -> EAPmsg; +eap_msg(_) -> undefined. + +%% @doc Convert a RADIUS request to the wire format. +-spec packet(req()) -> {binary(), req()} | no_return(). +packet(#{req_id := _, cmd := _, authenticator := _, body := Body, secret := _} = Req) + when is_binary(Body) -> + %% body must be fully prepared + encode_body(Req, Body); +packet(#{req_id := _, cmd := _, secret := _, attrs := Attrs, eap_msg := EAPmsg} = Req) + when is_list(Attrs) -> + Body0 = encode_attributes(Req, Attrs, <<>>), + Body1 = encode_eap_message(EAPmsg, Body0), + Body = encode_message_authenticator(Req, Body1), + encode_body(Req, Body); +packet(Req) -> + erlang:error(badarg, [Req]). + +-spec attrs(req()) -> {attribute_list(), req()} | no_return(). +attrs(#{attrs := Attrs, is_valid := IsValid} = Req) + when is_list(Attrs), IsValid =/= false -> + {Attrs, Req}; +attrs(#{body := Body, secret := _} = Req0) + when is_binary(Body) -> + try + #{attrs := Attrs} = Req = decode_body(Body, Req0), + {Attrs, Req} + catch + exit:_ -> + throw({bad_pdu, decoder_error}) + end; +attrs(Req) -> + erlang:error(badarg, [Req]). + +attr(Id, #{attrs := Attrs, is_valid := IsValid}) + when is_list(Attrs), IsValid =/= false -> + get_attr(Id, Attrs); +attr(_, _) -> + undefined. + +get_attr(_Id, []) -> + undefined; +get_attr(Id, [Head|Tail]) -> + case Head of + {#attribute{id = Id}, Value} -> Value; + {Id, Value} -> Value; + _ -> get_attr(Id, Tail) + end. + + +-spec new(command()) -> req(). +new(Command) -> + new(Command, undefined). + +-spec new(command(), 'undefined' | metrics_callback()) -> req(). +new(Command, MetricsCallback) + when MetricsCallback =:= undefined; is_function(MetricsCallback, 3) -> + #{is_valid => undefined, + cmd => Command, + + body => undefined, + attrs => [], + eap_msg => undefined, + msg_hmac => undefined, + + metrics_callback => MetricsCallback + }. + +-spec request(binary(), binary(), eradius_server:client(), 'undefined' | metrics_callback()) -> + req() | no_return(). +request(<<Cmd, ReqId, Len:16, Authenticator:16/bytes>> = Header, Body, + #{secret := Secret, client := ClientId}, MetricsCallback) -> + Command = decode_command(Cmd), + Req = new(Command, MetricsCallback), + mk_req(Command, ReqId, Len, Authenticator, Header, Body, + Req#{req_id => ReqId, request_authenticator => Authenticator, + client => ClientId, secret => Secret}). + +-spec response(binary(), binary(), req()) -> req() | no_return(); + (command(), undefined | attribute_list(), req()) -> req(). +response(<<Cmd, ReqId, Len:16, Authenticator:16/bytes>> = Header, Body, + #{req_id := ReqId, secret := _} = Req) -> + Command = decode_command(Cmd), + mk_req(Command, ReqId, Len, Authenticator, Header, Body, Req); + +response(Response, Attrs, Req) when is_atom(Response) -> + Req#{cmd := Response, body := undefined, attrs := Attrs, is_valid := undefined}. + +-spec set_secret(req(), secret()) -> req(). +set_secret(Req, Secret) -> + Req#{secret => Secret, is_valid := undefined}. + +-spec set_body(req(), binary()) -> req(). +set_body(Req, Body) when is_binary(Body) -> + Req#{body := Body, attrs := undefined, is_valid := undefined}. + +-spec set_attrs(attribute_list(), req()) -> req(). +set_attrs(Attrs, Req) when is_list(Attrs) -> + Req#{body := undefined, attrs := Attrs, is_valid := undefined}. + +add_attr(Id, Value, #{attrs := Attrs} = Req) + when is_list(Attrs) -> + Req#{attrs := [{Id, Value} | Attrs], is_valid := undefined}. + +set_attr(Id, Value, #{attrs := Attrs} = Req) + when is_list(Attrs) -> + Req#{attrs := lists:keystore(Id, 1, Attrs, {Id, Value}), is_valid := undefined}. + +-spec set_msg_hmac(boolean(), req()) -> req(). +set_msg_hmac(MsgHMAC, Req) + when is_boolean(MsgHMAC) -> + Req#{msg_hmac => MsgHMAC}. + +-spec set_eap_msg(binary(), req()) -> req(). +set_eap_msg(EAPmsg, Req) + when is_binary(EAPmsg) -> + Req#{body := undefined, eap_msg := EAPmsg}. + +-spec set_metrics_callback(undefined | metrics_callback(), req()) -> req(). +set_metrics_callback(MetricsCallback, Req) -> + Req#{metrics_callback => MetricsCallback}. + +-spec metrics_callback(Cb :: undefined | eradius_req:metrics_callback(), Event :: metrics_event(), MetaData :: term()) -> any(). +metrics_callback(Cb, Event, MetaData) + when is_function(Cb, 3) -> + Cb(Event, MetaData, undefined); +metrics_callback(_, _, _) -> + undefined. + +-spec record_metric(Event :: metrics_event(), MetaData :: term(), Req :: req()) -> req(). +record_metric(Event, MetaData, #{metrics_callback := Cb} = Req) + when is_function(Cb, 3) -> + Cb(Event, MetaData, Req); +record_metric(_, _, Req) -> + Req. + +%%%=================================================================== +%%% binary format handling +%%%=================================================================== + +%% ------------------------------------------------------------------------------------------ +%% -- Request Accessors +-spec random_authenticator() -> authenticator(). +random_authenticator() -> crypto:strong_rand_bytes(16). + +-spec zero_authenticator() -> authenticator(). +zero_authenticator() -> <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>>. + +mk_req(Cmd, ReqId, Len, Authenticator, Header, Body0, #{req_id := ReqId} = Req) + when byte_size(Body0) >= (Len - 20) -> + <<Body:(Len - 20)/bytes, _/binary>> = Body0, + <<Head:4/bytes, _/binary>> = Header, + + validate_authenticator(Cmd, Head, Authenticator, Body, Req), + Req#{cmd := Cmd, + authenticator => Authenticator, + is_valid := true, + msg_hmac := undefined, + eap_msg := undefined, + head => Head, + body := Body, + attrs := undefined}; +mk_req(_, ResponseReqId, _, _, _, _, #{req_id := ReqId}) + when ResponseReqId =/= ReqId -> + throw({bad_pdu, invalid_req_id}); +mk_req(_, _, Len, _, _, Body, _) + when byte_size(Body) =< (Len - 20) -> + throw({bad_pdu, invalid_packet_size}). + +%% ------------------------------------------------------------------------------------------ +%% -- Wire Encoding + +encode_body(#{req_id := ReqId, cmd := Cmd} = Req, Body) + when Cmd =:= request -> + Authenticator = random_authenticator(), + Packet = <<(encode_command(Cmd)):8, ReqId:8, (byte_size(Body) + 20):16, + Authenticator:16/binary, Body/binary>>, + {Packet, Req#{is_valid := true, request_authenticator => Authenticator}}; + +encode_body(#{req_id := ReqId, cmd := Cmd, secret := Secret} = Req, Body) + when Cmd =:= accreq; Cmd =:= coareq; Cmd =:= discreq -> + Head = <<(encode_command(Cmd)):8, ReqId:8, (byte_size(Body) + 20):16>>, + Authenticator = crypto:hash(md5, [Head, zero_authenticator(), Body, Secret]), + Packet = <<Head/binary, Authenticator:16/binary, Body/binary>>, + {Packet, Req#{is_valid := true, request_authenticator => Authenticator}}; + +encode_body(#{req_id := ReqId, cmd := Cmd, + request_authenticator := Authenticator, + secret := Secret} = Req, Body) -> + Head = <<(encode_command(Cmd)):8, ReqId:8, (byte_size(Body) + 20):16>>, + + ReplyAuthenticator = crypto:hash(md5, [Head, <<Authenticator:16/binary>>, Body, Secret]), + Packet = <<Head/binary, ReplyAuthenticator:16/binary, Body/binary>>, + {Packet, Req#{is_valid := true}}. + +-spec encode_command(command()) -> byte(). +encode_command(request) -> ?RAccess_Request; +encode_command(accept) -> ?RAccess_Accept; +encode_command(challenge) -> ?RAccess_Challenge; +encode_command(reject) -> ?RAccess_Reject; +encode_command(accreq) -> ?RAccounting_Request; +encode_command(accresp) -> ?RAccounting_Response; +encode_command(coareq) -> ?RCoa_Request; +encode_command(coaack) -> ?RCoa_Ack; +encode_command(coanak) -> ?RCoa_Nak; +encode_command(discreq) -> ?RDisconnect_Request; +encode_command(discack) -> ?RDisconnect_Ack; +encode_command(discnak) -> ?RDisconnect_Nak. + +-spec encode_message_authenticator(req(), binary()) -> binary(). +encode_message_authenticator(#{reqid := ReqId, cmd := Cmd, + authenticator := Authenticator, + secret := Secret, + msg_hmac := true}, Body) -> + Head = <<(encode_command(Cmd)):8, ReqId:8, (byte_size(Body) + 20 + 2 + 16):16>>, + HMAC = message_authenticator( + Secret, [Head, Authenticator, Body, + <<?RMessage_Authenticator,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>>]), + <<Body/binary, ?RMessage_Authenticator, 18, HMAC/binary>>; +encode_message_authenticator(_Req, Body) -> + Body. + +chunk(Bin, Length) -> + case Bin of + <<First:Length/bytes, Rest/binary>> -> {First, Rest}; + _ -> {Bin, <<>>} + end. + +encode_eap_attribute({<<>>, _}, EncReq) -> + EncReq; +encode_eap_attribute({Value, Rest}, Body) -> + EncAttr = <<?REAP_Message, (byte_size(Value) + 2):8, Value/binary>>, + encode_eap_attribute(chunk(Rest, 253), <<Body/binary, EncAttr/binary>>). + +-spec encode_eap_message(binary(), binary()) -> binary(). +encode_eap_message(EAP, EncReq) + when EAP =:= <<>>; EAP =:= undefined -> + EncReq; +encode_eap_message(EAP, EncReq) when is_binary(EAP) -> + encode_eap_attribute(chunk(EAP, 253), EncReq). + +-spec encode_attributes(req(), attribute_list(), binary()) -> binary(). +encode_attributes(Req, Attributes, Init) -> + lists:foldl( + fun ({A = #attribute{}, Val}, Body) -> + EncAttr = encode_attribute(Req, A, Val), + <<Body/binary, EncAttr/binary>>; + ({Id, Val}, Body) -> + case eradius_dict:lookup(attribute, Id) of + AttrRec = #attribute{} -> + EncAttr = encode_attribute(Req, AttrRec, Val), + <<Body/binary, EncAttr/binary>>; + _ -> + Body + end + end, Init, Attributes). + +-spec encode_attribute(eradius_req:req(), #attribute{}, term()) -> binary(). +encode_attribute(_Req, _Attr = #attribute{id = ?RMessage_Authenticator}, _) -> + %% message authenticator is handled through the msg_hmac flag + <<>>; +encode_attribute(_Req, _Attr = #attribute{id = ?REAP_Message}, _) -> + %% EAP-Message attributes are handled through the eap_msg field + <<>>; +encode_attribute(Req, Attr = #attribute{id = {Vendor, Id}}, Value) -> + EncValue = encode_attribute(Req, Attr#attribute{id = Id}, Value), + if byte_size(EncValue) + 6 > 255 -> + error(badarg, [{Vendor, Id}, Value]); + true -> ok + end, + <<?RVendor_Specific:8, (byte_size(EncValue) + 6):8, Vendor:32, EncValue/binary>>; +encode_attribute(Req, #attribute{type = {tagged, Type}, id = Id, enc = Enc}, Value) -> + case Value of + {Tag, UntaggedValue} when Tag >= 1, Tag =< 16#1F -> ok; + UntaggedValue -> Tag = 0 + end, + EncValue = encrypt_value(Req, encode_value(Type, UntaggedValue), Enc), + if byte_size(EncValue) + 3 > 255 -> + error(badarg, [Id, Value]); + true -> ok + end, + <<Id, (byte_size(EncValue) + 3):8, Tag:8, EncValue/binary>>; +encode_attribute(Req, #attribute{type = Type, id = Id, enc = Enc}, Value)-> + EncValue = encrypt_value(Req, encode_value(Type, Value), Enc), + if byte_size(EncValue) + 2 > 255 -> + error(badarg, [Id, Value]); + true -> ok + end, + <<Id, (byte_size(EncValue) + 2):8, EncValue/binary>>. + +-spec encrypt_value(req(), binary(), eradius_dict:attribute_encryption()) -> binary(). +encrypt_value(#{authenticator := Authenticator, secret := Secret}, Val, scramble) -> + scramble(Secret, Authenticator, Val); +encrypt_value(#{authenticator := Authenticator, secret := Secret}, Val, salt_crypt) -> + salt_encrypt(generate_salt(), Secret, Authenticator, Val); +encrypt_value(#{authenticator := Authenticator, secret := Secret}, Val, ascend) -> + ascend(Secret, Authenticator, Val); +encrypt_value(_Req, Val, no) -> + Val. + +-spec encode_value(eradius_dict:attribute_prim_type(), term()) -> binary(). +encode_value(_, V) when is_binary(V) -> + V; +encode_value(binary, V) -> + V; +encode_value(integer, V) -> + <<V:32>>; +encode_value(integer24, V) -> + <<V:24>>; +encode_value(integer64, V) -> + <<V:64>>; +encode_value(ipaddr, {A,B,C,D}) -> + <<A:8, B:8, C:8, D:8>>; +encode_value(ipv6addr, {A,B,C,D,E,F,G,H}) -> + <<A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>; +encode_value(ipv6prefix, {{A,B,C,D,E,F,G,H}, PLen}) -> + L = (PLen + 7) div 8, + <<IP:L/bytes, _R/binary>> = <<A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>, + <<0, PLen, IP/binary>>; +encode_value(string, V) when is_list(V) -> + unicode:characters_to_binary(V); +encode_value(octets, V) when is_list(V) -> + iolist_to_binary(V); +encode_value(octets, V) when is_integer(V) -> + <<V:32>>; +encode_value(date, V) when is_list(V) -> + unicode:characters_to_binary(V); +encode_value(date, Date = {{_,_,_},{_,_,_}}) -> + EpochSecs = calendar:datetime_to_gregorian_seconds(Date) - + calendar:datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}}), + <<EpochSecs:32>>. + +%% ------------------------------------------------------------------------------------------ +%% -- Wire Decoding + +-spec decode_body(binary(), req()) -> req(). +decode_body(Body, #{is_valid := true, head := Head} = Req0) -> + Req1 = Req0#{msg_hmac => 0, attrs => [], eap_msg => <<>>}, + Req2 = decode_attributes(Body, 0, Req1), + + case Req2 of + #{msg_hmac := Pos} when Pos > 0 -> + validate_packet_authenticator(Head, Body, Pos, Req2), + Req2#{msg_hmac := true}; + _ -> + Req2#{msg_hmac := false} + end. + +validate_packet_authenticator(Head, Body, Pos, + #{request_authenticator := Authenticator, secret := Secret}) -> + validate_packet_authenticator(Head, Body, Pos, Authenticator, Secret); +validate_packet_authenticator(Head, Body, Pos, + #{authenticator := Authenticator, secret := Secret}) -> + validate_packet_authenticator(Head, Body, Pos, Authenticator, Secret). + +validate_packet_authenticator(Head, Body, Pos, Auth, Secret) -> + case Body of + <<Before:Pos/bytes, Value:16/bytes, After/binary>> -> + case message_authenticator(Secret, [Head, Auth, Before, zero_authenticator(), After]) of + Value -> + ok; + _ -> + throw({bad_pdu, "Message-Authenticator Attribute is invalid"}) + end; + _ -> + throw({bad_pdu, "Message-Authenticator Attribute is malformed"}) + end. + +validate_authenticator(Cmd, Head, PacketAuthenticator, Body, + #{authenticator := RequestAuthenticator, secret := Secret}) + when Cmd =:= accept; Cmd =:= reject; Cmd =:= accresp; Cmd =:= coaack; + Cmd =:= coanak; Cmd =:= discack; Cmd =:= discnak; Cmd =:= challenge -> + compare_authenticator(crypto:hash(md5, [Head, RequestAuthenticator, Body, Secret]), PacketAuthenticator); +validate_authenticator(accreq, Head, PacketAuthenticator, Body, + #{secret := Secret}) -> + compare_authenticator(crypto:hash(md5, [Head, zero_authenticator(), Body, Secret]), PacketAuthenticator); +validate_authenticator(_Cmd, _Head, _RequestAuthenticator, _Body, _Req) -> + true. + +compare_authenticator(Authenticator, Authenticator) -> + true; +compare_authenticator(_RequestAuthenticator, _PacketAuthenticator) -> + throw({bad_pdu, "Authenticator Attribute is invalid"}). + +-spec decode_command(byte()) -> command(). +decode_command(?RAccess_Request) -> request; +decode_command(?RAccess_Accept) -> accept; +decode_command(?RAccess_Reject) -> reject; +decode_command(?RAccess_Challenge) -> challenge; +decode_command(?RAccounting_Request) -> accreq; +decode_command(?RAccounting_Response) -> accresp; +decode_command(?RCoa_Request) -> coareq; +decode_command(?RCoa_Ack) -> coaack; +decode_command(?RCoa_Nak) -> coanak; +decode_command(?RDisconnect_Request) -> discreq; +decode_command(?RDisconnect_Ack) -> discack; +decode_command(?RDisconnect_Nak) -> discnak; +decode_command(_) -> error({bad_pdu, "unknown request type"}). + +append_attr(Attr, #{attrs := Attrs} = Req) -> + Req#{attrs := [Attr | Attrs]}. + +decode_attributes(<<>>, _Pos, #{attrs := Attrs} = Req) -> + Req#{attrs := lists:reverse(Attrs)}; +decode_attributes(<<Type:8, ChunkLength:8, ChunkRest/binary>>, Pos, Req0) -> + ValueLength = ChunkLength - 2, + <<Value:ValueLength/binary, PacketRest/binary>> = ChunkRest, + Req = case eradius_dict:lookup(attribute, Type) of + AttrRec = #attribute{} -> + decode_attribute(Value, AttrRec, Pos + 2, Req0); + _ -> + append_attr({Type, Value}, Req0) + end, + decode_attributes(PacketRest, Pos + ChunkLength, Req). + +%% gotcha: the function returns a LIST of attribute-value pairs because +%% a vendor-specific attribute blob might contain more than one attribute. +-spec decode_attribute(binary(), #attribute{}, non_neg_integer(), req()) -> req(). +decode_attribute(<<VendorId:32/integer, ValueBin/binary>>, + #attribute{id = ?RVendor_Specific}, Pos, Req) -> + decode_vendor_specific_attribute(ValueBin, VendorId, Pos + 4, Req); +decode_attribute(<<Value/binary>>, + #attribute{id = ?REAP_Message}, _Pos, #{eap_msg := EAP} = Req) -> + Req#{eap_msg := <<EAP/binary, Value/binary>>}; +decode_attribute(<<EncValue/binary>>, + Attr = #attribute{ + id = ?RMessage_Authenticator, type = Type, enc = Encryption}, + Pos, Req) -> + AVP = {Attr, decode_value(decrypt_value(Req, EncValue, Encryption), Type)}, + append_attr(AVP, Req#{msg_hmac := Pos}); + +decode_attribute(<<EncValue/binary>>, + Attr = #attribute{type = Type, enc = Encryption}, _Pos, Req) + when is_atom(Type) -> + append_attr({Attr, decode_value(decrypt_value(Req, EncValue, Encryption), Type)}, Req); +decode_attribute(WholeBin = <<Tag:8, Bin/binary>>, + Attr = #attribute{type = {tagged, Type}}, _Pos, Req) -> + case {decode_tag_value(Tag), Attr#attribute.enc} of + {0, no} -> + %% decode including tag byte if tag is out of range + append_attr({Attr, {0, decode_value(WholeBin, Type)}}, Req); + {TagV, no} -> + append_attr({Attr, {TagV, decode_value(Bin, Type)}}, Req); + {TagV, Encryption} -> + %% for encrypted attributes, tag byte is never part of the value + AVP = {Attr, {TagV, decode_value(decrypt_value(Req, Bin, Encryption), Type)}}, + append_attr(AVP, Req) + end. + +-compile({inline, decode_tag_value/1}). +decode_tag_value(Tag) when (Tag >= 1) and (Tag =< 16#1F) -> Tag; +decode_tag_value(_OtherTag) -> 0. + +-spec decode_value(binary(), eradius_dict:attribute_prim_type()) -> term(). +decode_value(Bin, octets) -> + Bin; +decode_value(Bin, binary) -> + Bin; +decode_value(Bin, abinary) -> + Bin; +decode_value(Bin, string) -> + Bin; +decode_value(Bin, integer) -> + binary:decode_unsigned(Bin); +decode_value(Bin, integer24) -> + binary:decode_unsigned(Bin); +decode_value(Bin, integer64) -> + binary:decode_unsigned(Bin); +decode_value(Bin, date) -> + Int = binary:decode_unsigned(Bin), + calendar:now_to_universal_time({Int div 1000000, Int rem 1000000, 0}); +decode_value(<<B,C,D,E>>, ipaddr) -> + {B,C,D,E}; +decode_value(<<B:16,C:16,D:16,E:16,F:16,G:16,H:16,I:16>>, ipv6addr) -> + {B,C,D,E,F,G,H,I}; +decode_value(<<_0, PLen, P/binary>>, ipv6prefix) -> + <<B:16,C:16,D:16,E:16,F:16,G:16,H:16,I:16>> = pad_to(16, P), + {{B,C,D,E,F,G,H,I}, PLen}. + +-spec decrypt_value(req(), binary(), eradius_dict:attribute_encryption()) -> + eradius_dict:attr_value(). +decrypt_value(#{secret := Secret, authenticator := Authenticator}, Val, scramble) -> + scramble(Secret, Authenticator, Val); +decrypt_value(#{secret := Secret, authenticator := Authenticator}, Val, salt_crypt) -> + salt_decrypt(Secret, Authenticator, Val); +decrypt_value(#{secret := Secret, authenticator := Authenticator}, Val, ascend) -> + ascend(Secret, Authenticator, Val); +decrypt_value(_Req, Val, _Type) -> + Val. + +-spec decode_vendor_specific_attribute(binary(), non_neg_integer(), pos_integer(), req()) -> + req(). +decode_vendor_specific_attribute(<<>>, _VendorId, _Pos, Req) -> + Req; +decode_vendor_specific_attribute(<<Type:8, ChunkLength:8, ChunkRest/binary>>, + VendorId, Pos, Req0) -> + ValueLength = ChunkLength - 2, + <<Value:ValueLength/binary, PacketRest/binary>> = ChunkRest, + VendorAttrKey = {VendorId, Type}, + Req = case eradius_dict:lookup(attribute, VendorAttrKey) of + Attr = #attribute{} -> + decode_attribute(Value, Attr, Pos + 2, Req0); + _ -> + append_attr({VendorAttrKey, Value}, Req0) + end, + decode_vendor_specific_attribute(PacketRest, VendorId, Pos + ChunkLength, Req). + +%% ------------------------------------------------------------------------------------------ +%% -- Attribute Encryption +-spec scramble(secret(), authenticator(), binary()) -> binary(). +scramble(SharedSecret, RequestAuthenticator, <<PlainText/binary>>) -> + B = crypto:hash(md5, [SharedSecret, RequestAuthenticator]), + do_scramble(SharedSecret, B, pad_to(16, PlainText), << >>). + +do_scramble(SharedSecret, B, <<PlainText:16/binary, Remaining/binary>>, CipherText) -> + NewCipherText = crypto:exor(PlainText, B), + Bnext = crypto:hash(md5, [SharedSecret, NewCipherText]), + do_scramble(SharedSecret, Bnext, Remaining, <<CipherText/binary, NewCipherText/binary>>); + +do_scramble(_SharedSecret, _B, << >>, CipherText) -> + CipherText. + +-spec generate_salt() -> salt(). +generate_salt() -> + <<Salt1, Salt2>> = crypto:strong_rand_bytes(2), + <<(Salt1 bor 16#80), Salt2>>. + +-spec salt_encrypt(salt(), secret(), authenticator(), binary()) -> binary(). +salt_encrypt(Salt, SharedSecret, RequestAuthenticator, PlainText) -> + CipherText = do_salt_crypt(encrypt, Salt, SharedSecret, RequestAuthenticator, (pad_to(16, << (byte_size(PlainText)):8, PlainText/binary >>))), + <<Salt/binary, CipherText/binary>>. + +-spec salt_decrypt(secret(), authenticator(), binary()) -> binary(). +salt_decrypt(SharedSecret, RequestAuthenticator, <<Salt:2/binary, CipherText/binary>>) -> + << Length:8/integer, PlainText/binary >> = do_salt_crypt(decrypt, Salt, SharedSecret, RequestAuthenticator, CipherText), + if + Length < byte_size(PlainText) -> + binary:part(PlainText, 0, Length); + true -> + PlainText + end. + +do_salt_crypt(Op, Salt, SharedSecret, RequestAuthenticator, <<CipherText/binary>>) -> + B = crypto:hash(md5, [SharedSecret, RequestAuthenticator, Salt]), + salt_crypt(Op, SharedSecret, B, CipherText, << >>). + +salt_crypt(Op, SharedSecret, B, <<PlainText:16/binary, Remaining/binary>>, CipherText) -> + NewCipherText = crypto:exor(PlainText, B), + Bnext = case Op of + decrypt -> crypto:hash(md5, [SharedSecret, PlainText]); + encrypt -> crypto:hash(md5, [SharedSecret, NewCipherText]) + end, + salt_crypt(Op, SharedSecret, Bnext, Remaining, <<CipherText/binary, NewCipherText/binary>>); + +salt_crypt(_Op, _SharedSecret, _B, << >>, CipherText) -> + CipherText. + +-spec ascend(secret(), authenticator(), binary()) -> binary(). +ascend(SharedSecret, RequestAuthenticator, <<PlainText/binary>>) -> + Digest = crypto:hash(md5, [RequestAuthenticator, SharedSecret]), + crypto:exor(Digest, pad_to(16, PlainText)). + +%% @doc pad binary to specific length +%% See <a href="http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html"> +%% http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html +%% </a> +-compile({inline, pad_to/2}). +pad_to(Width, Binary) -> + case (Width - byte_size(Binary) rem Width) rem Width of + 0 -> Binary; + N -> <<Binary/binary, 0:(N*8)>> + end. + +%% @doc calculate the MD5 message authenticator +-if(?OTP_RELEASE >= 23). +%% crypto API changes in OTP >= 23 +message_authenticator(Secret, Msg) -> + crypto:mac(hmac, md5, Secret, Msg). +-else. +message_authenticator(Secret, Msg) -> + crypto:hmac(md5, Secret, Msg). + +-endif. diff --git a/src/eradius_server.erl b/src/eradius_server.erl index b5e50f86..6adfc3ac 100644 --- a/src/eradius_server.erl +++ b/src/eradius_server.erl @@ -1,491 +1,427 @@ -%% @doc -%% This module implements a generic RADIUS server. A handler callback module -%% is used to process requests. The handler module is selected based on the NAS that -%% sent the request. Requests from unknown NASs are discarded. +%% Copyright (c) 2002-2007, Martin Björklund and Torbjörn Törnkvist +%% Copyright (c) 2011, Travelping GmbH <info@travelping.com> %% -%% It is also possible to run request handlers on remote nodes. If configured, -%% the server process will balance load among connected nodes. -%% Please see the Overview page for a detailed description of the server configuration. +%% SPDX-License-Identifier: MIT %% -%% == Callback Description == -%% -%% There are two callbacks at the moment. -%% -%% === validate_arguments(Args :: list()) -> boolean() | {true, NewArgs :: list()} | Error :: term(). === -%% -%% This is optional callback and can be absent. During application configuration processing `eradius_config` -%% calls this for the handler to validate and transform handler arguments. -%% -%% === radius_request(#radius_request{}, #nas_prop{}, HandlerData :: term()) -> {reply, #radius_request{}} | noreply === -%% -%% This function is called for every RADIUS request that is received by the server. -%% Its first argument is a request record which contains the request type and AVPs. -%% The second argument is a NAS descriptor. The third argument is an opaque term from the -%% server configuration. -%% -%% Both records are defined in 'eradius_lib.hrl', but their definition is reproduced here for easy reference. -%% -%% ``` -%% -record(radius_request, { -%% reqid :: byte(), -%% cmd :: 'request' | 'accept' | 'challenge' | 'reject' | -%% 'accreq' | 'accresp' | 'coareq' | 'coaack' | 'coanak' | -%% 'discreq' | 'discack' | 'discnak' -%% attrs :: eradius_lib:attribute_list(), -%% secret :: eradius_lib:secret(), -%% authenticator :: eradius_lib:authenticator(), -%% msg_hmac :: boolean(), -%% eap_msg :: binary() -%% }). -%% -%% -record(nas_prop, { -%% server_ip :: inet:ip_address(), -%% server_port :: eradius_server:port_number(), -%% nas_ip :: inet:ip_address(), -%% nas_port :: eradius_server:port_number(), -%% nas_id :: term(), -%% metrics_info :: {atom_address(), atom_address()}, -%% secret :: eradius_lib:secret(), -%% trace :: boolean(), -%% handler_nodes :: 'local' | list(atom()) -%% }). -%% ''' -module(eradius_server). --export([start_link/3, start_link/4]). --export_type([port_number/0, req_id/0]). +-feature(maybe_expr, enable). -%% internal --export([do_radius/7, handle_request/3, handle_remote_request/5, stats/2]). +-behaviour(gen_server). --import(eradius_lib, [printable_peer/2]). +%% API +-export([start_instance/3, start_instance/4, stop_instance/1]). +-export([start_link/3, start_link/4]). +-export_type([req_id/0]). --behaviour(gen_server). +%% internal API +-export([do_radius/4]). +-ignore_xref([do_radius/4]). + +%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-ignore_xref([start_link/3, start_link/4]). +-ignore_xref([start_instance/3, start_instance/4, stop_instance/1]). + -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("kernel/include/logger.hrl"). -include("eradius_lib.hrl"). -include("dictionary.hrl"). -include("eradius_dict.hrl"). +-import(eradius_lib, [printable_peer/1, printable_peer/2]). + -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. +-export_type([handler/0]). + -type req_id() :: byte(). --type udp_socket() :: port(). --type udp_packet() :: {udp, udp_socket(), inet:ip_address(), port_number(), binary()}. +%% RADIUS request id + +-type socket_opts() :: #{family => inet | inet6, + ifaddr => inet:ip_address() | any, + port => inet:port_number(), + active_n => 'once' | non_neg_integer(), + ipv6_v6only => boolean, + inet_backend => inet | socket, + recbuf => non_neg_integer(), + sndbuf => non_neg_integer() + }. +%% Options to configure the RADIUS server UDP socket. + +-type socket_config() :: #{family := inet | inet6, + ifaddr := inet:ip_address() | any, + port := inet:port_number(), + active_n := 'once' | non_neg_integer(), + ipv6_v6only => boolean, + inet_backend => inet | socket, + recbuf => non_neg_integer(), + sndbuf => non_neg_integer() + }. +%% Options to configure the RADIUS server UDP socket. +%% Conceptually the same as `t:socket_opts/0', except that may fields are mandatory. + +-type server_opts() :: #{server_name => term(), + socket_opts => socket_opts(), + handler := {module(), term()}, + metrics_callback => eradius_req:metrics_callback(), + clients := map()}. +%% Options to configure the RADIUS server. + +-type server_config() :: #{server_name := term(), + socket_opts := socket_config(), + handler := {module(), term()}, + metrics_callback := undefined | eradius_req:metrics_callback(), + clients := map()}. +%% Options to configure the RADIUS server. +%% Conceptually the same as `t:server_opts/0', except that may fields are mandatory. + +-type client() :: #{client := binary(), + secret := eradius_req:secret()}. +%% RADIUS client settings + +-export_type([server_name/0, client/0]). -record(state, { - socket :: udp_socket(), % Socket Reference of opened UDP port - ip = {0,0,0,0} :: inet:ip_address(), % IP to which this socket is bound - port = 0 :: port_number(), % Port number we are listening on + name :: atom(), % server name + family :: inet:address_family(), + socket :: gen_udp:socket(), % Socket Reference of opened UDP port + server :: {inet:ip_address() | any, inet:port_number()}, % IP and port to which this socket is bound + active_n :: 'once' | non_neg_integer(), transacts :: ets:tid(), % ETS table containing current transactions - counter :: #server_counter{}, % statistics counter, - name :: atom() % server name + handler :: handler(), + metrics_callback :: eradius_req:metrics_callback(), + clients :: #{inet:ip_address() => client()} }). --optional_callbacks([validate_arguments/1]). - --callback validate_arguments(Args :: list()) -> - boolean() | {true, NewArgs :: list()}. - --callback radius_request(#radius_request{}, #nas_prop{}, HandlerData :: term()) -> - {reply, #radius_request{}} | noreply | {error, timeout}. - --spec start_link(atom(), inet:ip4_address(), port_number()) -> {ok, pid()} | {error, term()}. -start_link(ServerName, IP, Port) -> - start_link(ServerName, IP, Port, []). +-callback radius_request(eradius_req:req(), HandlerData :: term()) -> + {reply, eradius_req:req()} | noreply | {error, timeout}. + +%%%========================================================================= +%%% API +%%%========================================================================= + +-spec start_instance(IP :: 'any' | inet:ip_address(), Port :: inet:port_number(), + Opts :: server_opts()) -> gen_server:start_ret(). +start_instance(IP, Port, Opts) + when (IP =:= any orelse is_tuple(IP)) andalso + is_integer(Port) andalso Port >= 0 andalso Port < 65536 -> + eradius_server_sup:start_instance([IP, Port, Opts]). + +-spec start_instance(ServerName :: gen_server:server_name(), + IP :: 'any' | inet:ip_address(), Port :: inet:port_number(), + Opts :: server_opts()) -> gen_server:start_ret(). +start_instance(ServerName, IP, Port, Opts) + when (IP =:= any orelse is_tuple(IP)) andalso + is_integer(Port) andalso Port >= 0 andalso Port < 65536 -> + eradius_server_sup:start_instance([ServerName, IP, Port, Opts]). + +-spec stop_instance(Pid :: pid()) -> ok. +stop_instance(Pid) -> + try gen_server:call(Pid, stop) + catch exit:_ -> ok end. + +-spec start_link(IP :: 'any' | inet:ip_address(), Port :: inet:port_number(), + Opts :: server_opts()) -> gen_server:start_ret(). +start_link(IP, Port, #{handler := {_, _}, clients := #{}} = Opts) + when (IP =:= any orelse is_tuple(IP)) andalso + is_integer(Port) andalso Port >= 0 andalso Port < 65536 -> + maybe + {ok, Config} ?= config(IP, Port, Opts), + gen_server:start_link(?MODULE, [Config], []) + end. --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, Opts}, []). +-spec start_link(ServerName :: gen_server:server_name(), + IP :: 'any' | inet:ip_address(), Port :: inet:port_number(), + Opts :: server_opts()) -> gen_server:start_ret(). +start_link(ServerName, IP, Port, #{handler := {_, _}, clients := #{}} = Opts) + when (IP =:= any orelse is_tuple(IP)) andalso + is_integer(Port) andalso Port >= 0 andalso Port < 65536 -> + maybe + {ok, Config} ?= config(IP, Port, Opts), + gen_server:start_link(ServerName, ?MODULE, [Config], []) + end. -stats(Server, Function) -> - gen_server:call(Server, {stats, Function}). +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== -%% ------------------------------------------------------------------------------------------ -%% -- gen_server Callbacks %% @private -init({ServerName, IP, Port, Opts}) -> +init([#{server_name := ServerName, + socket_opts := #{family := Family, active_n := ActiveN, + ifaddr := IP, port := Port} = SocketOpts, + handler := Handler, metrics_callback := MetricsCallback, + clients := Clients} = _Config]) -> process_flag(trap_exit, true), - SockOpts0 = proplists:get_value(socket_opts, Opts, []), - SockOpts1 = add_sock_opt(recbuf, 8192, SockOpts0), - SockOpts = add_sock_opt(sndbuf, 131072, SockOpts1), - SockOptsDef = ?DEFAULT_RADIUS_SERVER_OPTS(IP) ++ SockOpts, - case gen_udp:open(Port, SockOptsDef) of + + InetOpts = inet_opts(SocketOpts, [{active, ActiveN}, binary, Family]), + Server = {IP, Port}, + ?LOG(debug, "Starting RADIUS server on ~s with socket options ~0p", + [printable_peer(Server), InetOpts]), + + case gen_udp:open(Port, InetOpts) of {ok, Socket} -> - {ok, #state{socket = Socket, - ip = IP, port = Port, name = ServerName, - transacts = ets:new(transacts, []), - counter = eradius_counter:init_counter({IP, Port, ServerName})}}; + State = + #state{ + name = ServerName, + family = Family, + socket = Socket, + server = {IP, Port}, + active_n = ActiveN, + handler = Handler, + clients = Clients, + transacts = ets:new(transacts, []), + metrics_callback = MetricsCallback + }, + {ok, State}; {error, Reason} -> + ?LOG(debug, "Starting RADIUS server on ~s failed with ~0p", + [printable_peer(Server), Reason]), {stop, Reason} end. %% @private -handle_info(ReqUDP = {udp, Socket, FromIP, FromPortNo, Packet}, - State = #state{name = ServerName, transacts = Transacts, ip = _IP, port = _Port}) -> - TS1 = erlang:monotonic_time(), - case lookup_nas(State, FromIP, Packet) of - {ok, ReqID, Handler, NasProp} -> - #nas_prop{server_ip = ServerIP, server_port = Port} = NasProp, - ReqKey = {FromIP, FromPortNo, ReqID}, - NNasProp = NasProp#nas_prop{nas_port = FromPortNo}, - eradius_counter:inc_counter(requests, NasProp), - case ets:lookup(Transacts, ReqKey) of - [] -> - HandlerPid = proc_lib:spawn_link(?MODULE, do_radius, [self(), ServerName, ReqKey, Handler, NNasProp, ReqUDP, TS1]), - ets:insert(Transacts, {ReqKey, {handling, HandlerPid}}), - ets:insert(Transacts, {HandlerPid, ReqKey}), - eradius_counter:inc_counter(pending, NasProp); - [{_ReqKey, {handling, HandlerPid}}] -> - %% handler process is still working on the request - ?LOG(debug, "~s From: ~s INF: Handler process ~p is still working on the request. duplicate request (being handled) ~p", - [printable_peer(ServerIP, Port), printable_peer(FromIP, FromPortNo), HandlerPid, ReqKey]), - eradius_counter:inc_counter(dupRequests, NasProp); - [{_ReqKey, {replied, HandlerPid}}] -> - %% handler process waiting for resend message - HandlerPid ! {self(), resend, Socket}, - ?LOG(debug, "~s From: ~s INF: Handler ~p waiting for resent message. duplicate request (resent) ~p", - [printable_peer(ServerIP, Port), printable_peer(FromIP, FromPortNo), HandlerPid, ReqKey]), - eradius_counter:inc_counter(dupRequests, NasProp), - eradius_counter:inc_counter(retransmissions, NasProp) - end, - NewState = State; - {discard, Reason} when Reason == no_nodes_local, Reason == no_nodes -> - NewState = State#state{counter = eradius_counter:inc_counter(discardNoHandler, State#state.counter)}; - {discard, _Reason} -> - NewState = State#state{counter = eradius_counter:inc_counter(invalidRequests, State#state.counter)} +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; +handle_call(_Call, _From, State) -> + {reply, ok, State}. + +%% @private +handle_cast(_Msg, State) -> + {noreply, State}. + +%% @private +handle_info({udp_passive, _Socket}, #state{socket = Socket, active_n = ActiveN} = State) -> + inet:setopts(Socket, [{active, ActiveN}]), + {noreply, State}; + +handle_info({udp, Socket, FromIP, FromPortNo, <<Header:20/bytes, Body/binary>>}, + #state{name = ServerName, server = Server, transacts = Transacts, + handler = Handler, clients = Clients, + metrics_callback = MetricsCallback} = State) + when is_map_key(FromIP, Clients) -> + NAS = maps:get(FromIP, Clients), + <<_, ReqId:8, _/binary>> = Header, + Req0 = eradius_req:request(Header, Body, NAS, MetricsCallback), + Req1 = Req0#{socket => Socket, + server => ServerName, + server_addr => Server, + client_addr => {FromIP, FromPortNo}}, + ReqKey = {FromIP, FromPortNo, ReqId}, + + case ets:lookup(Transacts, ReqKey) of + [] -> + Req = eradius_req:record_metric(request, #{}, Req1), + HandlerPid = + proc_lib:spawn_link(?MODULE, do_radius, [self(), Handler, ReqKey, Req]), + ets:insert(Transacts, {ReqKey, {handling, HandlerPid}}), + ets:insert(Transacts, {HandlerPid, ReqKey}); + + [{_ReqKey, {handling, HandlerPid}}] -> + %% handler process is still working on the request + ?LOG(debug, "~s From: ~s INF: Handler process ~p is still working on the request." + " duplicate request (being handled) ~p", + [printable_peer(Server), + printable_peer(FromIP, FromPortNo), HandlerPid, ReqKey]), + eradius_req:record_metric(discard, #{reason => duplicate}, Req1); + [{_ReqKey, {replied, HandlerPid}}] -> + %% handler process waiting for resend message + HandlerPid ! {self(), resend}, + ?LOG(debug, "~s From: ~s INF: Handler ~p waiting for resent message. " + "duplicate request (resent) ~p", + [printable_peer(Server), + printable_peer(FromIP, FromPortNo), HandlerPid, ReqKey]), + eradius_req:record_metric(retransmission, #{reason => duplicate}, Req1) end, - inet:setopts(Socket, [{active, once}]), - {noreply, NewState}; + flow_control(State), + {noreply, State}; + +handle_info({udp, _Socket, _FromIP, _FromPortNo, _Packet}, + #state{name = ServerName, metrics_callback = MetricsCallback} = State) -> + %% TBD: this should go into a malformed counter + eradius_req:metrics_callback(MetricsCallback, invalid_request, #{server => ServerName}), + flow_control(State), + {noreply, State}; + handle_info({replied, ReqKey, HandlerPid}, State = #state{transacts = Transacts}) -> ets:insert(Transacts, {ReqKey, {replied, HandlerPid}}), {noreply, State}; + handle_info({'EXIT', HandlerPid, _Reason}, State = #state{transacts = Transacts}) -> [ets:delete(Transacts, ReqKey) || {_, ReqKey} <- ets:take(Transacts, HandlerPid)], {noreply, State}; + handle_info(_Info, State) -> {noreply, State}. -%% @private --spec add_sock_opt(recbuf | sndbuf, pos_integer(), proplists:proplist()) -> proplists:proplist(). -add_sock_opt(OptName, Default, Opts) -> - case proplists:get_value(OptName, Opts) of - undefined -> - Buf = application:get_env(eradius, OptName, Default), - [{OptName, Buf} | Opts]; - _Val -> - Opts - end. - %% @private terminate(_Reason, State) -> gen_udp:close(State#state.socket), ok. -%% @private -handle_call({stats, pull}, _From, State = #state{counter = Counter}) -> - {reply, Counter, State#state{counter = eradius_counter:reset_counter(Counter)}}; -handle_call({stats, read}, _From, State = #state{counter = Counter}) -> - {reply, Counter, State}; -handle_call({stats, reset}, _From, State = #state{counter = Counter}) -> - {reply, ok, State#state{counter = eradius_counter:reset_counter(Counter)}}. - -%% -- unused callbacks -%% @private -handle_cast(_Msg, State) -> {noreply, State}. %% @private code_change(_OldVsn, State, _Extra) -> {ok, State}. --spec lookup_nas(#state{}, inet:ip_address(), binary()) -> {ok, req_id(), eradius_server_mon:handler(), #nas_prop{}} | {discard, invalid | malformed}. -lookup_nas(#state{ip = IP, port = Port}, NasIP, <<_Code, ReqID, _/binary>>) -> - case eradius_server_mon:lookup_handler(IP, Port, NasIP) of - {ok, Handler, NasProp} -> - {ok, ReqID, Handler, NasProp}; - {error, not_found} -> - {discard, invalid} - end; -lookup_nas(_State, _NasIP, _Packet) -> - {discard, malformed}. - -%% ------------------------------------------------------------------------------------------ -%% -- Request Handler +%%%========================================================================= +%%% handler functions +%%%========================================================================= + %% @private --spec do_radius(pid(), string(), term(), eradius_server_mon:handler(), #nas_prop{}, udp_packet(), integer()) -> any(). -do_radius(ServerPid, ServerName, ReqKey, Handler = {HandlerMod, _}, NasProp, {udp, Socket, FromIP, FromPort, EncRequest}, TS1) -> - #nas_prop{server_ip = ServerIP, server_port = Port} = NasProp, - Nodes = eradius_node_mon:get_module_nodes(HandlerMod), - case run_handler(Nodes, NasProp, Handler, EncRequest) of - {reply, EncReply, {ReqCmd, RespCmd}, Request} -> - ?LOG(debug, "~s From: ~s INF: Sending response for request ~p", - [printable_peer(ServerIP, Port), printable_peer(FromIP, FromPort), ReqKey]), - TS2 = erlang:monotonic_time(), - inc_counter({ReqCmd, RespCmd}, ServerName, NasProp, TS2 - TS1, Request), - gen_udp:send(Socket, FromIP, FromPort, EncReply), +-spec do_radius(pid(), handler(), term(), eradius_req:req()) -> any(). +do_radius(ServerPid, {HandlerMod, HandlerArg}, ReqKey, + #{server := Server, client_addr := Client} = Req0) -> + case apply_handler_mod(HandlerMod, HandlerArg, Req0) of + {reply, Packet, Resp0, Req} -> + ?LOG(debug, "~s From: ~s INF: Sending response for request ~0p", + [printable_peer(Server), printable_peer(Client), ReqKey]), + + Resp = eradius_req:record_metric(reply, #{request => Req}, Resp0), + send_response(Resp, Packet), case application:get_env(eradius, resend_timeout, 2000) of ResendTimeout when ResendTimeout > 0, is_integer(ResendTimeout) -> ServerPid ! {replied, ReqKey, self()}, - wait_resend_init(ServerPid, ReqKey, FromIP, FromPort, EncReply, ResendTimeout, ?RESEND_RETRIES); + wait_resend_init(ServerPid, ReqKey, Resp, Packet, ResendTimeout, ?RESEND_RETRIES); _ -> ok end; {discard, Reason} -> ?LOG(debug, "~s From: ~s INF: Handler discarded the request ~p for reason ~1000.p", - [printable_peer(ServerIP, Port), printable_peer(FromIP, FromPort), Reason, ReqKey]), - inc_discard_counter(Reason, NasProp); + [printable_peer(Server), printable_peer(Client), Reason, ReqKey]), + eradius_req:record_metric(discard, #{reason => Reason}, Req0); {exit, Reason} -> ?LOG(debug, "~s From: ~s INF: Handler exited for reason ~p, discarding request ~p", - [printable_peer(ServerIP, Port), printable_peer(FromIP, FromPort), Reason, ReqKey]), - inc_discard_counter(packetsDropped, NasProp) - end, - eradius_counter:dec_counter(pending, NasProp). + [printable_peer(Server), printable_peer(Client), Reason, ReqKey]), + eradius_req:record_metric(discard, #{reason => dropped}, Req0) + end. -wait_resend_init(ServerPid, ReqKey, FromIP, FromPort, EncReply, ResendTimeout, Retries) -> +wait_resend_init(ServerPid, ReqKey, Resp, Packet, ResendTimeout, Retries) -> erlang:send_after(ResendTimeout, self(), timeout), - wait_resend(ServerPid, ReqKey, FromIP, FromPort, EncReply, Retries). + wait_resend(ServerPid, ReqKey, Resp, Packet, Retries). -wait_resend(_ServerPid, _ReqKey, _FromIP, _FromPort, _EncReply, 0) -> ok; -wait_resend(ServerPid, ReqKey, FromIP, FromPort, EncReply, Retries) -> +wait_resend(_ServerPid, _ReqKey, _Resp, _Packet, 0) -> + ok; +wait_resend(ServerPid, ReqKey, Resp, Packet, Retries) -> receive - {ServerPid, resend, Socket} -> - gen_udp:send(Socket, FromIP, FromPort, EncReply), - wait_resend(ServerPid, ReqKey, FromIP, FromPort, EncReply, Retries - 1); + {ServerPid, resend} -> + send_response(Resp, Packet), + wait_resend(ServerPid, ReqKey, Resp, Packet, Retries - 1); timeout -> ok end. -run_handler([], _NasProp, _Handler, _EncRequest) -> - {discard, no_nodes}; -run_handler(NodesAvailable, NasProp = #nas_prop{handler_nodes = local}, Handler, EncRequest) -> - case lists:member(node(), NodesAvailable) of - true -> - handle_request(Handler, NasProp, EncRequest); - false -> - {discard, no_nodes_local} - end; -run_handler(NodesAvailable, NasProp, Handler, EncRequest) -> - case ordsets:intersection(lists:usort(NodesAvailable), lists:usort(NasProp#nas_prop.handler_nodes)) of - [LocalNode] when LocalNode == node() -> - handle_request(Handler, NasProp, EncRequest); - [RemoteNode] -> - run_remote_handler(RemoteNode, Handler, NasProp, EncRequest); - Nodes -> - %% humble testing at the erlang shell indicated that phash2 distributes N - %% very well even for small lenghts. - N = erlang:phash2(make_ref(), length(Nodes)) + 1, - case lists:nth(N, Nodes) of - LocalNode when LocalNode == node() -> - handle_request(Handler, NasProp, EncRequest); - RemoteNode -> - run_remote_handler(RemoteNode, Handler, NasProp, EncRequest) - end - end. - -run_remote_handler(Node, {HandlerMod, HandlerArgs}, NasProp, EncRequest) -> - RemoteArgs = [self(), HandlerMod, HandlerArgs, NasProp, EncRequest], - HandlerPid = spawn_link(Node, ?MODULE, handle_remote_request, RemoteArgs), - receive - {HandlerPid, ReturnValue} -> - ReturnValue - after - ?HANDLER_REPLY_TIMEOUT -> - %% this happens if the remote handler doesn't terminate - unlink(HandlerPid), - {discard, {remote_handler_reply_timeout, Node}} - end. - -%% @private --spec handle_request(eradius_server_mon:handler(), #nas_prop{}, binary()) -> any(). -handle_request({HandlerMod, HandlerArg}, NasProp = #nas_prop{secret = Secret, nas_ip = ServerIP, nas_port = Port}, EncRequest) -> - case eradius_lib:decode_request(EncRequest, Secret) of - Request = #radius_request{} -> - Sender = {ServerIP, Port, Request#radius_request.reqid}, - ?LOG(debug, "~s", [eradius_log:collect_message(Sender, Request)], - maps:from_list(eradius_log:collect_meta(Sender, Request))), - eradius_log:write_request(Sender, Request), - apply_handler_mod(HandlerMod, HandlerArg, Request, NasProp); - {bad_pdu, "Message-Authenticator Attribute is invalid"} -> - ?LOG(error, "~s INF: Message-Authenticator Attribute is invalid", - [printable_peer(ServerIP, Port)]), - {discard, bad_authenticator}; - {bad_pdu, "Authenticator Attribute is invalid"} -> - ?LOG(error, "~s INF: Authenticator Attribute is invalid", - [printable_peer(ServerIP, Port)]), - {discard, bad_authenticator}; - {bad_pdu, "unknown request type"} -> - ?LOG(error, "~s INF: unknown request type", - [printable_peer(ServerIP, Port)]), - {discard, unknown_req_type}; - {bad_pdu, Reason} -> - ?LOG(error, "~s INF: Could not decode the request, reason: ~s", - [printable_peer(ServerIP, Port), Reason]), - {discard, malformed} - end. - -%% @private -%% @doc this function is spawned on a remote node to handle a radius request. -%% remote handlers need to be upgraded if the signature of this function changes. -%% error reports go to the logger of the node that executes the request. -handle_remote_request(ReplyPid, HandlerMod, HandlerArg, NasProp, EncRequest) -> - Result = handle_request({HandlerMod, HandlerArg}, NasProp, EncRequest), - ReplyPid ! {self(), Result}. - --spec apply_handler_mod(module(), term(), #radius_request{}, #nas_prop{}) -> {discard, term()} | {exit, term()} | {reply, binary()}. -apply_handler_mod(HandlerMod, HandlerArg, Request, NasProp) -> - #nas_prop{server_ip = ServerIP, server_port = Port} = NasProp, - try HandlerMod:radius_request(Request, NasProp, HandlerArg) of - {reply, Reply = #radius_request{cmd = ReplyCmd, attrs = ReplyAttrs, msg_hmac = MsgHMAC, eap_msg = EAPmsg}} -> - Sender = {NasProp#nas_prop.nas_ip, NasProp#nas_prop.nas_port, Request#radius_request.reqid}, - EncReply = eradius_lib:encode_reply(Request#radius_request{cmd = ReplyCmd, attrs = ReplyAttrs, - msg_hmac = Request#radius_request.msg_hmac or MsgHMAC or (size(EAPmsg) > 0), - eap_msg = EAPmsg}), - ?LOG(debug, "~s", [eradius_log:collect_message(Sender, Reply)], - maps:from_list(eradius_log:collect_meta(Sender, Reply))), - eradius_log:write_request(Sender, Reply), - {reply, EncReply, {Request#radius_request.cmd, ReplyCmd}, Request}; +send_response(#{socket := Socket, client_addr := {ClientIP, ClientPort}}, Packet) -> + gen_udp:send(Socket, ClientIP, ClientPort, Packet). + +-spec apply_handler_mod(module(), term(), eradius_req:req()) -> + {discard, term()} | + {exit, term()} | + {reply, binary(), eradius_req:req(), eradius_req:req()}. +apply_handler_mod(HandlerMod, HandlerArg, + #{cmd := Cmd, req_id := ReqId, server := Server, client_addr := {ClientIP, _}} = Req) -> + try HandlerMod:radius_request(Req, HandlerArg) of + {reply, Resp0} -> + {Packet, Resp} = eradius_req:packet(Resp0), + {reply, Packet, Resp, Req}; noreply -> - ?LOG(error, "~s INF: Noreply for request ~p from handler ~p: returned value: ~p", - [printable_peer(ServerIP, Port), Request, HandlerArg, noreply]), + ?LOG(error, "~ts INF: Noreply for request ~tp from handler ~tp: returned value: ~tp", + [printable_peer(Server), ReqId, HandlerArg, noreply]), {discard, handler_returned_noreply}; {error, timeout} -> - ReqType = eradius_log:format_cmd(Request#radius_request.cmd), - ReqId = integer_to_list(Request#radius_request.reqid), - S = {NasProp#nas_prop.nas_ip, NasProp#nas_prop.nas_port, Request#radius_request.reqid}, - NAS = eradius_lib:get_attr(Request, ?NAS_Identifier), - NAS_IP = inet_parse:ntoa(NasProp#nas_prop.nas_ip), - ?LOG(error, "~s INF: Timeout after waiting for response to ~s(~s) from RADIUS NAS: ~s NAS_IP:~s", - [printable_peer(ServerIP, Port), ReqType, ReqId, NAS, NAS_IP], - maps:from_list(eradius_log:collect_meta(S, Request))), + ReqType = eradius_log:format_cmd(Cmd), + ?LOG(error, "~ts INF: Timeout after waiting for response to ~ts(~w) from RADIUS Client: ~s", + [printable_peer(Server), ReqType, ReqId, inet:ntoa(ClientIP)]), {discard, {bad_return, {error, timeout}}}; OtherReturn -> - ?LOG(error, "~s INF: Unexpected return for request ~p from handler ~p: returned value: ~p", - [printable_peer(ServerIP, Port), Request, HandlerArg, OtherReturn]), + ?LOG(error, "~ts INF: Unexpected return for request ~0tp from handler ~tp: returned value: ~tp", + [printable_peer(Server), ReqId, HandlerArg, OtherReturn]), {discard, {bad_return, OtherReturn}} catch Class:Reason:S -> - ?LOG(error, "~s INF: Handler crashed after request ~p, radius handler class: ~p, reason of crash: ~p, stacktrace: ~p", - [printable_peer(ServerIP, Port), Request, Class, Reason, S]), + ?LOG(error, "~ts INF: Handler crashed after request ~tp, radius handler class: ~tp, reason of crash: ~tp, stacktrace: ~tp", + [printable_peer(Server), ReqId, Class, Reason, S]), {exit, {Class, Reason}} end. -inc_counter({ReqCmd, RespCmd}, ServerName, NasProp, Ms, Request) -> - inc_request_counter(ReqCmd, ServerName, NasProp, Ms, Request), - inc_reply_counter(RespCmd, NasProp, Request). - -inc_request_counter(request, ServerName, NasProp, Ms, _) -> - eradius_counter:observe(eradius_request_duration_milliseconds, - NasProp, Ms, ServerName, "RADIUS request exeuction time"), - eradius_counter:observe(eradius_access_request_duration_milliseconds, - NasProp, Ms, ServerName, "Access-Request execution time"), - eradius_counter:inc_request_counter(accessRequests, NasProp); -inc_request_counter(accreq, ServerName, NasProp, Ms, Request) -> - eradius_counter:observe(eradius_request_duration_milliseconds, - NasProp, Ms, ServerName, "RADIUS request exeuction time"), - eradius_counter:observe(eradius_accounting_request_duration_milliseconds, - NasProp, Ms, ServerName, "Accounting-Request execution time"), - inc_request_counter_accounting(NasProp, Request); -inc_request_counter(coareq, ServerName, NasProp, Ms, _) -> - eradius_counter:observe(eradius_request_duration_milliseconds, - NasProp, Ms, ServerName, "RADIUS request exeuction time"), - eradius_counter:observe(eradius_coa_request_duration_milliseconds, - NasProp, Ms, ServerName, "Coa-Request execution time"), - eradius_counter:inc_request_counter(coaRequests, NasProp); -inc_request_counter(discreq, ServerName, NasProp, Ms, _) -> - eradius_counter:observe(eradius_request_duration_milliseconds, - NasProp, Ms, ServerName, "RADIUS request exeuction time"), - eradius_counter:observe(eradius_disconnect_request_duration_milliseconds, - NasProp, Ms, ServerName, "Disconnect-Request execution time"), - eradius_counter:inc_request_counter(discRequests, NasProp); -inc_request_counter(_Cmd, _ServerName, _NasProp, _Ms, _Request) -> - ok. - -inc_reply_counter(accept, NasProp, _) -> - eradius_counter:inc_counter(replies, NasProp), - eradius_counter:inc_reply_counter(accessAccepts, NasProp); -inc_reply_counter(reject, NasProp, _) -> - eradius_counter:inc_counter(replies, NasProp), - eradius_counter:inc_reply_counter(accessRejects, NasProp); -inc_reply_counter(challenge, NasProp, _) -> - eradius_counter:inc_counter(replies, NasProp), - eradius_counter:inc_reply_counter(accessChallenges, NasProp); -inc_reply_counter(accresp, NasProp, Request) -> - eradius_counter:inc_counter(replies, NasProp), - inc_response_counter_accounting(NasProp, Request); -inc_reply_counter(coaack, NasProp, _) -> - eradius_counter:inc_counter(replies, NasProp), - eradius_counter:inc_reply_counter(coaAcks, NasProp); -inc_reply_counter(coanak, NasProp, _) -> - eradius_counter:inc_counter(replies, NasProp), - eradius_counter:inc_reply_counter(coaNaks, NasProp); -inc_reply_counter(discack, NasProp, _) -> - eradius_counter:inc_counter(replies, NasProp), - eradius_counter:inc_reply_counter(discAcks, NasProp); -inc_reply_counter(discnak, NasProp, _) -> - eradius_counter:inc_counter(replies, NasProp), - eradius_counter:inc_reply_counter(discNaks, NasProp); -inc_reply_counter(_Cmd, _NasProp, _Request) -> +%%%========================================================================= +%%% internal functions +%%%========================================================================= + +-spec config(IP :: inet:ip_address() | any, inet:port_number(), + server_opts()) -> {ok, server_config()}. +config(IP, Port, #{handler := {_, _}, clients := Clients} = Opts0) + when (IP =:= any orelse is_tuple(IP)) andalso + is_map(Clients) andalso + is_integer(Port) andalso Port >= 0 andalso Port < 65536 -> + SocketOpts0 = maps:get(socket_opts, Opts0, #{}), + SocketOpts = #{family := Family, ifaddr := IfAddr} = + maps:merge(default_socket_opts(IP, Port), to_map(SocketOpts0)), + + Opts = + Opts0#{server_name => server_name(IP, Port, Opts0), + socket_opts => SocketOpts#{ifaddr := socket_ip(Family, IfAddr)}, + metrics_callback => maps:get(metrics_callback, Opts0, undefined), + clients => + maps:fold(fun(K, V, M) -> M#{socket_ip(Family, K) => V} end, #{}, Clients) + }, + {ok, Opts}. + +flow_control(#state{socket = Socket, active_n = once}) -> + inet:setopts(Socket, [{active, once}]); +flow_control(_) -> ok. -inc_request_counter_accounting(NasProp, #radius_request{attrs = Attrs}) -> - Requests = ets:match_spec_run(Attrs, server_request_counter_account_match_spec_compile()), - [eradius_counter:inc_request_counter(Type, NasProp) || Type <- Requests], - ok; -inc_request_counter_accounting(_, _) -> - ok. +server_name(_, _, #{server_name := ServerName}) -> + ServerName; +server_name(IP, Port, _) -> + iolist_to_binary(server_name(IP, Port)). -inc_response_counter_accounting(NasProp, #radius_request{attrs = Attrs}) -> - Responses = ets:match_spec_run(Attrs, server_response_counter_account_match_spec_compile()), - [eradius_counter:inc_reply_counter(Type, NasProp) || Type <- Responses], - ok; -inc_response_counter_accounting(_, _) -> - ok. +server_name(IP, Port) -> + [inet:ntoa(IP), $:, integer_to_list(Port)]. -inc_discard_counter(bad_authenticator, NasProp) -> - eradius_counter:inc_counter(badAuthenticators, NasProp); -inc_discard_counter(unknown_req_type, NasProp) -> - eradius_counter:inc_counter(unknownTypes, NasProp); -inc_discard_counter(malformed, NasProp) -> - eradius_counter:inc_counter(malformedRequests, NasProp); -inc_discard_counter(_Reason, NasProp) -> - eradius_counter:inc_counter(packetsDropped, NasProp). - -server_request_counter_account_match_spec_compile() -> - case persistent_term:get({?MODULE, ?FUNCTION_NAME}, undefined) of - undefined -> - MatchSpecCompile = - ets:match_spec_compile( - ets:fun2ms( - fun ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Start}) -> - accountRequestsStart; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Stop}) -> - accountRequestsStop; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Update}) -> - accountRequestsUpdate - end)), - persistent_term:put({?MODULE, ?FUNCTION_NAME}, MatchSpecCompile), - MatchSpecCompile; - MatchSpecCompile -> - MatchSpecCompile - end. +to_map(Opts) when is_list(Opts) -> + maps:from_list(Opts); +to_map(Opts) when is_map(Opts) -> + Opts. -server_response_counter_account_match_spec_compile() -> - case persistent_term:get({?MODULE, ?FUNCTION_NAME}, undefined) of - undefined -> - MatchSpecCompile = - ets:match_spec_compile( - ets:fun2ms( - fun ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Start}) -> - accountResponsesStart; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Stop}) -> - accountResponsesStop; - ({#attribute{id = ?RStatus_Type}, ?RStatus_Type_Update}) -> - accountResponsesUpdate - end)), - persistent_term:put({?MODULE, ?FUNCTION_NAME}, MatchSpecCompile), - MatchSpecCompile; - MatchSpecCompile -> - MatchSpecCompile +%% @private +socket_ip(_, any) -> + any; +socket_ip(inet, {_, _, _, _} = IP) -> + IP; +socket_ip(inet6, {_, _, _, _} = IP) -> + inet:ipv4_mapped_ipv6_address(IP); +socket_ip(inet6, {_, _, _, _,_, _, _, _} = IP) -> + IP. + +default_socket_opts(Port) -> + #{port => Port, + active_n => 100, + recbuf => application:get_env(eradius, recbuf, 8192), + sndbuf => application:get_env(eradius, sndbuf, 131072) + }. + +default_socket_opts(any, Port) -> + Opts = default_socket_opts(Port), + Opts#{family => inet6, + ifaddr => any, + ipv6_v6only => false}; +default_socket_opts({_, _, _, _} = IP, Port) -> + Opts = default_socket_opts(Port), + Opts#{family => inet, + ifaddr => IP}; +default_socket_opts({_, _, _, _, _, _, _, _} = IP, Port) -> + Opts = default_socket_opts(Port), + Opts#{family => inet6, + ifaddr => IP, + ipv6_v6only => false}. + +inet_opts(Config, Opts0) -> + Opts = + maps:to_list( + maps:with([recbuf, sndbuf, ifaddr, + ipv6_v6only, netns, bind_to_device, read_packets], Config)) ++ Opts0, + case Config of + #{inet_backend := Backend} when Backend =:= inet; Backend =:= socket -> + [{inet_backend, Backend} | Opts]; + _ -> + Opts end. diff --git a/src/eradius_server_mon.erl b/src/eradius_server_mon.erl deleted file mode 100644 index dabaa386..00000000 --- a/src/eradius_server_mon.erl +++ /dev/null @@ -1,169 +0,0 @@ -%% @private -%% @doc Manager for RADIUS server processes. -%% This module manages the RADIUS server registry and -%% validates and applies the server configuration from the application environment. -%% It starts all servers that are configured as part of its initialization, -%% then sends ping requests to all nodes that are part of the configuration in order -%% to keep them connected. --module(eradius_server_mon). --export([start_link/0, reconfigure/0, lookup_handler/3, lookup_pid/2, all_nas_keys/0]). --export_type([handler/0]). - --behaviour(gen_server). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). - --include_lib("kernel/include/logger.hrl"). --include("eradius_lib.hrl"). - --define(SERVER, ?MODULE). --define(NAS_TAB, eradius_nas_tab). --export_type([server/0]). - --import(eradius_lib, [printable_peer/2]). - --record(nas, { - key :: {server(), inet:ip_address()}, - server_name :: server_name(), - handler :: handler(), - prop :: #nas_prop{} - }). - -%% ------------------------------------------------------------------------------------------ -%% -- API -start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). - -%% @doc Apply NAS config from the application environment. -%% Walks the list of configured servers and NASs, -%% starting and stopping servers as necessary. -%% If the configuration is invalid, no servers are modified. -%% --spec reconfigure() -> ok | {error, invalid_config}. -reconfigure() -> - gen_server:call(?SERVER, reconfigure). - -%% @doc Fetch the RADIUS secret, handler and trace flag for a given server/NAS combination. -%% This is a very fast operation that is called for every -%% request by the RADIUS server process. --spec lookup_handler(inet:ip_address(), eradius_server:port_number(), inet:ip_address()) -> {ok, handler(), #nas_prop{}} | {error, not_found}. -lookup_handler(IP, Port, NasIP) -> - case ets:lookup(?NAS_TAB, {{IP, Port}, NasIP}) of - [] when NasIP == {0, 0, 0, 0} -> - {error, not_found}; - [] -> - lookup_handler(IP, Port, {0, 0, 0, 0}); - [Rec] -> - Prop = (Rec#nas.prop)#nas_prop{server_ip = IP, server_port = Port}, - {ok, Rec#nas.handler, Prop} - end. - -%% @doc Fetches the pid of RADIUS server at IP:Port, if there is one. --spec lookup_pid(inet:ip_address(), eradius_server:port_number()) -> {ok, pid()} | {error, not_found}. -lookup_pid(ServerIP, ServerPort) -> - gen_server:call(?SERVER, {lookup_pid, {ServerIP, ServerPort}}). - -%% @doc returns the list of all currently configured NASs --spec all_nas_keys() -> [term()]. -all_nas_keys() -> - ets:select(?NAS_TAB, [{#nas{key = '$1', _ = '_'}, [], ['$1']}]). - -%% ------------------------------------------------------------------------------------------ -%% -- gen_server callbacks --record(state, {running}). - -init([]) -> - ?NAS_TAB = ets:new(?NAS_TAB, [named_table, protected, {keypos, #nas.key}]), - case configure(#state{running = []}) of - {error, invalid_config} -> {stop, invalid_config}; - Else -> Else - end. - -handle_call({lookup_pid, Server}, _From, State) -> - case proplists:get_value(Server, State#state.running) of - undefined -> - {reply, {error, not_found}, State}; - Pid -> - {reply, {ok, Pid}, State} - end; -handle_call(reconfigure, _From, State) -> - case configure(State) of - {error, invalid_config} -> {reply, {error, invalid_config}, State}; - {ok, NState} -> {reply, ok, NState} - end; -handle_call(_Request, _From, State) -> - {noreply, State}. - -handle_info(_Info, State) -> - {noreply, State}. - -%% unused callbacks -handle_cast(_Msg, State) -> {noreply, State}. -terminate(_Reason, _State) -> ok. -code_change(_OldVsn, State, _Extra) -> {ok, State}. - -%% ------------------------------------------------------------------------------------------ -%% -- helpers - -configure(#state{running = Running}) -> - {ok, ConfServList} = application:get_env(servers), - case eradius_config:validate_config(ConfServList) of - {invalid, Message} -> - ?LOG(error, "Invalid server config, ~s", [Message]), - {error, invalid_config}; - ServList -> %% list of {ServerName, ServerAddr, NasHandler} tuples - NasList = lists:flatmap(fun(Server) -> server_naslist(Server) end, ServList), - Tab = ets:tab2list(?NAS_TAB), - ToDelete = Tab -- NasList, - ToInsert = NasList -- Tab, - update_nases(ToDelete, ToInsert), - NewServAddrs = [{ServerName, ServerAddr} || {ServerName, ServerAddr, _} <- ServList], - OldServAddrs =[{ServerName, ServerAddr} || {ServerName, ServerAddr, _} <- Running], - ToStop = OldServAddrs -- NewServAddrs, - ToStart = NewServAddrs -- OldServAddrs, - NewRunning = update_server(Running, ToStop, ToStart), - NasHandler = [ NasInfo || {_ServerName, _Addr, NasInfo} <- ServList], - eradius_node_mon:set_nodes(config_nodes(NasHandler)), - {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}}), - NasInfo = eradius_lib:make_addr_info({NasId, {NasIP, undefined}}), - #nas{key = {{IP, Port}, NasIP}, server_name = ServerName, handler = {HandlerMod, HandlerArgs}, - prop = #nas_prop{handler_nodes = HandlerNodes, nas_id = NasId, nas_ip = NasIP, secret = Secret, - metrics_info = {ServerInfo, NasInfo}}} - end, HandlerList). - -config_nodes(NasHandler) -> - ordsets:from_list(lists:concat([N || {_, _, N, _, _} <- NasHandler, N /= local, N/= node()])). - -update_server(Running, ToStop, ToStart) -> - Stopped = lists:map(fun(ServerAddr = {_ServerName, Addr}) -> - StoppedServer = {_, _, Pid} = lists:keyfind(Addr, 2, Running), - eradius_server_sup:stop_instance(ServerAddr, Pid), - StoppedServer - end, ToStop), - 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). diff --git a/src/eradius_server_sup.erl b/src/eradius_server_sup.erl index 2387baf9..624e1f77 100644 --- a/src/eradius_server_sup.erl +++ b/src/eradius_server_sup.erl @@ -1,12 +1,15 @@ %% @private %% @doc Supervisor for RADIUS server processes. -module(eradius_server_sup). --export([start_link/0, start_instance/1, stop_instance/2, all/0]). - -behaviour(supervisor). + +-export([start_link/0, start_instance/1, all/0]). + -export([init/1]). -import(eradius_lib, [printable_peer/2]). +-ignore_xref([start_link/0, all/0]). + -include_lib("kernel/include/logger.hrl"). -define(SERVER, ?MODULE). @@ -16,20 +19,8 @@ start_link() -> supervisor:start_link({local, ?SERVER}, ?MODULE, []). -start_instance(_ServerAddr = {ServerName, {IP, Port}}) -> - ?LOG(info, "Starting RADIUS Listener at ~s", [printable_peer(IP, Port)]), - supervisor:start_child(?SERVER, [ServerName, IP, Port]); - -start_instance(_ServerAddr = {ServerName, {IP, Port, Opts}}) -> - ?LOG(info, "Starting RADIUS Listener at ~s", [printable_peer(IP, Port)]), - supervisor:start_child(?SERVER, [ServerName, IP, Port, Opts]). - -stop_instance(_ServerAddr = {_ServerName, {IP, Port}}, Pid) -> - ?LOG(info, "Stopping RADIUS Listener at ~s", [printable_peer(IP, Port)]), - supervisor:terminate_child(?SERVER, Pid); - -stop_instance(ServerAddr = {_ServerName, {_IP, _Port, _Opts}}, Pid) -> - stop_instance(ServerAddr, Pid). +start_instance(Opts) -> + supervisor:start_child(?SERVER, Opts). all() -> lists:map(fun({_, Child, _, _}) -> Child end, supervisor:which_children(?SERVER)). diff --git a/src/eradius_server_top_sup.erl b/src/eradius_server_top_sup.erl deleted file mode 100644 index b5e8c57a..00000000 --- a/src/eradius_server_top_sup.erl +++ /dev/null @@ -1,27 +0,0 @@ -%% @private -%% @doc Supervisor for RADIUS server supervisor tree. -%% This is a one_for_all supervisor because the server_mon must always die when the server_sup goes down, and vice-versa. --module(eradius_server_top_sup). --behaviour(supervisor). - --export([start_link/0]). --export([init/1]). - --define(SERVER, ?MODULE). - -start_link() -> - supervisor:start_link({local, ?SERVER}, ?MODULE, []). - -%% ------------------------------------------------------------------------------------------ -%% -- supervisor callbacks -init([]) -> - RestartStrategy = one_for_all, - MaxRestarts = 10, - MaxSecondsBetweenRestarts = 1, - - SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts}, - - ServerSup = {sup, {eradius_server_sup, start_link, []}, permanent, infinity, supervisor, [eradius_server_sup]}, - ServerMon = {mon, {eradius_server_mon, start_link, []}, permanent, brutal_kill, worker, [eradius_server_mon]}, - - {ok, {SupFlags, [ServerSup, ServerMon]}}. diff --git a/src/eradius_sup.erl b/src/eradius_sup.erl index 86093a41..74ec2089 100644 --- a/src/eradius_sup.erl +++ b/src/eradius_sup.erl @@ -20,25 +20,19 @@ init([]) -> SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts}, DictServer = {dict, {eradius_dict, start_link, []}, permanent, brutal_kill, worker, [eradius_dict]}, - StatsServer = {counter, {eradius_counter, start_link, []}, permanent, brutal_kill, worker, [eradius_counter]}, - StatsCollect = {aggregator, {eradius_counter_aggregator, start_link, []}, permanent, brutal_kill, worker, [eradius_counter_aggregator]}, - NodeMon = {node_mon, {eradius_node_mon, start_link, []}, permanent, brutal_kill, worker, [eradius_node_mon]}, - RadiusLog = {radius_log, {eradius_log, start_link, []}, permanent, brutal_kill, worker, [eradius_log]}, - ServerTopSup = {server_top_sup, {eradius_server_top_sup, start_link, []}, permanent, infinity, supervisor, [eradius_server_top_sup]}, - ClientMngr = - #{id => client_mngr, - start => {eradius_client_mngr, start_link, []}, + ServerSup = + #{id => server_sup, + start => {eradius_server_sup, start_link, []}, restart => permanent, - shutdown => 500, - type => worker, - modules => [eradius_client_mngr]}, - ClientSocketSup = - #{id => eradius_client_socket_sup, - start => {eradius_client_socket_sup, start_link, []}, - restart => permanent, - shutdown => 5000, - type => supervisor, - modules => [eradius_client_socket_sup]}, + shutdown => infinity, + type => supervisor, + modules => [eradius_server_sup]}, + ClientTopSup = + #{id => client_top_sup, + start => {eradius_client_top_sup, start_link, []}, + restart => permanent, + shutdown => infinity, + type => supervisor, + modules => [eradius_client_top_sup]}, - {ok, {SupFlags, [DictServer, NodeMon, StatsServer, StatsCollect, RadiusLog, - ServerTopSup, ClientSocketSup, ClientMngr]}}. + {ok, {SupFlags, [DictServer, ServerSup, ClientTopSup]}}. diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index 14628d76..3214783b 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -14,31 +14,24 @@ %%% Defines %%%=================================================================== +-define(SERVER, eradius_test_handler). -define(HUT_SOCKET, eradius_client_socket). --define(BAD_SERVER_IP(Family), - {eradius_test_lib:localhost(Family, ip), 1820, "secret"}). --define(BAD_SERVER_INITIAL_RETRIES, 2). +-define(BAD_SERVER_INITIAL_RETRIES, 3). -define(BAD_SERVER_TUPLE_INITIAL(Family), - {{eradius_test_lib:localhost(Family, tuple), 1820}, - ?BAD_SERVER_INITIAL_RETRIES, - ?BAD_SERVER_INITIAL_RETRIES}). + {{eradius_test_lib:localhost(Family, mapped), 1920}, + ?BAD_SERVER_INITIAL_RETRIES, 0}). -define(BAD_SERVER_TUPLE(Family), - {{eradius_test_lib:localhost(Family, tuple), 1820}, - ?BAD_SERVER_INITIAL_RETRIES - 1, - ?BAD_SERVER_INITIAL_RETRIES}). --define(BAD_SERVER_IP_ETS_KEY(Family), - {eradius_test_lib:localhost(Family, tuple), 1820}). + {{eradius_test_lib:localhost(Family, mapped), 1920}, + ?BAD_SERVER_INITIAL_RETRIES, 1}). -define(GOOD_SERVER_INITIAL_RETRIES, 3). -define(GOOD_SERVER_TUPLE(Family), - {{eradius_test_lib:localhost(Family, tuple), 1812}, - ?GOOD_SERVER_INITIAL_RETRIES, - ?GOOD_SERVER_INITIAL_RETRIES}). + {{eradius_test_lib:localhost(Family, mapped), 1812}, + ?GOOD_SERVER_INITIAL_RETRIES, 0}). -define(GOOD_SERVER_2_TUPLE(Family), - {{eradius_test_lib:badhost(Family), 1813}, - ?GOOD_SERVER_INITIAL_RETRIES, - ?GOOD_SERVER_INITIAL_RETRIES}). + {{eradius_test_lib:localhost(Family, mapped), 1813}, + ?GOOD_SERVER_INITIAL_RETRIES, 0}). -define(RADIUS_SERVERS(Family), [?GOOD_SERVER_TUPLE(Family), @@ -91,13 +84,13 @@ init_per_group(inet, Config) -> init_per_group(socket, Config) -> [{inet_backend, socket} | Config]; init_per_group(ipv6 = Group, Config) -> - {skip, "no IPv6 server support (yet)"}; - %% case eradius_test_lib:has_ipv6_test_config() of - %% true -> - %% [{family, Group} | Config]; - %% _ -> - %% {skip, "IPv6 test IPs not configured"} - %% end; + %% {skip, "no IPv6 server support (yet)"}; + case eradius_test_lib:has_ipv6_test_config() of + true -> + [{family, Group} | Config]; + _ -> + {skip, "IPv6 test IPs not configured"} + end; init_per_group(ipv4_mapped_ipv6 = Group, Config) -> case eradius_test_lib:has_ipv6_test_config() of true -> @@ -117,18 +110,23 @@ start_handler(Config) -> Family = proplists:get_value(family, Config), eradius_test_handler:start(Backend, Family). +start_client(Config) -> + Backend = proplists:get_value(inet_backend, Config, inet), + Family = proplists:get_value(family, Config), + eradius_test_handler:start_client(Backend, Family). + init_per_testcase(send_request, Config) -> - application:stop(eradius), start_handler(Config), Config; init_per_testcase(send_request_failover, Config) -> - application:stop(eradius), start_handler(Config), Config; init_per_testcase(check_upstream_servers, Config) -> - application:stop(eradius), start_handler(Config), Config; +init_per_testcase(wanna_send, Config) -> + start_client(Config), + Config; init_per_testcase(_Test, Config) -> Config. @@ -147,8 +145,7 @@ end_per_testcase(_Test, Config) -> %% STUFF getSocketCount() -> - Counts = supervisor:count_children(eradius_client_socket_sup), - proplists:get_value(active, Counts). + eradius_client_mngr:get_socket_count(?SERVER). testSocket(undefined) -> true; @@ -185,7 +182,7 @@ parse_ip(Address) when is_list(Address) -> inet_parse:address(Address); parse_ip(T = {_, _, _, _}) -> {ok, T}; -parse_ip(T = {_, _, _, _, _, _}) -> +parse_ip(T = {_, _, _, _, _, _, _, _}) -> {ok, T}. %% CHECK @@ -221,66 +218,73 @@ check(#{sockets := OS, no_ports := _OP, idcounters := _OC, socket_id := {_, OA}} %% TESTS -send_request(Config) -> - Family = proplists:get_value(family, Config), - ?equal(accept, - eradius_test_handler:send_request(eradius_test_lib:localhost(Family, tuple))), - ?equal(accept, - eradius_test_handler:send_request(eradius_test_lib:localhost(Family, ip))), - ?equal(accept, - eradius_test_handler:send_request(eradius_test_lib:localhost(Family, string))), - ?equal(accept, - eradius_test_handler:send_request(eradius_test_lib:localhost(Family, binary))), +send_request(_Config) -> + ?equal(accept, eradius_test_handler:send_request(one)), ok. send(FUN, Ports, Address) -> meckStart(), - OldState = eradius_client_mngr:get_state(), + OldState = eradius_client_mngr:get_state(?SERVER), FUN(), - NewState = eradius_client_mngr:get_state(), + NewState = eradius_client_mngr:get_state(?SERVER), true = check(OldState, NewState, Ports, Address), meckStop(). wanna_send(_Config) -> - lists:map(fun(_) -> - IP = {rand:uniform(100), rand:uniform(100), rand:uniform(100), rand:uniform(100)}, - Port = rand:uniform(100), - FUN = fun() -> eradius_client_mngr:wanna_send({undefined, {IP, Port}}) end, + lists:map(fun(X) -> + Server = binary_to_atom(<<(X+$A)>>), + FUN = fun() -> eradius_client_mngr:wanna_send(?SERVER, [Server], []) end, send(FUN, null, null) - end, lists:seq(1, 10)). - -%% socket shutdown is done asynchronous, the tests need to wait a bit for it to finish. -reconf_address(_Config) -> + end, lists:seq(0, 9)). + +reconf_address(Config) -> + IP = case proplists:get_value(family, Config) of + ipv4 -> + {7, 13, 23, 42}; + ipv4_mapped_ipv6 -> + inet:ipv4_mapped_ipv6_address({7, 13, 23, 42}); + ipv6 -> + {16#fd96, 16#dcd2, 16#efdb, 16#41c3, 0, 0, 16#100, 1} + end, FUN = fun() -> - eradius_client_mngr:reconfigure(#{ip => "7.13.23.42"}), + eradius_client_mngr:reconfigure(?SERVER, #{ip => IP}), + %% socket shutdown is done asynchronous, + %% the tests need to wait a bit for it to finish. timer:sleep(100) end, - send(FUN, null, "7.13.23.42"). + send(FUN, null, inet:ntoa(IP)). reconf_ports_30(_Config) -> FUN = fun() -> - eradius_client_mngr:reconfigure(#{no_ports => 30}), + eradius_client_mngr:reconfigure(?SERVER, #{no_ports => 30}), + %% socket shutdown is done asynchronous, + %% the tests need to wait a bit for it to finish. timer:sleep(100) end, send(FUN, 30, null). reconf_ports_10(_Config) -> FUN = fun() -> - eradius_client_mngr:reconfigure(#{no_ports => 10}), + eradius_client_mngr:reconfigure(?SERVER, #{no_ports => 10}), + %% socket shutdown is done asynchronous, + %% the tests need to wait a bit for it to finish. timer:sleep(100) end, send(FUN, 10, null). send_request_failover(Config) -> Family = proplists:get_value(family, Config), - ?equal(accept, eradius_test_handler:send_request_failover(?BAD_SERVER_IP(Family))), + ?equal(accept, eradius_test_handler:send_request_failover(bad)), {ok, Timeout} = application:get_env(eradius, unreachable_timeout), timer:sleep(Timeout * 1000), - ?equal([?BAD_SERVER_TUPLE(Family)], - eradius_client_mngr:servers(?BAD_SERVER_IP_ETS_KEY(Family))), + ?equal(?BAD_SERVER_TUPLE(Family), eradius_client_mngr:server(?SERVER, bad)), ok. check_upstream_servers(Config) -> Family = proplists:get_value(family, Config), - ?equal(lists:keysort(1, ?RADIUS_SERVERS(Family)), eradius_client_mngr:servers()), + Servers = eradius_client_mngr:servers(?SERVER), + ct:pal("Servers: ~p~nExpected: ~p", [Servers, ?RADIUS_SERVERS(Family)]), + ?equal(true, + sets:is_subset(sets:from_list(?RADIUS_SERVERS(Family)), + sets:from_list(Servers))), ok. diff --git a/test/eradius_config_SUITE.erl b/test/eradius_config_SUITE.erl deleted file mode 100644 index 07dd2cba..00000000 --- a/test/eradius_config_SUITE.erl +++ /dev/null @@ -1,257 +0,0 @@ -%% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> - -%% Permission is hereby granted, free of charge, to any person obtaining a -%% copy of this software and associated documentation files (the "Software"), -%% to deal in the Software without restriction, including without limitation -%% the rights to use, copy, modify, merge, publish, distribute, sublicense, -%% and/or sell copies of the Software, and to permit persons to whom the -%% Software is furnished to do so, subject to the following conditions: - -%% The above copyright notice and this permission notice shall be included in -%% all copies or substantial portions of the Software. - -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -%% DEALINGS IN THE SOFTWARE. - --module(eradius_config_SUITE). --compile(export_all). --include("../include/eradius_lib.hrl"). --include("eradius_test.hrl"). - -all() -> [config_1, config_2, config_options, - config_socket_options, config_nas_removing, - config_with_ranges, log_test, generate_ip_list_test, - test_validate_server]. - -init_per_suite(Config) -> - {ok, _} = application:ensure_all_started(eradius), - Config. - -end_per_suite(_Config) -> - application:stop(eradius), - ok. - -config_1(_Config) -> - Conf = [{session_nodes, ['node1@host1', 'node2@host2']}, - {radius_callback, ?MODULE}, - {servers, [ - {root, {eradius_test_lib:localhost(ip), [1812, 1813]}} - ]}, - {root, [ - { {"NAS1", [arg1, arg2]}, - [{"10.18.14.2/30", <<"secret1">>}]}, - { {"NAS2", [arg1, arg2]}, - [{{10, 18, 14, 3}, <<"secret2">>, [{nas_id, <<"name">>}]}]} - ]}], - ok = apply_conf(Conf), - LocalHost = eradius_test_lib:localhost(tuple), - ?match({ok, {?MODULE,[arg1,arg2]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1813, - nas_id = <<"name">>, - nas_ip = {10,18,14,3}, - handler_nodes = ['node1@host1', 'node2@host2'] - }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,3})), - ?match({ok, {?MODULE,[arg1,arg2]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1812, - nas_id = <<"name">>, - nas_ip = {10,18,14,3}, - handler_nodes = ['node1@host1', 'node2@host2'] - }}, eradius_server_mon:lookup_handler(LocalHost, 1812, {10,18,14,3})), - ?match({ok, {?MODULE,[arg1,arg2]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1813, - nas_id = <<"NAS1_10.18.14.2">>, - nas_ip = {10,18,14,2}, - handler_nodes = ['node1@host1', 'node2@host2'] - }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,2})), - ok. - -config_2(_Config) -> - Conf = [{session_nodes, [ - {"NodeGroup1", ['node1@host1', 'node2@host2']}, - {"NodeGroup2", ['node3@host3', 'node4@host4']} - ] - }, - {servers, [ - {root, {eradius_test_lib:localhost(ip), [1812, 1813]}} - ]}, - {root, [ - { {handler1, "NAS1", [arg1, arg2]}, - [ {"10.18.14.3", <<"secret1">>, [{group, "NodeGroup1"}]}, - {"10.18.14.4", <<"secret1">>, [{group, "NodeGroup1"}]} ] }, - { {handler2, "NAS2", [arg3, arg4]}, - [ {"10.18.14.2", <<"secret2">>, [{group, "NodeGroup2"}]} ] } - ]}], - ok = apply_conf(Conf), - LocalHost = eradius_test_lib:localhost(tuple), - ?match({ok, {handler1,[arg1,arg2]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1813, - nas_id = <<"NAS1_10.18.14.3">>, - nas_ip = {10,18,14,3}, - handler_nodes = ['node1@host1', 'node2@host2'] - }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,3})), - ?match({ok, {handler1,[arg1,arg2]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1813, - nas_id = <<"NAS1_10.18.14.4">>, - nas_ip = {10,18,14,4}, - handler_nodes = ['node1@host1', 'node2@host2'] - }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,4})), - ?match({ok, {handler2,[arg3,arg4]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1813, - nas_id = <<"NAS2_10.18.14.2">>, - nas_ip = {10,18,14,2}, - handler_nodes = ['node3@host3', 'node4@host4'] - }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,2})), - ok. - -config_socket_options(_Config) -> - Opts = [{netns, "/var/run/netns/net1"}], - ?match(Opts, eradius_config:validate_socket_options(Opts)), - Invalid = [{buffer, 512}, {active, false}], - ?match({invalid, _}, eradius_config:validate_socket_options(Invalid)), - Invalid2 = [{buffer, 512}, {ip, {127, 0, 0, 10}}], - ?match({invalid, _}, eradius_config:validate_socket_options(Invalid2)), - ok. - -config_options(_Config) -> - Opts = [{socket_opts, [{recbuf, 8192}, - {sndbuf, 131072}, - {netns, "/var/run/netns/net1"}]}], - ?match(Opts, eradius_config:validate_options(Opts)), - ok. -test_validate_server(_Config) -> - SocketOpts = [{socket_opts, [{recbuf, 8192}, {sndbuf, 131072}, {netns, "/var/run/netns/net1"}]}], - Opts = {{127, 0, 0, 1}, 1812, SocketOpts}, - ?match(Opts, eradius_config:validate_server(Opts)), - Opts2 = {{127, 0, 0, 1}, "1812", SocketOpts}, - ?match({{127, 0, 0, 1}, 1812, SocketOpts}, eradius_config:validate_server(Opts2)), - Opts3 = {{127, 0, 0, 1}, 1812}, - ?match(Opts3, eradius_config:validate_server(Opts3)), - ok. - -config_nas_removing(_Config) -> - Conf = [{servers, [ {root, {eradius_test_lib:localhost(ip), [1812, 1813]}} ]}, - {root, [ ]}], - ok = apply_conf(Conf), - ?match([], ets:tab2list(eradius_nas_tab)), - ok. - -config_with_ranges(_Config) -> - Nodes = ['node1@host1', 'node2@host2'], - Conf = [{session_nodes, [ - {"NodeGroup", Nodes} - ] - }, - {servers, [ - {root, {eradius_test_lib:localhost(ip), [1812, 1813]}} - ]}, - {root, [ - { {handler, "NAS", []}, - [ {"10.18.14.2/30", <<"secret2">>, [{group, "NodeGroup"}]} ] } - ]}], - ok = apply_conf(Conf), - LocalHost = eradius_test_lib:localhost(tuple), - ?match({ok, {handler,[]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1812, - nas_id = <<"NAS_10.18.14.2">>, - nas_ip = {10,18,14,2}, - handler_nodes = Nodes - }}, eradius_server_mon:lookup_handler(LocalHost, 1812, {10,18,14,2})), - ?match({ok, {handler,[]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1812, - nas_id = <<"NAS_10.18.14.3">>, - nas_ip = {10,18,14,3}, - handler_nodes = Nodes - }}, eradius_server_mon:lookup_handler(LocalHost, 1812, {10,18,14,3})), - ?match({ok, {handler,[]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1813, - nas_id = <<"NAS_10.18.14.1">>, - nas_ip = {10,18,14,1}, - handler_nodes = Nodes - }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,1})), - ?match({ok, {handler,[]}, - #nas_prop{ - server_ip = LocalHost, - server_port = 1813, - nas_id = <<"NAS_10.18.14.2">>, - nas_ip = {10,18,14,2}, - handler_nodes = Nodes - }}, eradius_server_mon:lookup_handler(LocalHost, 1813, {10,18,14,2})), - ok. - -log_test(_Config) -> - LogFile0 = "./radius.log", - LogFile1 = "./radius1.log", - LogOn0 = [{logging, true}, {logfile, LogFile0}], - LogOn1 = [{logging, true}, {logfile, LogFile1}], - LogOff = [{logging, false}], - - %% via eradius_log:reconfigure/0 - set_env(LogOn0), - ok = eradius_log:reconfigure(), - ?match(true, logger_disabled /= gen_server:call(eradius_log, get_state)), - ?match(true, filelib:is_file(LogFile0)), - - set_env(LogOff), - ok = eradius_log:reconfigure(), - logger_disabled = gen_server:call(eradius_log, get_state), - - set_env(LogOn1), - ?match(false, filelib:is_file(LogFile1)), - ok = eradius_log:reconfigure(), - ?match(true, logger_disabled /= gen_server:call(eradius_log, get_state)), - ?match(true, filelib:is_file(LogFile1)), - - %% via eradius:config_change/3 - set_env(LogOff), - eradius:config_change([], LogOff, []), - logger_disabled = gen_server:call(eradius_log, get_state), - - set_env(LogOn0), - eradius:config_change([], LogOn1, []), - ?match(true, logger_disabled /= gen_server:call(eradius_log, get_state)), - - %% check default value for logging - application:unset_env(eradius, logging), - eradius:config_change([], [], [logging]), - logger_disabled = gen_server:call(eradius_log, get_state), - - ok. - -set_env(Config) -> - [application:set_env(eradius, Env, Value) || {Env, Value} <- Config]. - -apply_conf(Config) -> - set_env(Config), - eradius_server_mon:reconfigure(). - -generate_ip_list_test(_) -> - ?equal([{192, 168, 11, 148}, {192, 168, 11, 149}, {192, 168, 11, 150}, {192, 168, 11, 151}], - eradius_config:generate_ip_list({192, 168, 11, 150}, "30")), - eradius_config:generate_ip_list({192, 168, 11, 150}, 24), - ?equal(256, length(eradius_config:generate_ip_list({192, 168, 11, 150}, 24))), - ?equal(2048, length(eradius_config:generate_ip_list({192, 168, 11, 10}, 21))), - ?match({invalid, _}, eradius_config:generate_ip_list({192, 168, 11, 150}, "34")), - ok. diff --git a/test/eradius_lib_SUITE.erl b/test/eradius_lib_SUITE.erl index 8dc884c9..1b158973 100644 --- a/test/eradius_lib_SUITE.erl +++ b/test/eradius_lib_SUITE.erl @@ -19,9 +19,11 @@ %% DEALINGS IN THE SOFTWARE. -module(eradius_lib_SUITE). +-include_lib("stdlib/include/assert.hrl"). -include("eradius_lib.hrl"). -include("eradius_dict.hrl"). -include("eradius_test.hrl"). + -compile(export_all). -define(SALT, <<171,213>>). @@ -33,8 +35,14 @@ -define(CIPHER_TEXT, <<171,213,166,95,152,126,124,120,86,10,78,216,190,216,26,87,55,15>>). -define(ENC_PASSWORD, 186,128,194,207,68,25,190,19,23,226,48,206,244,143,56,238). -define(ENC_PASSWORD_ASCEND, 222,170,194,83,115,231,228,55,75,17,20,6,198,33,112,197). --define(PDU, #radius_request{ reqid = 1, secret = ?SECRET, authenticator = ?REQUEST_AUTHENTICATOR }). - +-define(ENC_REQ, #{reqid => 1, + secret => ?SECRET, + authenticator => ?REQUEST_AUTHENTICATOR, + attrs => []}). +-define(DEC_REQ, #{reqid => 1, + secret => ?SECRET, + request_authenticator => ?REQUEST_AUTHENTICATOR, + attrs => []}). %% test callbacks all() -> [ipv6prefix, @@ -62,10 +70,11 @@ all() -> [ipv6prefix, init_per_suite(Config) -> Config. end_per_suite(_Config) -> ok. -init_per_testcase(Test, Config) when Test == dec_vendor_integer_t - orelse Test == dec_vendor_string_t - orelse Test == dec_vendor_ipv4_t - orelse Test == vendor_attribute_id_conflict_test -> +init_per_testcase(Test, Config) when + Test == dec_vendor_integer_t orelse + Test == dec_vendor_string_t orelse + Test == dec_vendor_ipv4_t orelse + Test == vendor_attribute_id_conflict_test -> application:set_env(eradius, tables, [dictionary]), eradius_dict:start_link(), eradius_dict:unload_tables([dictionary_3gpp]), @@ -85,80 +94,80 @@ ipv6prefix(_Config) -> ok. ipv6prefix_enc_dec(Prefix) -> - Bin = eradius_lib:encode_value(ipv6prefix, Prefix), - eradius_lib:decode_value(Bin, ipv6prefix). + Bin = eradius_req:encode_value(ipv6prefix, Prefix), + eradius_req:decode_value(Bin, ipv6prefix). salt_encrypt_test(_) -> - ?equal(?CIPHER_TEXT, eradius_lib:salt_encrypt(?SALT, ?SECRET, ?REQUEST_AUTHENTICATOR, << ?PLAIN_TEXT >>)). + ?equal(?CIPHER_TEXT, eradius_req:salt_encrypt(?SALT, ?SECRET, ?REQUEST_AUTHENTICATOR, << ?PLAIN_TEXT >>)). salt_decrypt_test(_) -> - ?equal(<< ?PLAIN_TEXT >>, eradius_lib:salt_decrypt(?SECRET, ?REQUEST_AUTHENTICATOR, ?CIPHER_TEXT)). + ?equal(<< ?PLAIN_TEXT >>, eradius_req:salt_decrypt(?SECRET, ?REQUEST_AUTHENTICATOR, ?CIPHER_TEXT)). scramble_enc_test(_) -> - ?equal(<< ?ENC_PASSWORD >>, eradius_lib:scramble(?SECRET, ?REQUEST_AUTHENTICATOR, << ?PLAIN_TEXT >>)). + ?equal(<< ?ENC_PASSWORD >>, eradius_req:scramble(?SECRET, ?REQUEST_AUTHENTICATOR, << ?PLAIN_TEXT >>)). scramble_dec_test(_) -> - ?equal(?PLAIN_TEXT_PADDED, eradius_lib:scramble(?SECRET, ?REQUEST_AUTHENTICATOR, << ?ENC_PASSWORD >>)). + ?equal(?PLAIN_TEXT_PADDED, eradius_req:scramble(?SECRET, ?REQUEST_AUTHENTICATOR, << ?ENC_PASSWORD >>)). ascend_enc_test(_) -> - ?equal(<< ?ENC_PASSWORD_ASCEND >>, eradius_lib:ascend(?SECRET, ?REQUEST_AUTHENTICATOR, << ?PLAIN_TEXT >>)). + ?equal(<< ?ENC_PASSWORD_ASCEND >>, eradius_req:ascend(?SECRET, ?REQUEST_AUTHENTICATOR, << ?PLAIN_TEXT >>)). ascend_dec_test(_) -> - ?equal(?PLAIN_TEXT_PADDED, eradius_lib:ascend(?SECRET, ?REQUEST_AUTHENTICATOR, << ?ENC_PASSWORD_ASCEND >>)). + ?equal(?PLAIN_TEXT_PADDED, eradius_req:ascend(?SECRET, ?REQUEST_AUTHENTICATOR, << ?ENC_PASSWORD_ASCEND >>)). enc_simple_test(_) -> L = length(?USER) + 2, - ?equal(<< ?RUser_Name, L:8, ?USER >>, eradius_lib:encode_attribute(?PDU, #attribute{id = ?RUser_Name, type = string, enc = no}, << ?USER >>)). + ?equal(<< ?RUser_Name, L:8, ?USER >>, eradius_req:encode_attribute(?ENC_REQ, #attribute{id = ?RUser_Name, type = string, enc = no}, << ?USER >>)). enc_scramble_test(_) -> L = 16 + 2, - ?equal(<< ?RUser_Passwd, L:8, ?ENC_PASSWORD >>, eradius_lib:encode_attribute(?PDU, #attribute{id = ?RUser_Passwd, type = string, enc = scramble}, << ?PLAIN_TEXT >>)). + ?equal(<< ?RUser_Passwd, L:8, ?ENC_PASSWORD >>, eradius_req:encode_attribute(?ENC_REQ, #attribute{id = ?RUser_Passwd, type = string, enc = scramble}, << ?PLAIN_TEXT >>)). enc_salt_test(_) -> L = 16 + 4, - << ?RUser_Passwd, L:8, Enc/binary >> = eradius_lib:encode_attribute(?PDU, #attribute{id = ?RUser_Passwd, type = string, enc = salt_crypt}, << ?PLAIN_TEXT >>), + << ?RUser_Passwd, L:8, Enc/binary >> = eradius_req:encode_attribute(?ENC_REQ, #attribute{id = ?RUser_Passwd, type = string, enc = salt_crypt}, << ?PLAIN_TEXT >>), %% need to decrypt to verfiy due to salt - ?equal(<< ?PLAIN_TEXT >>, eradius_lib:salt_decrypt(?SECRET, ?REQUEST_AUTHENTICATOR, Enc)). + ?equal(<< ?PLAIN_TEXT >>, eradius_req:salt_decrypt(?SECRET, ?REQUEST_AUTHENTICATOR, Enc)). enc_ascend_test(_) -> L = 16 + 2, - ?equal(<< ?RUser_Passwd, L:8, ?ENC_PASSWORD_ASCEND >>, eradius_lib:encode_attribute(?PDU, #attribute{id = ?RUser_Passwd, type = string, enc = ascend}, << ?PLAIN_TEXT >>)). + ?equal(<< ?RUser_Passwd, L:8, ?ENC_PASSWORD_ASCEND >>, eradius_req:encode_attribute(?ENC_REQ, #attribute{id = ?RUser_Passwd, type = string, enc = ascend}, << ?PLAIN_TEXT >>)). enc_vendor_test(_) -> L = length(?USER), E = << ?RVendor_Specific, (L+8):8, 18681:32, 1:8, (L+2):8, ?USER >>, - ?equal(E, eradius_lib:encode_attribute(?PDU, #attribute{id = {18681,1}, type = string, enc = no}, << ?USER >>)). + ?equal(E, eradius_req:encode_attribute(?ENC_REQ, #attribute{id = {18681,1}, type = string, enc = no}, << ?USER >>)). enc_vendor_octet_test(_) -> E = << ?RVendor_Specific, (4+8):8, 311:32, 7:8, (4+2):8, 7:32 >>, - ?equal(E, eradius_lib:encode_attribute(?PDU, #attribute{id = {311,7}, type = octets, enc = no}, 7)). + ?equal(E, eradius_req:encode_attribute(?ENC_REQ, #attribute{id = {311,7}, type = octets, enc = no}, 7)). -decode_attribute(A, B, C) -> - eradius_lib:decode_attribute(A, B, C, 0, #decoder_state{}). +decode_attribute(TLV, Req, Attr) -> + eradius_req:decode_attribute(TLV, Attr, 0, Req). dec_simple_integer_test(_) -> - State = decode_attribute(<<0,0,0,1>>, ?PDU, #attribute{id = 40, type = integer, enc = no}), - [{_, 1}] = State#decoder_state.attrs. + State = decode_attribute(<<0,0,0,1>>, ?DEC_REQ, #attribute{id = 40, type = integer, enc = no}), + ?assertMatch(#{attrs := [{_, 1}]}, State). dec_simple_string_test(_) -> - State = decode_attribute(<<"29113">>, ?PDU, #attribute{id = 44, type = string, enc = no}), - [{_, <<"29113">>}] = State#decoder_state.attrs. + State = decode_attribute(<<"29113">>, ?DEC_REQ, #attribute{id = 44, type = string, enc = no}), + ?assertMatch(#{attrs := [{_, <<"29113">>}]}, State). dec_simple_ipv4_test(_) -> - State = decode_attribute(<<10,33,0,1>>, ?PDU, #attribute{id = 4, type = ipaddr, enc = no}), - [{_, {10,33,0,1}}] = State#decoder_state.attrs. + State = decode_attribute(<<10,33,0,1>>, ?DEC_REQ, #attribute{id = 4, type = ipaddr, enc = no}), + ?assertMatch(#{attrs := [{_, {10,33,0,1}}]}, State). dec_vendor_integer_t(_) -> - State = decode_attribute(<<0,0,40,175,3,6,0,0,0,0>>, ?PDU, #attribute{id = ?RVendor_Specific, type = octets, enc = no}), - [{_, <<0, 0, 0, 0>>}] = State#decoder_state.attrs. + State = decode_attribute(<<0,0,40,175,3,6,0,0,0,0>>, ?DEC_REQ, #attribute{id = ?RVendor_Specific, type = octets, enc = no}), + ?assertMatch(#{attrs := [{_, <<0, 0, 0, 0>>}]}, State). dec_vendor_string_t(_) -> - State = decode_attribute(<<0,0,40,175,8,7,"23415">>, ?PDU, #attribute{id = ?RVendor_Specific, type = octets, enc = no}), - [{_, <<"23415">>}] = State#decoder_state.attrs. + State = decode_attribute(<<0,0,40,175,8,7,"23415">>, ?DEC_REQ, #attribute{id = ?RVendor_Specific, type = octets, enc = no}), + ?assertMatch(#{attrs := [{_, <<"23415">>}]}, State). dec_vendor_ipv4_t(_) -> - State = decode_attribute(<<0,0,40,175,6,6,212,183,144,246>>, ?PDU, #attribute{id = ?RVendor_Specific, type = octets, enc = no}), - [{_, <<212,183,144,246>>}] = State#decoder_state.attrs. + State = decode_attribute(<<0,0,40,175,6,6,212,183,144,246>>, ?DEC_REQ, #attribute{id = ?RVendor_Specific, type = octets, enc = no}), + ?assertMatch(#{attrs := [{_, <<212,183,144,246>>}]}, State). vendor_attribute_id_conflict_test(_) -> #attribute{} = eradius_dict:lookup(attribute, 52), diff --git a/test/eradius_logtest.erl b/test/eradius_logtest.erl deleted file mode 100644 index 125422ab..00000000 --- a/test/eradius_logtest.erl +++ /dev/null @@ -1,134 +0,0 @@ -%% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> - -%% Permission is hereby granted, free of charge, to any person obtaining a -%% copy of this software and associated documentation files (the "Software"), -%% to deal in the Software without restriction, including without limitation -%% the rights to use, copy, modify, merge, publish, distribute, sublicense, -%% and/or sell copies of the Software, and to permit persons to whom the -%% Software is furnished to do so, subject to the following conditions: - -%% The above copyright notice and this permission notice shall be included in -%% all copies or substantial portions of the Software. - -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -%% DEALINGS IN THE SOFTWARE. - --module(eradius_logtest). - --export([start/0, test/0, radius_request/3, validate_arguments/1, test_client/0, test_client/1, test_proxy/0, test_proxy/1]). --import(eradius_lib, [get_attr/2]). - --include_lib("eradius/include/eradius_lib.hrl"). --include_lib("eradius/include/eradius_dict.hrl"). --include_lib("eradius/include/dictionary.hrl"). --include_lib("eradius/include/dictionary_3gpp.hrl"). - --define(ALLOWD_USERS, [undefined, <<"user">>, <<"user@domain">>, <<"proxy_test">>]). --define(SECRET, <<"secret">>). --define(SECRET2, <<"proxy_secret">>). --define(SECRET3, <<"test_secret">>). - --define(CLIENT_REQUESTS_COUNT, 1). --define(CLIENT_PROXY_REQUESTS_COUNT, 4). - --define(NAS1_ACCESS_REQS, 1). --define(NAS2_ACCESS_REQS, 4). - -start() -> - application:load(eradius), - ProxyConfig = [{default_route, {eradius_test_lib:localhost(tuple), 1813, ?SECRET}}, - {options, [{type, realm}, {strip, true}, {separator, "@"}]}, - {routes, [{"test", {eradius_test_lib:localhost(tuple), 1815, ?SECRET3}} - ]} - ], - Config = [{radius_callback, eradius_logtest}, - {servers, [{root, {eradius_test_lib:localhost(ip), [1812, 1813]}}, - {test, {eradius_test_lib:localhost(ip), [1815]}}, - {proxy, {eradius_test_lib:localhost(ip), [11812, 11813]}} - ]}, - {session_nodes, [node()]}, - {root, [ - { {eradius_logtest, "root", [] }, [{"127.0.0.1/24", ?SECRET, [{nas_id, <<"Test_Nas_Id">>}]}] } - ]}, - {test, [ - { {eradius_logtest, "test", [] }, [{eradius_test_lib:localhost(ip), ?SECRET3, [{nas_id, <<"Test_Nas_Id_test">>}]}] } - ]}, - {proxy, [ - { {eradius_proxy, "proxy", ProxyConfig }, [{eradius_test_lib:localhost(ip), ?SECRET2, [{nas_id, <<"Test_Nas_proxy">>}]}] } - ]} - ], - [application:set_env(eradius, Key, Value) || {Key, Value} <- Config], - {ok, _} = application:ensure_all_started(eradius), - spawn(fun() -> - eradius:modules_ready([?MODULE, eradius_proxy]), - timer:sleep(infinity) - end), - ok. - -test() -> - %% application:set_env(lager, handlers, [{lager_journald_backend, []}]), - eradius_logtest:start(), - eradius_logtest:test_client(), - eradius_logtest:test_proxy(), - ok. - -radius_request(#radius_request{cmd = request} = Request, _NasProp, _) -> - UserName = get_attr(Request, ?User_Name), - case lists:member(UserName, ?ALLOWD_USERS) of - true -> - {reply, #radius_request{cmd = accept}}; - false -> - {reply, #radius_request{cmd = reject}} - end; - -radius_request(#radius_request{cmd = accreq}, _NasProp, _) -> - {reply, #radius_request{cmd = accresp}}. - -validate_arguments(_Args) -> true. - -test_client() -> - test_client(request). - -test_client(Command) -> - eradius_dict:load_tables([dictionary, dictionary_3gpp]), - Request = eradius_lib:set_attributes(#radius_request{cmd = Command, msg_hmac = true}, attrs("user")), - send_request(eradius_test_lib:localhost(tuple), 1813, ?SECRET, Request). - -test_proxy() -> - test_proxy(request). - -test_proxy(Command) -> - eradius_dict:load_tables([dictionary, dictionary_3gpp]), - send_request(eradius_test_lib:localhost(tuple), 11813, ?SECRET2, #radius_request{cmd = Command}), - Request = eradius_lib:set_attributes(#radius_request{cmd = Command, msg_hmac = true}, attrs("proxy_test")), - send_request(eradius_test_lib:localhost(tuple), 11813, ?SECRET2, Request), - Request2 = eradius_lib:set_attributes(#radius_request{cmd = Command, msg_hmac = true}, attrs("user@test")), - send_request(eradius_test_lib:localhost(tuple), 11813, ?SECRET2, Request2), - Request3 = eradius_lib:set_attributes(#radius_request{cmd = Command, msg_hmac = true}, attrs("user@domain@test")), - send_request(eradius_test_lib:localhost(tuple), 11813, ?SECRET2, Request3). - -send_request(Ip, Port, Secret, Request) -> - case eradius_client:send_request({Ip, Port, Secret}, Request) of - {ok, Result, Auth} -> - eradius_lib:decode_request(Result, Secret, Auth); - Error -> - Error - end. - -attrs(User) -> - [{?NAS_Port, 8888}, - {?User_Name, User}, - {?NAS_IP_Address, {88,88,88,88}}, - {?Calling_Station_Id, "0123456789"}, - {?Service_Type, 2}, - {?Framed_Protocol, 7}, - {30,"some.id.com"}, %Called-Station-Id - {61,18}, %NAS_PORT_TYPE - {{10415,1}, "1337"}, %X_3GPP-IMSI - {{127,42},18} %Unbekannte ID - ]. diff --git a/test/eradius_metrics_SUITE.erl b/test/eradius_metrics_SUITE.erl index a94dff65..834b771e 100644 --- a/test/eradius_metrics_SUITE.erl +++ b/test/eradius_metrics_SUITE.erl @@ -21,59 +21,35 @@ -module(eradius_metrics_SUITE). -compile(export_all). +-include_lib("stdlib/include/assert.hrl"). -include_lib("eradius/include/eradius_lib.hrl"). -include_lib("eradius/include/eradius_dict.hrl"). -include_lib("eradius/include/dictionary.hrl"). -include("eradius_test.hrl"). +-define(SERVER, ?MODULE). -define(SECRET, <<"secret">>). -define(ATTRS_GOOD, [{?NAS_Identifier, "good"}, {?RStatus_Type, ?RStatus_Type_Start}]). -define(ATTRS_BAD, [{?NAS_Identifier, "bad"}]). -define(ATTRS_ERROR, [{?NAS_Identifier, "error"}]). -define(ATTRS_AS_RECORD, [{#attribute{id = ?RStatus_Type}, ?RStatus_Type_Start}]). --define(LOCALHOST, eradius_test_lib:localhost(atom)). + +%%%=================================================================== +%%% Setup +%%%=================================================================== %% test callbacks all() -> [good_requests, bad_requests, error_requests, request_with_attrs_as_record]. init_per_suite(Config) -> + logger:set_primary_config(level, debug), application:load(eradius), - EradiusConfig = [{radius_callback, ?MODULE}, - {servers, [{good, {eradius_test_lib:localhost(ip), [1812]}}, %% for 'positive' responses, e.g. access accepts - {bad, {eradius_test_lib:localhost(ip), [1813]}}, %% for 'negative' responses, e.g. coa naks - {error, {eradius_test_lib:localhost(ip), [1814]}} %% here things go wrong, e.g. duplicate requests - ]}, - {session_nodes, [node()]}, - {servers_pool, - [{test_pool, [{eradius_test_lib:localhost(tuple), 1814, ?SECRET}, - {eradius_test_lib:localhost(tuple), 1813, ?SECRET}, - {eradius_test_lib:localhost(tuple), 1812, ?SECRET}]}] - }, - {good, [ - { {"good", [] }, [{"127.0.0.2", ?SECRET, [{nas_id, <<"good_nas">>}]}] } - ]}, - {bad, [ - { {"bad", [] }, [{"127.0.0.2", ?SECRET, [{nas_id, <<"bad_nas">>}]}] } - ]}, - {error, [ - { {"error", [] }, [{"127.0.0.2", ?SECRET, [{nas_id, <<"error_nas">>}]}] } - ]}, - {tables, [dictionary]}, - {client_ip, {127,0,0,2}}, - {client_ports, 20}, - {counter_aggregator, false}, - {server_status_metrics_enabled, true} - ], - [application:set_env(eradius, Key, Value) || {Key, Value} <- EradiusConfig], - application:set_env(prometheus, collectors, [eradius_prometheus_collector]), - %% prometheus is not included directly to eradius but prometheus_eradius_collector - %% should include it - application:ensure_all_started(prometheus), + application:set_env(eradius, unreachable_timeout, 2), {ok, _} = application:ensure_all_started(eradius), - spawn(fun() -> - eradius:modules_ready([?MODULE]), - timer:sleep(infinity) - end), + + ok = start_client(Config), + ok = start_servers(Config), + Config. end_per_suite(_Config) -> @@ -82,10 +58,65 @@ end_per_suite(_Config) -> ok. init_per_testcase(_, Config) -> - eradius_client_mngr:init_server_status_metrics(), + application:stop(prometheus), + {ok, _} = application:ensure_all_started(prometheus), + eradius_metrics_prometheus:init(#{}), Config. -%% tests +%%%=================================================================== +%%% Helper +%%%=================================================================== + +start_client(_Config) -> + Backend = inet, Family = ipv4, + ClientConfig = + #{inet_backend => Backend, + family => eradius_test_lib:inet_family(Family), + ip => eradius_test_lib:localhost(Family, native), + servers => #{good => #{ip => eradius_test_lib:localhost(Family, native), + port => 1812, + secret => ?SECRET, + retries => 3}, + bad => #{ip => eradius_test_lib:localhost(Family, native), + port => 1813, + secret => ?SECRET, + retries => 3}, + error => #{ip => eradius_test_lib:localhost(Family, native), + port => 1814, + secret => ?SECRET, + retries => 3} + }, + metrics_callback => fun eradius_metrics_prometheus:client_metrics_callback/3 + }, + case eradius_client_mngr:start_client({local, ?SERVER}, ClientConfig) of + {ok, _} -> ok; + {error, {already_started, _}} -> ok + end. + +start_servers(_Config) -> + Family = ipv4, + + SrvOpts = + fun(Name, NasId) -> + #{handler => {?MODULE, []}, + server_name => Name, + metrics_callback => fun eradius_metrics_prometheus:server_metrics_callback/3, + clients => #{eradius_test_lib:localhost(Family, native) => + #{secret => ?SECRET, client => NasId}} + } + end, + eradius:start_server( + eradius_test_lib:localhost(Family, native), 1812, SrvOpts(good, <<"good_nas">>)), + eradius:start_server( + eradius_test_lib:localhost(Family, native), 1813, SrvOpts(bad, <<"bad_nas">>)), + eradius:start_server( + eradius_test_lib:localhost(Family, native), 1814, SrvOpts(error, <<"error_nas">>)), + ok. + +%%%=================================================================== +%%% tests +%%%=================================================================== + good_requests(_Config) -> Requests = [{request, access, access_accept}, {accreq, accounting, accounting}, @@ -107,66 +138,81 @@ error_requests(_Config) -> check_single_request(error, request, access, access_accept). request_with_attrs_as_record(_Config) -> - ok = send_request(accreq, eradius_test_lib:localhost(tuple), 1812, ?ATTRS_AS_RECORD, [{server_name, good}, {client_name, test_records}]), - ok = check_metric(accreq, client_accounting_requests_total, [{server_name, good}, {client_name, test_records}, {acct_type, start}], 1). + ok = send_request(good, accreq, ?ATTRS_AS_RECORD, + #{server_name => good, client_name => test_records}), + check_metric(accreq, eradius_client_accounting_requests_total, [{"server_name", good}, {"acct_type", start}], 1). %% helpers check_single_request(good, EradiusRequestType, _RequestType, _ResponseType) -> - ok = send_request(EradiusRequestType, eradius_test_lib:localhost(tuple), 1812, ?ATTRS_GOOD, [{server_name, good}, {client_name, test}]), - ok = check_metric(client_access_requests_total, [{server_name, good}], 1), - ok = check_metric_multi(EradiusRequestType, client_accounting_requests_total, [{server_name, good}], 1), - ok = check_metric_multi({bad_type, EradiusRequestType}, client_accounting_requests_total, [{server_name, good}, {acct_type, bad_type}], 0), - ok = check_metric(EradiusRequestType, client_accounting_requests_total, [{server_name, good}, {acct_type, start}], 1), - ok = check_metric(EradiusRequestType, client_accounting_requests_total, [{server_name, good}, {acct_type, stop}], 0), - ok = check_metric(EradiusRequestType, client_accounting_requests_total, [{server_name, good}, {acct_type, update}], 0), - ok = check_metric(client_accept_responses_total, [{server_name, good}], 1), - ok = check_metric(accept_responses_total, [{server_name, good}], 1), - ok = check_metric(access_requests_total, [{server_name, good}], 1), - ok = check_metric(server_status, true, [eradius_test_lib:localhost(tuple), 1812]); + ok = send_request(good, EradiusRequestType, ?ATTRS_GOOD, + #{server_name => good, client_name => test}), + + Metrics = prometheus_text_format:format(default), + ERadM = re:run(Metrics, "^eradius.*", [multiline, global, {capture, all, binary}]), + ct:pal("Metrics:~n~p~n", [ERadM]), + + check_metric(eradius_client_access_requests_total, [{"server_name", good}], 1), + check_metric_multi(EradiusRequestType, eradius_client_accounting_requests_total, [{"server_name", good}], 1), + check_metric_multi({bad_type, EradiusRequestType}, eradius_client_accounting_requests_total, [{"server_name", good}, {"acct_type", bad_type}], 0), + check_metric(EradiusRequestType, eradius_client_accounting_requests_total, [{"server_name", good}, {"acct_type", start}], 1), + check_metric(EradiusRequestType, eradius_client_accounting_requests_total, [{"server_name", good}, {"acct_type", stop}], 0), + check_metric(EradiusRequestType, eradius_client_accounting_requests_total, [{"server_name", good}, {"acct_type", update}], 0), + check_metric(eradius_client_accept_responses_total, [{"server_name", good}], 1), + check_metric(eradius_accept_responses_total, [{"server_name", good}], 1), + check_metric(eradius_access_requests_total, [{"server_name", good}], 1), + check_metric(eradius_server_status, true, [eradius_test_lib:localhost(ipv4, native), 1812]); check_single_request(bad, EradiusRequestType, _RequestType, _ResponseType) -> - ok = send_request(EradiusRequestType, eradius_test_lib:localhost(tuple), 1813, ?ATTRS_BAD, [{server_name, bad}, {client_name, test}]), - ok = check_metric(client_access_requests_total, [{server_name, bad}], 1), - ok = check_metric(client_reject_responses_total, [{server_name, bad}], 1), - ok = check_metric(access_requests_total, [{server_name, bad}], 1), - ok = check_metric(reject_responses_total, [{server_name, bad}], 1), - ok = check_metric(server_status, true, [eradius_test_lib:localhost(tuple), 1813]); + ok = send_request(bad, EradiusRequestType, ?ATTRS_BAD, + #{server_name => bad, client_name => test}), + check_metric(eradius_client_access_requests_total, [{"server_name", bad}], 1), + check_metric(eradius_client_reject_responses_total, [{"server_name", bad}], 1), + check_metric(eradius_access_requests_total, [{"server_name", bad}], 1), + check_metric(eradius_reject_responses_total, [{"server_name", bad}], 1), + check_metric(eradius_server_status, true, [eradius_test_lib:localhost(ipv4, native), 1813]); check_single_request(error, EradiusRequestType, _RequestType, _ResponseType) -> - ok = send_request(EradiusRequestType, eradius_test_lib:localhost(tuple), 1814, ?ATTRS_ERROR, - [{server_name, error}, {client_name, test}, {timeout, 1000}, - {failover, [{eradius_test_lib:localhost(tuple), 1812, ?SECRET}]}]), - ok = check_metric(client_access_requests_total, [{server_name, error}], 1), - ok = check_metric(client_retransmissions_total, [{server_name, error}], 1), - ok = check_metric(access_requests_total, [{server_name, error}], 1), - ok = check_metric(accept_responses_total, [{server_name, error}], 1), - ok = check_metric(duplicated_requests_total, [{server_name, error}], 1), - ok = check_metric(client_requests_total, [{server_name, error}], 1), - ok = check_metric(requests_total, [{server_name, error}], 2), - ok = check_metric(server_status, false, [eradius_test_lib:localhost(tuple), 1812]), - ok = check_metric(server_status, false, [eradius_test_lib:localhost(tuple), 1813]), - ok = check_metric(server_status, true, [eradius_test_lib:localhost(tuple), 1814]), - ok = check_metric(server_status, undefined, [eradius_test_lib:localhost(tuple), 1815]). - + ok = send_request(error, EradiusRequestType, ?ATTRS_ERROR, + #{server_name => error, client_name => test, timeout => 100, + failover => []}), + check_metric(eradius_client_access_requests_total, [{"server_name", error}], 1), + check_metric(eradius_client_retransmissions_total, [{"server_name", error}], 1), + check_metric(eradius_access_requests_total, [{"server_name", error}], 1), + check_metric(eradius_accept_responses_total, [{"server_name", error}], 1), + check_metric(eradius_duplicated_requests_total, [{"server_name", error}], 1), + check_metric(eradius_client_requests_total, [{"server_name", error}], 1), + check_metric(eradius_requests_total, [{"server_name", error}], 2), + check_metric(eradius_server_status, undefined, [eradius_test_lib:localhost(ipv4, native), 1812]), + check_metric(eradius_server_status, undefined, [eradius_test_lib:localhost(ipv4, native), 1813]), + check_metric(eradius_server_status, true, [eradius_test_lib:localhost(ipv4, native), 1814]), + ok. check_total_requests(good, N) -> - ok = check_metric(requests_total, [{server_name, good}], N), - ok = check_metric(replies_total, [{server_name, good}], N); + check_metric(eradius_requests_total, [{"server_name", good}], N), + check_metric(eradius_replies_total, [{"server_name", good}], N); check_total_requests(bad, N) -> - ok = check_metric(requests_total, [{server_name, bad}], N), - ok = check_metric(replies_total, [{server_name, bad}], N). + check_metric(eradius_requests_total, [{"server_name", bad}], N), + check_metric(eradius_replies_total, [{"server_name", bad}], N). -check_metric_multi({bad_type, accreq}, Id, Labels, _) -> - case eradius_prometheus_collector:fetch_counter(Id, Labels) of - [] -> - ok; - _ -> - {error, Id, Labels} - end; +check_metric_multi({bad_type, accreq}, Id, Labels, _Count) -> + Values = prometheus_counter:values(default, Id), + Filtered = + lists:filter( + fun({ValueLabels, _}) -> Labels -- ValueLabels =:= [] end, + Values), + ct:pal("check_metric-accreg-bad: ~p, ~p~nFetch: ~p~nFilteredL ~p~n", + [Id, Labels, Values, Filtered]), + ?assertEqual([], Filtered); check_metric_multi(accreq, Id, Labels, Count) -> - case eradius_prometheus_collector:fetch_counter(Id, Labels) of - [{Count, _} | _] -> - ok; - _ -> - {error, Id, Count} + Values = prometheus_counter:values(default, Id), + Filtered = + lists:filter( + fun({ValueLabels, _}) -> Labels -- ValueLabels =:= [] end, + Values), + ct:pal("check_metric-accreg-#1: ~p, ~p~nFetch: ~p~nFilteredL ~p~n", + [Id, Labels, Values, Filtered]), + case Filtered of + [{_, Count}|_] -> ok; + [] when Count =:= 0 -> ok; + _ -> ?assertMatch([{_, Count}|_], Filtered) end; check_metric_multi(_, _, _, _) -> ok. @@ -176,50 +222,56 @@ check_metric(accreq, Id, Labels, Count) -> check_metric(_, _, _, _) -> ok. -check_metric(server_status, Value, Labels) -> - ?equal(Value, prometheus_boolean:value(server_status, Labels)), - ok; +check_metric(eradius_server_status = Id, Value, LabelValues) -> + ct:pal("check_metric-#0 ~p: ~p, ~p", [Id, LabelValues, Value]), + ?assertEqual(Value, prometheus_boolean:value(Id, LabelValues)); check_metric(Id, Labels, Count) -> - case eradius_prometheus_collector:fetch_counter(Id, Labels) of - [{Count, _}] -> - ok; - _ -> - {error, Id, Count} + Values = prometheus_counter:values(default, Id), + Filtered = + lists:filter( + fun({ValueLabels, _}) -> Labels -- ValueLabels =:= [] end, + Values), + ct:pal("check_metric-#1: ~p, ~p, ~p~nFetch: ~p~nFilteredL ~p~n", + [Id, Labels, Count, Values, Filtered]), + case Filtered of + [{_, Count}|_] -> ok; + [] when Count =:= 0 -> ok; + _ -> ?assertMatch([{_, Count}|_], Filtered) end. -send_request(Command, IP, Port, Attrs, Opts) -> +send_request(ServerName, Command, Attrs, Opts) -> ok = eradius_dict:load_tables([dictionary]), - Request = eradius_lib:set_attributes(#radius_request{cmd = Command}, Attrs), - send_radius_request(IP, Port, ?SECRET, Request, Opts). + Req0 = eradius_req:new(Command), + Req = eradius_req:set_attrs(Attrs, Req0), + send_radius_request(ServerName, Req, Opts). -send_radius_request(Ip, Port, Secret, Request, Opts) -> - case eradius_client:send_request({Ip, Port, Secret}, Request, Opts) of - {ok, _Result, _Auth} -> +send_radius_request(ServerName, Req, Opts) -> + case eradius_client:send_request(?SERVER, ServerName, Req, Opts) of + {{ok, _Result}, _ReqN} -> ok; Error -> Error end. %% RADIUS NAS callbacks for 'good' requests -radius_request(#radius_request{cmd = request}, #nas_prop{nas_id = <<"good_nas">>}, _) -> - {reply, #radius_request{cmd = accept}}; -radius_request(#radius_request{cmd = accreq}, #nas_prop{nas_id = <<"good_nas">>}, _) -> - {reply, #radius_request{cmd = accresp}}; -radius_request(#radius_request{cmd = coareq}, #nas_prop{nas_id = <<"good_nas">>}, _) -> - {reply, #radius_request{cmd = coaack}}; -radius_request(#radius_request{cmd = discreq}, #nas_prop{nas_id = <<"good_nas">>}, _) -> - {reply, #radius_request{cmd = discack}}; +radius_request(#{cmd := request, client := <<"good_nas">>} = Req, _) -> + {reply, Req#{cmd := accept}}; +radius_request(#{cmd := accreq, client := <<"good_nas">>} = Req, _) -> + {reply, Req#{cmd := accresp}}; +radius_request(#{cmd := coareq, client := <<"good_nas">>} = Req, _) -> + {reply, Req#{cmd := coaack}}; +radius_request(#{cmd := discreq, client := <<"good_nas">>} = Req, _) -> + {reply, Req#{cmd := discack}}; %% RADIUS NAS callbacks for 'bad' requests -radius_request(#radius_request{cmd = request}, #nas_prop{nas_id = <<"bad_nas">>}, _) -> - {reply, #radius_request{cmd = reject}}; -radius_request(#radius_request{cmd = coareq}, #nas_prop{nas_id = <<"bad_nas">>}, _) -> - {reply, #radius_request{cmd = coanak}}; -radius_request(#radius_request{cmd = discreq}, #nas_prop{nas_id = <<"bad_nas">>}, _) -> - {reply, #radius_request{cmd = discnak}}; +radius_request(#{cmd := request, client := <<"bad_nas">>} = Req, _) -> + {reply, Req#{cmd := reject}}; +radius_request(#{cmd := coareq, client := <<"bad_nas">>} = Req, _) -> + {reply, Req#{cmd := coanak}}; +radius_request(#{cmd := discreq, client := <<"bad_nas">>} = Req, _) -> + {reply, Req#{cmd := discnak}}; %% RADIUS NAS callbacks for 'error' requests -radius_request(#radius_request{cmd = request}, #nas_prop{nas_id = <<"error_nas">>}, _) -> - timer:sleep(1500), %% this will by default trigger one resend - {reply, #radius_request{cmd = accept}}. - +radius_request(#{cmd := request, client := <<"error_nas">>} = Req, _) -> + timer:sleep(150), %% this will by default trigger one resend + {reply, Req#{cmd := accept}}. diff --git a/test/eradius_proxy_SUITE.erl b/test/eradius_proxy_SUITE.erl deleted file mode 100644 index 85c6a2e2..00000000 --- a/test/eradius_proxy_SUITE.erl +++ /dev/null @@ -1,166 +0,0 @@ -%% Copyright (c) 2010-2017 by Travelping GmbH <info@travelping.com> - -%% Permission is hereby granted, free of charge, to any person obtaining a -%% copy of this software and associated documentation files (the "Software"), -%% to deal in the Software without restriction, including without limitation -%% the rights to use, copy, modify, merge, publish, distribute, sublicense, -%% and/or sell copies of the Software, and to permit persons to whom the -%% Software is furnished to do so, subject to the following conditions: - -%% The above copyright notice and this permission notice shall be included in -%% all copies or substantial portions of the Software. - -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -%% DEALINGS IN THE SOFTWARE. - --module(eradius_proxy_SUITE). --compile(export_all). --include("eradius_lib.hrl"). --include("dictionary.hrl"). --include("eradius_test.hrl"). - -all() -> [ - resolve_routes_test, - validate_arguments_test, - validate_options_test, - new_request_test, - get_key_test, - strip_test - ]. - -resolve_routes_test(_) -> - DefaultRoute = {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}, - Prod = {eradius_test_lib:localhost(tuple), 1812, <<"prod">>}, - Test = {eradius_test_lib:localhost(tuple), 11813, <<"test">>}, - Dev = {eradius_test_lib:localhost(tuple), 11814, <<"dev">>}, - {ok, R1} = re:compile("prod"), - {ok, R2} = re:compile("test"), - {ok, R3} = re:compile("^dev_.*"), - Routes = [{R1, Prod}, {R2, Test, [{pool, test_pool}]}, {R3, Dev}], - %% default - ?equal({undefined, DefaultRoute}, eradius_proxy:resolve_routes(undefined, DefaultRoute, Routes,[])), - ?equal({"user", DefaultRoute}, eradius_proxy:resolve_routes(<<"user">>, DefaultRoute, Routes, [])), - ?equal({"user@prod", Prod}, eradius_proxy:resolve_routes(<<"user@prod">>, DefaultRoute, Routes,[])), - ?equal({"user@test", {Test, [{pool, test_pool}]}}, eradius_proxy:resolve_routes(<<"user@test">>, DefaultRoute, Routes,[])), - %% strip - Opts = [{strip, true}], - ?equal({"user", DefaultRoute}, eradius_proxy:resolve_routes(<<"user">>, DefaultRoute, Routes, Opts)), - ?equal({"user", Prod}, eradius_proxy:resolve_routes(<<"user@prod">>, DefaultRoute, Routes, Opts)), - ?equal({"user", {Test, [{pool, test_pool}]}}, eradius_proxy:resolve_routes(<<"user@test">>, DefaultRoute, Routes, Opts)), - ?equal({"user", Dev}, eradius_proxy:resolve_routes(<<"user@dev_server">>, DefaultRoute, Routes, Opts)), - ?equal({"user", DefaultRoute}, eradius_proxy:resolve_routes(<<"user@dev-server">>, DefaultRoute, Routes, Opts)), - - %% prefix - Opts1 = [{type, prefix}, {separator, "/"}], - ?equal({"user/example", DefaultRoute}, eradius_proxy:resolve_routes(<<"user/example">>, DefaultRoute, Routes, Opts1)), - ?equal({"test/user", {Test, [{pool, test_pool}]}}, eradius_proxy:resolve_routes(<<"test/user">>, DefaultRoute, Routes, Opts1)), - %% prefix and strip - Opts2 = Opts ++ Opts1, - ?equal({"example", DefaultRoute}, eradius_proxy:resolve_routes(<<"user/example">>, DefaultRoute, Routes, Opts2)), - ?equal({"user", {Test, [{pool, test_pool}]}}, eradius_proxy:resolve_routes(<<"test/user">>, DefaultRoute, Routes, Opts2)), - ok. - -validate_arguments_test(_) -> - GoodConfig = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}}, - {options, [{type, realm}, {strip, true}, {separator, "@"}]}, - {routes, [{"test_1", {eradius_test_lib:localhost(tuple), 1815, <<"secret1">>}, [{pool, test_pool}]}, - {"test_2", {<<"localhost">>, 1816, <<"secret2">>}} - ]} - ], - GoodOldConfig = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}, test_pool}, - {options, [{type, realm}, {strip, true}, {separator, "@"}]}, - {routes, [{"test_1", {eradius_test_lib:localhost(tuple), 1815, <<"secret1">>}, [{pool, test_pool}]}, - {"test_2", {<<"localhost">>, 1816, <<"secret2">>}} - ]} - ], - - BadConfig = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}}, - {options, [{type, abc}]} - ], - BadConfig1 = [{default_route, {eradius_test_lib:localhost(tuple), 0, <<"secret">>}}], - BadConfig2 = [{default_route, {abc, 123, <<"secret">>}}], - BadConfig3 = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}}, - {options, [{type, realm}, {strip, true}, {separator, "@"}]}, - {routes, [{"test_1", {wrong_ip, 1815, <<"secret1">>}}, - {"test_2", {<<"localhost">>, 1816, <<"secret2">>}} - ]}], - BadConfig4 = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}}, - {options, [{type, realm}, {strip, true}, {separator, "@"}, {timeout, "wrong"}]}, - {routes, [{"test", {wrong_ip, 1815, <<"secret1">>}}, - {"test_2", {<<"localhost">>, 1816, <<"secret2">>}} - ]}], - BadConfig5 = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}}, - {options, [{type, realm}, {strip, true}, {separator, "@"}, {retries, "wrong"}]}, - {routes, [{"test", {wrong_ip, 1815, <<"secret1">>}}, - {"test_2", {"localhost", 1816, <<"secret2">>}} - ]}], - BadConfig6 = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>, [{pool, "wrong_pool"}]}}], - BadConfig7 = [{default_route, {eradius_test_lib:localhost(tuple), 1813, <<"secret">>}}, - {routes, [{"test", {wrong_ip, 1815, <<"secret1">>}, [{pool, "wrong_pool"}]}]}], - {Result, ConfigData} = eradius_proxy:validate_arguments(GoodConfig), - ?equal(true, Result), - {Valid, _} = eradius_proxy:validate_arguments(GoodOldConfig), - ?equal(true, Valid), - {routes, Routes} = lists:keyfind(routes, 1, ConfigData), - [{{CompiledRegexp_1, _, _, _, _}, _, _}, {{CompiledRegexp_2, _, _, _, _}, _, _}] = Routes, - ?equal(re_pattern, CompiledRegexp_1), - ?equal(re_pattern, CompiledRegexp_2), - ?equal(default_route, eradius_proxy:validate_arguments([])), - ?equal(options, eradius_proxy:validate_arguments(BadConfig)), - ?equal(default_route, eradius_proxy:validate_arguments(BadConfig1)), - ?equal(default_route, eradius_proxy:validate_arguments(BadConfig2)), - ?equal(routes, eradius_proxy:validate_arguments(BadConfig3)), - ?equal(options, eradius_proxy:validate_arguments(BadConfig4)), - ?equal(options, eradius_proxy:validate_arguments(BadConfig5)), - ?equal(default_route, eradius_proxy:validate_arguments(BadConfig6)), - ?equal(routes, eradius_proxy:validate_arguments(BadConfig7)), - ok. - -validate_options_test(_) -> - DefaultOptions = [{type, realm}, {strip, false}, {separator, "@"}], - ?equal(true, eradius_proxy:validate_options(DefaultOptions)), - ?equal(true, eradius_proxy:validate_options([{type, prefix}, {separator, "/"}, {strip, true}])), - ?equal(true, eradius_proxy:validate_options(DefaultOptions ++ [{timeout, 5000}])), - ?equal(true, eradius_proxy:validate_options(DefaultOptions ++ [{retries, 5}])), - ?equal(true, eradius_proxy:validate_options(DefaultOptions ++ [{timeout, 5000}, {retries, 5}])), - ?equal(false, eradius_proxy:validate_options([{type, unknow}])), - ?equal(false, eradius_proxy:validate_options([strip, abc])), - ?equal(false, eradius_proxy:validate_options([abc, abc])), - ?equal(false, eradius_proxy:validate_options(DefaultOptions ++ [{timeout, "5000"}])), - ?equal(false, eradius_proxy:validate_options(DefaultOptions ++ [{retries, "5"}])), - ok. - -new_request_test(_) -> - Req0 = #radius_request{}, - Req1 = eradius_lib:set_attr(Req0, ?User_Name, "user1"), - ?equal(Req0, eradius_proxy:new_request(Req0, "user", "user")), - ?equal(Req1, eradius_proxy:new_request(Req0, "user", "user1")), - ?equal(Req0, eradius_proxy:new_request(Req0, undefined, undefined)), - ok. - -get_key_test(_) -> - ?equal({"example", "user@example"}, eradius_proxy:get_key("user@example", realm, false, "@")), - ?equal({"user", "user/domain@example"}, eradius_proxy:get_key("user/domain@example", prefix, false, "/")), - ?equal({"example", "user"}, eradius_proxy:get_key("user@example", realm, true, "@")), - ?equal({"example", "user@domain"}, eradius_proxy:get_key("user@domain@example", realm, true, "@")), - ?equal({"user", "domain@example"}, eradius_proxy:get_key("user/domain@example", prefix, true, "/")), - ?equal({"user", "domain/domain2@example"}, eradius_proxy:get_key("user/domain/domain2@example", prefix, true, "/")), - ?equal({not_found, []}, eradius_proxy:get_key([], realm, false, "@")), - ok. - - -strip_test(_) -> - ?equal("user", eradius_proxy:strip("user", realm, false, "@")), - ?equal("user", eradius_proxy:strip("user", prefix, false, "@")), - ?equal("user", eradius_proxy:strip("user", realm, true, "@")), - ?equal("user", eradius_proxy:strip("user", prefix, true, "@")), - ?equal("user", eradius_proxy:strip("user@example", realm, true, "@")), - ?equal("user2@example", eradius_proxy:strip("user/user2@example", prefix, true, "/")), - ?equal("user/user2@example", eradius_proxy:strip("user/user2@example@roaming", realm, true, "@")), - ?equal("user/user2", eradius_proxy:strip("user/user2@example", realm, true, "@")), - ok. diff --git a/test/eradius_test_handler.erl b/test/eradius_test_handler.erl index 753f4db3..700cb22f 100644 --- a/test/eradius_test_handler.erl +++ b/test/eradius_test_handler.erl @@ -1,68 +1,81 @@ -module(eradius_test_handler). +-compile([export_all, nowarn_export_all]). -behaviour(eradius_server). --export([start/0, start/2, stop/0, send_request/1, send_request_failover/1, radius_request/3]). - -include("include/eradius_lib.hrl"). +-define(SERVER, ?MODULE). + start() -> start(inet, ipv4). start(Backend, Family) -> + application:stop(eradius), + application:load(eradius), - application:set_env(eradius, radius_callback, ?MODULE), - %% application:set_env(eradius, client_ip, eradius_test_lib:localhost(tuple)), - application:set_env(eradius, session_nodes, local), - application:set_env(eradius, one, - [{{"ONE", []}, [{eradius_test_lib:localhost(ip), "secret"}]}]), - application:set_env(eradius, two, - [{{"TWO", [{default_route, {{127, 0, 0, 2}, 1813, <<"secret">>}}]}, - [{eradius_test_lib:localhost(ip), "secret"}]}]), - application:set_env(eradius, servers, - [{one, {eradius_test_lib:localhost(ip), [1812]}}, - {two, {eradius_test_lib:localhost(ip), [1813]}}]), application:set_env(eradius, unreachable_timeout, 2), - application:set_env(eradius, servers_pool, - [{test_pool, - [{eradius_test_lib:localhost(tuple), 1812, "secret"}, - %% fake upstream server for fail-over - {eradius_test_lib:localhost(string), 1820, "secret", [{timeout, 1000}, {retries, 2}]}]}]), application:ensure_all_started(eradius), + ok = start_client(Backend, Family), + + SrvOpts = #{handler => {?MODULE, []}, + clients => #{eradius_test_lib:localhost(Family, native) => + #{secret => "secret", client => <<"ONE">>}}}, + {ok, _} = eradius:start_server( + eradius_test_lib:localhost(Family, native), 1812, SrvOpts#{server_name => one}), + {ok, _} = eradius:start_server( + eradius_test_lib:localhost(Family, native), 1813, SrvOpts#{server_name => two}), + ok. + +stop() -> + application:stop(eradius), + application:unload(eradius). + +start_client(Backend, Family) -> + application:ensure_all_started(eradius), + + Clients = + maps:from_list( + [{binary_to_atom(<<(X+$A)>>), #{ip => eradius_test_lib:localhost(Family, native), + port => 1820 + X, secret => "secret"}} + || X <- lists:seq(0, 9)]), ClientConfig = #{inet_backend => Backend, family => eradius_test_lib:inet_family(Family), - ip => eradius_test_lib:localhost(Family, tuple), - servers => [{one, {eradius_test_lib:localhost(Family, ip), [1812]}}, - {two, {eradius_test_lib:localhost(Family, ip), [1813]}}], - servers_pool => - [{test_pool, [{eradius_test_lib:localhost(Family, tuple), 1812, "secret"}, - %% fake upstream server for fail-over - {eradius_test_lib:localhost(Family, string), 1820, "secret", - [{timeout, 1000}, {retries, 2}]}]}] + ip => eradius_test_lib:localhost(Family, native), + servers => Clients#{one => #{ip => eradius_test_lib:localhost(Family, native), + port => 1812, + secret => "secret", + retries => 3}, + two => #{ip => eradius_test_lib:localhost(Family, native), + port => 1813, + secret => "secret", + retries => 3}, + bad => #{ip => eradius_test_lib:localhost(Family, native), + port => 1920, + secret => "secret", + retries => 3}, + test_pool => [one, two]} }, - eradius_client_mngr:reconfigure(ClientConfig), - eradius:modules_ready([?MODULE]). - -stop() -> - application:stop(eradius), - application:unload(eradius), - application:start(eradius). + case eradius_client_mngr:start_client({local, ?SERVER}, ClientConfig) of + {ok, _} -> ok; + {error, {already_started, _}} -> ok + end. -send_request(IP) -> - {ok, R, A} = eradius_client:send_request({IP, 1812, "secret"}, #radius_request{cmd = request}, []), - #radius_request{cmd = Cmd} = eradius_lib:decode_request(R, <<"secret">>, A), +send_request(ServerName) -> + ct:pal("about to send"), + {{ok, Resp}, _Req} = + eradius_client:send_request(?SERVER, ServerName, eradius_req:new(request), #{}), + {_, #{cmd := Cmd}} = eradius_req:attrs(Resp), Cmd. send_request_failover(Server) -> - {ok, Pools} = application:get_env(eradius, servers_pool), - SecondaryServers = proplists:get_value(test_pool, Pools), - {ok, R, A} = eradius_client:send_request(Server, #radius_request{cmd = request}, [{retries, 1}, - {timeout, 2000}, - {failover, SecondaryServers}]), - #radius_request{cmd = Cmd} = eradius_lib:decode_request(R, <<"secret">>, A), + Opts = #{retries => 1, timeout => 2000, failover => [test_pool]}, + {{ok, Resp}, _Req} = + eradius_client:send_request(?SERVER, Server, eradius_req:new(request), Opts), + {_, #{cmd := Cmd}} = eradius_req:attrs(Resp), Cmd. -radius_request(#radius_request{cmd = request}, _Nasprop, _Args) -> - {reply, #radius_request{cmd = accept}}. +radius_request(Req = #{cmd := request}, _Args) -> + {reply, eradius_req:set_attrs([], Req#{cmd := accept})}. diff --git a/test/eradius_test_lib.erl b/test/eradius_test_lib.erl index bde63471..fbd70740 100644 --- a/test/eradius_test_lib.erl +++ b/test/eradius_test_lib.erl @@ -6,6 +6,9 @@ -compile([export_all, nowarn_export_all]). +-define(IP4_LOOPBACK, {127, 0, 0, 1}). +-define(IP6_LOOPBACK, {0, 0, 0, 0, 0, 0, 0, 1}). + %%%=================================================================== %%% Helper functions %%%=================================================================== @@ -26,28 +29,11 @@ inet_family(ipv4) -> inet; inet_family(ipv6) -> inet6; inet_family(ipv4_mapped_ipv6) -> inet6. -badhost(Family) - when Family =:= ipv4; Family =:= ipv4_mapped_ipv6 -> - {127, 0, 0, 2}; -badhost(ipv6) -> - {0, 0, 0, 0, 0, 0, 0, 2}. - -localhost(Type) -> - localhost(ipv4, Type). - -localhost(Family, string) when Family =:= ipv4; Family =:= ipv4_mapped_ipv6 -> - "ip4-loopback"; -localhost(ipv6, string) -> - "ip6-loopback"; -localhost(Family, binary) -> - list_to_binary(localhost(Family, string)); -localhost(Family, tuple) when Family =:= ipv4; Family =:= ipv4_mapped_ipv6 -> - {ok, IP} = inet:getaddr(localhost(ipv4, string), inet), - IP; -localhost(ipv6, tuple) -> - {ok, IP} = inet:getaddr(localhost(ipv6, string), inet6), - IP; -localhost(Family, ip) -> - inet:ntoa(localhost(Family, tuple)); -localhost(Family, atom) -> - list_to_atom(localhost(Family, ip)). +localhost(ipv4, _) -> + ?IP4_LOOPBACK; +localhost(ipv6, _) -> + ?IP6_LOOPBACK; +localhost(ipv4_mapped_ipv6, native) -> + ?IP4_LOOPBACK; +localhost(ipv4_mapped_ipv6, mapped) -> + inet:ipv4_mapped_ipv6_address(?IP4_LOOPBACK). From f5429221b58cdee663239d68e6aa74f9460769e9 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 23 Jul 2025 09:53:12 +0200 Subject: [PATCH 09/38] fix retries counter handling and reset --- src/eradius_client_mngr.erl | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index b1490baa..105df1be 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -182,7 +182,7 @@ servers(ServerRef) -> #state{servers = Servers} = sys:get_state(ServerRef), maps:fold( fun(_, #{ip := IP, port := Port, retries := Retries, failed := Failed} = _, M) - when Failed < Retries -> + when Failed =< Retries -> [{{IP, Port}, Retries, Failed} | M]; (_, _, M) -> M end, [], Servers). @@ -246,20 +246,16 @@ handle_call({wanna_send, Candidates, Tried}, _From, {reply, Error, State0} end; -handle_call({failed, Peer}, _From, #state{servers = Servers0} = State0) -> - Servers = - case Servers0 of - #{Peer := #{retries := Retries, failed := Failed} = Server} - when Failed < Retries -> - Servers0#{Peer := Server#{failed := Failed + 1}}; - #{Peer := #{retries := Retries, failed := Failed} = Server} - when Failed =:= Retries -> - erlang:start_timer(?DEFAULT_DOWN_TIME, self(), {reset, Peer}), - Servers0#{Peer := Server#{failed := Failed + 1}}; - _ -> - Servers0 - end, - State = State0#state{servers = Servers}, +handle_call({failed, Peer}, _From, #state{servers = Servers} = State0) + when is_map_key(Peer, Servers) -> + #{retries := Retries, failed := Failed} = Server = map_get(Peer, Servers), + case Failed > Retries of + true -> erlang:start_timer(?DEFAULT_DOWN_TIME, self(), {reset, Peer}); + false -> ok + end, + State = State0#state{servers = Servers#{Peer := Server#{failed := Failed + 1}}}, + {reply, ok, State}; +handle_call({failed, _Peer}, _From, State) -> {reply, ok, State}; %% @private @@ -364,7 +360,7 @@ select_servers([Candidate|More], Servers, Selected) -> #{Candidate := [_|_] = Pool} -> select_servers(More, Servers, select_servers(Pool, Servers, Selected)); #{Candidate := #{retries := Retries, failed := Failed}} - when Failed < Retries -> + when Failed =< Retries -> select_servers(More, Servers, [Candidate | Selected]); _ -> select_servers(More, Servers, Selected) From 5dee5c079dd53deeea09c51202a9e9b7bf883cad Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Fri, 9 Jan 2026 11:48:03 +0100 Subject: [PATCH 10/38] fix typespecs for ServerName --- src/eradius_client_mngr.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index 105df1be..52bab28b 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -125,7 +125,7 @@ start_client(Opts) -> eradius_client_top_sup:start_client([Opts]). %% @doc Start a new, named RADIUS client that is managed by the eradius applications supervisor tree. --spec start_client(server_name(), client_opts()) -> +-spec start_client(gen_server:server_name(), client_opts()) -> {ok, pid()} | {error, supervisor:startchild_err()}. start_client(ServerName, Opts) -> maybe @@ -143,7 +143,7 @@ start_link(Owner, Opts) -> end. %% @private --spec start_link(pid(), server_name(), client_opts()) -> +-spec start_link(pid(), gen_server:server_name(), client_opts()) -> {ok, pid()} | {error, supervisor:startchild_err()}. start_link(Owner, ServerName, Opts) -> maybe From 4af3ac5f71b067857d91712493490998690aea70 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 4 Mar 2026 10:29:27 +0100 Subject: [PATCH 11/38] Fix scramble encryption, Message-Authenticator HMAC, and optional client field - packet/1: pre-generate random authenticator for 'request' cmd before encoding attributes, so that scramble encryption (User-Password) and Message-Authenticator HMAC computation both use the same authenticator value that will be written into the packet header - encode_body/2 for 'request': use pre-generated authenticator from packet/1 if present, instead of always generating a new one - encode_message_authenticator/2: fix typo 'reqid' -> 'req_id' in pattern match, so Message-Authenticator HMAC is actually computed when msg_hmac=true - request/4: make 'client' field optional in NAS map (fall back to <<>>), since NAS maps often only carry 'secret' and omitting 'client' caused a function_clause crash on the server side when decoding incoming packets --- src/eradius_req.erl | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/eradius_req.erl b/src/eradius_req.erl index 94cfb1ef..0256dc08 100644 --- a/src/eradius_req.erl +++ b/src/eradius_req.erl @@ -156,8 +156,15 @@ packet(#{req_id := _, cmd := _, authenticator := _, body := Body, secret := _} = when is_binary(Body) -> %% body must be fully prepared encode_body(Req, Body); -packet(#{req_id := _, cmd := _, secret := _, attrs := Attrs, eap_msg := EAPmsg} = Req) +packet(#{req_id := _, cmd := Cmd, secret := _, attrs := Attrs, eap_msg := EAPmsg} = Req0) when is_list(Attrs) -> + %% For 'request' cmd, the authenticator is random. Pre-generate it so that + %% scramble encryption (User-Password) and Message-Authenticator HMAC can + %% both use the same value before encode_body writes it into the packet header. + Req = case Cmd of + request -> Req0#{authenticator => random_authenticator()}; + _ -> Req0 + end, Body0 = encode_attributes(Req, Attrs, <<>>), Body1 = encode_eap_message(EAPmsg, Body0), Body = encode_message_authenticator(Req, Body1), @@ -218,7 +225,8 @@ new(Command, MetricsCallback) -spec request(binary(), binary(), eradius_server:client(), 'undefined' | metrics_callback()) -> req() | no_return(). request(<<Cmd, ReqId, Len:16, Authenticator:16/bytes>> = Header, Body, - #{secret := Secret, client := ClientId}, MetricsCallback) -> + #{secret := Secret} = NAS, MetricsCallback) -> + ClientId = maps:get(client, NAS, <<>>), Command = decode_command(Cmd), Req = new(Command, MetricsCallback), mk_req(Command, ReqId, Len, Authenticator, Header, Body, @@ -321,7 +329,9 @@ mk_req(_, _, Len, _, _, Body, _) encode_body(#{req_id := ReqId, cmd := Cmd} = Req, Body) when Cmd =:= request -> - Authenticator = random_authenticator(), + %% Use pre-generated authenticator if present (set by packet/1 for scramble + %% encryption), otherwise generate a fresh one. + Authenticator = maps:get(authenticator, Req, random_authenticator()), Packet = <<(encode_command(Cmd)):8, ReqId:8, (byte_size(Body) + 20):16, Authenticator:16/binary, Body/binary>>, {Packet, Req#{is_valid := true, request_authenticator => Authenticator}}; @@ -357,7 +367,7 @@ encode_command(discack) -> ?RDisconnect_Ack; encode_command(discnak) -> ?RDisconnect_Nak. -spec encode_message_authenticator(req(), binary()) -> binary(). -encode_message_authenticator(#{reqid := ReqId, cmd := Cmd, +encode_message_authenticator(#{req_id := ReqId, cmd := Cmd, authenticator := Authenticator, secret := Secret, msg_hmac := true}, Body) -> From 9f4e7f5d02ba3a66616aecbe97e962b5cf364d33 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 4 Mar 2026 10:29:33 +0100 Subject: [PATCH 12/38] Fix reconfigure server list update and start_client/1 return value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - handle_call({reconfigure, ...}): also update #state.servers when reconfiguring, so that new servers are actually used for sending requests (previously only config was updated, leaving servers empty → no_active_servers) - start_client/1: return the client manager pid instead of the supervisor pid, so callers can pass it directly to eradius_client:send_request/3,4 (previously the supervisor pid was returned, causing send_request to crash with {wanna_send,...} arriving at the supervisor gen_server) --- src/eradius_client_mngr.erl | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index 52bab28b..4b42d96c 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -119,10 +119,16 @@ %%%========================================================================= %% @doc Start a new RADIUS client that is managed by the eradius applications supervisor tree. +%% Returns the client manager pid (usable with eradius_client:send_request/3,4). -spec start_client(client_opts()) -> {ok, pid()} | {error, supervisor:startchild_err()}. start_client(Opts) -> - eradius_client_top_sup:start_client([Opts]). + case eradius_client_top_sup:start_client([Opts]) of + {ok, SupPid} -> + client_mngr_pid(SupPid); + Error -> + Error + end. %% @doc Start a new, named RADIUS client that is managed by the eradius applications supervisor tree. -spec start_client(gen_server:server_name(), client_opts()) -> @@ -261,8 +267,9 @@ handle_call({failed, _Peer}, _From, State) -> %% @private handle_call({reconfigure, Opts}, _From, #state{config = OConfig} = State0) -> case client_config(maps:merge(OConfig, Opts)) of - {ok, Config} -> - State = reconfigure_address(Config, State0#state{config = Config}), + {ok, #{servers := Servers} = Config} -> + State1 = State0#state{config = Config, servers = Servers}, + State = reconfigure_address(Config, State1), {reply, ok, State}; {error, _} = Error -> @@ -509,3 +516,11 @@ find_socket_process(PortIdx, Sockets, #state{owner = Owner, config = Config}) -> Socket -> {Socket, Sockets} end. + +client_mngr_pid(SupPid) -> + case lists:keyfind(eradius_client_mngr, 1, supervisor:which_children(SupPid)) of + {eradius_client_mngr, Pid, worker, _} when is_pid(Pid) -> + {ok, Pid}; + _ -> + {error, not_started} + end. From a79315d3ba03379db5e8549c7ba85313086d375d Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 1 Apr 2026 09:35:16 +0200 Subject: [PATCH 13/38] Apply consistent formatting fixes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- rebar.config | 12 ++++++------ src/eradius.erl | 2 +- src/eradius_client_mngr.erl | 10 +++++----- src/eradius_req.erl | 2 +- test/eradius_test_handler.erl | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/rebar.config b/rebar.config index 9a819778..5b24c38a 100644 --- a/rebar.config +++ b/rebar.config @@ -43,13 +43,13 @@ %% == ExDoc == {ex_doc, [ - {extras, ["README.md", "METRICS.md", "LICENSE"]}, - {main, "README.md"}, - {source_url, "https://github.com/travelping/eradius"} -]}. + {extras, ["README.md", "METRICS.md", "LICENSE"]}, + {main, "README.md"}, + {source_url, "https://github.com/travelping/eradius"} + ]}. %% == Hex == {hex, [ - {doc, #{provider => ex_doc}} -]}. + {doc, #{provider => ex_doc}} + ]}. diff --git a/src/eradius.erl b/src/eradius.erl index 13c6a4e0..a9c2c5fb 100644 --- a/src/eradius.erl +++ b/src/eradius.erl @@ -5,7 +5,7 @@ %% API -export([load_tables/1, load_tables/2, - start_server/3, start_server/4]). + start_server/3, start_server/4]). -ignore_xref([load_tables/1, load_tables/2, start_server/3, start_server/4]). diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index 4b42d96c..b9058522 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -296,7 +296,7 @@ handle_info({timeout, _, {reset, Peer}}, #state{servers = Servers0} = State0) -> {noreply, State}; handle_info(_Info, State) -> - {noreply, State}. + {noreply, State}. %% @private terminate(Reason, _State) -> @@ -425,13 +425,13 @@ client_config_name(Tag, #{family := inet6, ip := IP, ipv6_v6only := true} = Opts when IP =:= any; IP =:= {0, 0, 0, 0, 0, 0, 0, 0} -> client_config_name("*", Tag, Opts); client_config_name(Tag, #{family := inet6, ip := any} = Opts) -> - client_config_name("[::]", Tag, Opts); + client_config_name("[::]", Tag, Opts); client_config_name(Tag, #{family := inet, ip := any} = Opts) -> - client_config_name("[0.0.0.0]", Tag, Opts); + client_config_name("[0.0.0.0]", Tag, Opts); client_config_name(Tag, #{family := inet6, ip := IP} = Opts) -> - client_config_name([$[, inet:ntoa(IP), $]], Tag, Opts); + client_config_name([$[, inet:ntoa(IP), $]], Tag, Opts); client_config_name(Tag, #{family := inet, ip := IP} = Opts) -> - client_config_name(inet:ntoa(IP), Tag, Opts). + client_config_name(inet:ntoa(IP), Tag, Opts). client_config_name(IP, Tag, Opts) -> {ok, Opts#{name => iolist_to_binary([IP, Tag])}}. diff --git a/src/eradius_req.erl b/src/eradius_req.erl index 0256dc08..0fe97a3e 100644 --- a/src/eradius_req.erl +++ b/src/eradius_req.erl @@ -411,7 +411,7 @@ encode_attributes(Req, Attributes, Init) -> <<Body/binary, EncAttr/binary>>; _ -> Body - end + end end, Init, Attributes). -spec encode_attribute(eradius_req:req(), #attribute{}, term()) -> binary(). diff --git a/test/eradius_test_handler.erl b/test/eradius_test_handler.erl index 700cb22f..b77e18b2 100644 --- a/test/eradius_test_handler.erl +++ b/test/eradius_test_handler.erl @@ -21,7 +21,7 @@ start(Backend, Family) -> SrvOpts = #{handler => {?MODULE, []}, clients => #{eradius_test_lib:localhost(Family, native) => - #{secret => "secret", client => <<"ONE">>}}}, + #{secret => "secret", client => <<"ONE">>}}}, {ok, _} = eradius:start_server( eradius_test_lib:localhost(Family, native), 1812, SrvOpts#{server_name => one}), {ok, _} = eradius:start_server( From 4e0fec7ec46003b8be6d04afcfb83bf903306218 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 1 Apr 2026 09:58:41 +0200 Subject: [PATCH 14/38] Fix dialyzer errors: binary type annotation and inet_backend option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix `secret := binary` typo to `secret := binary()` in server_opts() and server() types — `binary` without parens is the atom, not the type - Add missing `inet_backend => inet | socket` to client_opts() type, which is already used by eradius_client_socket - Fix test secrets from "secret" (string) to <<"secret">> (binary) to match the server_opts() contract Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- src/eradius_client_mngr.erl | 5 +++-- test/eradius_test_handler.erl | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index b9058522..dfa4a87d 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -48,14 +48,14 @@ -type server_opts() :: #{ip := inet:ip_address(), port := inet:port_number(), - secret := binary, + secret := binary(), retries => non_neg_integer(), timeout => non_neg_integer()}. %% Options to describe a RADIUS server. -type server() :: #{ip := inet:ip_address(), port := inet:port_number(), - secret := binary, + secret := binary(), retries := non_neg_integer(), timeout := non_neg_integer(), failed := non_neg_integer()}. @@ -72,6 +72,7 @@ #{name => server_name(), servers := #{server_name() := server_opts() | server_pool()}, family => inet | inet6, + inet_backend => inet | socket, ip => any | inet:ip_address(), active_n => once | non_neg_integer(), no_ports => non_neg_integer(), diff --git a/test/eradius_test_handler.erl b/test/eradius_test_handler.erl index b77e18b2..6ed36b23 100644 --- a/test/eradius_test_handler.erl +++ b/test/eradius_test_handler.erl @@ -21,7 +21,7 @@ start(Backend, Family) -> SrvOpts = #{handler => {?MODULE, []}, clients => #{eradius_test_lib:localhost(Family, native) => - #{secret => "secret", client => <<"ONE">>}}}, + #{secret => <<"secret">>, client => <<"ONE">>}}}, {ok, _} = eradius:start_server( eradius_test_lib:localhost(Family, native), 1812, SrvOpts#{server_name => one}), {ok, _} = eradius:start_server( @@ -38,7 +38,7 @@ start_client(Backend, Family) -> Clients = maps:from_list( [{binary_to_atom(<<(X+$A)>>), #{ip => eradius_test_lib:localhost(Family, native), - port => 1820 + X, secret => "secret"}} + port => 1820 + X, secret => <<"secret">>}} || X <- lists:seq(0, 9)]), ClientConfig = #{inet_backend => Backend, @@ -46,15 +46,15 @@ start_client(Backend, Family) -> ip => eradius_test_lib:localhost(Family, native), servers => Clients#{one => #{ip => eradius_test_lib:localhost(Family, native), port => 1812, - secret => "secret", + secret => <<"secret">>, retries => 3}, two => #{ip => eradius_test_lib:localhost(Family, native), port => 1813, - secret => "secret", + secret => <<"secret">>, retries => 3}, bad => #{ip => eradius_test_lib:localhost(Family, native), port => 1920, - secret => "secret", + secret => <<"secret">>, retries => 3}, test_pool => [one, two]} }, From d110a3278d700d970de0a565e97bc8bc1db0b9af Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 1 Apr 2026 10:10:09 +0200 Subject: [PATCH 15/38] Use consistent dialyzer invocation and restore plt_apps config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change GH workflow from `rebar3 as dialyzer do dialyzer` to `rebar3 as dialyzer dialyzer` to match the documented command - Restore `{plt_apps, all_deps}` to dialyzer config — was dropped during the rewrite, required for full dependency PLT coverage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- .github/workflows/main.yml | 2 +- rebar.config | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6ae07da8..0bf00885 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,7 +25,7 @@ jobs: run: | export ERL_AFLAGS="+pc unicode -enable-feature all" rebar3 do xref, ct - rebar3 as dialyzer do dialyzer + rebar3 as dialyzer dialyzer - name: Tar Test Output if: ${{ always() }} run: tar -cf - _build/test/logs/ | zstd -15 -o ct-logs-${{ matrix.otp }}.tar.zst diff --git a/rebar.config b/rebar.config index 5b24c38a..ed1b3326 100644 --- a/rebar.config +++ b/rebar.config @@ -37,6 +37,7 @@ {dialyzer, [%%{warnings, [unmatched_returns, underspecs]}, + {plt_apps, all_deps}, {plt_extra_apps, [prometheus]} ]}. From 805357c412f830b269e4311db900e16d4ce760e8 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 1 Apr 2026 10:14:20 +0200 Subject: [PATCH 16/38] Fix unused variable warning in eradius_client_socket Remove unused `mode` binding from the udp handle_info clause. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- src/eradius_client_socket.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eradius_client_socket.erl b/src/eradius_client_socket.erl index 1515744f..9178ecf9 100644 --- a/src/eradius_client_socket.erl +++ b/src/eradius_client_socket.erl @@ -90,7 +90,7 @@ handle_info({udp_passive, _Socket}, #state{socket = Socket, active_n = ActiveN} {noreply, State}; handle_info({udp, Socket, FromIP, FromPort, Response}, - State = #state{socket = Socket, mode = Mode}) -> + State = #state{socket = Socket}) -> flow_control(State), NState = case Response of From b210b9e7f6584003165117ee4ec1e079b2b8e009 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 1 Apr 2026 16:15:14 +0200 Subject: [PATCH 17/38] Add inline documentation and require OTP 27.3 Convert all %% @doc / %% @moduledoc EDoc comments to OTP 27 native -doc / -moduledoc attributes throughout the codebase. Remove -feature(maybe_expr, enable) directives now that maybe is standard. Drop support for OTP releases prior to 27.3. Update minimum_otp_vsn in rebar.config, the CI matrix, and the README version support section. Add comprehensive inline documentation to all public API modules: eradius, eradius_server, eradius_client, eradius_client_mngr, and eradius_req. Document the handler callback, all public functions, and key types. Add module-level usage examples. Expand README with a v3 quickstart covering server implementation, client setup, and failover, and a "Why a major, breaking rewrite?" section explaining the rationale for the API incompatibility. Update ex_doc configuration with groups_for_modules for cleaner navigation in generated docs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- .github/workflows/main.yml | 4 +- README.md | 158 +++++++++++++++++++----- rebar.config | 10 +- src/eradius.erl | 37 +++++- src/eradius_auth.erl | 49 ++++---- src/eradius_client.erl | 73 ++++++++--- src/eradius_client_mngr.erl | 73 ++++++++--- src/eradius_dict.erl | 3 +- src/eradius_eap_packet.erl | 18 +-- src/eradius_lib.erl | 10 +- src/eradius_log.erl | 70 ++++++----- src/eradius_metrics_prometheus.erl | 17 ++- src/eradius_req.erl | 190 ++++++++++++++++++++++++++--- src/eradius_server.erl | 95 ++++++++++++++- src/eradius_server_sup.erl | 3 +- 15 files changed, 640 insertions(+), 170 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0bf00885..88f4848a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - otp: [22, 23, 24, 25, 26, 27, 28] + otp: [27.3, 28, 29] container: image: erlang:${{ matrix.otp }}-alpine steps: @@ -23,7 +23,7 @@ jobs: run: rebar3 compile - name: Run tests run: | - export ERL_AFLAGS="+pc unicode -enable-feature all" + export ERL_AFLAGS="+pc unicode" rebar3 do xref, ct rebar3 as dialyzer dialyzer - name: Tar Test Output diff --git a/README.md b/README.md index 50266f35..b41a7ffe 100644 --- a/README.md +++ b/README.md @@ -5,50 +5,62 @@ [![Build Status][gh badge]][gh] [![Erlang Versions][erlang version badge]][gh] -eradius is a library to add [RADIUS] client and server funtionality to Erlang applications. +eradius is a library for embedding [RADIUS] client and server functionality in Erlang applications. -v3 of eradius is in many places a full rewrite of the v2 versions and is not API compatible -with older versions. - -Previous versions provided generic, standalone [RADIUS] servers and proxies. This generic -functionality is better handled by existing, feature rich, stand alone RADIUS servers. - -This versions aims a providing support for implementing a versatile, simple to use -RADIUS client and a flexible library for implementing sue case specific RADIUS server -functionality. - -v2 was based on the original Jungerl code, some piece of it might have survived. -This fork of `eradius` is a radical deviation from the original -Jungerl code. It contains a generic [RADIUS](https://en.wikipedia.org/wiki/RADIUS) client, support for -several authentication mechanisms and dynamic configuration -(it implements the `config_change/3` application callback). +v3 is a ground-up rewrite. It is **not API-compatible with v2**. # Contents +* [Why a major, breaking rewrite?](#why-a-major-breaking-rewrite) * [Erlang Version Support](#erlang-version-support) * [Building eradius](#building-eradius) * [Using eradius](#using-eradius) * [Metrics](#metrics) * [Tables](#tables) -# Erlang Version Support +# Why a major, breaking rewrite? -All minor version of the current major release and the highest minor version of the -previous major release will be supported. -At the time of writing, OTP `25.3`, `26.0` `26.1` and `26.2` are supported. -When in doubt check the `otp_release` section in [main.yml](.github/workflows/main.yml) for -tested versions. +**The API was already broken.** In the period leading up to v3, several features were added +to v2 that introduced incompatible changes in call signatures (per-server timeout/retries, +IPv6 support, socket option tuning). A clean slate was cheaper than continued patching. + +**Request dispatching is not a RADIUS concern.** v2 included cluster-aware request routing — +distributing requests across nodes, tracking node availability, and selecting targets based +on cluster state. None of that logic is specific to RADIUS. It belongs in a generic +load-balancer or service-mesh layer that the application already has (or should have). +Keeping it in eradius created coupling between the protocol library and deployment topology. -> #### NOTE {: .tip} -> -> OTP-25 does not support the `-feature(maybe_expr, enable).` compiler directive, it is therefore necessary to pass the `-enable-feature all` option to the compiler. +**The built-in proxy belongs elsewhere.** v2 shipped a RADIUS proxy implementation. A +production-grade proxy requires policy engines, accounting correlation, attribute rewriting, +and operational tooling that a library can never provide adequately. Purpose-built RADIUS +servers (FreeRADIUS, Radiator) do this far better. The proxy in v2 was a maintenance +liability that gave a false sense of completeness. -## Planned incompatiblities +**Jungerl heritage.** The codebase traces back to Martin Björklund and Torbjörn Törnkvist's +2002–2007 Jungerl code, written before records were replaced by maps, before `supervisor` +had modern child-spec conventions, and before the OTP documentation system existed. +Continuing to layer features on top meant carrying forward API shapes that had no rationale +beyond historical accident. -### v3.1 will require OTP-27 +**Maps over records.** The entire request/response lifecycle is now expressed as a single +`eradius_req:req()` map. Handler modules receive a plain map, transform it, and return it. +No record definitions to import, no opaque types to thread through. The same approach +applies to client and server configuration — one map, no positional tuples. -The current planning is to switch to documentation support introduced with OTP-27 for the v3.1 -release. That release will therefore drop support for pre OTP-27 releases. +**Metrics as a callback.** v2 hard-wired Prometheus. v3 exposes a `metrics_callback` +option: a 3-arity fun the application provides. Any metrics backend works. Applications +that do not want metrics pay no overhead. + +**OTP 27 as baseline.** Requiring OTP 27.3 lets the library use `maybe` expressions +natively, the new `-doc`/`-moduledoc` attributes for proper documentation tooling, and +the latest supervision and gen_server interfaces without compatibility shims. + +# Erlang Version Support + +eradius v3.0 requires OTP-27.3 or later. + +When in doubt check the `otp_release` section in [main.yml](.github/workflows/main.yml) for +the exact versions tested in CI. # Building eradius @@ -58,8 +70,90 @@ $ rebar3 compile # Using eradius -Eradius client are started and configured through their APIs. See `m:eradius_server` and -`m:eradius_client` for the APIs and settings. +## Implementing a RADIUS server + +A RADIUS server handler is a module that implements the `eradius_server` behaviour. +It receives decoded `eradius_req:req()` maps and returns a response: + +```erlang +-module(my_handler). +-behaviour(eradius_server). + +-include_lib("eradius/include/eradius_lib.hrl"). %% attribute macros + +radius_request(#{cmd := request} = Req, _HandlerData) -> + %% Inspect attributes + {Attrs, _} = eradius_req:attrs(Req), + UserName = proplists:get_value(?User_Name, Attrs), + io:format("Access-Request from ~s~n", [UserName]), + + %% Build and return the response + Resp = eradius_req:set_attrs([], Req#{cmd := accept}), + {reply, Resp}; +radius_request(_Req, _HandlerData) -> + noreply. +``` + +Start the server (requires the `eradius` application to be running): + +```erlang +{ok, _Pid} = eradius:start_server({127,0,0,1}, 1812, + #{server_name => my_radius_server, + handler => {my_handler, []}, + clients => #{{127,0,0,1} => #{secret => <<"mysecret">>, + client => <<"my-nas">>}}}). +``` + +The `clients` map controls which source IPs are accepted. Packets from unlisted IPs are +silently discarded. See `eradius_server:server_opts()` for the full option reference. + +## Sending RADIUS requests (client) + +Start a named client manager, then use `eradius_client:send_request/3,4` to send requests: + +```erlang +%% Start a client manager once (e.g. in your application start) +{ok, _} = eradius_client_mngr:start_client({local, my_client}, + #{family => inet, + ip => any, + servers => #{ + auth_server => #{ip => {192,168,1,1}, + port => 1812, + secret => <<"mysecret">>, + retries => 3, + timeout => 5000} + }}), + +%% Send an Access-Request +Req = eradius_req:set_attrs( + [{?User_Name, <<"alice">>}, + {?User_Password, <<"password">>}], + eradius_req:new(request)), + +{{ok, Resp}, _Req} = eradius_client:send_request(my_client, auth_server, Req, #{}), +accept = eradius_req:cmd(Resp). +``` + +## Failover between servers + +Define a pool as a list of server names, then pass the pool in the `failover` option: + +```erlang +{ok, _} = eradius_client_mngr:start_client({local, my_client}, + #{family => inet, + ip => any, + servers => #{ + primary => #{ip => {10,0,0,1}, port => 1812, secret => <<"s1">>}, + secondary => #{ip => {10,0,0,2}, port => 1812, secret => <<"s2">>}, + auth_pool => [primary, secondary] + }}), + +%% Try primary first; on failure, try each server in auth_pool +{{ok, Resp}, _} = eradius_client:send_request(my_client, primary, Req, + #{retries => 1, failover => [auth_pool]}). +``` + +See `eradius_client:options()` and `eradius_client_mngr:client_opts()` for all options. # Metrics @@ -85,5 +179,5 @@ Example: [hexpm downloads]: https://img.shields.io/hexpm/dt/eradius.svg?style=flat-square [gh]: https://github.com/travelping/eradius/actions/workflows/main.yml [gh badge]: https://img.shields.io/github/workflow/status/travelping/eradius/CI?style=flat-square -[erlang version badge]: https://img.shields.io/badge/erlang-25.3%20to%2026.2-blue.svg?style=flat-square +[erlang version badge]: https://img.shields.io/badge/erlang-27.3%2B-blue.svg?style=flat-square [RADIUS]: https://en.wikipedia.org/wiki/RADIUS diff --git a/rebar.config b/rebar.config index ed1b3326..0657d0e3 100644 --- a/rebar.config +++ b/rebar.config @@ -1,6 +1,6 @@ %%-*-Erlang-*- {erl_opts, [debug_info]}. -{minimum_otp_vsn, "25.3"}. +{minimum_otp_vsn, "27.3"}. {pre_hooks, [{compile, "escript dicts_compiler.erl compile"}, {clean, "escript dicts_compiler.erl clean"}]}. @@ -46,7 +46,13 @@ {ex_doc, [ {extras, ["README.md", "METRICS.md", "LICENSE"]}, {main, "README.md"}, - {source_url, "https://github.com/travelping/eradius"} + {source_url, "https://github.com/travelping/eradius"}, + {groups_for_modules, [ + {"Server", [eradius, eradius_server]}, + {"Client", [eradius_client, eradius_client_mngr]}, + {"Request/Response", [eradius_req]}, + {"Dictionaries", [eradius_dict]} + ]} ]}. %% == Hex == diff --git a/src/eradius.erl b/src/eradius.erl index a9c2c5fb..40b7a9d9 100644 --- a/src/eradius.erl +++ b/src/eradius.erl @@ -1,6 +1,7 @@ -%% @doc Main module of the eradius application. -module(eradius). +-moduledoc "Main module of the eradius application.". + -behaviour(application). %% API @@ -20,22 +21,52 @@ %%% API %%%========================================================================= -%% @doc Load RADIUS dictionaries from the default directory. +-doc "Load RADIUS dictionaries from the default directory.". -spec load_tables(list(eradius_dict:table_name())) -> ok | {error, {consult, eradius_dict:table_name()}}. load_tables(Tables) -> eradius_dict:load_tables(Tables). -%% @doc Load RADIUS dictionaries from a certain directory. +-doc "Load RADIUS dictionaries from a certain directory.". -spec load_tables(Dir :: file:filename(), Tables :: [Table :: eradius_dict:table_name()]) -> ok | {error, {consult, Table :: eradius_dict:table_name()}}. load_tables(Dir, Tables) -> eradius_dict:load_tables(Dir, Tables). +-doc """ +Start a RADIUS server under the eradius supervision tree. + +`IP` is the address to listen on (`any` binds on all interfaces). +`Port` is the UDP port (e.g. 1812 for authentication, 1813 for accounting). +`Opts` must include `handler` (a `{Module, HandlerData}` tuple) and `clients` +(a map of `inet:ip_address() => t:eradius_server:client/0`). + +Example: +``` +{ok, Pid} = eradius:start_server({127,0,0,1}, 1812, + #{handler => {my_handler, []}, + clients => #{{127,0,0,1} => #{secret => <<"mysecret">>, + client => <<"my-nas">>}}}). +``` + +See `eradius_server:start_instance/3`. +""". +-spec start_server(IP :: 'any' | inet:ip_address(), Port :: inet:port_number(), + Opts :: eradius_server:server_opts()) -> gen_server:start_ret(). start_server(IP, Port, #{handler := {_, _}, clients := #{}} = Opts) when (IP =:= any orelse is_tuple(IP)) andalso is_integer(Port) andalso Port >= 0 andalso Port < 65536 -> eradius_server:start_instance(IP, Port, Opts). +-doc """ +Start a named RADIUS server under the eradius supervision tree. + +Same as `start_server/3` but registers the process under `ServerName`. + +See `eradius_server:start_instance/4`. +""". +-spec start_server(ServerName :: gen_server:server_name(), + IP :: 'any' | inet:ip_address(), Port :: inet:port_number(), + Opts :: eradius_server:server_opts()) -> gen_server:start_ret(). start_server(ServerName, IP, Port, #{handler := {_, _}, clients := #{}} = Opts) when (IP =:= any orelse is_tuple(IP)) andalso is_integer(Port) andalso Port >= 0 andalso Port < 65536 -> diff --git a/src/eradius_auth.erl b/src/eradius_auth.erl index 2ec7eb7b..a74f7805 100644 --- a/src/eradius_auth.erl +++ b/src/eradius_auth.erl @@ -4,9 +4,10 @@ %% SPDX-License-Identifier: MIT %% -%% @doc user authentication helper functions -module(eradius_auth). +-moduledoc "user authentication helper functions". + -export([check_password/2]). -export([pap/2, chap/3, ms_chap/3, ms_chap_v2/4]). -export([des_key_from_hash/1, nt_password_hash/1, challenge_response/2, @@ -36,8 +37,10 @@ %% ------------------------------------------------------------------------------------------ %% -- high level interface -%% @doc check the request password using all available authentication mechanisms. -%% Tries CHAP, then MS-CHAP, then MS-CHAPv2, finally PAP. +-doc """ +check the request password using all available authentication mechanisms. + Tries CHAP, then MS-CHAP, then MS-CHAPv2, finally PAP. +""". -spec check_password(binary(), eradius_req:req()) -> false | {boolean(), eradius_req:attribute_list()}. check_password(Password, #{authenticator := Authenticator, is_valid := true} = Req) -> @@ -86,7 +89,7 @@ lookup_auth_attrs(Attrs, []) -> %% ------------------------------------------------------------------------------------------ %% -- PAP/CHAP -%% @doc PAP authentication +-doc "PAP authentication". -spec pap(binary(), binary()) -> boolean(). pap(<<C, PasswdRest/binary>>, <<C, ReqRest/binary>>) -> pap(PasswdRest, ReqRest); @@ -105,13 +108,13 @@ only_null(<<_, _/binary>>) -> only_null(<<>>) -> true. -%% @doc CHAP authentication +-doc "CHAP authentication". -spec chap(binary(), binary(), binary()) -> boolean(). chap(Passwd, <<ChapId, ChapPassword/binary>>, ChapChallenge) -> EncPassword = crypto:hash(md5, [ChapId, Passwd, ChapChallenge]), EncPassword == ChapPassword. -%% @doc build a des key from a hash +-doc "build a des key from a hash". set_des_parity_odd(P0) -> P1 = P0 bxor (P0 bsr 4), P2 = P1 bxor (P1 bsr 2), @@ -122,35 +125,37 @@ set_des_parity_odd(P0) -> des_key_from_hash(Hash) -> << << (set_des_parity_odd(X)) >> || <<X:7>> <= Hash >>. -%% @doc turn a plain text password (either binary or list representation) -%% into UTF-16 binary representation. +-doc """ +turn a plain text password (either binary or list representation) +into UTF-16 binary representation. +""". ascii_to_unicode(Bin) when is_binary(Bin) -> << <<X, 0>> || <<X>> <= Bin >>; ascii_to_unicode(Lst) when is_list(Lst) -> list_to_binary([[X, 0] || X <- Lst]). -%% @doc calculte MD4 hash for value +-doc "calculte MD4 hash for value". nt_hash(Value) -> crypto:hash(md4, Value). -%% @doc calculate the MD4 hash of a plain text password the NT way +-doc "calculate the MD4 hash of a plain text password the NT way". nt_password_hash(Passwd) -> crypto:hash(md4, ascii_to_unicode(Passwd)). %% ------------------------------------------------------------------------------------------ %% -- MS-CHAP -%% @doc generate MS-CHAP NT-Response +-doc "generate MS-CHAP NT-Response". v2_generate_nt_response(AuthenticatorChallenge, PeerChallenge, UserName, PasswdHash) -> ChallengeHash = v2_challenge_hash(PeerChallenge, AuthenticatorChallenge, UserName), challenge_response(ChallengeHash, PasswdHash). -%% @doc calculate MS-CHAP challenge hash +-doc "calculate MS-CHAP challenge hash". v2_challenge_hash(PeerChallenge, AuthenticatorChallenge, UserName) -> binary:part(crypto:hash(sha, [PeerChallenge, AuthenticatorChallenge, UserName]), 0, 8). %% crypto API changes in OTP >= 23 -if(?OTP_RELEASE >= 23). -%% @doc calculate MS-CHAP challenge response +-doc "calculate MS-CHAP challenge response". challenge_response(Challenge, PasswdHash) -> Hash = eradius_lib:pad_to(21, PasswdHash), <<Key1:8/binary, Key2:8/binary, Key3:8/binary>> = des_key_from_hash(Hash), @@ -161,7 +166,7 @@ challenge_response(Challenge, PasswdHash) -> <<Resp1/binary, Resp2/binary, Resp3/binary>>. -else. -%% @doc calculate MS-CHAP challenge response +-doc "calculate MS-CHAP challenge response". challenge_response(Challenge, PasswdHash) -> Hash = eradius_lib:pad_to(21, PasswdHash), <<Key1:8/binary, Key2:8/binary, Key3:8/binary>> = des_key_from_hash(Hash), @@ -199,7 +204,7 @@ lm_password_hash(Password) -> << Key1:7/binary, Key2:7/binary >> = lm_password(Password), << (des_hash(Key1))/binary, (des_hash(Key2))/binary >>. -%% @doc MS-CHAP authentication +-doc "MS-CHAP authentication". -spec ms_chap(binary(), binary(), binary()) -> {boolean(), eradius_req:attribute_list()}. ms_chap(Passwd, Challenge, <<_Ident:1/binary, Flags:1/integer-unit:8, LMResponse:24/binary, NTResponse:24/binary>>) -> @@ -223,7 +228,7 @@ ms_chap(Passwd, Challenge, true -> {true, ms_chap_attrs(LmPasswdHash, NtPasswdHash)} end. -%% @doc MS-CHAP-V2 authentication +-doc "MS-CHAP-V2 authentication". -spec ms_chap_v2(binary(), binary(), binary(), binary()) -> {boolean(), eradius_req:attribute_list()}. ms_chap_v2(UserName, Passwd, AuthenticatorChallenge, @@ -238,13 +243,13 @@ ms_chap_v2(UserName, Passwd, AuthenticatorChallenge, {false, []} end. -%% @doc calculate MS-CHAP response attributes +-doc "calculate MS-CHAP response attributes". ms_chap_attrs(LmPasswdHash, NtPasswdHash) -> Key1 = binary:part(LmPasswdHash, 0, 8), Key2 = binary:part(nt_hash(NtPasswdHash), 0, 16), [{?MS_CHAP_MPPE_Keys, <<Key1/binary, Key2/binary>>}]. -%% @doc calculate MS-CHAPv2 response attributes +-doc "calculate MS-CHAPv2 response attributes". ms_chap_v2_attrs(UserName, PasswdHash, AuthenticatorChallenge, Ident, PeerChallenge, Response) -> SFlag = 1, LFlag = 0, EncPolicy = 2, %% TODO: determine from config or something... {SendKey, ReceiveKey} = mppe_generate_session_keys(PasswdHash, Response, 128), @@ -257,20 +262,20 @@ ms_chap_v2_attrs(UserName, PasswdHash, AuthenticatorChallenge, Ident, PeerChalle %% ------------------------------------------------------------------------------------------ %% -- MS-CHAP-V2 MPPE key functions -%% @doc calculate MPPE master key +-doc "calculate MPPE master key". mppe_get_master_key(PasswordHashHash, NTResponse) -> binary:part(crypto:hash(sha, [PasswordHashHash, NTResponse, mppe_magic1()]), 0, 16). -%% @doc calculate next MPPE key from current +-doc "calculate next MPPE key from current". mppe_get_new_key_from_sha(StartKey, SessionKey, SessionKeyLength) -> Key = crypto:hash(sha, [StartKey, mppe_sha_pad1(), SessionKey, mppe_sha_pad2()]), binary:part(Key, 0, SessionKeyLength). -%% @doc calculate first MPPE send key +-doc "calculate first MPPE send key". mppe_get_asymetric_send_start_key(MasterKey, SessionKeyLength) -> mppe_get_new_key_from_sha(MasterKey, mppe_magic3(), SessionKeyLength). -%% @doc calculate first MPPE recieve key +-doc "calculate first MPPE recieve key". mppe_get_asymetric_recv_start_key(MasterKey, SessionKeyLength) -> mppe_get_new_key_from_sha(MasterKey, mppe_magic2(), SessionKeyLength). diff --git a/src/eradius_client.erl b/src/eradius_client.erl index 5eb9cc30..a9c1d336 100644 --- a/src/eradius_client.erl +++ b/src/eradius_client.erl @@ -2,20 +2,44 @@ %% Copyright (c) 2011, Travelping GmbH <info@travelping.com> %% %% SPDX-License-Identifier: MIT -%% -%% @doc This module contains a RADIUS client that can be used to send authentication and accounting requests. -%% A counter is kept for every client instance in order to determine the next request id and sender port -%% for each outgoing request. -%% -%% The client uses OS-assigned ports. The maximum number of open ports can be specified through the -%% ``client_ports'' application environment variable, it defaults to ``20''. The number of ports should not -%% be set too low. If ``N'' ports are opened, the maximum number of concurrent requests is ``N * 256''. -%% -%% The IP address used to send requests is read <emph>once</emph> (at startup) from the ``client_ip'' -%% parameter. Changing it currently requires a restart. It can be given as a string or ip address tuple, -%% or the atom ``any'' (the default), which uses whatever address the OS selects. + -module(eradius_client). +-moduledoc """ +Send RADIUS requests using a managed client instance. + +This module provides the `send_request/3,4` API for sending RADIUS requests through +a client manager started with `eradius_client_mngr:start_client/1,2`. + +== Basic usage == + +``` +%% Start a named client manager first +{ok, _} = eradius_client_mngr:start_client({local, my_client}, + #{family => inet, + ip => any, + servers => #{ + auth => #{ip => {192,168,1,1}, port => 1812, + secret => <<"mysecret">>, retries => 3} + }}), + +%% Send an access request +Req = eradius_req:set_attrs([{?User_Name, <<"alice">>}], eradius_req:new(request)), +{{ok, Resp}, _Req} = eradius_client:send_request(my_client, auth, Req, #{}). +``` + +== Retries and timeout == + +Per-request options in `t:options/0` override the server defaults configured in +`t:eradius_client_mngr:server_opts/0`. The defaults are 3 retries and a 5-second timeout. + +== Failover == + +The `failover` option accepts a list of server pool names. If the primary server fails +(all retries exhausted), the request is retried against the first available server +from each failover pool in order. +""". + %% API -export([send_request/3, send_request/4]). -ignore_xref([send_request/3, send_request/4]). @@ -51,7 +75,7 @@ %%% API %%%========================================================================= -%% @equiv send_request(ServerRef, ServerName, Req, []) +-doc "Equivalent to `send_request(ServerRef, ServerName, Req, #{})`.". -spec send_request(gen_server:server_ref(), eradius_client_mngr:server_name() | [eradius_client_mngr:server_name()], eradius_req:req()) -> @@ -59,8 +83,27 @@ send_request(ServerRef, ServerName, #{cmd := _, payload := _} = Req) -> send_request(ServerRef, ServerName, Req, #{}). -%% @doc Send a radius request to the given server or server pool. -%% If no answer is received within the specified timeout, the request will be sent again. +-doc """ +Send a RADIUS request to a server or server pool. + +`ServerRef` is the pid or registered name of a client manager started with +`eradius_client_mngr:start_client/1,2`. + +`ServerName` identifies the target: either an atom/binary server name configured in the +client's `servers` map, or a list of names to try in order (first reachable wins). + +`Req` is the request to send, built with `eradius_req:new/1` and populated with +`eradius_req:set_attrs/2`. The `cmd` field must be a valid client command +(`request`, `accreq`, `coareq`, or `discreq`). + +`Opts` may contain: +- `retries` — number of retransmissions before giving up (default: 3) +- `timeout` — milliseconds to wait per attempt (default: 5000) +- `failover` — list of server pool names to try if the primary fails + +Returns `{{ok, Response}, Request}` on success or `{{error, Reason}, Request}` on failure. +`Reason` is `timeout` if all retries were exhausted, or `socket_down` if the socket closed. +""". -spec send_request(gen_server:server_ref(), eradius_client_mngr:server_name() | [eradius_client_mngr:server_pool()], eradius_req:req(), options()) -> diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index dfa4a87d..d8645274 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -2,20 +2,47 @@ %% Copyright (c) 2011, Travelping GmbH <info@travelping.com> %% %% SPDX-License-Identifier: MIT -%% -%% @doc This module contains the management logic for the RADIUS client instances. -%% A counter is kept for every client instance in order to determine the next request id and sender port -%% for each outgoing request. -%% -%% The client uses OS-assigned ports. The maximum number of open ports can be specified through the -%% ``client_ports'' option, it defaults to ``20''. The number of ports should not -%% be set too low. If ``N'' ports are opened, the maximum number of concurrent requests is ``N * 256''. -%% -%% The IP address used to send requests is configured through the ``ip'' option. -%% Changing it currently requires a restart. It can be given as a string or ip address tuple, -%% or the atom ``any'' (the default), which uses whatever address the OS selects. + -module(eradius_client_mngr). --feature(maybe_expr, enable). + +-moduledoc """ +RADIUS client instance manager. + +Manages a pool of UDP sockets and the state of configured RADIUS servers. +Each client manager is a `gen_server` supervised by the eradius application tree. +Requests are dispatched through `m:eradius_client`. + +== Configuration == + +A client is started with a `t:client_opts/0` map. The mandatory `servers` key maps +server names to either a `t:server_opts/0` map (a concrete server) or a +`t:server_pool/0` list of server names (a named pool for failover): + +``` +{ok, _} = eradius_client_mngr:start_client({local, my_client}, + #{family => inet, + ip => any, + servers => #{ + primary => #{ip => {10,0,0,1}, port => 1812, + secret => <<"secret1">>, retries => 3}, + secondary => #{ip => {10,0,0,2}, port => 1812, + secret => <<"secret2">>}, + auth_pool => [primary, secondary] + }}). +``` + +== Failure tracking == + +The manager tracks server failures. A server that fails to respond is marked as +unreachable for a configurable period (`unreachable_timeout` application env, default 2 s). +Pool-based failover in `m:eradius_client` automatically skips unreachable servers. + +== Socket pool == + +The client opens `no_ports` UDP sockets (default: 1) on OS-assigned ports. +Each socket supports up to 256 concurrent requests (one per RADIUS request id). +Increase `no_ports` for higher concurrency requirements. +""". -behaviour(gen_server). @@ -60,7 +87,7 @@ timeout := non_neg_integer(), failed := non_neg_integer()}. %% Options to describe a RADIUS server. -%% Conceptually the same as `t:server_opts/0', except that may fields are mandatory. +%% Conceptually the same as server_opts(), except that mandatory fields use :=. -type server_pool() :: [server_name()]. %% List of server names that form a pool. @@ -94,7 +121,7 @@ metrics_callback := 'undefined' | eradius_req:metrics_callback() }. %% Options to configure the RADIUS client. -%% Conceptually the same as `t:client_opts/0', except that may fields are mandatory. +%% Conceptually the same as client_opts(), except that mandatory fields use :=. -export_type([server_name/0, server_pool/0, servers/0, client_opts/0]). @@ -119,8 +146,10 @@ %%% API %%%========================================================================= -%% @doc Start a new RADIUS client that is managed by the eradius applications supervisor tree. -%% Returns the client manager pid (usable with eradius_client:send_request/3,4). +-doc """ +Start a new RADIUS client that is managed by the eradius applications supervisor tree. +Returns the client manager pid (usable with eradius_client:send_request/3,4). +""". -spec start_client(client_opts()) -> {ok, pid()} | {error, supervisor:startchild_err()}. start_client(Opts) -> @@ -131,7 +160,7 @@ start_client(Opts) -> Error end. -%% @doc Start a new, named RADIUS client that is managed by the eradius applications supervisor tree. +-doc "Start a new, named RADIUS client that is managed by the eradius applications supervisor tree.". -spec start_client(gen_server:server_name(), client_opts()) -> {ok, pid()} | {error, supervisor:startchild_err()}. start_client(ServerName, Opts) -> @@ -167,7 +196,13 @@ wanna_send(Server, Peer, Tried) -> request_failed(Server, Peer) -> gen_server:call(Server, {failed, Peer}). -%% @doc reconfigure the Radius client +-doc """ +Reconfigure a running RADIUS client manager. + +Merges `Opts` into the current configuration. The manager will update its server list +and socket pool accordingly, closing sockets that are no longer needed and opening new ones. +Waits up to 15 seconds for the reconfiguration to complete. +""". reconfigure(ServerRef, Opts) -> gen_server:call(ServerRef, {reconfigure, Opts}, ?RECONFIGURE_TIMEOUT). diff --git a/src/eradius_dict.erl b/src/eradius_dict.erl index 00847dc4..c5b9bf14 100644 --- a/src/eradius_dict.erl +++ b/src/eradius_dict.erl @@ -3,9 +3,10 @@ %% %% SPDX-License-Identifier: MIT %% -%% @doc Dictionary server -module(eradius_dict). +-moduledoc "Dictionary server". + -behaviour(gen_server). %% API diff --git a/src/eradius_eap_packet.erl b/src/eradius_eap_packet.erl index 85176d61..f49e63ef 100644 --- a/src/eradius_eap_packet.erl +++ b/src/eradius_eap_packet.erl @@ -1,7 +1,7 @@ -%% @doc -%% This module implements the EAP RFC-3748 message encoder and decoder functions -module(eradius_eap_packet). +-moduledoc "This module implements the EAP RFC-3748 message encoder and decoder functions". + -export([start/0, lookup_type/1, register_type/2, unregister_type/1]). -export([decode/1, encode/3, decode_eap_type/2, encode_eap_type/1]). @@ -20,18 +20,18 @@ start() -> gen_server:start({local, ?SERVER}, ?MODULE, [], []). -%% @doc lookup the handler module for an extended EAP type +-doc "lookup the handler module for an extended EAP type". lookup_type(Type) -> case ets:lookup(?SERVER, Type) of [{Type, Module}] -> {ok, Module}; _ -> false end. -%% @doc register the handler module for an extended EAP type +-doc "register the handler module for an extended EAP type". register_type(Type, Module) -> gen_server:call(?SERVER, {register, Type, Module}). -%% @doc unregister the handler module for an extended EAP type +-doc "unregister the handler module for an extended EAP type". unregister_type(Type) -> gen_server:call(?SERVER, {unregister, Type}). @@ -63,7 +63,7 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. %% ------------------------------------------------------------------------------------------ %% -- decoder functions -%% @doc decode a EPA message +-doc "decode a EPA message". decode(<<Code:8, Id:8, Len:16, Rest/binary>>) -> DataLen = Len - 4, case Rest of @@ -73,7 +73,7 @@ decode(<<Code:8, Id:8, Len:16, Rest/binary>>) -> {error, invalid_length} end. -%% @doc endecode a EPA message +-doc "endecode a EPA message". encode(Code, Id, Msg) -> Data = encode_payload(Code, Msg), Len = size(Data) + 4, @@ -97,7 +97,7 @@ decode_payload(Code, Id, Data) _ -> {error, invalid_length} end. -%% @doc EAP decoder functions for RFC-3784 types +-doc "EAP decoder functions for RFC-3784 types". %% 1 Identity decode_eap_type(1, Data) -> @@ -149,7 +149,7 @@ encode_payload(Code, _Msg) when Code == success; Code == failure -> <<>>. -%% @doc EAP encoder functions for RFC-3784 types +-doc "EAP encoder functions for RFC-3784 types". %% 1 Identity encode_eap_type({identity, Data}) diff --git a/src/eradius_lib.erl b/src/eradius_lib.erl index 9840b341..c1bfeb16 100644 --- a/src/eradius_lib.erl +++ b/src/eradius_lib.erl @@ -20,10 +20,12 @@ %% ------------------------------------------------------------------------------------------ %% -- Request Accessors -%% @doc pad binary to specific length -%% See <a href="http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html"> -%% http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html -%% </a> +-doc """ +pad binary to specific length + See <a href="http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html"> + http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html + </a> +""". -compile({inline, pad_to/2}). pad_to(Width, Binary) -> case (Width - byte_size(Binary) rem Width) rem Width of diff --git a/src/eradius_log.erl b/src/eradius_log.erl index 21b19c11..93c46b02 100644 --- a/src/eradius_log.erl +++ b/src/eradius_log.erl @@ -20,26 +20,30 @@ %%% API %%%=================================================================== -%% @doc copy RADIUS AVPs into `m:logger' metadata. -%% -%% Helper function for use in a RADIUS server handler that will -%% copy all RADIUS attributes into `m:logger' metadata. -%% -%% <blockquote><h4 class="warning">WARNING</h4> -%% The function will format all attribute values to strings, -%% the resulting processing load can be significant. -%% </blockquote> +-doc """ +copy RADIUS AVPs into `m:logger` metadata. + +Helper function for use in a RADIUS server handler that will +copy all RADIUS attributes into `m:logger` metadata. + +<blockquote><h4 class="warning">WARNING</h4> +The function will format all attribute values to strings, +the resulting processing load can be significant. +</blockquote> +""". -spec update_logger_process_metadata(eradius_req:req()) -> ok. update_logger_process_metadata(Req) -> Metadata = maps:from_list(collect_meta(Req)), logger:update_process_metadata(Metadata). -%% @doc Serialize `t:eradius_req:req/0' object into a proplist. -%% -%% Helper function for use in a RADIUS server handler that will -%% serialize a RADIUS `t:eradius_req:req/0' object into a Key/Value lists. -%% -%% All values will be converted to human readable strings. +-doc """ +Serialize `t:eradius_req:req/0` object into a proplist. + +Helper function for use in a RADIUS server handler that will +serialize a RADIUS `t:eradius_req:req/0` object into a Key/Value lists. + +All values will be converted to human readable strings. +""". -spec collect_meta(eradius_req:req()) -> [{term(), term()}]. collect_meta(#{cmd := Cmd, req_id := ReqId, attrs := Attrs}) -> RequestType = binary_to_list(format_cmd(Cmd)), @@ -48,27 +52,31 @@ collect_meta(#{cmd := Cmd, req_id := ReqId, attrs := Attrs}) -> {request_id, RequestId}| [collect_attr(Key, Val) || {Key, Val} <- Attrs]]. -%% @doc Format `t:eradius_req:req/0' object into a RADIUS short log entry -%% -%% The short log format is not part of any RADIUS RFC, but has been -%% used by many RADIUS server implementations. -%% -%% The format is: `<Client-IP>:<Client-Port> [<Request-Id>]: <Command> [AcctStatusType]' +-doc """ +Format `t:eradius_req:req/0` object into a RADIUS short log entry + +The short log format is not part of any RADIUS RFC, but has been +used by many RADIUS server implementations. + +The format is: `<Client-IP>:<Client-Port> [<Request-Id>]: <Command> [AcctStatusType]` +""". -spec line(eradius_req:req()) -> iolist(). line(#{cmd := Cmd, req_id := ReqId, server_addr := {IP, Port}} = Req) -> StatusType = format_acct_status_type(Req), io_lib:format("~s:~p [~p]: ~s ~s", [inet:ntoa(IP), Port, ReqId, format_cmd(Cmd), StatusType]). -%% @doc Format `t:eradius_req:req/0' object into a RADIUS log entry -%% -%% The long log format is not part of any RADIUS RFC, but has been -%% used by many RADIUS server implementations in the past. -%% -%% The format is: -%% ``` -%% <TimeStamp> <Client-IP>:<Client-Port> [<Request-Id>] <Command> -%% [<Key> = <Value>]+ -%% ''' +-doc """ +Format `t:eradius_req:req/0` object into a RADIUS log entry + +The long log format is not part of any RADIUS RFC, but has been +used by many RADIUS server implementations in the past. + +The format is: +``` +<TimeStamp> <Client-IP>:<Client-Port> [<Request-Id>] <Command> + [<Key> = <Value>]+ +``` +""". -spec format_req(eradius_req:req()) -> binary(). format_req(Req) -> Time = diff --git a/src/eradius_metrics_prometheus.erl b/src/eradius_metrics_prometheus.erl index a1dee4a7..ac3e7fa4 100644 --- a/src/eradius_metrics_prometheus.erl +++ b/src/eradius_metrics_prometheus.erl @@ -2,9 +2,10 @@ %% %% SPDX-License-Identifier: MIT %% -%% @doc Provides metrics callbacks for recording metrics with prometheus.erl -module(eradius_metrics_prometheus). +-moduledoc "Provides metrics callbacks for recording metrics with prometheus.erl". + -export([init/1, reset/0]). -export([client_metrics_callback/3, server_metrics_callback/3]). @@ -27,7 +28,7 @@ %%% Setup %%%========================================================================= -%% @doc Initialize the prometheus metrics +-doc "Initialize the prometheus metrics". -spec init(#{histogram_buckets => [pos_integer()], client_metrics => boolean(), server_metrics => boolean()}) -> ok. @@ -307,8 +308,10 @@ init_server_metrics(_Config) -> %%% Metrics Handler %%%========================================================================= -%% @doc Function for use as `t:eradius_req:metrics_callback/0' for a `t:eradius_req:req/0' -%% object in a RADIUS client to record prometheus metrics +-doc """ +Function for use as `t:eradius_req:metrics_callback/0` for a `t:eradius_req:req/0` +object in a RADIUS client to record prometheus metrics +""". -spec client_metrics_callback(Event :: eradius_req:metrics_event(), MetaData :: term(), Req :: eradius_req:req()) -> eradius_req:req(). @@ -410,8 +413,10 @@ client_reply_metrics(MetaData, Labels, end, client_request_duration(MetaData, Labels, Req). -%% @doc Function for use as `t:eradius_req:metrics_callback/0' for a `t:eradius_req:req/0' -%% object in a RADIUS server to record prometheus metrics +-doc """ +Function for use as `t:eradius_req:metrics_callback/0` for a `t:eradius_req:req/0` +object in a RADIUS server to record prometheus metrics +""". -spec server_metrics_callback(Event :: eradius_req:metrics_event(), MetaData :: term(), Req :: eradius_req:req()) -> eradius_req:req(). diff --git a/src/eradius_req.erl b/src/eradius_req.erl index 0fe97a3e..40a14b90 100644 --- a/src/eradius_req.erl +++ b/src/eradius_req.erl @@ -4,6 +4,49 @@ -module(eradius_req). +-moduledoc """ +Central RADIUS request/response type and encoding module. + +This module defines the `t:req/0` map type that represents a RADIUS packet +throughout its lifecycle — from construction through wire encoding and response decoding. + +== Request lifecycle == + +A typical outgoing (client-side) request goes through these steps: + +1. Create a request: `new/1` or `new/2` +2. Add attributes: `add_attr/3`, `set_attr/3`, or `set_attrs/2` +3. Encode to wire format: `packet/1` +4. Send over the network (handled by `m:eradius_client`) +5. Receive the raw response +6. Decode: `response/3` +7. Read the response command and attributes: `cmd/1`, `attrs/1` + +A server-side incoming request uses `request/4` to decode the wire data into a +`t:req/0`, then the handler module receives it via `radius_request/2` +(see `m:eradius_server`). The handler builds a response by updating the request map +and returning it; `packet/1` encodes it for sending. + +== Request map fields == + +The `t:req/0` type is an open map. Public fields that handlers and clients may read: + +<dl> + <dt>`cmd`</dt><dd>The RADIUS command atom (e.g. `request`, `accept`, `reject`).</dd> + <dt>`is_valid`</dt><dd>Validation state: `undefined`, `true`, or `false`.</dd> + <dt>`req_id`</dt><dd>RADIUS request identifier byte.</dd> + <dt>`authenticator`</dt><dd>16-byte authenticator field from the packet header.</dd> + <dt>`request_authenticator`</dt><dd>The original request authenticator (server-side).</dd> + <dt>`client`</dt><dd>Client name (server-side, populated from the `clients` config).</dd> + <dt>`client_addr`</dt><dd>`{IP, Port}` of the client (server-side).</dd> + <dt>`server`</dt><dd>Server name atom (server-side).</dd> + <dt>`server_addr`</dt><dd>`{IP, Port}` the server is listening on.</dd> +</dl> + +Private fields (`body`, `head`, `socket`, `arrival_time`) are used internally and +should not be accessed directly by application code. +""". + -export([is_valid/1, req_id/1, cmd/1, @@ -112,45 +155,57 @@ %%% API %%%========================================================================= -%% @doc Return validation state of the request. -%% -%% - `true' for a requests if has been encoded to binary form, -%% -%% - `true' for a response if has been decoded from binary form -%% and the authenticator has been validate, -%% -%% - `true' for a response if has been decoded from binary form -%% and the authenticator failed to validate, -%% -%% - `undefined' otherwise -%% @end +-doc """ +Return validation state of the request. + +- `true` for a requests if has been encoded to binary form, + +- `true` for a response if has been decoded from binary form + and the authenticator has been validate, + +- `true` for a response if has been decoded from binary form + and the authenticator failed to validate, + +- `undefined` otherwise +""". -spec is_valid(req()) -> true | false | undefined. is_valid(#{is_valid := IsValid}) -> IsValid. +-doc "Return the RADIUS request identifier, or `undefined` if not yet assigned.". -spec req_id(req()) -> byte() | undefined. req_id(#{req_id := ReqId}) -> ReqId; req_id(_) -> undefined. +-doc "Return the RADIUS command of the request or response.". -spec cmd(req()) -> command(). cmd(#{cmd := Cmd}) -> Cmd. +-doc "Return the authenticator field from the packet header, or `undefined` if not set.". -spec authenticator(req()) -> authenticator() | undefined. authenticator(#{authenticator := Authenticator}) -> Authenticator; authenticator(_) -> undefined. +-doc """ +Return the original request authenticator (server-side), or `undefined` if not available. + +On the server side, this holds the authenticator from the incoming request packet, which +is needed to validate and construct the response authenticator. +""". -spec request_authenticator(req()) -> authenticator() | undefined. request_authenticator(#{authenticator := Authenticator}) -> Authenticator; request_authenticator(_) -> undefined. +-doc "Return the Message-Authenticator HMAC flag or value, or `undefined` if not set.". -spec msg_hmac(req()) -> boolean() | undefined. msg_hmac(#{msg_hmac := MsgHMAC}) -> MsgHMAC; msg_hmac(_) -> undefined. +-doc "Return the EAP-Message payload, or `undefined` if not present.". -spec eap_msg(req()) -> binary() | undefined. eap_msg(#{eap_msg := EAPmsg}) -> EAPmsg; eap_msg(_) -> undefined. -%% @doc Convert a RADIUS request to the wire format. +-doc "Convert a RADIUS request to the wire format.". -spec packet(req()) -> {binary(), req()} | no_return(). packet(#{req_id := _, cmd := _, authenticator := _, body := Body, secret := _} = Req) when is_binary(Body) -> @@ -172,6 +227,15 @@ packet(#{req_id := _, cmd := Cmd, secret := _, attrs := Attrs, eap_msg := EAPmsg packet(Req) -> erlang:error(badarg, [Req]). +-doc """ +Decode and return the attribute list from the request. + +If the attributes have already been decoded, returns them directly. Otherwise, +decodes the raw binary body. The updated request (with decoded attrs cached) +is returned as the second element of the tuple. + +Raises `{bad_pdu, decoder_error}` if the body cannot be decoded. +""". -spec attrs(req()) -> {attribute_list(), req()} | no_return(). attrs(#{attrs := Attrs, is_valid := IsValid} = Req) when is_list(Attrs), IsValid =/= false -> @@ -188,6 +252,12 @@ attrs(#{body := Body, secret := _} = Req0) attrs(Req) -> erlang:error(badarg, [Req]). +-doc """ +Return the value of a single attribute by id or attribute record, or `undefined`. + +Requires the request to have a decoded attribute list (i.e. `is_valid` is not `false`). +Returns `undefined` if the attribute is not present. +""". attr(Id, #{attrs := Attrs, is_valid := IsValid}) when is_list(Attrs), IsValid =/= false -> get_attr(Id, Attrs); @@ -204,10 +274,19 @@ get_attr(Id, [Head|Tail]) -> end. +-doc "Create a new RADIUS request with the given command. Equivalent to `new(Command, undefined)`.". -spec new(command()) -> req(). new(Command) -> new(Command, undefined). +-doc """ +Create a new RADIUS request with the given command and metrics callback. + +`Command` is one of the RADIUS command atoms: `request`, `accreq`, `coareq`, `discreq`, +`accept`, `reject`, `challenge`, `accresp`, `coaack`, `coanak`, `discack`, `discnak`. + +`MetricsCallback` may be `undefined` or a 3-arity fun — see `t:metrics_callback/0`. +""". -spec new(command(), 'undefined' | metrics_callback()) -> req(). new(Command, MetricsCallback) when MetricsCallback =:= undefined; is_function(MetricsCallback, 3) -> @@ -222,6 +301,15 @@ new(Command, MetricsCallback) metrics_callback => MetricsCallback }. +-doc """ +Decode an incoming RADIUS request from wire format (server-side). + +Parses the 20-byte `Header` and `Body` received from a UDP socket, populates the +`t:req/0` map with the decoded command, request id, authenticator, shared secret +and client identity from the `NAS` client config map. + +Raises on malformed input (bad command code or truncated header). +""". -spec request(binary(), binary(), eradius_server:client(), 'undefined' | metrics_callback()) -> req() | no_return(). request(<<Cmd, ReqId, Len:16, Authenticator:16/bytes>> = Header, Body, @@ -233,6 +321,18 @@ request(<<Cmd, ReqId, Len:16, Authenticator:16/bytes>> = Header, Body, Req#{req_id => ReqId, request_authenticator => Authenticator, client => ClientId, secret => Secret}). +-doc """ +Decode a RADIUS response from wire format, or build a response from a command and attrs. + +Two forms: + +- `response(Header, Body, Req)` — decodes the 20-byte `Header` and `Body` received from + a UDP socket as a reply to `Req`. Validates that the response `req_id` matches the + original request. Returns an updated `t:req/0` with the response command and payload. + +- `response(Command, Attrs, Req)` — sets the command and attribute list directly without + encoding to binary. Useful for building test responses or intermediate server-side state. +""". -spec response(binary(), binary(), req()) -> req() | no_return(); (command(), undefined | attribute_list(), req()) -> req(). response(<<Cmd, ReqId, Len:16, Authenticator:16/bytes>> = Header, Body, @@ -243,40 +343,85 @@ response(<<Cmd, ReqId, Len:16, Authenticator:16/bytes>> = Header, Body, response(Response, Attrs, Req) when is_atom(Response) -> Req#{cmd := Response, body := undefined, attrs := Attrs, is_valid := undefined}. +-doc """ +Set the shared secret on the request. + +Resets `is_valid` to `undefined` since the encoded form is now stale. +""". -spec set_secret(req(), secret()) -> req(). set_secret(Req, Secret) -> Req#{secret => Secret, is_valid := undefined}. +-doc """ +Set the raw binary body of the request, replacing any decoded attribute list. + +Resets `attrs` to `undefined` and `is_valid` to `undefined`. +""". -spec set_body(req(), binary()) -> req(). set_body(Req, Body) when is_binary(Body) -> Req#{body := Body, attrs := undefined, is_valid := undefined}. +-doc """ +Replace the attribute list of the request. + +Clears the encoded body and resets `is_valid` to `undefined`. +""". -spec set_attrs(attribute_list(), req()) -> req(). set_attrs(Attrs, Req) when is_list(Attrs) -> Req#{body := undefined, attrs := Attrs, is_valid := undefined}. +-doc """ +Prepend an attribute to the attribute list. + +Does not check for duplicates — use `set_attr/3` if uniqueness is required. +""". add_attr(Id, Value, #{attrs := Attrs} = Req) when is_list(Attrs) -> Req#{attrs := [{Id, Value} | Attrs], is_valid := undefined}. +-doc "Set an attribute, replacing any existing entry with the same id.". set_attr(Id, Value, #{attrs := Attrs} = Req) when is_list(Attrs) -> Req#{attrs := lists:keystore(Id, 1, Attrs, {Id, Value}), is_valid := undefined}. +-doc """ +Enable or disable the Message-Authenticator HMAC attribute. + +When set to `true`, `packet/1` will append a Message-Authenticator attribute +to the encoded packet. +""". -spec set_msg_hmac(boolean(), req()) -> req(). set_msg_hmac(MsgHMAC, Req) when is_boolean(MsgHMAC) -> Req#{msg_hmac => MsgHMAC}. +-doc """ +Set the EAP-Message payload. + +The binary is automatically split into EAP-Message attributes during encoding by `packet/1`. +Clears the encoded body. +""". -spec set_eap_msg(binary(), req()) -> req(). set_eap_msg(EAPmsg, Req) when is_binary(EAPmsg) -> Req#{body := undefined, eap_msg := EAPmsg}. +-doc """ +Replace the metrics callback on the request. + +Pass `undefined` to disable metrics recording. The callback is a 3-arity fun; +see `t:metrics_callback/0`. +""". -spec set_metrics_callback(undefined | metrics_callback(), req()) -> req(). set_metrics_callback(MetricsCallback, Req) -> Req#{metrics_callback => MetricsCallback}. +-doc """ +Invoke the metrics callback without a request context. + +Used for server-level events that are not associated with a specific `t:req/0`. +Returns `undefined` if no callback is set. +""". -spec metrics_callback(Cb :: undefined | eradius_req:metrics_callback(), Event :: metrics_event(), MetaData :: term()) -> any(). metrics_callback(Cb, Event, MetaData) when is_function(Cb, 3) -> @@ -284,6 +429,13 @@ metrics_callback(Cb, Event, MetaData) metrics_callback(_, _, _) -> undefined. +-doc """ +Record a metrics event on the request, returning the updated request. + +If the request has a `t:metrics_callback/0` set, calls it with `Event`, `MetaData`, +and the request. The callback may update and return a modified request. +If no callback is set, returns the request unchanged. +""". -spec record_metric(Event :: metrics_event(), MetaData :: term(), Req :: req()) -> req(). record_metric(Event, MetaData, #{metrics_callback := Cb} = Req) when is_function(Cb, 3) -> @@ -719,10 +871,12 @@ ascend(SharedSecret, RequestAuthenticator, <<PlainText/binary>>) -> Digest = crypto:hash(md5, [RequestAuthenticator, SharedSecret]), crypto:exor(Digest, pad_to(16, PlainText)). -%% @doc pad binary to specific length -%% See <a href="http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html"> -%% http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html -%% </a> +-doc """ +pad binary to specific length + See <a href="http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html"> + http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html + </a> +""". -compile({inline, pad_to/2}). pad_to(Width, Binary) -> case (Width - byte_size(Binary) rem Width) rem Width of @@ -730,7 +884,7 @@ pad_to(Width, Binary) -> N -> <<Binary/binary, 0:(N*8)>> end. -%% @doc calculate the MD5 message authenticator +-doc "calculate the MD5 message authenticator". -if(?OTP_RELEASE >= 23). %% crypto API changes in OTP >= 23 message_authenticator(Secret, Msg) -> diff --git a/src/eradius_server.erl b/src/eradius_server.erl index 6adfc3ac..f27c1f63 100644 --- a/src/eradius_server.erl +++ b/src/eradius_server.erl @@ -2,9 +2,57 @@ %% Copyright (c) 2011, Travelping GmbH <info@travelping.com> %% %% SPDX-License-Identifier: MIT -%% + -module(eradius_server). --feature(maybe_expr, enable). + +-moduledoc """ +RADIUS server behaviour and instance management. + +This module implements a UDP-based RADIUS server and defines the `eradius_server` +behaviour that application handler modules must implement. + +== Handler behaviour == + +A handler module implements the `radius_request/2` callback: + +``` +-module(my_handler). +-behaviour(eradius_server). + +radius_request(#{cmd := request} = Req, _HandlerData) -> + %% Build response by updating the request map + Resp = eradius_req:set_attrs([{?User_Name, <<"accepted">>}], Req#{cmd := accept}), + {reply, Resp}. +``` + +The handler receives the decoded `t:eradius_req:req/0` map and returns: +- `{reply, Resp}` — send the response packet to the client +- `noreply` — do not respond (request is silently discarded) +- `{error, timeout}` — discard with a timeout reason logged + +== Client authentication == + +Incoming requests are authenticated by matching the source IP against the `clients` +map in `t:server_opts/0`. Each entry maps a client IP to a `t:client/0` map containing +the shared secret. Packets from unlisted IPs are silently discarded. + +== Starting a server == + +Servers are typically started via `m:eradius`: + +``` +{ok, Pid} = eradius:start_server({127,0,0,1}, 1812, + #{handler => {my_handler, []}, + clients => #{{127,0,0,1} => #{secret => <<"mysecret">>, + client => <<"my-nas">>}}}). +``` + +Or directly for named instances: + +``` +{ok, Pid} = eradius:start_server({local, my_server}, {127,0,0,1}, 1812, Opts). +``` +""". -behaviour(gen_server). @@ -60,7 +108,7 @@ sndbuf => non_neg_integer() }. %% Options to configure the RADIUS server UDP socket. -%% Conceptually the same as `t:socket_opts/0', except that may fields are mandatory. +%% Conceptually the same as socket_opts(), except that mandatory fields use :=. -type server_opts() :: #{server_name => term(), socket_opts => socket_opts(), @@ -75,13 +123,13 @@ metrics_callback := undefined | eradius_req:metrics_callback(), clients := map()}. %% Options to configure the RADIUS server. -%% Conceptually the same as `t:server_opts/0', except that may fields are mandatory. +%% Conceptually the same as server_opts(), except that mandatory fields use :=. -type client() :: #{client := binary(), secret := eradius_req:secret()}. %% RADIUS client settings --export_type([server_name/0, client/0]). +-export_type([server_name/0, client/0, server_opts/0, socket_opts/0]). -record(state, { name :: atom(), % server name @@ -95,6 +143,18 @@ clients :: #{inet:ip_address() => client()} }). +-doc """ +Handle an incoming RADIUS request. + +Called by the server for each valid incoming packet. `Req` is the decoded +request map. `HandlerData` is the second element of the `{Module, HandlerData}` +tuple from the `handler` key in `t:server_opts/0`. + +Return values: +- `{reply, Resp}` — encode `Resp` and send it back to the client +- `noreply` — do not send a response +- `{error, timeout}` — discard the request with a timeout reason +""". -callback radius_request(eradius_req:req(), HandlerData :: term()) -> {reply, eradius_req:req()} | noreply | {error, timeout}. @@ -102,6 +162,17 @@ %%% API %%%========================================================================= +-doc """ +Start a RADIUS server instance under the eradius supervision tree. + +`IP` is the address to bind to (`any` binds to all interfaces). +`Port` is the UDP port number. +`Opts` must include at minimum `handler` and `clients`; see `t:server_opts/0`. + +Returns the pid of the started server process. + +See `eradius:start_server/3`. +""". -spec start_instance(IP :: 'any' | inet:ip_address(), Port :: inet:port_number(), Opts :: server_opts()) -> gen_server:start_ret(). start_instance(IP, Port, Opts) @@ -109,6 +180,14 @@ start_instance(IP, Port, Opts) is_integer(Port) andalso Port >= 0 andalso Port < 65536 -> eradius_server_sup:start_instance([IP, Port, Opts]). +-doc """ +Start a named RADIUS server instance under the eradius supervision tree. + +Same as `start_instance/3` but registers the server process under `ServerName` +(e.g. `{local, my_server}`). + +See `eradius:start_server/4`. +""". -spec start_instance(ServerName :: gen_server:server_name(), IP :: 'any' | inet:ip_address(), Port :: inet:port_number(), Opts :: server_opts()) -> gen_server:start_ret(). @@ -117,6 +196,12 @@ start_instance(ServerName, IP, Port, Opts) is_integer(Port) andalso Port >= 0 andalso Port < 65536 -> eradius_server_sup:start_instance([ServerName, IP, Port, Opts]). +-doc """ +Stop a running RADIUS server instance. + +Sends a stop request to the server process and waits for it to terminate. +Returns `ok` even if the process is already stopped. +""". -spec stop_instance(Pid :: pid()) -> ok. stop_instance(Pid) -> try gen_server:call(Pid, stop) diff --git a/src/eradius_server_sup.erl b/src/eradius_server_sup.erl index 624e1f77..61a6655b 100644 --- a/src/eradius_server_sup.erl +++ b/src/eradius_server_sup.erl @@ -1,6 +1,7 @@ %% @private -%% @doc Supervisor for RADIUS server processes. -module(eradius_server_sup). + +-moduledoc "Supervisor for RADIUS server processes.". -behaviour(supervisor). -export([start_link/0, start_instance/1, all/0]). From 0af5c615e47606ddf7583ebcff2ed0bf7ba9f157 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 1 Apr 2026 16:23:12 +0200 Subject: [PATCH 18/38] Apply rebar3 fmt formatting fixes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- rebar.config | 10 +++++----- src/eradius_auth.erl | 2 +- src/eradius_client.erl | 14 +++++++------- src/eradius_client_mngr.erl | 18 +++++++++--------- src/eradius_lib.erl | 6 +++--- src/eradius_log.erl | 2 +- src/eradius_req.erl | 2 +- src/eradius_server.erl | 6 +++--- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/rebar.config b/rebar.config index 0657d0e3..e258ae03 100644 --- a/rebar.config +++ b/rebar.config @@ -48,11 +48,11 @@ {main, "README.md"}, {source_url, "https://github.com/travelping/eradius"}, {groups_for_modules, [ - {"Server", [eradius, eradius_server]}, - {"Client", [eradius_client, eradius_client_mngr]}, - {"Request/Response", [eradius_req]}, - {"Dictionaries", [eradius_dict]} - ]} + {"Server", [eradius, eradius_server]}, + {"Client", [eradius_client, eradius_client_mngr]}, + {"Request/Response", [eradius_req]}, + {"Dictionaries", [eradius_dict]} + ]} ]}. %% == Hex == diff --git a/src/eradius_auth.erl b/src/eradius_auth.erl index a74f7805..3f44b83c 100644 --- a/src/eradius_auth.erl +++ b/src/eradius_auth.erl @@ -39,7 +39,7 @@ -doc """ check the request password using all available authentication mechanisms. - Tries CHAP, then MS-CHAP, then MS-CHAPv2, finally PAP. +Tries CHAP, then MS-CHAP, then MS-CHAPv2, finally PAP. """. -spec check_password(binary(), eradius_req:req()) -> false | {boolean(), eradius_req:attribute_list()}. diff --git a/src/eradius_client.erl b/src/eradius_client.erl index a9c1d336..4403f001 100644 --- a/src/eradius_client.erl +++ b/src/eradius_client.erl @@ -16,12 +16,12 @@ a client manager started with `eradius_client_mngr:start_client/1,2`. ``` %% Start a named client manager first {ok, _} = eradius_client_mngr:start_client({local, my_client}, - #{family => inet, - ip => any, - servers => #{ - auth => #{ip => {192,168,1,1}, port => 1812, - secret => <<"mysecret">>, retries => 3} - }}), + #{family => inet, + ip => any, + servers => #{ + auth => #{ip => {192,168,1,1}, port => 1812, + secret => <<"mysecret">>, retries => 3} + }}), %% Send an access request Req = eradius_req:set_attrs([{?User_Name, <<"alice">>}], eradius_req:new(request)), @@ -101,7 +101,7 @@ client's `servers` map, or a list of names to try in order (first reachable wins - `timeout` — milliseconds to wait per attempt (default: 5000) - `failover` — list of server pool names to try if the primary fails -Returns `{{ok, Response}, Request}` on success or `{{error, Reason}, Request}` on failure. + Returns `{{ok, Response}, Request}` on success or `{{error, Reason}, Request}` on failure. `Reason` is `timeout` if all retries were exhausted, or `socket_down` if the socket closed. """. -spec send_request(gen_server:server_ref(), diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index d8645274..f3b9a470 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -20,15 +20,15 @@ server names to either a `t:server_opts/0` map (a concrete server) or a ``` {ok, _} = eradius_client_mngr:start_client({local, my_client}, - #{family => inet, - ip => any, - servers => #{ - primary => #{ip => {10,0,0,1}, port => 1812, - secret => <<"secret1">>, retries => 3}, - secondary => #{ip => {10,0,0,2}, port => 1812, - secret => <<"secret2">>}, - auth_pool => [primary, secondary] - }}). + #{family => inet, + ip => any, + servers => #{ + primary => #{ip => {10,0,0,1}, port => 1812, + secret => <<"secret1">>, retries => 3}, + secondary => #{ip => {10,0,0,2}, port => 1812, + secret => <<"secret2">>}, + auth_pool => [primary, secondary] + }}). ``` == Failure tracking == diff --git a/src/eradius_lib.erl b/src/eradius_lib.erl index c1bfeb16..b5ecfbbc 100644 --- a/src/eradius_lib.erl +++ b/src/eradius_lib.erl @@ -22,9 +22,9 @@ -doc """ pad binary to specific length - See <a href="http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html"> - http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html - </a> +See <a href="http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html"> +http://www.erlang.org/pipermail/erlang-questions/2008-December/040709.html +</a> """. -compile({inline, pad_to/2}). pad_to(Width, Binary) -> diff --git a/src/eradius_log.erl b/src/eradius_log.erl index 93c46b02..bb2c82ca 100644 --- a/src/eradius_log.erl +++ b/src/eradius_log.erl @@ -74,7 +74,7 @@ used by many RADIUS server implementations in the past. The format is: ``` <TimeStamp> <Client-IP>:<Client-Port> [<Request-Id>] <Command> - [<Key> = <Value>]+ +[<Key> = <Value>]+ ``` """. -spec format_req(eradius_req:req()) -> binary(). diff --git a/src/eradius_req.erl b/src/eradius_req.erl index 40a14b90..c6394a1d 100644 --- a/src/eradius_req.erl +++ b/src/eradius_req.erl @@ -32,7 +32,7 @@ and returning it; `packet/1` encodes it for sending. The `t:req/0` type is an open map. Public fields that handlers and clients may read: <dl> - <dt>`cmd`</dt><dd>The RADIUS command atom (e.g. `request`, `accept`, `reject`).</dd> +<dt>`cmd`</dt><dd>The RADIUS command atom (e.g. `request`, `accept`, `reject`).</dd> <dt>`is_valid`</dt><dd>Validation state: `undefined`, `true`, or `false`.</dd> <dt>`req_id`</dt><dd>RADIUS request identifier byte.</dd> <dt>`authenticator`</dt><dd>16-byte authenticator field from the packet header.</dd> diff --git a/src/eradius_server.erl b/src/eradius_server.erl index f27c1f63..0ca1d722 100644 --- a/src/eradius_server.erl +++ b/src/eradius_server.erl @@ -42,9 +42,9 @@ Servers are typically started via `m:eradius`: ``` {ok, Pid} = eradius:start_server({127,0,0,1}, 1812, - #{handler => {my_handler, []}, - clients => #{{127,0,0,1} => #{secret => <<"mysecret">>, - client => <<"my-nas">>}}}). + #{handler => {my_handler, []}, + clients => #{{127,0,0,1} => #{secret => <<"mysecret">>, + client => <<"my-nas">>}}}). ``` Or directly for named instances: From 2e661cc4d88a57e7a0ba8fc363133786be548f09 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 1 Apr 2026 16:32:20 +0200 Subject: [PATCH 19/38] Update meck to 1.1.1 and prometheus to 6.1.2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- rebar.config | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rebar.config b/rebar.config index e258ae03..9d106135 100644 --- a/rebar.config +++ b/rebar.config @@ -6,11 +6,11 @@ {clean, "escript dicts_compiler.erl clean"}]}. {profiles, [ - {dialyzer, [{deps, [{prometheus, "4.11.0"}]}]}, + {dialyzer, [{deps, [{prometheus, "6.1.2"}]}]}, {test, [ {erl_opts, [nowarn_export_all]}, - {deps, [{meck, "0.9.2"}, - {prometheus, "4.11.0"}]} + {deps, [{meck, "1.1.1"}, + {prometheus, "6.1.2"}]} ]} ]}. From aaf1bd7926d2342f1756283c0757faf28cb357f2 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 1 Apr 2026 16:45:11 +0200 Subject: [PATCH 20/38] Fix prometheus 6.x label key format in metrics test suite prometheus 6.x returns label values as proplists with atom keys (e.g. {server_name, good}) instead of string keys ("server_name", good) used in 4.x. Update all check_metric/check_metric_multi calls to use atom-keyed label filters. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- test/eradius_metrics_SUITE.erl | 50 +++++++++++++++++----------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/test/eradius_metrics_SUITE.erl b/test/eradius_metrics_SUITE.erl index 834b771e..28455667 100644 --- a/test/eradius_metrics_SUITE.erl +++ b/test/eradius_metrics_SUITE.erl @@ -140,7 +140,7 @@ error_requests(_Config) -> request_with_attrs_as_record(_Config) -> ok = send_request(good, accreq, ?ATTRS_AS_RECORD, #{server_name => good, client_name => test_records}), - check_metric(accreq, eradius_client_accounting_requests_total, [{"server_name", good}, {"acct_type", start}], 1). + check_metric(accreq, eradius_client_accounting_requests_total, [{server_name, good}, {acct_type, start}], 1). %% helpers check_single_request(good, EradiusRequestType, _RequestType, _ResponseType) -> @@ -151,46 +151,46 @@ check_single_request(good, EradiusRequestType, _RequestType, _ResponseType) -> ERadM = re:run(Metrics, "^eradius.*", [multiline, global, {capture, all, binary}]), ct:pal("Metrics:~n~p~n", [ERadM]), - check_metric(eradius_client_access_requests_total, [{"server_name", good}], 1), - check_metric_multi(EradiusRequestType, eradius_client_accounting_requests_total, [{"server_name", good}], 1), - check_metric_multi({bad_type, EradiusRequestType}, eradius_client_accounting_requests_total, [{"server_name", good}, {"acct_type", bad_type}], 0), - check_metric(EradiusRequestType, eradius_client_accounting_requests_total, [{"server_name", good}, {"acct_type", start}], 1), - check_metric(EradiusRequestType, eradius_client_accounting_requests_total, [{"server_name", good}, {"acct_type", stop}], 0), - check_metric(EradiusRequestType, eradius_client_accounting_requests_total, [{"server_name", good}, {"acct_type", update}], 0), - check_metric(eradius_client_accept_responses_total, [{"server_name", good}], 1), - check_metric(eradius_accept_responses_total, [{"server_name", good}], 1), - check_metric(eradius_access_requests_total, [{"server_name", good}], 1), + check_metric(eradius_client_access_requests_total, [{server_name, good}], 1), + check_metric_multi(EradiusRequestType, eradius_client_accounting_requests_total, [{server_name, good}], 1), + check_metric_multi({bad_type, EradiusRequestType}, eradius_client_accounting_requests_total, [{server_name, good}, {acct_type, bad_type}], 0), + check_metric(EradiusRequestType, eradius_client_accounting_requests_total, [{server_name, good}, {acct_type, start}], 1), + check_metric(EradiusRequestType, eradius_client_accounting_requests_total, [{server_name, good}, {acct_type, stop}], 0), + check_metric(EradiusRequestType, eradius_client_accounting_requests_total, [{server_name, good}, {acct_type, update}], 0), + check_metric(eradius_client_accept_responses_total, [{server_name, good}], 1), + check_metric(eradius_accept_responses_total, [{server_name, good}], 1), + check_metric(eradius_access_requests_total, [{server_name, good}], 1), check_metric(eradius_server_status, true, [eradius_test_lib:localhost(ipv4, native), 1812]); check_single_request(bad, EradiusRequestType, _RequestType, _ResponseType) -> ok = send_request(bad, EradiusRequestType, ?ATTRS_BAD, #{server_name => bad, client_name => test}), - check_metric(eradius_client_access_requests_total, [{"server_name", bad}], 1), - check_metric(eradius_client_reject_responses_total, [{"server_name", bad}], 1), - check_metric(eradius_access_requests_total, [{"server_name", bad}], 1), - check_metric(eradius_reject_responses_total, [{"server_name", bad}], 1), + check_metric(eradius_client_access_requests_total, [{server_name, bad}], 1), + check_metric(eradius_client_reject_responses_total, [{server_name, bad}], 1), + check_metric(eradius_access_requests_total, [{server_name, bad}], 1), + check_metric(eradius_reject_responses_total, [{server_name, bad}], 1), check_metric(eradius_server_status, true, [eradius_test_lib:localhost(ipv4, native), 1813]); check_single_request(error, EradiusRequestType, _RequestType, _ResponseType) -> ok = send_request(error, EradiusRequestType, ?ATTRS_ERROR, #{server_name => error, client_name => test, timeout => 100, failover => []}), - check_metric(eradius_client_access_requests_total, [{"server_name", error}], 1), - check_metric(eradius_client_retransmissions_total, [{"server_name", error}], 1), - check_metric(eradius_access_requests_total, [{"server_name", error}], 1), - check_metric(eradius_accept_responses_total, [{"server_name", error}], 1), - check_metric(eradius_duplicated_requests_total, [{"server_name", error}], 1), - check_metric(eradius_client_requests_total, [{"server_name", error}], 1), - check_metric(eradius_requests_total, [{"server_name", error}], 2), + check_metric(eradius_client_access_requests_total, [{server_name, error}], 1), + check_metric(eradius_client_retransmissions_total, [{server_name, error}], 1), + check_metric(eradius_access_requests_total, [{server_name, error}], 1), + check_metric(eradius_accept_responses_total, [{server_name, error}], 1), + check_metric(eradius_duplicated_requests_total, [{server_name, error}], 1), + check_metric(eradius_client_requests_total, [{server_name, error}], 1), + check_metric(eradius_requests_total, [{server_name, error}], 2), check_metric(eradius_server_status, undefined, [eradius_test_lib:localhost(ipv4, native), 1812]), check_metric(eradius_server_status, undefined, [eradius_test_lib:localhost(ipv4, native), 1813]), check_metric(eradius_server_status, true, [eradius_test_lib:localhost(ipv4, native), 1814]), ok. check_total_requests(good, N) -> - check_metric(eradius_requests_total, [{"server_name", good}], N), - check_metric(eradius_replies_total, [{"server_name", good}], N); + check_metric(eradius_requests_total, [{server_name, good}], N), + check_metric(eradius_replies_total, [{server_name, good}], N); check_total_requests(bad, N) -> - check_metric(eradius_requests_total, [{"server_name", bad}], N), - check_metric(eradius_replies_total, [{"server_name", bad}], N). + check_metric(eradius_requests_total, [{server_name, bad}], N), + check_metric(eradius_replies_total, [{server_name, bad}], N). check_metric_multi({bad_type, accreq}, Id, Labels, _Count) -> Values = prometheus_counter:values(default, Id), From 828a01fc0954286938d137c443ddbc008e0e01aa Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 1 Apr 2026 17:03:02 +0200 Subject: [PATCH 21/38] Fix OTP 29 deprecation of catch clauses without explicit class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OTP 28 deprecated try...catch clauses that omit the exception class (which silently defaulted to catching only throw); OTP 29 removes them. - eradius_eap_packet: `catch _` → `catch _:_` to catch all classes - eradius_client_socket: `{nodedown, _}` → `exit:{nodedown, _}` to correctly match the exit exception thrown by gen_server:call on node down Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- src/eradius_client_socket.erl | 2 +- src/eradius_eap_packet.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/eradius_client_socket.erl b/src/eradius_client_socket.erl index 9178ecf9..8ef99bd1 100644 --- a/src/eradius_client_socket.erl +++ b/src/eradius_client_socket.erl @@ -34,7 +34,7 @@ send_request(Socket, Peer, ReqId, Request, Timeout) -> catch exit:{noproc, _} -> {error, closed}; - {nodedown, _} -> + exit:{nodedown, _} -> {error, closed} end. diff --git a/src/eradius_eap_packet.erl b/src/eradius_eap_packet.erl index f49e63ef..67da8049 100644 --- a/src/eradius_eap_packet.erl +++ b/src/eradius_eap_packet.erl @@ -83,7 +83,7 @@ do_decode_payload(Code, Id, Data) -> try decode_payload(Code, Id, Data) catch - _ -> {error, invalid_payload} + _:_ -> {error, invalid_payload} end. decode_payload(Code, Id, <<Type:8, TypeData/binary>>) From 2d99dc2b49449adf1acc0ba3a4216ca28d765e34 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 1 Apr 2026 17:05:56 +0200 Subject: [PATCH 22/38] Fix OTP 29 deprecation of plain catch expression in dicts_compiler.erl Replace `case catch l2i(Id) of` with `try l2i(Id) of ... catch error:_ ->` as the standalone `catch Expr` expression is deprecated in OTP 28 and removed in OTP 29. `list_to_integer` raises error:badarg on failure, so the explicit `error:_` class is correct. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- dicts_compiler.erl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dicts_compiler.erl b/dicts_compiler.erl index 6e4eef27..913bf576 100755 --- a/dicts_compiler.erl +++ b/dicts_compiler.erl @@ -217,9 +217,10 @@ pd(["VALUE", Attr, Name, Id], _VendId) -> pd(_X, _VendId) -> false. id2i(Id) -> - case catch l2i(Id) of - I when is_integer(I) -> I; - {'EXIT', _} -> + try l2i(Id) of + I when is_integer(I) -> I + catch + error:_ -> hex2i(Id) end. From 7a12b6c60c9d02601de12027d5ea06f63afe8e1f Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Wed, 1 Apr 2026 17:14:31 +0200 Subject: [PATCH 23/38] Suppress OTP 29 deprecated catch error in prometheus dependency prometheus 6.1.2 uses the plain `catch Expr` form in prometheus_quantile_summary.erl which OTP 29 made a hard error. Add a rebar3 override to pass nowarn_deprecated_catch when compiling prometheus until upstream ships a fix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- rebar.config | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rebar.config b/rebar.config index 9d106135..daea6d5a 100644 --- a/rebar.config +++ b/rebar.config @@ -5,6 +5,11 @@ {pre_hooks, [{compile, "escript dicts_compiler.erl compile"}, {clean, "escript dicts_compiler.erl clean"}]}. +%% Suppress OTP 29 hard-error for plain catch in prometheus until upstream fixes it +{overrides, [ + {add, prometheus, [{erl_opts, [nowarn_deprecated_catch]}]} + ]}. + {profiles, [ {dialyzer, [{deps, [{prometheus, "6.1.2"}]}]}, {test, [ From 20e21a65a7be1f479cb258358af08e09cd29f6d4 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 2 Apr 2026 08:19:47 +0200 Subject: [PATCH 24/38] Fix issues raised in Copilot PR review - .github/workflows/main.yml: artifact path was .tar.xz but the tar command produces .tar.zst; upload would always silently fail - src/eradius_eap_packet.erl: fix doc typos 'endecode a EPA' -> 'encode an EAP' - src/eradius_dict.erl: code_change returned {ok, state} (atom) instead of {ok, State} (variable), corrupting server state on hot code upgrade - src/eradius_client_socket.erl: handle_call catch-all returned {noreply} which would hang callers; use {reply, {error, bad_call}, State} - src/eradius.app.src: capitalize 'Github' -> 'GitHub' - src/eradius_log.erl: line/1 and format_sender/1 always used server_addr but the docstring says '<Client-IP>'; add peer_addr/1 helper that picks client_addr (peer/NAS on server side) and falls back to server_addr (remote server on client side) - test/eradius_client_SUITE.erl: fix typo 'Adress' -> 'Address' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- .github/workflows/main.yml | 2 +- src/eradius.app.src | 2 +- src/eradius_client_socket.erl | 2 +- src/eradius_dict.erl | 2 +- src/eradius_eap_packet.erl | 2 +- src/eradius_log.erl | 10 ++++++++-- test/eradius_client_SUITE.erl | 2 +- 7 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 88f4848a..98504c76 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,7 +34,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: test-output-${{ matrix.otp }} - path: ct-logs-${{ matrix.otp }}.tar.xz + path: ct-logs-${{ matrix.otp }}.tar.zst slack: needs: test diff --git a/src/eradius.app.src b/src/eradius.app.src index deffd7dd..76004039 100644 --- a/src/eradius.app.src +++ b/src/eradius.app.src @@ -20,7 +20,7 @@ ]}, {maintainers, ["Andreas Schultz", "Vladimir Tarasenko", "Yury Gargay"]}, {licenses, ["MIT"]}, - {links, [{"Github", "https://github.com/travelping/eradius"}]}, + {links, [{"GitHub", "https://github.com/travelping/eradius"}]}, %% List copied from rebar3_hex.hrl ?DEFAULT_FILES, adding "Makefile" {files, ["applications", "src", "c_src", "include/eradius_*.hrl", "rebar.config.script" ,"priv/dictionaries", "rebar.config", "rebar.lock" diff --git a/src/eradius_client_socket.erl b/src/eradius_client_socket.erl index 8ef99bd1..aad98e47 100644 --- a/src/eradius_client_socket.erl +++ b/src/eradius_client_socket.erl @@ -74,7 +74,7 @@ handle_call({send_request, {IP, Port}, ReqId, Request, Timeout}, From, end; handle_call(_Request, _From, State) -> - {noreply, State}. + {reply, {error, bad_call}, State}. handle_cast(close, #state{pending = Pending} = State) when map_size(Pending) =:= 0 -> diff --git a/src/eradius_dict.erl b/src/eradius_dict.erl index c5b9bf14..61109a74 100644 --- a/src/eradius_dict.erl +++ b/src/eradius_dict.erl @@ -100,7 +100,7 @@ handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> ok. %% @private -code_change(_OldVsn, _NewVsn, _State) -> {ok, state}. +code_change(_OldVsn, _NewVsn, State) -> {ok, State}. %%%========================================================================= %%% internal functions diff --git a/src/eradius_eap_packet.erl b/src/eradius_eap_packet.erl index 67da8049..2a5cc16d 100644 --- a/src/eradius_eap_packet.erl +++ b/src/eradius_eap_packet.erl @@ -73,7 +73,7 @@ decode(<<Code:8, Id:8, Len:16, Rest/binary>>) -> {error, invalid_length} end. --doc "endecode a EPA message". +-doc "encode an EAP message". encode(Code, Id, Msg) -> Data = encode_payload(Code, Msg), Len = size(Data) + 4, diff --git a/src/eradius_log.erl b/src/eradius_log.erl index bb2c82ca..e6faf84e 100644 --- a/src/eradius_log.erl +++ b/src/eradius_log.erl @@ -61,7 +61,8 @@ used by many RADIUS server implementations. The format is: `<Client-IP>:<Client-Port> [<Request-Id>]: <Command> [AcctStatusType]` """. -spec line(eradius_req:req()) -> iolist(). -line(#{cmd := Cmd, req_id := ReqId, server_addr := {IP, Port}} = Req) -> +line(#{cmd := Cmd, req_id := ReqId} = Req) -> + {IP, Port} = peer_addr(Req), StatusType = format_acct_status_type(Req), io_lib:format("~s:~p [~p]: ~s ~s", [inet:ntoa(IP), Port, ReqId, format_cmd(Cmd), StatusType]). @@ -98,9 +99,14 @@ format_message(Time, #{cmd := Cmd} = Req) -> BinPacket = format_packet(Req), <<BinTStamp/binary, " ", BinSender/binary, " ", BinCommand/binary, "\n", BinPacket/binary, "\n">>. -format_sender(#{req_id := ReqId, server_addr := {IP, Port}}) -> +format_sender(#{req_id := ReqId} = Req) -> + {IP, Port} = peer_addr(Req), <<(format_ip(IP))/binary, $:, (i2b(Port))/binary, " [", (i2b(ReqId))/binary, $]>>. +%% Use client_addr (the peer/NAS) on server side; fall back to server_addr on client side. +peer_addr(#{client_addr := Addr}) -> Addr; +peer_addr(#{server_addr := Addr}) -> Addr. + %% @private format_cmd(request) -> <<"Access-Request">>; format_cmd(accept) -> <<"Access-Accept">>; diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index 3214783b..6d401e04 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -198,7 +198,7 @@ check(#{sockets := OS, no_ports := _OP, idcounters := _OC, socket_id := {_, OA}} #{sockets := NS, no_ports := NP, idcounters := NC, socket_id := {_, NA}}, P, A) -> {ok, PA} = parse_ip(A), - test(PA == NA, "Adress not configured") and + test(PA == NA, "Address not configured") and case NA of OA -> ct:pal("NP: ~p, NC: ~p", [NP, NC]), From 9336997e38dd6961a1f989bb67c30265baf0ce3d Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 4 Jun 2026 08:25:40 +0200 Subject: [PATCH 25/38] Fix req-id port wrap: rem NumberOfPorts, not NumberOfPorts-1 (NumberOfPorts - 1) skipped the last port and crashed with badarith (rem 0) when no_ports = 1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- src/eradius_client_mngr.erl | 2 +- test/eradius_client_SUITE.erl | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index f3b9a470..d78c0889 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -534,7 +534,7 @@ next_port_and_req_id(Peer, NumberOfPorts, Counters) -> #{Peer := {NextPortIdx, ReqId}} when ReqId < 255 -> NextReqId = (ReqId + 1); #{Peer := {PortIdx, 255}} -> - NextPortIdx = (PortIdx + 1) rem (NumberOfPorts - 1), + NextPortIdx = (PortIdx + 1) rem NumberOfPorts, NextReqId = 0; _ -> NextPortIdx = erlang:phash2(Peer, NumberOfPorts), diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index 6d401e04..3dacbf78 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -58,7 +58,8 @@ common() -> reconf_ports_10, wanna_send, send_request_failover, - check_upstream_servers + check_upstream_servers, + no_ports_one_wraps ]. -spec groups() -> [ct_suite:ct_group_def(), ...]. @@ -288,3 +289,22 @@ check_upstream_servers(Config) -> sets:is_subset(sets:from_list(?RADIUS_SERVERS(Family)), sets:from_list(Servers))), ok. + +no_ports_one_wraps() -> + [{doc, "wanna_send must not crash when no_ports = 1 and the req-id wraps past 255"}]. +no_ports_one_wraps(Config) -> + Family = proplists:get_value(family, Config, ipv4), + {ok, _} = application:ensure_all_started(eradius), + Server = #{ip => eradius_test_lib:localhost(Family, native), port => 1812, + secret => <<"secret">>, retries => 3}, + {ok, Client} = + eradius_client_mngr:start_client( + #{family => eradius_test_lib:inet_family(Family), ip => any, no_ports => 1, + servers => #{test_server => Server}}), + %% 257 allocations force the {PortIdx, 255} wrap branch at least once + lists:foreach( + fun(_) -> + ?match({ok, {_Sock, _ReqId, test_server, _Srv, _Info}}, + eradius_client_mngr:wanna_send(Client, [test_server], [])) + end, lists:seq(1, 257)), + ok. From cb241651de1a6f5e1d0a2c0e7630b9cefa5cdbc2 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 4 Jun 2026 08:27:27 +0200 Subject: [PATCH 26/38] Bound client socket call timeout so callers never hang gen_server:call used infinity; if a pending request entry was overwritten by a same-ReqId request the caller waited forever. Use Timeout + margin and map exit:{timeout,_} to {error,timeout}. Reply to unknown calls instead of leaving them unanswered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- src/eradius_client_socket.erl | 20 +++++++++++++++++--- test/eradius_client_SUITE.erl | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/eradius_client_socket.erl b/src/eradius_client_socket.erl index aad98e47..4659183d 100644 --- a/src/eradius_client_socket.erl +++ b/src/eradius_client_socket.erl @@ -18,6 +18,12 @@ -record(state, {family, socket, active_n, pending, mode, counter}). +%% Safety margin added to the per-request timeout for the gen_server:call. +%% The socket process enforces the real timeout and replies {error,timeout}; +%% this bound only fires if the socket fails to reply at all (e.g. a pending +%% entry was overwritten by a same-ReqId request). +-define(CALL_TIMEOUT_MARGIN, 1000). + %%%========================================================================= %%% API %%%========================================================================= @@ -30,14 +36,22 @@ start_link(Config) -> send_request(Socket, Peer, ReqId, Request, Timeout) -> try - gen_server:call(Socket, {send_request, Peer, ReqId, Request, Timeout}, infinity) + gen_server:call(Socket, {send_request, Peer, ReqId, Request, Timeout}, + call_timeout(Timeout)) catch exit:{noproc, _} -> {error, closed}; exit:{nodedown, _} -> - {error, closed} + {error, closed}; + exit:{timeout, _} -> + {error, timeout} end. +%% infinity is passed through unchanged: a request configured with an +%% infinite per-attempt timeout is a caller decision, not this layer's to cap. +call_timeout(infinity) -> infinity; +call_timeout(Timeout) when is_integer(Timeout) -> Timeout + ?CALL_TIMEOUT_MARGIN. + close(Socket) -> gen_server:cast(Socket, close). @@ -74,7 +88,7 @@ handle_call({send_request, {IP, Port}, ReqId, Request, Timeout}, From, end; handle_call(_Request, _From, State) -> - {reply, {error, bad_call}, State}. + {reply, {error, unknown_request}, State}. handle_cast(close, #state{pending = Pending} = State) when map_size(Pending) =:= 0 -> diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index 3dacbf78..7dbca1e7 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -59,7 +59,8 @@ common() -> wanna_send, send_request_failover, check_upstream_servers, - no_ports_one_wraps + no_ports_one_wraps, + clobber_does_not_hang ]. -spec groups() -> [ct_suite:ct_group_def(), ...]. @@ -308,3 +309,35 @@ no_ports_one_wraps(Config) -> eradius_client_mngr:wanna_send(Client, [test_server], [])) end, lists:seq(1, 257)), ok. + +clobber_does_not_hang() -> + [{doc, "A pending request whose entry is overwritten by a same-ReqId " + "request must still return {error,timeout} to its caller, not hang"}]. +clobber_does_not_hang(Config) -> + Family = proplists:get_value(family, Config, ipv4), + {ok, Sock} = eradius_client_socket:start_link( + #{family => eradius_test_lib:inet_family(Family), active_n => 10}), + %% port 1 on loopback: packets go out, no reply ever comes back + Peer = {eradius_test_lib:localhost(Family, native), 1}, + ReqId = 1, + Packet = <<1, ReqId, 0, 20, 0:128>>, %% 20-byte minimal RADIUS header + Caller = self(), + %% First caller: stays pending (2 s socket-side timeout) + P1 = spawn(fun() -> + R = eradius_client_socket:send_request( + Sock, Peer, ReqId, Packet, 2000), + Caller ! {p1, R} + end), + timer:sleep(200), + %% Second caller: SAME ReqId -> overwrites P1's pending entry + spawn(fun() -> + eradius_client_socket:send_request(Sock, Peer, ReqId, Packet, 2000) + end), + %% P1 must not hang; with the bounded call timeout it gets {error,timeout} + receive + {p1, Result} -> + ?equal({error, timeout}, Result) + after 6000 -> + exit(P1, kill), + ct:fail("P1 hung after its pending entry was clobbered") + end. From 3c9a96c5549a6e114acde5ff23dbb7b18af035f0 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 4 Jun 2026 08:28:52 +0200 Subject: [PATCH 27/38] client_mngr: reply {error,unknown_request} to unexpected calls Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- src/eradius_client_mngr.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index d78c0889..83e6e12f 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -314,7 +314,7 @@ handle_call({reconfigure, Opts}, _From, #state{config = OConfig} = State0) -> %% @private handle_call(_OtherCall, _From, State) -> - {noreply, State}. + {reply, {error, unknown_request}, State}. %% @private handle_cast(_Msg, State) -> {noreply, State}. From b49e27478c6530140ec610692d9ed4d7a5aeb025 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 4 Jun 2026 10:12:31 +0200 Subject: [PATCH 28/38] client_socket: dedicate+connect to one server, key pending on ReqId The socket now connects to its server (OS-assigned source port, kernel filters replies to the peer) and keys its pending map on ReqId alone. Groundwork for the per-server port pool. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- src/eradius_client_socket.erl | 82 ++++++++++++++++++++++------------- test/eradius_client_SUITE.erl | 63 ++++++++++++++++++++++----- 2 files changed, 104 insertions(+), 41 deletions(-) diff --git a/src/eradius_client_socket.erl b/src/eradius_client_socket.erl index 4659183d..2d36cd0e 100644 --- a/src/eradius_client_socket.erl +++ b/src/eradius_client_socket.erl @@ -16,7 +16,10 @@ -ignore_xref([start_link/1]). --record(state, {family, socket, active_n, pending, mode, counter}). +-record(state, {family, socket, active_n, pending, mode, + server_addr, cooldown, cooldown_done = false}). + +-define(DEFAULT_REQID_REUSE_TIMEOUT, 30000). %% Safety margin added to the per-request timeout for the gen_server:call. %% The socket process enforces the real timeout and replies {error,timeout}; @@ -59,30 +62,47 @@ close(Socket) -> %%% gen_server callbacks %%%=================================================================== -init([#{family := Family, active_n := ActiveN} = Config]) -> +init([#{family := Family, active_n := ActiveN, + server_addr := {SrvIP, SrvPort}} = Config]) -> Opts = inet_opts(Config, [{active, ActiveN}, binary, Family]), {ok, Socket} = gen_udp:open(0, Opts), + case connect(Socket, Family, SrvIP, SrvPort) of + {ok, ConnIP} -> + State = #state{ + family = Family, + socket = Socket, + active_n = ActiveN, + pending = #{}, + mode = active, + server_addr = {ConnIP, SrvPort}, + cooldown = maps:get(reqid_reuse_timeout, Config, + ?DEFAULT_REQID_REUSE_TIMEOUT) + }, + {ok, State}; + {error, Reason} -> + gen_udp:close(Socket), + {stop, Reason} + end. - State = #state{ - family = Family, - socket = Socket, - active_n = ActiveN, - pending = #{}, - mode = active - }, - {ok, State}. - -handle_call({send_request, {IP, Port}, ReqId, Request, Timeout}, From, - #state{family = Family, socket = Socket} = State) -> - case send_ip(Family, IP) of - {ok, SendIP} -> - case gen_udp:send(Socket, SendIP, Port, Request) of +%% Map the server IP into the socket family and connect; close+stop on any error. +connect(Socket, Family, SrvIP, SrvPort) -> + case send_ip(Family, SrvIP) of + {ok, ConnIP} -> + case gen_udp:connect(Socket, ConnIP, SrvPort) of ok -> - ReqKey = {SendIP, Port, ReqId}, - {noreply, pending_request(ReqKey, From, Timeout, State)}; + {ok, ConnIP}; {error, _} = Error -> - {reply, Error, State} + Error end; + {error, _} = Error -> + Error + end. + +handle_call({send_request, _Peer, ReqId, Request, Timeout}, From, + #state{socket = Socket} = State) -> + case gen_udp:send(Socket, Request) of + ok -> + {noreply, pending_request(ReqId, From, Timeout, State)}; {error, _} = Error -> {reply, Error, State} end; @@ -103,26 +123,27 @@ handle_info({udp_passive, _Socket}, #state{socket = Socket, active_n = ActiveN} inet:setopts(Socket, [{active, ActiveN}]), {noreply, State}; -handle_info({udp, Socket, FromIP, FromPort, Response}, +handle_info({udp, Socket, _FromIP, _FromPort, Response}, State = #state{socket = Socket}) -> flow_control(State), NState = case Response of <<Header:20/binary, Body/binary>> -> <<_, ReqId:8, _/binary>> = Header, - request_done({FromIP, FromPort, ReqId}, {ok, Header, Body}, State); + request_done(ReqId, {ok, Header, Body}, State); _ -> %% discard reply because it was malformed State end, noreply_or_stop(NState); -handle_info({timeout, TRef, ReqKey}, #state{pending = Pending} = State) -> +handle_info({timeout, TRef, ReqId}, #state{pending = Pending} = State) + when is_integer(ReqId) -> NState = case Pending of - #{ReqKey := {From, TRef}} -> + #{ReqId := {From, TRef}} -> gen_server:reply(From, {error, timeout}), - State#state{pending = maps:remove(ReqKey, Pending)}; + State#state{pending = maps:remove(ReqId, Pending)}; _ -> State end, @@ -152,17 +173,16 @@ noreply_or_stop(#state{pending = Pending, mode = inactive} = State) noreply_or_stop(State) -> {noreply, State}. -pending_request(ReqKey, From, Timeout, - #state{pending = Pending} = State) -> - TRef = erlang:start_timer(Timeout, self(), ReqKey), - State#state{pending = Pending#{ReqKey => {From, TRef}}}. +pending_request(ReqId, From, Timeout, #state{pending = Pending} = State) -> + TRef = erlang:start_timer(Timeout, self(), ReqId), + State#state{pending = Pending#{ReqId => {From, TRef}}}. -request_done(ReqKey, Reply, #state{pending = Pending} = State) -> +request_done(ReqId, Reply, #state{pending = Pending} = State) -> case Pending of - #{ReqKey := {From, TRef}} -> + #{ReqId := {From, TRef}} -> gen_server:reply(From, Reply), erlang:cancel_timer(TRef), - State#state{pending = maps:remove(ReqKey, Pending)}; + State#state{pending = maps:remove(ReqId, Pending)}; _ -> State end. diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index 7dbca1e7..8d8962ea 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -60,7 +60,8 @@ common() -> send_request_failover, check_upstream_servers, no_ports_one_wraps, - clobber_does_not_hang + clobber_does_not_hang, + connected_socket_matches_reply ]. -spec groups() -> [ct_suite:ct_group_def(), ...]. @@ -315,10 +316,17 @@ clobber_does_not_hang() -> "request must still return {error,timeout} to its caller, not hang"}]. clobber_does_not_hang(Config) -> Family = proplists:get_value(family, Config, ipv4), + InetFamily = eradius_test_lib:inet_family(Family), + LH = eradius_test_lib:localhost(Family, native), + BindIP = eradius_test_lib:localhost(Family, mapped), + %% black-hole server: a real UDP socket that never replies, so the connected + %% client socket sees a listener (no ICMP econnrefused) and the clobber is exercised. + {ok, BH} = gen_udp:open(0, [binary, {active, false}, InetFamily, {ip, BindIP}]), + {ok, BHPort} = inet:port(BH), + Peer = {LH, BHPort}, {ok, Sock} = eradius_client_socket:start_link( - #{family => eradius_test_lib:inet_family(Family), active_n => 10}), - %% port 1 on loopback: packets go out, no reply ever comes back - Peer = {eradius_test_lib:localhost(Family, native), 1}, + #{family => InetFamily, active_n => 10, server_addr => Peer, + reqid_reuse_timeout => 30000}), ReqId = 1, Packet = <<1, ReqId, 0, 20, 0:128>>, %% 20-byte minimal RADIUS header Caller = self(), @@ -334,10 +342,45 @@ clobber_does_not_hang(Config) -> eradius_client_socket:send_request(Sock, Peer, ReqId, Packet, 2000) end), %% P1 must not hang; with the bounded call timeout it gets {error,timeout} + Result = + receive + {p1, R} -> R + after 6000 -> + exit(P1, kill), + ct:fail("P1 hung after its pending entry was clobbered") + end, + gen_udp:close(BH), + eradius_client_socket:close(Sock), + ?equal({error, timeout}, Result). + +connected_socket_matches_reply() -> + [{doc, "A connected socket sends without an explicit dest and matches a " + "reply to the pending request by ReqId alone"}]. +connected_socket_matches_reply(Config) -> + Family = proplists:get_value(family, Config, ipv4), + InetFamily = eradius_test_lib:inet_family(Family), + LH = eradius_test_lib:localhost(Family, native), + BindIP = eradius_test_lib:localhost(Family, mapped), + {ok, Server} = gen_udp:open(0, [binary, {active, false}, InetFamily, {ip, BindIP}]), + {ok, SrvPort} = inet:port(Server), + {ok, Sock} = eradius_client_socket:start_link( + #{family => InetFamily, active_n => 10, + server_addr => {LH, SrvPort}, reqid_reuse_timeout => 30000}), + ReqId = 7, + Req = <<1, ReqId, 0, 20, 0:128>>, + Caller = self(), + spawn(fun() -> + R = eradius_client_socket:send_request( + Sock, {LH, SrvPort}, ReqId, Req, 3000), + Caller ! {done, R} + end), + {ok, {FromIP, FromPort, <<_, ReqId, _/binary>>}} = gen_udp:recv(Server, 0, 2000), + Reply = <<2, ReqId, 0, 20, 1:128>>, + ok = gen_udp:send(Server, FromIP, FromPort, Reply), receive - {p1, Result} -> - ?equal({error, timeout}, Result) - after 6000 -> - exit(P1, kill), - ct:fail("P1 hung after its pending entry was clobbered") - end. + {done, Result} -> + ?match({ok, <<2, ReqId, _/binary>>, <<>>}, Result) + after 4000 -> + ct:fail("connected socket did not deliver the reply") + end, + gen_udp:close(Server). From 1f20be69b798fed0efba7051fc8a0cf4e2d7c27c Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 4 Jun 2026 10:24:49 +0200 Subject: [PATCH 29/38] client_socket: add retire/1 cooldown lifecycle A retired socket holds its (bound) source port for reqid_reuse_timeout (the quarantine), keeps serving in-flight requests, then exits normally once cooldown elapsed AND pending is empty. Force-closed at 2x cooldown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- src/eradius_client_socket.erl | 26 ++++++++++++++- test/eradius_client_SUITE.erl | 63 ++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/eradius_client_socket.erl b/src/eradius_client_socket.erl index 2d36cd0e..b8bd4aba 100644 --- a/src/eradius_client_socket.erl +++ b/src/eradius_client_socket.erl @@ -9,7 +9,7 @@ -behaviour(gen_server). %% API --export([new/2, start_link/1, send_request/5, close/1]). +-export([new/2, start_link/1, send_request/5, retire/1, close/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -21,6 +21,10 @@ -define(DEFAULT_REQID_REUSE_TIMEOUT, 30000). +%% Hard ceiling (multiple of the cooldown) after which a retired socket is +%% force-closed even if a straggler request is still pending, to bound resources. +-define(FORCE_CLOSE_FACTOR, 2). + %% Safety margin added to the per-request timeout for the gen_server:call. %% The socket process enforces the real timeout and replies {error,timeout}; %% this bound only fires if the socket fails to reply at all (e.g. a pending @@ -55,6 +59,9 @@ send_request(Socket, Peer, ReqId, Request, Timeout) -> call_timeout(infinity) -> infinity; call_timeout(Timeout) when is_integer(Timeout) -> Timeout + ?CALL_TIMEOUT_MARGIN. +retire(Socket) -> + gen_server:cast(Socket, retire). + close(Socket) -> gen_server:cast(Socket, close). @@ -110,6 +117,14 @@ handle_call({send_request, _Peer, ReqId, Request, Timeout}, From, handle_call(_Request, _From, State) -> {reply, {error, unknown_request}, State}. +handle_cast(retire, #state{mode = retiring} = State) -> + %% idempotent: a second retire must not start another timer set + {noreply, State}; +handle_cast(retire, #state{cooldown = Cooldown} = State) -> + erlang:start_timer(Cooldown, self(), cooldown_expired), + erlang:start_timer(Cooldown * ?FORCE_CLOSE_FACTOR, self(), force_close), + noreply_or_stop(State#state{mode = retiring}); + handle_cast(close, #state{pending = Pending} = State) when map_size(Pending) =:= 0 -> {stop, normal, State}; @@ -137,6 +152,12 @@ handle_info({udp, Socket, _FromIP, _FromPort, Response}, end, noreply_or_stop(NState); +handle_info({timeout, _TRef, cooldown_expired}, State) -> + noreply_or_stop(State#state{cooldown_done = true}); + +handle_info({timeout, _TRef, force_close}, State) -> + {stop, normal, State}; + handle_info({timeout, TRef, ReqId}, #state{pending = Pending} = State) when is_integer(ReqId) -> NState = @@ -170,6 +191,9 @@ flow_control(_) -> noreply_or_stop(#state{pending = Pending, mode = inactive} = State) when map_size(Pending) =:= 0 -> {stop, normal, State}; +noreply_or_stop(#state{pending = Pending, mode = retiring, cooldown_done = true} = State) + when map_size(Pending) =:= 0 -> + {stop, normal, State}; noreply_or_stop(State) -> {noreply, State}. diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index 8d8962ea..3cb46598 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -61,7 +61,9 @@ common() -> check_upstream_servers, no_ports_one_wraps, clobber_does_not_hang, - connected_socket_matches_reply + connected_socket_matches_reply, + retire_holds_then_closes, + retire_waits_for_pending ]. -spec groups() -> [ct_suite:ct_group_def(), ...]. @@ -384,3 +386,62 @@ connected_socket_matches_reply(Config) -> ct:fail("connected socket did not deliver the reply") end, gen_udp:close(Server). + +retire_holds_then_closes() -> + [{doc, "A retired socket with no pending requests stays alive during the " + "cooldown and exits normally once it elapses"}]. +retire_holds_then_closes(Config) -> + Family = proplists:get_value(family, Config, ipv4), + InetFamily = eradius_test_lib:inet_family(Family), + LH = eradius_test_lib:localhost(Family, native), + {ok, Sock} = eradius_client_socket:start_link( + #{family => InetFamily, active_n => 10, + server_addr => {LH, 1}, reqid_reuse_timeout => 700}), + MRef = erlang:monitor(process, Sock), + eradius_client_socket:retire(Sock), + timer:sleep(300), + ?equal(true, is_process_alive(Sock)), + receive + {'DOWN', MRef, process, Sock, Reason} -> + ?equal(normal, Reason) + after 2000 -> + ct:fail("retired socket did not close after cooldown") + end. + +retire_waits_for_pending() -> + [{doc, "A retired socket does not close while a request is still pending; " + "it closes after the pending request resolves and cooldown elapsed"}]. +retire_waits_for_pending(Config) -> + Family = proplists:get_value(family, Config, ipv4), + InetFamily = eradius_test_lib:inet_family(Family), + LH = eradius_test_lib:localhost(Family, native), + BindIP = eradius_test_lib:localhost(Family, mapped), + %% black-hole server (real listener, never replies): avoids ICMP econnrefused. + {ok, BH} = gen_udp:open(0, [binary, {active, false}, InetFamily, {ip, BindIP}]), + {ok, BHPort} = inet:port(BH), + Peer = {LH, BHPort}, + %% cooldown 1500ms; force-close at 2x = 3000ms. Retire first so the cooldown + %% starts at t=0, then send a request whose 2500ms timeout resolves between + %% the cooldown (1500ms) and the force-close (3000ms): the close is driven by + %% pending-drain, with comfortable margins around the 1800ms alive-check. + {ok, Sock} = eradius_client_socket:start_link( + #{family => InetFamily, active_n => 10, + server_addr => Peer, reqid_reuse_timeout => 1500}), + MRef = erlang:monitor(process, Sock), + Caller = self(), + eradius_client_socket:retire(Sock), + spawn(fun() -> + R = eradius_client_socket:send_request( + Sock, Peer, 3, <<1, 3, 0, 20, 0:128>>, 2500), + Caller ! {p, R} + end), + %% at ~1800ms the cooldown has elapsed but the request is still pending -> alive + timer:sleep(1800), + ?equal(true, is_process_alive(Sock)), + receive {p, {error, timeout}} -> ok after 4000 -> ct:fail("request never resolved") end, + receive + {'DOWN', MRef, process, Sock, normal} -> ok + after 4000 -> + ct:fail("retired socket did not close after pending drained") + end, + gen_udp:close(BH). From f60925aeb30c0c100372b13444893724be69b005 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 4 Jun 2026 10:31:45 +0200 Subject: [PATCH 30/38] client_mngr: add max_ports_per_server + reqid_reuse_timeout opts no_ports now means K (concurrent active fillers per server). New opts default to 256 sockets/server cap and a 30s reqid reuse cooldown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- src/eradius_client_mngr.erl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index 83e6e12f..6a9bd1b2 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -141,6 +141,9 @@ Increase `no_ports` for higher concurrency requirements. -define(RECONFIGURE_TIMEOUT, 15000). -define(DEFAULT_MAX_RETRIES, 20). -define(DEFAULT_DOWN_TIME, 1000). +-define(DEFAULT_K_PORTS, 10). +-define(DEFAULT_MAX_PORTS_PER_SERVER, 256). +-define(DEFAULT_REQID_REUSE_TIMEOUT, 30000). %%%========================================================================= %%% API @@ -372,7 +375,9 @@ socket_id_str({_, IP}) when is_atom(IP) -> default_client_opts() -> #{family => inet6, ip => any, - no_ports => 10, + no_ports => ?DEFAULT_K_PORTS, + max_ports_per_server => ?DEFAULT_MAX_PORTS_PER_SERVER, + reqid_reuse_timeout => ?DEFAULT_REQID_REUSE_TIMEOUT, active_n => 100, recbuf => 8192, sndbuf => 131072, From 771345392436f03a494705e432e68cb24eef90da Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 4 Jun 2026 10:36:31 +0200 Subject: [PATCH 31/38] client_mngr: per-server port pool allocator (roll at 256, retire) Replaces the blind rotating counter + static socket array with per-server pools of K connected 'filler' sockets. Each filler issues ids 0..255 then is retired (cooled+closed by the socket); a fresh one is opened on demand. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- src/eradius_client_mngr.erl | 219 ++++++++++++++++++++-------------- test/eradius_client_SUITE.erl | 52 +++++++- 2 files changed, 180 insertions(+), 91 deletions(-) diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index 6a9bd1b2..a0ef1d1c 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -102,7 +102,9 @@ Increase `no_ports` for higher concurrency requirements. inet_backend => inet | socket, ip => any | inet:ip_address(), active_n => once | non_neg_integer(), - no_ports => non_neg_integer(), + no_ports => pos_integer(), + max_ports_per_server => pos_integer(), + reqid_reuse_timeout => pos_integer(), recbuf => non_neg_integer(), sndbuf => non_neg_integer(), metrics_callback => eradius_req:metrics_callback() @@ -125,6 +127,13 @@ Increase `no_ports` for higher concurrency requirements. -export_type([server_name/0, server_pool/0, servers/0, client_opts/0]). +-define(RECONFIGURE_TIMEOUT, 15000). +-define(DEFAULT_MAX_RETRIES, 20). +-define(DEFAULT_DOWN_TIME, 1000). +-define(DEFAULT_K_PORTS, 10). +-define(DEFAULT_MAX_PORTS_PER_SERVER, 256). +-define(DEFAULT_REQID_REUSE_TIMEOUT, 30000). + -record(state, { owner :: pid(), config :: client_config(), @@ -132,18 +141,19 @@ Increase `no_ports` for higher concurrency requirements. client_addr :: any | inet:ip_address(), servers :: servers(), socket_id :: {Family :: inet | inet6, IP :: any | inet:ip_address()}, - no_ports = 1 :: pos_integer(), - idcounters = maps:new() :: map(), - sockets = array:new() :: array:array(), + k_ports = ?DEFAULT_K_PORTS :: pos_integer(), + max_ports = ?DEFAULT_MAX_PORTS_PER_SERVER :: pos_integer(), + reqid_reuse_timeout = ?DEFAULT_REQID_REUSE_TIMEOUT :: pos_integer(), + pools = #{} :: #{server_addr() => pool()}, + socket_refs = #{} :: #{reference() => server_addr()}, + no_ports_rejections = 0 :: non_neg_integer(), metrics_callback :: undefined | eradius_req:metrics_callback() }). --define(RECONFIGURE_TIMEOUT, 15000). --define(DEFAULT_MAX_RETRIES, 20). --define(DEFAULT_DOWN_TIME, 1000). --define(DEFAULT_K_PORTS, 10). --define(DEFAULT_MAX_PORTS_PER_SERVER, 256). --define(DEFAULT_REQID_REUSE_TIMEOUT, 30000). +-type server_addr() :: {inet:ip_address(), inet:port_number()}. +-type filler() :: #{pid := pid(), monitor := reference(), + next_id := 0..255, issued := 0..256}. +-type pool() :: #{active := [filler()], cooling := [pid()]}. %%%========================================================================= %%% API @@ -260,33 +270,35 @@ init([Owner, #{name := ClientName, servers := Servers, config = Config, servers = Servers, socket_id = socket_id(Config), - no_ports = NPorts, + k_ports = NPorts, + max_ports = maps:get(max_ports_per_server, Config, + ?DEFAULT_MAX_PORTS_PER_SERVER), + reqid_reuse_timeout = maps:get(reqid_reuse_timeout, Config, + ?DEFAULT_REQID_REUSE_TIMEOUT), metrics_callback = MetricsCallback }, {ok, State}. %% @private handle_call({wanna_send, Candidates, Tried}, _From, - #state{ - client_name = ClientName, - client_addr = ClientAddr, - servers = Servers, - no_ports = NoPorts, idcounters = IdCounters, - sockets = Sockets, - metrics_callback = MetricsCallback} = State0) -> + #state{client_name = ClientName, client_addr = ClientAddr, + servers = Servers, + metrics_callback = MetricsCallback} = State0) -> case select_server(Candidates, Tried, Servers) of {ok, {ServerName, #{ip := IP, port := Port} = Server}} -> ServerAddr = {IP, Port}, - {PortIdx, ReqId, NewIdCounters} = - next_port_and_req_id(ServerAddr, NoPorts, IdCounters), - {SocketProcess, NewSockets} = find_socket_process(PortIdx, Sockets, State0), - State = State0#state{idcounters = NewIdCounters, sockets = NewSockets}, - ReqInfo = - #{server => ServerName, server_addr => ServerAddr, - client => ClientName, client_addr => ClientAddr, - metrics_callback => MetricsCallback}, - Reply = {ok, {SocketProcess, ReqId, ServerName, Server, ReqInfo}}, - {reply, Reply, State}; + case allocate(ServerAddr, State0) of + {ok, Pid, ReqId, State} -> + ReqInfo = + #{server => ServerName, server_addr => ServerAddr, + client => ClientName, client_addr => ClientAddr, + metrics_callback => MetricsCallback}, + {reply, {ok, {Pid, ReqId, ServerName, Server, ReqInfo}}, State}; + {error, no_ports, #state{no_ports_rejections = N} = State1} -> + State = State1#state{no_ports_rejections = N + 1}, + ?LOG(warning, "RADIUS client port pool for ~p saturated", [ServerAddr]), + {reply, {error, no_ports}, State} + end; {error, _} = Error -> {reply, Error, State0} end; @@ -366,6 +378,9 @@ where(ServerName) -> socket_id(#{family := Family, ip := IP}) -> {Family, IP}. +%% Retained for an upcoming task that reworks reconfigure_address to close +%% sockets and log the new client address. +-compile({nowarn_unused_function, socket_id_str/1}). socket_id_str({_, IP}) when is_tuple(IP) -> inet:ntoa(IP); socket_id_str({_, IP}) when is_atom(IP) -> @@ -486,76 +501,100 @@ client_config(Opts0) -> end. reconfigure_address(#{no_ports := NPorts} = Config, - #state{socket_id = OAdd, sockets = Sockts} = State) -> + #state{socket_id = OAdd, socket_refs = Refs} = State) -> NAdd = socket_id(Config), case OAdd of - NAdd -> - reconfigure_ports(NPorts, State); - _ -> - ?LOG(info, "Reopening RADIUS client sockets (client_ip changed to ~s)", - [socket_id_str(NAdd)]), - array:map( - fun(_PortIdx, undefined) -> - ok; - (_PortIdx, Socket) -> - eradius_client_socket:close(Socket) - end, Sockts), - Counters = fix_counters(NPorts, State#state.idcounters), - State#state{sockets = array:new(), socket_id = NAdd, - no_ports = NPorts, idcounters = Counters} + NAdd -> reconfigure_ports(NPorts, State); + _ -> + %% address changed: drop all pools. Demonitor first so the abandoned + %% sockets' DOWNs don't linger in the mailbox. (Task A8 also closes them.) + maps:foreach(fun(Ref, _SA) -> erlang:demonitor(Ref, [flush]) end, Refs), + State#state{socket_id = NAdd, k_ports = NPorts, + pools = #{}, socket_refs = #{}} end. -reconfigure_ports(NPorts, #state{no_ports = OPorts, sockets = Sockets} = State) -> - if - OPorts =< NPorts -> - State#state{no_ports = NPorts}; - true -> - Counters = fix_counters(NPorts, State#state.idcounters), - NSockets = close_sockets(NPorts, Sockets), - State#state{sockets = NSockets, no_ports = NPorts, idcounters = Counters} - end. +reconfigure_ports(NPorts, State) -> + State#state{k_ports = NPorts}. + +-spec new_pool() -> pool(). +new_pool() -> #{active => [], cooling => []}. -fix_counters(NPorts, Counters) -> - maps:map(fun(_Peer, Value = {NextPortIdx, _NextReqId}) when NextPortIdx < NPorts -> Value; - (_Peer, {_NextPortIdx, NextReqId}) -> {0, NextReqId} - end, Counters). - -close_sockets(NPorts, Sockets) -> - case array:size(Sockets) =< NPorts of - true -> - Sockets; - false -> - List = array:to_list(Sockets), - {_, Rest} = lists:split(NPorts, List), - lists:map( - fun(undefined) -> ok; - (Socket) -> eradius_client_socket:close(Socket) - end, Rest), - array:resize(NPorts, Sockets) +pool_of(ServerAddr, #state{pools = Pools}) -> + maps:get(ServerAddr, Pools, new_pool()). + +put_pool(ServerAddr, Pool, #state{pools = Pools} = State) -> + State#state{pools = Pools#{ServerAddr => Pool}}. + +%% active fillers + cooling (retired-but-open) sockets. cooling pids are reclaimed +%% when their socket exits, by the 'DOWN' handler (Task A7); until then pool_total +%% only grows, so allocate may report {error, no_ports} once the cap is reached. +pool_total(#{active := A, cooling := C}) -> length(A) + length(C). + +%% Allocate {Pid, ReqId} for ServerAddr, growing/rolling the pool as needed. +-spec allocate(server_addr(), #state{}) -> + {ok, pid(), 0..255, #state{}} | {error, no_ports, #state{}}. +allocate(ServerAddr, State0) -> + State1 = ensure_active(ServerAddr, State0), + Pool = pool_of(ServerAddr, State1), + case maps:get(active, Pool) of + [] -> + {error, no_ports, State1}; + [#{pid := Pid, next_id := ReqId, issued := Issued0} = F | Rest] -> + Issued = Issued0 + 1, + case Issued >= 256 of + true -> + %% filler exhausted: retire it (socket cools+closes itself), + %% move it to cooling, and open a replacement. + ok = eradius_client_socket:retire(Pid), + Pool1 = Pool#{active := Rest, + cooling := [Pid | maps:get(cooling, Pool)]}, + State2 = put_pool(ServerAddr, Pool1, State1), + State3 = ensure_active(ServerAddr, State2), + {ok, Pid, ReqId, State3}; + false -> + %% round-robin: advance this filler and move it to the tail. + F1 = F#{next_id := (ReqId + 1) rem 256, issued := Issued}, + Pool1 = Pool#{active := Rest ++ [F1]}, + {ok, Pid, ReqId, put_pool(ServerAddr, Pool1, State1)} + end end. -next_port_and_req_id(Peer, NumberOfPorts, Counters) -> - case Counters of - #{Peer := {NextPortIdx, ReqId}} when ReqId < 255 -> - NextReqId = (ReqId + 1); - #{Peer := {PortIdx, 255}} -> - NextPortIdx = (PortIdx + 1) rem NumberOfPorts, - NextReqId = 0; - _ -> - NextPortIdx = erlang:phash2(Peer, NumberOfPorts), - NextReqId = 0 - end, - NewCounters = Counters#{Peer => {NextPortIdx, NextReqId}}, - {NextPortIdx, NextReqId, NewCounters}. +%% Open fillers until there are K active (or the per-server cap is hit, or a +%% socket open fails). Idempotent. +-spec ensure_active(server_addr(), #state{}) -> #state{}. +ensure_active(ServerAddr, #state{k_ports = K, max_ports = Max} = State) -> + Pool = pool_of(ServerAddr, State), + Active = maps:get(active, Pool), + case length(Active) < K andalso pool_total(Pool) < Max of + true -> + case open_filler(ServerAddr, State) of + {ok, Filler, State1} -> + Pool1 = pool_of(ServerAddr, State1), + Pool2 = Pool1#{active := maps:get(active, Pool1) ++ [Filler]}, + ensure_active(ServerAddr, put_pool(ServerAddr, Pool2, State1)); + {error, _} -> + State + end; + false -> + State + end. -find_socket_process(PortIdx, Sockets, #state{owner = Owner, config = Config}) -> - case array:get(PortIdx, Sockets) of - undefined -> - {ok, Supervisor} = eradius_client_sup:socket_supervisor(Owner), - {ok, Socket} = eradius_client_socket:new(Supervisor, Config), - {Socket, array:set(PortIdx, Socket, Sockets)}; - Socket -> - {Socket, Sockets} +-spec open_filler(server_addr(), #state{}) -> + {ok, filler(), #state{}} | {error, term()}. +open_filler(ServerAddr, #state{owner = Owner, config = Config, + reqid_reuse_timeout = RT, + socket_refs = Refs} = State) -> + {ok, Supervisor} = eradius_client_sup:socket_supervisor(Owner), + SockConfig = Config#{server_addr => ServerAddr, reqid_reuse_timeout => RT}, + case eradius_client_socket:new(Supervisor, SockConfig) of + {ok, Pid} -> + Ref = erlang:monitor(process, Pid), + Filler = #{pid => Pid, monitor => Ref, next_id => 0, issued => 0}, + {ok, Filler, State#state{socket_refs = Refs#{Ref => ServerAddr}}}; + {error, _} = Error -> + ?LOG(warning, "could not open RADIUS client socket for ~p: ~p", + [ServerAddr, Error]), + Error end. client_mngr_pid(SupPid) -> diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index 3cb46598..9cbbaf5d 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -63,7 +63,9 @@ common() -> clobber_does_not_hang, connected_socket_matches_reply, retire_holds_then_closes, - retire_waits_for_pending + retire_waits_for_pending, + client_config_defaults, + pool_rolls_and_retires ]. -spec groups() -> [ct_suite:ct_group_def(), ...]. @@ -445,3 +447,51 @@ retire_waits_for_pending(Config) -> ct:fail("retired socket did not close after pending drained") end, gen_udp:close(BH). + +client_config_defaults() -> + [{doc, "new client config carries no_ports (K), max_ports and " + "reqid_reuse_timeout with sane defaults"}]. +client_config_defaults(Config) -> + Family = proplists:get_value(family, Config, ipv4), + {ok, _} = application:ensure_all_started(eradius), + Server = #{ip => eradius_test_lib:localhost(Family, native), port => 1812, + secret => <<"secret">>, retries => 3}, + {ok, Client} = + eradius_client_mngr:start_client( + #{family => eradius_test_lib:inet_family(Family), ip => any, + servers => #{test_server => Server}}), + St = eradius_client_mngr:get_state(Client), + ?equal(10, maps:get(k_ports, St)), + ?equal(256, maps:get(max_ports, St)), + ?equal(30000, maps:get(reqid_reuse_timeout, St)), + ok. + +pool_rolls_and_retires() -> + [{doc, "with no_ports=1 the single filler issues ids 0..255 then rolls to a " + "fresh socket on the 257th send; the exhausted socket is retired"}]. +pool_rolls_and_retires(Config) -> + Family = proplists:get_value(family, Config, ipv4), + {ok, _} = application:ensure_all_started(eradius), + Server = #{ip => eradius_test_lib:localhost(Family, native), port => 1812, + secret => <<"secret">>, retries => 3}, + {ok, Client} = + eradius_client_mngr:start_client( + #{family => eradius_test_lib:inet_family(Family), ip => any, + no_ports => 1, reqid_reuse_timeout => 30000, + servers => #{test_server => Server}}), + Allocs = + [begin + {ok, {Pid, ReqId, test_server, _Srv, _Info}} = + eradius_client_mngr:wanna_send(Client, [test_server], []), + {Pid, ReqId} + end || _ <- lists:seq(1, 257)], + {Pids, Ids} = lists:unzip(Allocs), + ?equal(lists:seq(0, 255) ++ [0], Ids), + First256 = lists:sublist(Pids, 256), + ?equal(1, length(lists:usort(First256))), + Socket257 = lists:nth(257, Pids), + ?equal(false, lists:member(Socket257, First256)), + ?equal(2, length(lists:usort(Pids))), + %% the retired socket was told to retire but is still alive (cooling, 30s) + ?equal(true, is_process_alive(hd(First256))), + ok. From 7ad167a33625eb851926dfec6f1e4e901f9e2a4c Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 4 Jun 2026 10:47:25 +0200 Subject: [PATCH 32/38] test: per-server pool cap backpressures with {error, no_ports} Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- test/eradius_client_SUITE.erl | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index 9cbbaf5d..0b3175ab 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -65,7 +65,8 @@ common() -> retire_holds_then_closes, retire_waits_for_pending, client_config_defaults, - pool_rolls_and_retires + pool_rolls_and_retires, + pool_cap_backpressures ]. -spec groups() -> [ct_suite:ct_group_def(), ...]. @@ -495,3 +496,28 @@ pool_rolls_and_retires(Config) -> %% the retired socket was told to retire but is still alive (cooling, 30s) ?equal(true, is_process_alive(hd(First256))), ok. + +pool_cap_backpressures() -> + [{doc, "with no_ports=1 and max_ports_per_server=2, once both sockets are " + "exhausted-and-cooling wanna_send returns {error, no_ports}"}]. +pool_cap_backpressures(Config) -> + Family = proplists:get_value(family, Config, ipv4), + {ok, _} = application:ensure_all_started(eradius), + Server = #{ip => eradius_test_lib:localhost(Family, native), port => 1812, + secret => <<"secret">>, retries => 3}, + {ok, Client} = + eradius_client_mngr:start_client( + #{family => eradius_test_lib:inet_family(Family), ip => any, + no_ports => 1, max_ports_per_server => 2, + reqid_reuse_timeout => 60000, %% long: cooling sockets stay open + servers => #{test_server => Server}}), + %% 512 allocations exhaust 2 sockets (256 ids each); both go to cooling and + %% cannot be replaced (cap = 2). The 513th allocation must be rejected. + ok = lists:foreach( + fun(_) -> + {ok, {_Pid, _Id, test_server, _S, _I}} = + eradius_client_mngr:wanna_send(Client, [test_server], []) + end, lists:seq(1, 512)), + ?equal({error, no_ports}, + eradius_client_mngr:wanna_send(Client, [test_server], [])), + ok. From 992696094d01f9badf2d391d2da1e4a4dead7a85 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 4 Jun 2026 10:49:48 +0200 Subject: [PATCH 33/38] client_mngr: reclaim pool slots when sockets exit (DOWN handler) Monitors on filler/cooling sockets let the manager drop a socket from its pool when it closes (post-cooldown) or crashes, keeping the cap accounting accurate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- src/eradius_client_mngr.erl | 21 +++++++++++++++++++-- test/eradius_client_SUITE.erl | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index a0ef1d1c..f07831bd 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -334,6 +334,18 @@ handle_call(_OtherCall, _From, State) -> %% @private handle_cast(_Msg, State) -> {noreply, State}. +%% @private +handle_info({'DOWN', Ref, process, Pid, _Reason}, + #state{socket_refs = Refs} = State) -> + case maps:take(Ref, Refs) of + {ServerAddr, Refs1} -> + Pool = pool_of(ServerAddr, State), + Pool1 = remove_socket(Pid, Ref, Pool), + {noreply, put_pool(ServerAddr, Pool1, State#state{socket_refs = Refs1})}; + error -> + {noreply, State} + end; + %% @private handle_info({timeout, _, {reset, Peer}}, #state{servers = Servers0} = State0) -> Servers = @@ -526,10 +538,15 @@ put_pool(ServerAddr, Pool, #state{pools = Pools} = State) -> State#state{pools = Pools#{ServerAddr => Pool}}. %% active fillers + cooling (retired-but-open) sockets. cooling pids are reclaimed -%% when their socket exits, by the 'DOWN' handler (Task A7); until then pool_total -%% only grows, so allocate may report {error, no_ports} once the cap is reached. +%% when their socket exits (the 'DOWN' handler), bounding the per-server total. pool_total(#{active := A, cooling := C}) -> length(A) + length(C). +%% Drop a dead socket (by pid) from a pool, whether it was an active filler or +%% a cooling socket. +remove_socket(Pid, _Ref, #{active := Active, cooling := Cooling} = Pool) -> + Pool#{active := [F || F <- Active, maps:get(pid, F) =/= Pid], + cooling := lists:delete(Pid, Cooling)}. + %% Allocate {Pid, ReqId} for ServerAddr, growing/rolling the pool as needed. -spec allocate(server_addr(), #state{}) -> {ok, pid(), 0..255, #state{}} | {error, no_ports, #state{}}. diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index 0b3175ab..d22d850e 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -66,7 +66,8 @@ common() -> retire_waits_for_pending, client_config_defaults, pool_rolls_and_retires, - pool_cap_backpressures + pool_cap_backpressures, + cooling_socket_reclaimed ]. -spec groups() -> [ct_suite:ct_group_def(), ...]. @@ -521,3 +522,34 @@ pool_cap_backpressures(Config) -> ?equal({error, no_ports}, eradius_client_mngr:wanna_send(Client, [test_server], [])), ok. + +cooling_socket_reclaimed() -> + [{doc, "after a retired socket finishes its cooldown and exits, the manager " + "drops it from the pool (cooling shrinks back to empty)"}]. +cooling_socket_reclaimed(Config) -> + Family = proplists:get_value(family, Config, ipv4), + {ok, _} = application:ensure_all_started(eradius), + Server = #{ip => eradius_test_lib:localhost(Family, native), port => 1812, + secret => <<"secret">>, retries => 3}, + {ok, Client} = + eradius_client_mngr:start_client( + #{family => eradius_test_lib:inet_family(Family), ip => any, + no_ports => 1, reqid_reuse_timeout => 400, + servers => #{test_server => Server}}), + %% 256 allocations exhaust the single filler -> it retires (cooling=1), + %% and a replacement filler opens (active=1). The client has exactly one + %% server, so read its sole pool via maps:values (avoids family-mapped keys). + ok = lists:foreach( + fun(_) -> + {ok, _} = eradius_client_mngr:wanna_send(Client, [test_server], []) + end, lists:seq(1, 256)), + #{pools := P0} = eradius_client_mngr:get_state(Client), + [#{cooling := Cool0}] = maps:values(P0), + ?equal(1, length(Cool0)), + %% wait out the cooldown (400ms); the cooling socket exits and is reclaimed + timer:sleep(1200), + #{pools := P1} = eradius_client_mngr:get_state(Client), + [#{active := Act1, cooling := Cool1}] = maps:values(P1), + ?equal(0, length(Cool1)), + ?equal(1, length(Act1)), + ok. From 1a29d961ee21a1cdb5190a45a497799d4d9cedc7 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 4 Jun 2026 11:00:20 +0200 Subject: [PATCH 34/38] client_mngr: reconfigure closes/rebuilds pools; rewrite legacy tests Address change closes all pool sockets (and demonitors) then resets; removed servers have their pools dropped. Legacy socket-array/idcounters test assertions rewritten against the pool model; no_ports_one_wraps removed (superseded by pool_rolls_and_retires). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- src/eradius_client_mngr.erl | 53 +++++++++-- test/eradius_client_SUITE.erl | 172 ++++++++-------------------------- 2 files changed, 83 insertions(+), 142 deletions(-) diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index f07831bd..2b78cf9c 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -319,8 +319,14 @@ handle_call({failed, _Peer}, _From, State) -> handle_call({reconfigure, Opts}, _From, #state{config = OConfig} = State0) -> case client_config(maps:merge(OConfig, Opts)) of {ok, #{servers := Servers} = Config} -> - State1 = State0#state{config = Config, servers = Servers}, - State = reconfigure_address(Config, State1), + State1 = State0#state{ + config = Config, servers = Servers, + max_ports = maps:get(max_ports_per_server, Config, + ?DEFAULT_MAX_PORTS_PER_SERVER), + reqid_reuse_timeout = maps:get(reqid_reuse_timeout, Config, + ?DEFAULT_REQID_REUSE_TIMEOUT)}, + State2 = reconfigure_address(Config, State1), + State = drop_removed_pools(Servers, State2), {reply, ok, State}; {error, _} = Error -> @@ -390,9 +396,6 @@ where(ServerName) -> socket_id(#{family := Family, ip := IP}) -> {Family, IP}. -%% Retained for an upcoming task that reworks reconfigure_address to close -%% sockets and log the new client address. --compile({nowarn_unused_function, socket_id_str/1}). socket_id_str({_, IP}) when is_tuple(IP) -> inet:ntoa(IP); socket_id_str({_, IP}) when is_atom(IP) -> @@ -516,15 +519,47 @@ reconfigure_address(#{no_ports := NPorts} = Config, #state{socket_id = OAdd, socket_refs = Refs} = State) -> NAdd = socket_id(Config), case OAdd of - NAdd -> reconfigure_ports(NPorts, State); - _ -> - %% address changed: drop all pools. Demonitor first so the abandoned - %% sockets' DOWNs don't linger in the mailbox. (Task A8 also closes them.) + NAdd -> + reconfigure_ports(NPorts, State); + _ -> + ?LOG(info, "Reopening RADIUS client sockets (client_ip changed to ~s)", + [socket_id_str(NAdd)]), + close_all_pools(State), maps:foreach(fun(Ref, _SA) -> erlang:demonitor(Ref, [flush]) end, Refs), State#state{socket_id = NAdd, k_ports = NPorts, pools = #{}, socket_refs = #{}} end. +close_all_pools(#state{pools = Pools}) -> + maps:foreach( + fun(_ServerAddr, Pool) -> + lists:foreach(fun eradius_client_socket:close/1, pool_pids(Pool)) + end, Pools), + ok. + +%% Drop pools whose server is no longer configured; close their sockets and +%% demonitor them. +drop_removed_pools(Servers, #state{pools = Pools} = State) -> + Active = sets:from_list( + maps:fold(fun(_, #{ip := IP, port := Port}, Acc) -> [{IP, Port} | Acc]; + (_, _Pool, Acc) -> Acc + end, [], Servers)), + Drop = [SA || SA <- maps:keys(Pools), not sets:is_element(SA, Active)], + lists:foldl(fun drop_one_pool/2, State, Drop). + +drop_one_pool(ServerAddr, #state{pools = Pools, socket_refs = Refs} = State) -> + lists:foreach(fun eradius_client_socket:close/1, + pool_pids(maps:get(ServerAddr, Pools))), + Refs1 = + maps:filter( + fun(Ref, SA) when SA =:= ServerAddr -> erlang:demonitor(Ref, [flush]), false; + (_Ref, _SA) -> true + end, Refs), + State#state{pools = maps:remove(ServerAddr, Pools), socket_refs = Refs1}. + +pool_pids(#{active := Active, cooling := Cooling}) -> + [maps:get(pid, F) || F <- Active] ++ Cooling. + reconfigure_ports(NPorts, State) -> State#state{k_ports = NPorts}. diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index d22d850e..833d18ab 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -59,7 +59,6 @@ common() -> wanna_send, send_request_failover, check_upstream_servers, - no_ports_one_wraps, clobber_does_not_hang, connected_socket_matches_reply, retire_holds_then_closes, @@ -135,6 +134,13 @@ init_per_testcase(check_upstream_servers, Config) -> Config; init_per_testcase(wanna_send, Config) -> start_client(Config), + %% The named client persists across the group; a preceding reconf_address + %% may have left it bound to a non-existent IP. Restore a usable client_ip + %% and the default port count so socket opens succeed. + Family = proplists:get_value(family, Config), + ok = eradius_client_mngr:reconfigure( + ?SERVER, #{ip => eradius_test_lib:localhost(Family, native), + no_ports => 10}), Config; init_per_testcase(_Test, Config) -> Config. @@ -151,135 +157,54 @@ end_per_testcase(check_upstream_servers, Config) -> end_per_testcase(_Test, Config) -> Config. -%% STUFF - -getSocketCount() -> - eradius_client_mngr:get_socket_count(?SERVER). - -testSocket(undefined) -> - true; -testSocket(Pid) -> - not is_process_alive(Pid). - -split(N, List) -> split2(N, [], List). - -split2(0, List1, List2) -> {lists:reverse(List1), List2}; -split2(_, List1, []) -> {lists:reverse(List1), []}; -split2(N, List1, [L|List2]) -> split2(N-1, [L|List1], List2). - -meckStart() -> - ok = meck:new(eradius_client_socket, [passthrough]), - ok = meck:expect(eradius_client_socket, init, - fun(_) -> {ok, undefined} end), - ok = meck:expect(eradius_client_socket, handle_call, - fun(_Request, _From, State) -> {noreply, State} end), - ok = meck:expect(eradius_client_socket, handle_cast, - fun(close, State) -> {stop, normal, State}; - (_Request, State) -> {noreply, State} end), - ok = meck:expect(eradius_client_socket, handle_info, - fun(_Info, State) -> {noreply, State} end), - ok. - -meckStop() -> - ok = meck:unload(eradius_client_socket). - -parse_ip(undefined) -> - {ok, undefined}; -parse_ip(any) -> - {ok, any}; -parse_ip(Address) when is_list(Address) -> - inet_parse:address(Address); -parse_ip(T = {_, _, _, _}) -> - {ok, T}; -parse_ip(T = {_, _, _, _, _, _, _, _}) -> - {ok, T}. - -%% CHECK - -test(true, _Msg) -> true; -test(false, Msg) -> - ct:pal("~s", [Msg]), - false. - -check(OldState, NewState = #{no_ports := P}, null, A) -> check(OldState, NewState, P, A); -check(OldState, NewState = #{socket_id := {_, A}}, P, null) -> check(OldState, NewState, P, A); -check(#{sockets := OS, no_ports := _OP, idcounters := _OC, socket_id := {_, OA}}, - #{sockets := NS, no_ports := NP, idcounters := NC, socket_id := {_, NA}}, - P, A) -> - {ok, PA} = parse_ip(A), - test(PA == NA, "Address not configured") and - case NA of - OA -> - ct:pal("NP: ~p, NC: ~p", [NP, NC]), - {_, Rest} = split(NP, array:to_list(OS)), - test(P == NP,"Ports not configured") and - test(maps:fold( fun(_Peer, {NextPortIdx, _NextReqId}, Akk) -> - Akk and (NextPortIdx =< NP) - end, true, NC), "Invalid port counter") and - test(getSocketCount() =< NP, "Sockets not closed") and - test(array:size(NS) =< NP, "Socket array not resized") and - test(lists:all(fun(Pid) -> testSocket(Pid) end, Rest), "Sockets still available"); - _ -> - test(array:size(NS) == 0, "Socket array not cleaned") and - test(getSocketCount() == 0, "Sockets not closed") and - test(lists:all(fun(Pid) -> testSocket(Pid) end, array:to_list(OS)), "Sockets still available") - end. - %% TESTS send_request(_Config) -> ?equal(accept, eradius_test_handler:send_request(one)), ok. -send(FUN, Ports, Address) -> - meckStart(), - OldState = eradius_client_mngr:get_state(?SERVER), - FUN(), - NewState = eradius_client_mngr:get_state(?SERVER), - true = check(OldState, NewState, Ports, Address), - meckStop(). +%% true iff every pool keeps at most K active fillers (family-agnostic). +all_pools_within_k(St, K) -> + lists:all(fun(#{active := A}) -> length(A) =< K end, + maps:values(maps:get(pools, St))). wanna_send(_Config) -> - lists:map(fun(X) -> - Server = binary_to_atom(<<(X+$A)>>), - FUN = fun() -> eradius_client_mngr:wanna_send(?SERVER, [Server], []) end, - send(FUN, null, null) - end, lists:seq(0, 9)). + %% Allocations stay within K active fillers per server. Server `one` + %% is configured by eradius_test_handler:start_client/2 at port 1812. + K = 10, + lists:foreach( + fun(_) -> + {ok, {_Pid, _Id, one, _S, _I}} = + eradius_client_mngr:wanna_send(?SERVER, [one], []) + end, lists:seq(1, 50)), + St = eradius_client_mngr:get_state(?SERVER), + ?equal(true, all_pools_within_k(St, K)), + ok. reconf_address(Config) -> IP = case proplists:get_value(family, Config) of - ipv4 -> - {7, 13, 23, 42}; - ipv4_mapped_ipv6 -> - inet:ipv4_mapped_ipv6_address({7, 13, 23, 42}); - ipv6 -> - {16#fd96, 16#dcd2, 16#efdb, 16#41c3, 0, 0, 16#100, 1} + ipv4 -> {7, 13, 23, 42}; + ipv4_mapped_ipv6 -> inet:ipv4_mapped_ipv6_address({7, 13, 23, 42}); + ipv6 -> {16#fd96, 16#dcd2, 16#efdb, 16#41c3, 0, 0, 16#100, 1} end, - FUN = fun() -> - eradius_client_mngr:reconfigure(?SERVER, #{ip => IP}), - %% socket shutdown is done asynchronous, - %% the tests need to wait a bit for it to finish. - timer:sleep(100) - end, - send(FUN, null, inet:ntoa(IP)). + {ok, _} = eradius_client_mngr:wanna_send(?SERVER, [one], []), + ok = eradius_client_mngr:reconfigure(?SERVER, #{ip => IP}), + timer:sleep(100), + St = eradius_client_mngr:get_state(?SERVER), + ?equal(#{}, maps:get(pools, St)), + ok. reconf_ports_30(_Config) -> - FUN = fun() -> - eradius_client_mngr:reconfigure(?SERVER, #{no_ports => 30}), - %% socket shutdown is done asynchronous, - %% the tests need to wait a bit for it to finish. - timer:sleep(100) - end, - send(FUN, 30, null). + ok = eradius_client_mngr:reconfigure(?SERVER, #{no_ports => 30}), + St = eradius_client_mngr:get_state(?SERVER), + ?equal(30, maps:get(k_ports, St)), + ok. reconf_ports_10(_Config) -> - FUN = fun() -> - eradius_client_mngr:reconfigure(?SERVER, #{no_ports => 10}), - %% socket shutdown is done asynchronous, - %% the tests need to wait a bit for it to finish. - timer:sleep(100) - end, - send(FUN, 10, null). + ok = eradius_client_mngr:reconfigure(?SERVER, #{no_ports => 10}), + St = eradius_client_mngr:get_state(?SERVER), + ?equal(10, maps:get(k_ports, St)), + ok. send_request_failover(Config) -> Family = proplists:get_value(family, Config), @@ -298,25 +223,6 @@ check_upstream_servers(Config) -> sets:from_list(Servers))), ok. -no_ports_one_wraps() -> - [{doc, "wanna_send must not crash when no_ports = 1 and the req-id wraps past 255"}]. -no_ports_one_wraps(Config) -> - Family = proplists:get_value(family, Config, ipv4), - {ok, _} = application:ensure_all_started(eradius), - Server = #{ip => eradius_test_lib:localhost(Family, native), port => 1812, - secret => <<"secret">>, retries => 3}, - {ok, Client} = - eradius_client_mngr:start_client( - #{family => eradius_test_lib:inet_family(Family), ip => any, no_ports => 1, - servers => #{test_server => Server}}), - %% 257 allocations force the {PortIdx, 255} wrap branch at least once - lists:foreach( - fun(_) -> - ?match({ok, {_Sock, _ReqId, test_server, _Srv, _Info}}, - eradius_client_mngr:wanna_send(Client, [test_server], [])) - end, lists:seq(1, 257)), - ok. - clobber_does_not_hang() -> [{doc, "A pending request whose entry is overwritten by a same-ReqId " "request must still return {error,timeout} to its caller, not hang"}]. From 2c82ce32588f62d201c98b655751ae7c562320f5 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 4 Jun 2026 11:28:29 +0200 Subject: [PATCH 35/38] test: source-port rotation across id wrap; metrics sum over source ports Adds rotation_uses_fresh_source_ports (across a 256-id wrap the client sends from a fresh OS source port; no {srcport,id} pair repeats). With the K-filler pool a RADIUS server sees the client across several source ports, so eradius_metrics_SUITE sums the server-side per-nas_ip request/reply counters instead of expecting a single label-set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- test/eradius_client_SUITE.erl | 55 +++++++++++++++++++++++++++++++++- test/eradius_metrics_SUITE.erl | 23 +++++++++++--- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index 833d18ab..8318e877 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -66,7 +66,8 @@ common() -> client_config_defaults, pool_rolls_and_retires, pool_cap_backpressures, - cooling_socket_reclaimed + cooling_socket_reclaimed, + rotation_uses_fresh_source_ports ]. -spec groups() -> [ct_suite:ct_group_def(), ...]. @@ -459,3 +460,55 @@ cooling_socket_reclaimed(Config) -> ?equal(0, length(Cool1)), ?equal(1, length(Act1)), ok. + +rotation_uses_fresh_source_ports() -> + [{doc, "across a 256-id wrap the client sends from a different source port, " + "so no {srcport, ReqId} pair repeats while the old port is cooling"}]. +rotation_uses_fresh_source_ports(Config) -> + Family = proplists:get_value(family, Config, ipv4), + {ok, _} = application:ensure_all_started(eradius), + InetFamily = eradius_test_lib:inet_family(Family), + BindIP = eradius_test_lib:localhost(Family, mapped), + LH = eradius_test_lib:localhost(Family, native), + {ok, Server} = gen_udp:open(0, [binary, {active, false}, InetFamily, {ip, BindIP}]), + {ok, SrvPort} = inet:port(Server), + {ok, Client} = + eradius_client_mngr:start_client( + #{family => InetFamily, ip => any, no_ports => 1, + reqid_reuse_timeout => 60000, + servers => #{s => #{ip => LH, port => SrvPort, + secret => <<"secret">>, retries => 1}}}), + Test = self(), + %% echo server: reply to each request and report the observed source port + _Echo = spawn_link(fun() -> echo_loop(Server, Test) end), + %% 257 synchronous sends: ids run 0..255 on socket 1, then the 257th rolls + %% to a fresh socket (id 0 again) on a new OS-assigned source port. + Pairs = + [begin + {ok, {Pid, ReqId, s, _S, _I}} = + eradius_client_mngr:wanna_send(Client, [s], []), + {ok, _H, _B} = + eradius_client_socket:send_request( + Pid, {LH, SrvPort}, ReqId, <<1, ReqId, 0, 20, 0:128>>, 2000), + receive + {observed, Port, ReqId} -> {Port, ReqId} + after 2000 -> + ct:fail("server did not observe request id ~p", [ReqId]) + end + end || _ <- lists:seq(1, 257)], + gen_udp:close(Server), + %% no {source port, ReqId} pair repeats within the cooldown window + ?equal(length(Pairs), length(lists:usort(Pairs))), + %% the id-0 wrap landed on a second, distinct source port + ?equal(true, length(lists:usort([P || {P, _} <- Pairs])) >= 2), + ok. + +echo_loop(Server, Test) -> + case gen_udp:recv(Server, 0, 5000) of + {ok, {FromIP, FromPort, <<_, ReqId, _/binary>>}} -> + gen_udp:send(Server, FromIP, FromPort, <<2, ReqId, 0, 20, 0:128>>), + Test ! {observed, FromPort, ReqId}, + echo_loop(Server, Test); + {error, _} -> + ok + end. diff --git a/test/eradius_metrics_SUITE.erl b/test/eradius_metrics_SUITE.erl index 28455667..9a573872 100644 --- a/test/eradius_metrics_SUITE.erl +++ b/test/eradius_metrics_SUITE.erl @@ -186,11 +186,26 @@ check_single_request(error, EradiusRequestType, _RequestType, _ResponseType) -> ok. check_total_requests(good, N) -> - check_metric(eradius_requests_total, [{server_name, good}], N), - check_metric(eradius_replies_total, [{server_name, good}], N); + check_metric_sum(eradius_requests_total, [{server_name, good}], N), + check_metric_sum(eradius_replies_total, [{server_name, good}], N); check_total_requests(bad, N) -> - check_metric(eradius_requests_total, [{server_name, bad}], N), - check_metric(eradius_replies_total, [{server_name, bad}], N). + check_metric_sum(eradius_requests_total, [{server_name, bad}], N), + check_metric_sum(eradius_replies_total, [{server_name, bad}], N). + +%% Sum a server-side counter across all matching label-sets. The client's +%% per-server pool issues requests from several source ports (round-robin over +%% K fillers), so the server records them under several nas_ip label-sets; the +%% total across those must equal N. +check_metric_sum(Id, Labels, Count) -> + Values = prometheus_counter:values(default, Id), + Filtered = + lists:filter( + fun({ValueLabels, _}) -> Labels -- ValueLabels =:= [] end, + Values), + Sum = lists:sum([V || {_, V} <- Filtered]), + ct:pal("check_metric_sum: ~p, ~p, expect ~p~nFiltered ~p~n", + [Id, Labels, Count, Filtered]), + ?assertEqual(Count, Sum). check_metric_multi({bad_type, accreq}, Id, Labels, _Count) -> Values = prometheus_counter:values(default, Id), From 0174187a9e81d73805565f15b0b23967a21d7b1b Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 4 Jun 2026 11:39:28 +0200 Subject: [PATCH 36/38] client: temporary pool sockets; doc/typespec/dead-code cleanup - socket_sup: restart => temporary. Pool sockets are manager-managed (opened on demand, monitored, reclaimed on exit); auto-restart would orphan a socket the manager never learns about. - Refresh the eradius_client_mngr 'Socket pool' moduledoc for the per-server dynamic pool (no_ports=K, reqid_reuse_timeout, max_ports_per_server). - Add max_ports_per_server/reqid_reuse_timeout to client_config() type; tighten no_ports to pos_integer(). - Remove now-unused test-only get_socket_count/1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- src/eradius_client_mngr.erl | 26 ++++++++++++++------------ src/eradius_client_socket_sup.erl | 6 +++++- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index 2b78cf9c..f6c52437 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -39,9 +39,15 @@ Pool-based failover in `m:eradius_client` automatically skips unreachable server == Socket pool == -The client opens `no_ports` UDP sockets (default: 1) on OS-assigned ports. -Each socket supports up to 256 concurrent requests (one per RADIUS request id). -Increase `no_ports` for higher concurrency requirements. +For each RADIUS server the client maintains a dynamic pool of connected UDP sockets +on OS-assigned source ports. Up to `no_ports` (default: 10) sockets are actively +issuing request ids at once; each socket issues request ids 0..255 exactly once, then +is retired — held open for `reqid_reuse_timeout` ms (default: 30000) so its source port +is not reused while the server's duplicate-detection window is still open — and finally +closed. Reusing a request id therefore always happens on a fresh source port, which the +server sees as a distinct client. The total number of sockets per server is bounded by +`max_ports_per_server` (default: 256); once reached, `wanna_send` returns `{error, no_ports}` +so the caller can apply backpressure. """. -behaviour(gen_server). @@ -58,8 +64,8 @@ Increase `no_ports` for higher concurrency requirements. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -ifdef(TEST). --export([get_state/1, servers/1, server/2, get_socket_count/1]). --ignore_xref([get_state/1, servers/1, server/2, get_socket_count/1]). +-export([get_state/1, servers/1, server/2]). +-ignore_xref([get_state/1, servers/1, server/2]). -endif. -ignore_xref([start_client/1, start_client/2]). @@ -117,7 +123,9 @@ Increase `no_ports` for higher concurrency requirements. family := inet | inet6, ip := any | inet:ip_address(), active_n := once | non_neg_integer(), - no_ports := non_neg_integer(), + no_ports := pos_integer(), + max_ports_per_server => pos_integer(), + reqid_reuse_timeout => pos_integer(), recbuf := non_neg_integer(), sndbuf := non_neg_integer(), metrics_callback := 'undefined' | eradius_req:metrics_callback() @@ -227,12 +235,6 @@ get_state(ServerRef) -> Values = tl(tuple_to_list(State)), maps:from_list(lists:zip(Keys, Values)). -get_socket_count(ServerRef) -> - #state{owner = Owner} = sys:get_state(ServerRef), - {ok, SockSup} = eradius_client_sup:socket_supervisor(Owner), - Counts = supervisor:count_children(SockSup), - proplists:get_value(active, Counts). - servers(ServerRef) -> #state{servers = Servers} = sys:get_state(ServerRef), maps:fold( diff --git a/src/eradius_client_socket_sup.erl b/src/eradius_client_socket_sup.erl index db2d5957..fdd03e67 100644 --- a/src/eradius_client_socket_sup.erl +++ b/src/eradius_client_socket_sup.erl @@ -45,9 +45,13 @@ init([]) -> intensity => 5, period => 10}, + %% Pool sockets are managed by eradius_client_mngr: it opens them on demand, + %% monitors them, and reclaims them on exit. They must NOT auto-restart -- a + %% restarted socket would be a pid the manager never learns about (an orphaned + %% FD/source port). The manager reopens a replacement on the next allocation. Child = #{id => eradius_client_socket, start => {eradius_client_socket, start_link, []}, - restart => transient, + restart => temporary, shutdown => 5000, type => worker, modules => [eradius_client_socket]}, From 390f8aa9d8ee0a0c5b9f8e2f1776b591b4f215b4 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 4 Jun 2026 11:54:36 +0200 Subject: [PATCH 37/38] client_mngr: use queue for the active filler ring The active filler list is a FIFO round-robin (take head, re-enqueue at tail; new fillers appended; retired head dropped). Model it with the queue module: queue:out/in for the rotation, queue:filter for by-pid removal on DOWN. No behaviour change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- src/eradius_client_mngr.erl | 26 +++++++++++++------------- test/eradius_client_SUITE.erl | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index f6c52437..a9b1eb53 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -161,7 +161,7 @@ so the caller can apply backpressure. -type server_addr() :: {inet:ip_address(), inet:port_number()}. -type filler() :: #{pid := pid(), monitor := reference(), next_id := 0..255, issued := 0..256}. --type pool() :: #{active := [filler()], cooling := [pid()]}. +-type pool() :: #{active := queue:queue(filler()), cooling := [pid()]}. %%%========================================================================= %%% API @@ -560,13 +560,13 @@ drop_one_pool(ServerAddr, #state{pools = Pools, socket_refs = Refs} = State) -> State#state{pools = maps:remove(ServerAddr, Pools), socket_refs = Refs1}. pool_pids(#{active := Active, cooling := Cooling}) -> - [maps:get(pid, F) || F <- Active] ++ Cooling. + [maps:get(pid, F) || F <- queue:to_list(Active)] ++ Cooling. reconfigure_ports(NPorts, State) -> State#state{k_ports = NPorts}. -spec new_pool() -> pool(). -new_pool() -> #{active => [], cooling => []}. +new_pool() -> #{active => queue:new(), cooling => []}. pool_of(ServerAddr, #state{pools = Pools}) -> maps:get(ServerAddr, Pools, new_pool()). @@ -576,12 +576,12 @@ put_pool(ServerAddr, Pool, #state{pools = Pools} = State) -> %% active fillers + cooling (retired-but-open) sockets. cooling pids are reclaimed %% when their socket exits (the 'DOWN' handler), bounding the per-server total. -pool_total(#{active := A, cooling := C}) -> length(A) + length(C). +pool_total(#{active := A, cooling := C}) -> queue:len(A) + length(C). %% Drop a dead socket (by pid) from a pool, whether it was an active filler or %% a cooling socket. remove_socket(Pid, _Ref, #{active := Active, cooling := Cooling} = Pool) -> - Pool#{active := [F || F <- Active, maps:get(pid, F) =/= Pid], + Pool#{active := queue:filter(fun(F) -> maps:get(pid, F) =/= Pid end, Active), cooling := lists:delete(Pid, Cooling)}. %% Allocate {Pid, ReqId} for ServerAddr, growing/rolling the pool as needed. @@ -590,15 +590,15 @@ remove_socket(Pid, _Ref, #{active := Active, cooling := Cooling} = Pool) -> allocate(ServerAddr, State0) -> State1 = ensure_active(ServerAddr, State0), Pool = pool_of(ServerAddr, State1), - case maps:get(active, Pool) of - [] -> + case queue:out(maps:get(active, Pool)) of + {empty, _} -> {error, no_ports, State1}; - [#{pid := Pid, next_id := ReqId, issued := Issued0} = F | Rest] -> + {{value, #{pid := Pid, next_id := ReqId, issued := Issued0} = F}, Rest} -> Issued = Issued0 + 1, case Issued >= 256 of true -> %% filler exhausted: retire it (socket cools+closes itself), - %% move it to cooling, and open a replacement. + %% drop it from the active queue, and open a replacement. ok = eradius_client_socket:retire(Pid), Pool1 = Pool#{active := Rest, cooling := [Pid | maps:get(cooling, Pool)]}, @@ -606,9 +606,9 @@ allocate(ServerAddr, State0) -> State3 = ensure_active(ServerAddr, State2), {ok, Pid, ReqId, State3}; false -> - %% round-robin: advance this filler and move it to the tail. + %% round-robin: advance this filler and re-enqueue it at the tail. F1 = F#{next_id := (ReqId + 1) rem 256, issued := Issued}, - Pool1 = Pool#{active := Rest ++ [F1]}, + Pool1 = Pool#{active := queue:in(F1, Rest)}, {ok, Pid, ReqId, put_pool(ServerAddr, Pool1, State1)} end end. @@ -619,12 +619,12 @@ allocate(ServerAddr, State0) -> ensure_active(ServerAddr, #state{k_ports = K, max_ports = Max} = State) -> Pool = pool_of(ServerAddr, State), Active = maps:get(active, Pool), - case length(Active) < K andalso pool_total(Pool) < Max of + case queue:len(Active) < K andalso pool_total(Pool) < Max of true -> case open_filler(ServerAddr, State) of {ok, Filler, State1} -> Pool1 = pool_of(ServerAddr, State1), - Pool2 = Pool1#{active := maps:get(active, Pool1) ++ [Filler]}, + Pool2 = Pool1#{active := queue:in(Filler, maps:get(active, Pool1))}, ensure_active(ServerAddr, put_pool(ServerAddr, Pool2, State1)); {error, _} -> State diff --git a/test/eradius_client_SUITE.erl b/test/eradius_client_SUITE.erl index 8318e877..9e298990 100644 --- a/test/eradius_client_SUITE.erl +++ b/test/eradius_client_SUITE.erl @@ -166,7 +166,7 @@ send_request(_Config) -> %% true iff every pool keeps at most K active fillers (family-agnostic). all_pools_within_k(St, K) -> - lists:all(fun(#{active := A}) -> length(A) =< K end, + lists:all(fun(#{active := A}) -> queue:len(A) =< K end, maps:values(maps:get(pools, St))). wanna_send(_Config) -> @@ -458,7 +458,7 @@ cooling_socket_reclaimed(Config) -> #{pools := P1} = eradius_client_mngr:get_state(Client), [#{active := Act1, cooling := Cool1}] = maps:values(P1), ?equal(0, length(Cool1)), - ?equal(1, length(Act1)), + ?equal(1, queue:len(Act1)), ok. rotation_uses_fresh_source_ports() -> From d3a6f68b19a8d35a9de17779db12eedc8f6e7d16 Mon Sep 17 00:00:00 2001 From: Andreas Schultz <andreas.schultz@travelping.com> Date: Thu, 4 Jun 2026 12:28:25 +0200 Subject: [PATCH 38/38] client_mngr: delete_with for active filler removal (unique pid) A filler's pid is unique, so removing it from the active queue on DOWN is a single-element delete; queue:delete_with expresses that and short-circuits instead of rebuilding the whole queue like queue:filter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- src/eradius_client_mngr.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eradius_client_mngr.erl b/src/eradius_client_mngr.erl index a9b1eb53..c905743c 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -581,7 +581,7 @@ pool_total(#{active := A, cooling := C}) -> queue:len(A) + length(C). %% Drop a dead socket (by pid) from a pool, whether it was an active filler or %% a cooling socket. remove_socket(Pid, _Ref, #{active := Active, cooling := Cooling} = Pool) -> - Pool#{active := queue:filter(fun(F) -> maps:get(pid, F) =/= Pid end, Active), + Pool#{active := queue:delete_with(fun(F) -> maps:get(pid, F) =:= Pid end, Active), cooling := lists:delete(Pid, Cooling)}. %% Allocate {Pid, ReqId} for ServerAddr, growing/rolling the pool as needed.