From 95a7b6d54d5fdcaf835e824a1212c621f748ee1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibaut=20VAR=C3=88NE?= Date: Thu, 25 May 2023 11:53:55 +0200 Subject: [PATCH] uspot: accounting: implement Accounting-On/Off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The RFC[1] says about Acct-Status-Type: It MAY be used by the client to mark the start of accounting (for example, upon booting) by specifying Accounting-On and to mark the end of accounting (for example, just before a scheduled reboot) by specifying Accounting-Off. The RFC errata[2] further specifies that Accounting-On and Accounting-Off messages apply to the whole NAS. The RFC also mandates that[3]: Either NAS-IP-Address or NAS-Identifier MUST be present in a RADIUS Accounting-Request. It SHOULD contain a NAS-Port or NAS- Port-Type attribute or both unless the service does not involve a port or the NAS does not distinguish among its ports. And[4]: An Accounting-Request packet MUST have an Acct-Session-Id. The Acct-Session-Id SHOULD contain UTF-8 encoded 10646 characters. Finally the freeRADIUS recommendations here[5] suggest that: 1. Acct-Status-Type = Accounting-On should not be used to indicate sub-system reboot. 2. IANA should allocate two new values for Acct-Status-Type: Subsystem-On, and Subsystem-Off. These values have meaning similar to Accounting-On and Accounting-Off, except that they apply to a subystem of the NAS. 3. NASes should use these new values to indicate subsystem on/off. 4. The Called-Station-Id attribute should contain values unique to each subsystem. 5. The NAS should signal that the entire system has rebooted by using the existing Accounting-On and Accounting-Off values, with a value for Called-Station-Id that is global to the NAS, or to omit it entirely. In order to reconcile all this, this commit implements Accounting-On and Accounting-Off requests as follows: - When accounting.uc is started, it loops through each uspot interface and keeps track of the acct_server seen for each interface. Then for each interface that do not use a previously seen server, it generates a unique session ID, and sends an Accounting-On request to the RADIUS server, using this session ID and the configured NAS-ID. - When accounting.uc stops, it sends an Accounting-Off request for each uspot interface for which an Accounting-On message was previously sent, using the same global session ID. If/when the Subsystem-On/Subsystem-Off values are implemented, this commit can be revisited to simply lift the restriction on unique servers and change the acct_type value accordingly. Finally, it appears that while NAS-ID is provided in the request thus making NAS-IP unnecessary, libradcli still includes this field in the request. Likewise, it also insists on sending a NAS-Port attribute. [1]: https://datatracker.ietf.org/doc/html/rfc2866#section-5.1 [2]: https://www.rfc-editor.org/errata_search.php?rfc=2866 [3]: https://datatracker.ietf.org/doc/html/rfc2866#section-4.1 [4]: https://datatracker.ietf.org/doc/html/rfc2866#section-5.5 [5]: https://freeradius.org/rfc/acct_status_type_subsystem.html Signed-off-by: Thibaut VARĂˆNE --- .../uspot/files/usr/share/uspot/accounting.uc | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/feeds/ucentral/uspot/files/usr/share/uspot/accounting.uc b/feeds/ucentral/uspot/files/usr/share/uspot/accounting.uc index 0e0b590c4..a49eeab96 100755 --- a/feeds/ucentral/uspot/files/usr/share/uspot/accounting.uc +++ b/feeds/ucentral/uspot/files/usr/share/uspot/accounting.uc @@ -231,6 +231,40 @@ function client_reset(interface, mac, reason) { client_kick(interface, mac, false); } +function radius_accton(interface) +{ + // assign a global interface session ID for Accounting-On/Off messages + let math = require('math'); + let sessionid = ''; + + for (let i = 0; i < 16; i++) + sessionid += sprintf('%x', math.rand() % 16); + + interfaces[interface].sessionid = sessionid; + + const acct_type_accton = 7; // Accounting-On + let payload = { + acct_type: acct_type_accton, + acct_session: sessionid, + }; + payload = radius_init(interface, null, payload); + payload.acct = true; + radius_call(interface, null, payload); + debug(interface, null, 'acct-on call'); +} + +function radius_acctoff(interface) +{ + const acct_type_acctoff = 8; // Accounting-Off + let payload = { + acct_type: acct_type_acctoff, + acct_session: interfaces[interface].sessionid, + }; + payload = radius_init(interface, null, payload); + payload.acct = true; + radius_call(interface, null, payload); + debug(interface, null, 'acct-off call'); +} function accounting(interface) { let list = ubus.call('spotfilter', 'client_list', { interface }); @@ -277,8 +311,30 @@ function accounting(interface) { } } +function start() +{ + let seen = {}; + + for (let interface, data in interfaces) { + if (!data.settings.acct_server || (data.settings.acct_server in seen)) + continue; // avoid sending duplicate requests to the same server + seen[data.settings.acct_server] = 1; + radius_accton(interface); + } +} + +function stop() +{ + for (let interface, data in interfaces) { + if (data.sessionid) // we have previously sent Accounting-On + radius_acctoff(interface); + } +} + uloop.init(); +start(); + uloop.timer(10000, function() { for (let interface in interfaces) accounting(interface); @@ -286,3 +342,5 @@ uloop.timer(10000, function() { }); uloop.run(); + +stop();