diff --git a/.github/workflows/hex.yaml b/.github/workflows/hex.yaml index 85107a68..66dcda79 100644 --- a/.github/workflows/hex.yaml +++ b/.github/workflows/hex.yaml @@ -7,9 +7,9 @@ on: jobs: publish: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 container: - image: erlang:23.2.5.0-alpine + image: erlang:26.2-alpine steps: - name: Prepare run: | @@ -17,7 +17,7 @@ jobs: apk --no-cache upgrade apk --no-cache add gcc git libc-dev libc-utils libgcc linux-headers make bash \ musl-dev musl-utils ncurses-dev pcre2 pkgconf scanelf wget zlib - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: work around for permission issue run: | git config --global --add safe.directory /__w/eradius/eradius @@ -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 7fd1e840..ec4dcc42 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,21 +8,37 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - otp: [22, 23, 24, 25] + otp: [25.3, 26.0, 26.1, 26.2, 27.0-rc3] 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@v3 + uses: actions/checkout@v4 - 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 - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 if: always() steps: - name: Slack notification diff --git a/README.md b/README.md index 751bfaf1..4fc09217 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,45 @@ 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**: 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 eradius\_server and +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, [] } -``` -Each server is tuple ({}): -``` -Server == { , { , [] } } | { , { , [], } } -ExtraServerOptions == [] -ExtraSocketOptions == [{socket_opts, [socket_setopt()]}] (see: https://erlang.org/doc/man/inet.html#setopts-2) -ServerOption == {rate_config, | } -``` - -Rate configuration can be configured per server, in extra configuration, with a symbolic name or directly in server -``` -{SymbolicNameLimit, RateConfigList} -RateConfigList == [] -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 == Reference to a previously defined server. -Handler == { , [] } -``` - -If only one handler module is used, it can be defined globally as `{radius_callback, }`. -If more than one handler modules are used, they have to be given in the HandlerDefinition: - -``` -HandlerDefinition == {, , } | {, } -HandlerMod == Handler module to process the received requests. -NasId == String describing the Source. -HandlerArgs == List of arguments givent the handler module. -Source == {, } | {, , []} -SourceOption == {group, } | {nas_id, } - -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, [{, ['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}]}, - {{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 +78,11 @@ Example: [dictionary, dictionary_cisco, dictionary_travelping] ``` - + [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/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 +%% +%% 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, <>) -> +-spec ms_chap_v2(binary(), binary(), binary(), binary()) -> + {boolean(), eradius_req:attribute_list()}. +ms_chap_v2(UserName, Passwd, AuthenticatorChallenge, + <>) -> PasswdHash = nt_password_hash(Passwd), ExpectedResponse = v2_generate_nt_response(AuthenticatorChallenge, PeerChallenge, UserName, PasswdHash), @@ -211,7 +235,7 @@ ms_chap_v2(UserName, Passwd, AuthenticatorChallenge, < {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 ce58debb..319ca125 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 once (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,385 +30,111 @@ -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(), + server_name => atom(), + metrics_info => {atom(), atom(), atom()}}. --export_type([nas_address/0, options/0]). +-export_type([options/0]). +-define(DEFAULT_REQUEST_OPTS, #{retries => 3, timeout => 5_000, failover => []}). -define(SERVER, ?MODULE). %%%========================================================================= %%% API %%%========================================================================= -%% @equiv send_request(NAS, Request, []) --spec send_request(nas_address(), #radius_request{}) -> {ok, binary()} | {error, 'timeout' | 'socket_down'}. -send_request(NAS, Request) -> - send_request(NAS, Request, []). +%% @equiv send_request(NAS, 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. %% 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} | UpstreamServers]) of - [] -> - no_active_servers; - {{IP, Port, Secret}, _NewPool} -> - SendReqFn(); - {NewPeer, []} -> - %% Special case, we don't have servers in the pool anymore, but we need - %% to preserve `failover` option to mark current server as inactive if - %% it will fail - NewOptions = lists:keyreplace(failover, 1, Options, {failover, undefined}), - send_request(NewPeer, Request, NewOptions); - {NewPeer, NewPool} -> - %% current server is not in list of active servers, so use another one - NewOptions = lists:keyreplace(failover, 1, Options, {failover, NewPool}), - send_request(NewPeer, Request, NewOptions) - end - end; -send_request({_IP, _Port, _Secret}, _Request, _Options) -> - error(badarg). - -%% @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. --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; - {NewPeer, NewPool} -> - %% leave only active upstream servers - NewOptions = lists:keyreplace(failover, 1, Options, {failover, NewPool}), - send_request(NewPeer, Request, NewOptions) + {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 3a2e1a20..380e239d 100644 --- a/src/eradius_client_mngr.erl +++ b/src/eradius_client_mngr.erl @@ -3,149 +3,196 @@ %% %% 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 `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 `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} | Pool]) when is_list(Host) -> - try - IP = get_ip(Host), - find_suitable_peer([{IP, Port, Secret} | Pool]) - catch _:_ -> - %% can't resolve ip by some reasons, just ignore it - find_suitable_peer(Pool) - end; -find_suitable_peer([{IP, Port, Secret} | Pool]) -> - case ets:lookup(?MODULE, {IP, Port}) of - [] -> - find_suitable_peer(Pool); - [{{IP, Port}, _Retries, _InitialRetries}] -> - {{IP, Port, Secret}, Pool} - end; -find_suitable_peer([{IP, Port, Secret, _Opts} | 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 +200,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 +278,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 +305,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 +327,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 +502,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..4ae8e83f 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,9 +91,10 @@ 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 Response of + <> -> + <<_, ReqId:8, _/binary>> = Header, + NState = request_done({FromIP, FromPort, ReqId}, {ok, Header, Body}, State), case Mode of inactive when map_size(State#state.pending) =:= 0 -> {stop, normal, NState}; @@ -98,7 +102,7 @@ handle_info({udp, Socket, FromIP, FromPort, Response}, flow_control(NState), {noreply, NState} end; - {bad_pdu, _} -> + _ -> %% discard reply because it was malformed flow_control(State), {noreply, State} 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 +%% +%% 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 +%% +%% 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 +%% +%% 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), - <>. - -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 -> - <> = <>, - Wildcard = 16#ffffffff bsr Mask, - <> = << (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, B, C, D}]; -generate_ip(S, E) -> - <> = <>, - [{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 +%% +%% 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(<>) -> encode(Code, Id, Msg) -> Data = encode_payload(Code, Msg), Len = size(Data) + 4, - <>. + <<(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 +%% +%% 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, <>}. - -%% @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, <>, Body, Req#radius_request.secret]), - <>. - --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 = <>, - HMAC = message_authenticator(Req#radius_request.secret, [Head, ReqAuth, Body, <>]), - {<>, BodySize + 2 + 16}. - -chunk(Bin, Length) -> - case Bin of - <> -> {First, Rest}; - _ -> {Bin, <<>>} - end. - -encode_eap_attribute({<<>>, _}, EncReq) -> - EncReq; -encode_eap_attribute({Value, Rest}, {Body, BodySize}) -> - EncAttr = <>, - EncReq = {<>, 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), - {<>, BodySize + byte_size(EncAttr)}; - ({ID, Val}, {Body, BodySize}) -> - case eradius_dict:lookup(attribute, ID) of - AttrRec = #attribute{} -> - EncAttr = encode_attribute(Req, AttrRec, Val), - {<>, 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, - <>; -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, - <>; -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, - <>. - --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) -> - <>; -encode_value(integer24, V) -> - <>; -encode_value(integer64, V) -> - <>; -encode_value(ipaddr, {A,B,C,D}) -> - <>; -encode_value(ipv6addr, {A,B,C,D,E,F,G,H}) -> - <>; -encode_value(ipv6prefix, {{A,B,C,D,E,F,G,H}, PLen}) -> - L = (PLen + 7) div 8, - <> = <>, - <<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) -> - <>; -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}}), - <>. - -%% ------------------------------------------------------------------------------------------ -%% -- 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(<>, 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, <>, 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 - <> -> - case message_authenticator(Secret, [<>, 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, <>, 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, - 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(<>, Req, #attribute{id = ?RVendor_Specific}, Pos, State) -> - decode_vendor_specific_attribute(Req, VendorID, ValueBin, Pos + 4, State); -decode_attribute(<>, _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(<>, 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(<>, 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 = <>, 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(<>, 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 -> - <> = Bin, - {B,C,D,E}; - ipv6addr -> - <> = Bin, - {B,C,D,E,F,G,H,I}; - ipv6prefix -> - <<0,PLen,P/binary>> = Bin, - <> = 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; - _ -> 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}, - _, <>, scramble) -> - scramble(Secret, Authenticator, Val); -decrypt_value(#radius_request{secret = Secret}, - #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(Secret, Authenticator, Val); -decrypt_value(_Req, _State, <>, _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, <>, Pos, State) -> - ValueLength = ChunkLength - 2, - <> = 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, <>) -> - 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..f830f6af 100644 --- a/src/eradius_log.erl +++ b/src/eradius_log.erl @@ -1,148 +1,66 @@ -%% 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}. +-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). + +-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]]. + +-spec line(eradius_req:req()) -> iolist(). +line(#{cmd := Cmd, req_id := ReqId, nas := {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]). + +-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, nas := {IP, Port}}) -> + <<(format_ip(IP))/binary, $:, (i2b(Port))/binary, " [", (i2b(ReqId))/binary, $]>>. format_cmd(request) -> <<"Access-Request">>; format_cmd(accept) -> <<"Access-Accept">>; @@ -160,8 +78,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 +159,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])). @@ -296,8 +215,8 @@ hexchar(X) when X >= 10, X < 16 -> 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..7f893524 --- /dev/null +++ b/src/eradius_metrics_prometheus.erl @@ -0,0 +1,566 @@ +%% Copyright (c) 2024, Travelping GmbH <info@travelping.com> +%% +%% SPDX-License-Identifier: MIT +-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 +%%%========================================================================= + +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 +%%%========================================================================= + +-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). + +-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 5961b13b..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}]}, -%% {{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 87e358e7..6ba799d8 100644 --- a/src/eradius_server.erl +++ b/src/eradius_server.erl @@ -1,90 +1,84 @@ -%% @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()}. +-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() + }. +-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() + }. +-type server_opts() :: #{server_name => term(), + socket_opts => socket_opts(), + handler := {module(), term()}, + metrics_callback => eradius_req:metrics_callback(), + clients := map()}. +-type server_config() :: #{server_name := term(), + socket_opts := socket_config(), + handler := {module(), term()}, + metrics_callback := undefined | eradius_req:metrics_callback(), + clients := map()}. +-type client() :: #{client := binary(), + secret := eradius_req:secret()}. +-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]). @@ -92,400 +86,333 @@ -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(info, "~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(info, "~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) -> +%%%========================================================================= +%%% 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_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) -> - ok. +server_name(_, _, #{server_name := ServerName}) -> + ServerName; +server_name(IP, Port, _) -> + iolist_to_binary(server_name(IP, Port)). -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(IP, Port) -> + [inet:ntoa(IP), $:, integer_to_list(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. +to_map(Opts) when is_list(Opts) -> + maps:from_list(Opts); +to_map(Opts) when is_map(Opts) -> + Opts. -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. - -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 a7a34386..5f21db26 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, 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, {tuple, native}), 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, {tuple, native}), 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, {tuple, native}), 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, {tuple, native}), 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..bdbf1266 100644 --- a/test/eradius_metrics_SUITE.erl +++ b/test/eradius_metrics_SUITE.erl @@ -21,11 +21,13 @@ -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"}]). @@ -33,47 +35,22 @@ -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), - {ok, _} = application:ensure_all_started(eradius), - spawn(fun() -> - eradius:modules_ready([?MODULE]), - timer:sleep(infinity) - end), + application:set_env(eradius, unreachable_timeout, 2), + {ok, _} = application:ensure_all_started([eradius]), + + ok = start_client(Config), + ok = start_servers(Config), + Config. end_per_suite(_Config) -> @@ -82,10 +59,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, tuple), + servers => #{good => #{ip => eradius_test_lib:localhost(Family, tuple), + port => 1812, + secret => ?SECRET, + retries => 3}, + bad => #{ip => eradius_test_lib:localhost(Family, tuple), + port => 1813, + secret => ?SECRET, + retries => 3}, + error => #{ip => eradius_test_lib:localhost(Family, tuple), + 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, tuple) => + #{secret => ?SECRET, client => NasId}} + } + end, + eradius:start_server( + eradius_test_lib:localhost(Family, tuple), 1812, SrvOpts(good, <<"good_nas">>)), + eradius:start_server( + eradius_test_lib:localhost(Family, tuple), 1813, SrvOpts(bad, <<"bad_nas">>)), + eradius:start_server( + eradius_test_lib:localhost(Family, tuple), 1814, SrvOpts(error, <<"error_nas">>)), + ok. + +%%%=================================================================== +%%% tests +%%%=================================================================== + good_requests(_Config) -> Requests = [{request, access, access_accept}, {accreq, accounting, accounting}, @@ -107,66 +139,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(tuple), 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(tuple), 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(tuple), 1812]), + check_metric(eradius_server_status, undefined, [eradius_test_lib:localhost(tuple), 1813]), + check_metric(eradius_server_status, true, [eradius_test_lib:localhost(tuple), 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 +223,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 7427ac86..33b2c9d0 100644 --- a/test/eradius_test_handler.erl +++ b/test/eradius_test_handler.erl @@ -1,67 +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"}]}]), application:ensure_all_started(eradius), + ok = start_client(Backend, Family), + + SrvOpts = #{handler => {?MODULE, []}, + clients => #{eradius_test_lib:localhost(Family, tuple) => + #{secret => "secret", client => <<"ONE">>}}}, + {ok, _} = eradius:start_server( + eradius_test_lib:localhost(Family, tuple), 1812, SrvOpts#{server_name => one}), + {ok, _} = eradius:start_server( + eradius_test_lib:localhost(Family, tuple), 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, tuple), + 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"}]}] + servers => Clients#{one => #{ip => eradius_test_lib:localhost(Family, tuple), + port => 1812, + secret => "secret", + retries => 3}, + two => #{ip => eradius_test_lib:localhost(Family, tuple), + port => 1813, + secret => "secret", + retries => 3}, + bad => #{ip => eradius_test_lib:localhost(Family, tuple), + 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..e7c73107 100644 --- a/test/eradius_test_lib.erl +++ b/test/eradius_test_lib.erl @@ -41,12 +41,24 @@ localhost(ipv6, string) -> "ip6-loopback"; localhost(Family, binary) -> list_to_binary(localhost(Family, string)); -localhost(Family, tuple) when Family =:= ipv4; Family =:= ipv4_mapped_ipv6 -> +localhost(Family, tuple) -> + localhost(Family, {tuple, mapped}); +localhost(Family, {tuple, mapped}) when Family =:= ipv4; Family =:= ipv4_mapped_ipv6 -> {ok, IP} = inet:getaddr(localhost(ipv4, string), inet), IP; -localhost(ipv6, tuple) -> +localhost(ipv6, {tuple, mapped}) -> {ok, IP} = inet:getaddr(localhost(ipv6, string), inet6), IP; +localhost(ipv4, {tuple, native}) -> + {ok, IP} = inet:getaddr(localhost(ipv4, string), inet), + IP; +localhost(ipv4_mapped_ipv6, {tuple, native}) -> + {ok, IP} = inet:getaddr(localhost(ipv4, string), inet), + inet:ipv4_mapped_ipv6_address(IP); +localhost(ipv6, {tuple, native}) -> + {ok, IP} = inet:getaddr(localhost(ipv6, string), inet6), + IP; + localhost(Family, ip) -> inet:ntoa(localhost(Family, tuple)); localhost(Family, atom) ->