diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:49:46 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:49:46 +0000 |
commit | 50b37d4a27d3295a29afca2286f1a5a086142cec (patch) | |
tree | 9212f763934ee090ef72d823f559f52ce387f268 /raddb/mods-config/sql/main/postgresql | |
parent | Initial commit. (diff) | |
download | freeradius-upstream.tar.xz freeradius-upstream.zip |
Adding upstream version 3.2.1+dfsg.upstream/3.2.1+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'raddb/mods-config/sql/main/postgresql')
6 files changed, 1621 insertions, 0 deletions
diff --git a/raddb/mods-config/sql/main/postgresql/extras/cisco_h323_db_schema.sql b/raddb/mods-config/sql/main/postgresql/extras/cisco_h323_db_schema.sql new file mode 100644 index 0000000..0fabd43 --- /dev/null +++ b/raddb/mods-config/sql/main/postgresql/extras/cisco_h323_db_schema.sql @@ -0,0 +1,295 @@ +/* + * $Id$ + * + * --- Peter Nixon [ codemonkey@peternixon.net ] + * + * This is a custom SQL schema for doing H323 and SIP VoIP accounting + * with FreeRadius and Cisco equipment. It is currently known to work + * with 3640, 5300 and 5350 series as well as CSPS (Cisco SIP Proxy + * Server). It will scale A LOT better than the default radius schema + * which is designed for simple dialup installations of FreeRadius. + * + * For this schema to work properly you MUST use + * raddb/mods-config/sql/postgresql/voip-postpaid.conf rather than + * raddb/mods-config/sql/postgresql/dialup.conf + * + * If you wish to do RADIUS Authentication using the same database, + * you MUST use use raddb/mods-config/sql/postgresql/schema.sql as well as this schema. + */ + +/* + * Table structure for 'Start' tables + */ + +CREATE TABLE StartVoIP ( + RadAcctId BIGSERIAL PRIMARY KEY, + AcctTime TIMESTAMP with time zone NOT NULL, + h323SetupTime TIMESTAMP with time zone, + H323ConnectTime TIMESTAMP with time zone, + UserName VARCHAR(64), + RadiusServerName VARCHAR(32), + NASIPAddress INET NOT NULL, + CalledStationId VARCHAR(80), + CallingStationId VARCHAR(80), + AcctDelayTime INTEGER, + H323GWID VARCHAR(32), + h323CallOrigin VARCHAR(10), + CallID VARCHAR(80) NOT NULL, + processed BOOLEAN DEFAULT false +); +create index startvoipcombo on startvoip (AcctTime, nasipaddress); + + +CREATE TABLE StartTelephony ( + RadAcctId BIGSERIAL PRIMARY KEY, + AcctTime TIMESTAMP with time zone NOT NULL, + h323SetupTime TIMESTAMP with time zone, + H323ConnectTime TIMESTAMP with time zone, + UserName VARCHAR(64), + RadiusServerName VARCHAR(32), + NASIPAddress INET NOT NULL, + CalledStationId VARCHAR(80), + CallingStationId VARCHAR(80), + AcctDelayTime INTEGER, + H323GWID VARCHAR(32), + h323CallOrigin VARCHAR(10), + CallID VARCHAR(80) NOT NULL, + processed BOOLEAN DEFAULT false +); +create index starttelephonycombo on starttelephony (AcctTime, nasipaddress); + + + +/* + * Table structure for 'Stop' tables + */ +CREATE TABLE StopVoIP ( + RadAcctId BIGSERIAL PRIMARY KEY, + AcctTime TIMESTAMP with time zone NOT NULL, + H323SetupTime TIMESTAMP with time zone, + H323ConnectTime TIMESTAMP with time zone, + H323DisconnectTime TIMESTAMP with time zone, + UserName VARCHAR(32), + RadiusServerName VARCHAR(32), + NASIPAddress INET NOT NULL, + AcctSessionTime BIGINT, + AcctInputOctets BIGINT, + AcctOutputOctets BIGINT, + CalledStationId VARCHAR(80), + CallingStationId VARCHAR(80), + AcctDelayTime SMALLINT, + CiscoNASPort VARCHAR(1), + H323GWID VARCHAR(32), + H323CallOrigin VARCHAR(10), + H323DisconnectCause VARCHAR(20), + H323RemoteAddress INET, + H323VoiceQuality INTEGER, + CallID VARCHAR(80) NOT NULL, + processed BOOLEAN DEFAULT false +); +create UNIQUE index stopvoipcombo on stopvoip (AcctTime, nasipaddress, CallID); + + +CREATE TABLE StopTelephony ( + RadAcctId BIGSERIAL PRIMARY KEY, + AcctTime TIMESTAMP with time zone NOT NULL, + H323SetupTime TIMESTAMP with time zone NOT NULL, + H323ConnectTime TIMESTAMP with time zone NOT NULL, + H323DisconnectTime TIMESTAMP with time zone NOT NULL, + UserName VARCHAR(32) DEFAULT '' NOT NULL, + RadiusServerName VARCHAR(32), + NASIPAddress INET NOT NULL, + AcctSessionTime BIGINT, + AcctInputOctets BIGINT, + AcctOutputOctets BIGINT, + CalledStationId VARCHAR(80), + CallingStationId VARCHAR(80), + AcctDelayTime SMALLINT, + CiscoNASPort VARCHAR(16), + H323GWID VARCHAR(32), + H323CallOrigin VARCHAR(10), + H323DisconnectCause VARCHAR(20), + H323RemoteAddress INET, + H323VoiceQuality INTEGER, + CallID VARCHAR(80) NOT NULL, + processed BOOLEAN DEFAULT false +); +-- You can have more than one record that is identical except for CiscoNASPort if you have a dial peer hungroup +-- configured for multiple PRIs. +create UNIQUE index stoptelephonycombo on stoptelephony (AcctTime, nasipaddress, CallID, CiscoNASPort); + +/* + * Table structure for 'gateways' + * + * This table should list the IP addresses, names and locations of all your gateways + * This can be used to make more useful reports. + * + * Note: This table should be removed in favour of using the "nas" table. + */ + +CREATE TABLE gateways ( + gw_ip INET NOT NULL, + gw_name VARCHAR(32) NOT NULL, + gw_city VARCHAR(32) +); + + +/* + * Table structure for 'customers' + * + * This table should list your Customers names and company + * This can be used to make more useful reports. + */ + +CREATE TABLE customers ( + cust_id SERIAL NOT NULL, + company VARCHAR(32), + customer VARCHAR(32) +); + +/* + * Table structure for 'cust_gw' + * + * This table should list the IP addresses and Customer IDs of all your Customers gateways + * This can be used to make more useful reports. + */ + +CREATE TABLE cust_gw ( + cust_gw INET PRIMARY KEY, + cust_id INTEGER NOT NULL, + "location" VARCHAR(32) +); + + +CREATE VIEW customerip AS + SELECT gw.cust_gw AS ipaddr, cust.company, cust.customer, gw."location" FROM customers cust, cust_gw gw WHERE (cust.cust_id = gw.cust_id); + + +-- create plpgsql language (You need to be a database superuser to be able to do this) +CREATE FUNCTION "plpgsql_call_handler" () RETURNS LANGUAGE_HANDLER AS '$libdir/plpgsql' LANGUAGE C; +CREATE TRUSTED LANGUAGE "plpgsql" HANDLER "plpgsql_call_handler"; + +/* + * Function 'strip_dot' + * removes "." from the start of cisco timestamps + * + * From the cisco website: + * "A timestamp that is preceded by an asterisk (*) or a dot (.) may not be accurate. + * An asterisk (*) means that after a gateway reboot, the gateway clock was not manually set + * and the gateway has not synchronized with an NTP server yet. A dot (.) means the gateway + * NTP has lost synchronization with an NTP server." + * + * We therefore do not bother to strip asterisks (*) from timestamps, as you NEED ntp setup + * unless you don't care about billing at all! + * + * * Example useage: + * insert into mytable values (strip_dot('.16:46:02.356 EET Wed Dec 11 2002')); + * + */ + + +CREATE OR REPLACE FUNCTION strip_dot (VARCHAR) RETURNS TIMESTAMPTZ AS ' + DECLARE + original_timestamp ALIAS FOR $1; + BEGIN + IF original_timestamp = '''' THEN + RETURN NULL; + END IF; + IF substring(original_timestamp from 1 for 1) = ''.'' THEN + RETURN substring(original_timestamp from 2); + ELSE + RETURN original_timestamp; + END IF; + END; +' LANGUAGE 'plpgsql'; + + +CREATE OR REPLACE FUNCTION pick_id (VARCHAR, VARCHAR) RETURNS VARCHAR AS ' + DECLARE + h323confid ALIAS FOR $1; + callid ALIAS FOR $2; + BEGIN + IF h323confid <> '''' THEN + RETURN h323confid; + END IF; + IF callid <> '''' THEN + RETURN callid; + END IF; + RETURN NULL; + END; +' LANGUAGE 'plpgsql'; + + + +/* + * Table structure for 'isdn_error_codes' table + * + * Taken from cisco.com this data can be JOINED against h323DisconnectCause to + * give human readable error reports. + * + */ + + +CREATE TABLE isdn_error_codes ( + error_code VARCHAR(2) PRIMARY KEY, + desc_short VARCHAR(90), + desc_long TEXT +); + +/* + * Data for 'isdn_error_codes' table + */ + +INSERT INTO isdn_error_codes VALUES ('1', 'Unallocated (unassigned) number', 'The ISDN number was sent to the switch in the correct format; however, the number is not assigned to any destination equipment.'); +INSERT INTO isdn_error_codes VALUES ('10', 'Normal call clearing', 'Normal call clearing has occurred.'); +INSERT INTO isdn_error_codes VALUES ('11', 'User busy', 'The called system acknowledges the connection request but is unable to accept the call because all B channels are in use.'); +INSERT INTO isdn_error_codes VALUES ('12', 'No user responding', 'The connection cannot be completed because the destination does not respond to the call.'); +INSERT INTO isdn_error_codes VALUES ('13', 'No answer from user (user alerted)', 'The destination responds to the connection request but fails to complete the connection within the prescribed time. The problem is at the remote end of the connection.'); +INSERT INTO isdn_error_codes VALUES ('15', 'Call rejected', 'The destination is capable of accepting the call but rejected the call for an unknown reason.'); +INSERT INTO isdn_error_codes VALUES ('16', 'Number changed', 'The ISDN number used to set up the call is not assigned to any system.'); +INSERT INTO isdn_error_codes VALUES ('1A', 'Non-selected user clearing', 'The destination is capable of accepting the call but rejected the call because it was not assigned to the user.'); +INSERT INTO isdn_error_codes VALUES ('1B', 'Designation out of order', 'The destination cannot be reached because the interface is not functioning correctly, and a signaling message cannot be delivered. This might be a temporary condition, but it could last for an extended period of time. For example, the remote equipment might be turned off.'); +INSERT INTO isdn_error_codes VALUES ('1C', 'Invalid number format', 'The connection could be established because the destination address was presented in an unrecognizable format or because the destination address was incomplete.'); +INSERT INTO isdn_error_codes VALUES ('1D', 'Facility rejected', 'The facility requested by the user cannot be provided by the network.'); +INSERT INTO isdn_error_codes VALUES ('1E', 'Response to STATUS ENQUIRY', 'The status message was generated in direct response to the prior receipt of a status enquiry message.'); +INSERT INTO isdn_error_codes VALUES ('1F', 'Normal, unspecified', 'Reports the occurrence of a normal event when no standard cause applies. No action required.'); +INSERT INTO isdn_error_codes VALUES ('2', 'No route to specified transit network', 'The ISDN exchange is asked to route the call through an unrecognized intermediate network.'); +INSERT INTO isdn_error_codes VALUES ('22', 'No circuit/channel available', 'The connection cannot be established because no appropriate channel is available to take the call.'); +INSERT INTO isdn_error_codes VALUES ('26', 'Network out of order', 'The destination cannot be reached because the network is not functioning correctly, and the condition might last for an extended period of time. An immediate reconnect attempt will probably be unsuccessful.'); +INSERT INTO isdn_error_codes VALUES ('29', 'Temporary failure', 'An error occurred because the network is not functioning correctly. The problem will be resolved shortly.'); +INSERT INTO isdn_error_codes VALUES ('2A', 'Switching equipment congestion', 'The destination cannot be reached because the network switching equipment is temporarily overloaded.'); +INSERT INTO isdn_error_codes VALUES ('2B', 'Access information discarded', 'The network cannot provide the requested access information.'); +INSERT INTO isdn_error_codes VALUES ('2C', 'Requested circuit/channel not available', 'The remote equipment cannot provide the requested channel for an unknown reason. This might be a temporary problem.'); +INSERT INTO isdn_error_codes VALUES ('2F', 'Resources unavailable, unspecified', 'The requested channel or service is unavailable for an unknown reason. This might be a temporary problem.'); +INSERT INTO isdn_error_codes VALUES ('3', 'No route to destination', 'The call was routed through an intermediate network that does not serve the destination address.'); +INSERT INTO isdn_error_codes VALUES ('31', 'Quality of service unavailable', 'The requested quality of service cannot be provided by the network. This might be a subscription problem.'); +INSERT INTO isdn_error_codes VALUES ('32', 'Requested facility not subscribed', 'The remote equipment supports the requested supplementary service by subscription only.'); +INSERT INTO isdn_error_codes VALUES ('39', 'Bearer capability not authorized', 'The user requested a bearer capability that the network provides, but the user is not authorized to use it. This might be a subscription problem.'); +INSERT INTO isdn_error_codes VALUES ('3A', 'Bearer capability not presently available', 'The network normally provides the requested bearer capability, but it is unavailable at the present time. This might be due to a temporary network problem or to a subscription problem.'); +INSERT INTO isdn_error_codes VALUES ('3F', 'Service or option not available, unspecified', 'The network or remote equipment was unable to provide the requested service option for an unspecified reason. This might be a subscription problem.'); +INSERT INTO isdn_error_codes VALUES ('41', 'Bearer capability not implemented', 'The network cannot provide the bearer capability requested by the user.'); +INSERT INTO isdn_error_codes VALUES ('42', 'Channel type not implemented', 'The network or the destination equipment does not support the requested channel type.'); +INSERT INTO isdn_error_codes VALUES ('45', 'Requested facility not implemented', 'The remote equipment does not support the requested supplementary service.'); +INSERT INTO isdn_error_codes VALUES ('46', 'Only restricted digital information bearer capability is available', 'The network is unable to provide unrestricted digital information bearer capability.'); +INSERT INTO isdn_error_codes VALUES ('4F', 'Service or option not implemented, unspecified', 'The network or remote equipment is unable to provide the requested service option for an unspecified reason. This might be a subscription problem.'); +INSERT INTO isdn_error_codes VALUES ('51', 'Invalid call reference value', 'The remote equipment received a call with a call reference that is not currently in use on the user-network interface.'); +INSERT INTO isdn_error_codes VALUES ('52', 'Identified channel does not exist', 'The receiving equipment is requested to use a channel that is not activated on the interface for calls.'); +INSERT INTO isdn_error_codes VALUES ('53', 'A suspended call exists, but this call identity does not', 'The network received a call resume request. The call resume request contained a Call Identify information element that indicates that the call identity is being used for a suspended call.'); +INSERT INTO isdn_error_codes VALUES ('54', 'Call identity in use', 'The network received a call resume request. The call resume request contained a Call Identify information element that indicates that it is in use for a suspended call.'); +INSERT INTO isdn_error_codes VALUES ('55', 'No call suspended', 'The network received a call resume request when there was not a suspended call pending. This might be a transient error that will be resolved by successive call retries.'); +INSERT INTO isdn_error_codes VALUES ('56', 'Call having the requested call identity has been cleared', 'The network received a call resume request. The call resume request contained a Call Identity information element, which once indicated a suspended call. However, the suspended call was cleared either by timeout or by the remote user.'); +INSERT INTO isdn_error_codes VALUES ('58', 'Incompatible destination', 'Indicates that an attempt was made to connect to non-ISDN equipment. For example, to an analog line.'); +INSERT INTO isdn_error_codes VALUES ('5B', 'Invalid transit network selection', 'The ISDN exchange was asked to route the call through an unrecognized intermediate network.'); +INSERT INTO isdn_error_codes VALUES ('5F', 'Invalid message, unspecified', 'An invalid message was received, and no standard cause applies. This is usually due to a D-channel error. If this error occurs systematically, report it to your ISDN service provider.'); +INSERT INTO isdn_error_codes VALUES ('6', 'Channel unacceptable', 'The service quality of the specified channel is insufficient to accept the connection.'); +INSERT INTO isdn_error_codes VALUES ('60', 'Mandatory information element is missing', 'The receiving equipment received a message that did not include one of the mandatory information elements. This is usually due to a D-channel error. If this error occurs systematically, report it to your ISDN service provider.'); +INSERT INTO isdn_error_codes VALUES ('61', 'Message type non-existent or not implemented', 'The receiving equipment received an unrecognized message, either because the message type was invalid or because the message type was valid but not supported. The cause is due to either a problem with the remote configuration or a problem with the local D channel.'); +INSERT INTO isdn_error_codes VALUES ('62', 'Message not compatible with call state or message type non-existent or not implemented', 'The remote equipment received an invalid message, and no standard cause applies. This cause is due to a D-channel error. If this error occurs systematically, report it to your ISDN service provider.'); +INSERT INTO isdn_error_codes VALUES ('63', 'Information element non-existent or not implemented', 'The remote equipment received a message that includes information elements, which were not recognized. This is usually due to a D-channel error. If this error occurs systematically, report it to your ISDN service provider.'); +INSERT INTO isdn_error_codes VALUES ('64', 'Invalid information element contents', 'The remote equipment received a message that includes invalid information in the information element. This is usually due to a D-channel error.'); +INSERT INTO isdn_error_codes VALUES ('65', 'Message not compatible with call state', 'The remote equipment received an unexpected message that does not correspond to the current state of the connection. This is usually due to a D-channel error.'); +INSERT INTO isdn_error_codes VALUES ('66', 'Recovery on timer expires', 'An error-handling (recovery) procedure was initiated by a timer expiry. This is usually a temporary problem.'); +INSERT INTO isdn_error_codes VALUES ('6F', 'Protocol error, unspecified', 'An unspecified D-channel error when no other standard cause applies.'); +INSERT INTO isdn_error_codes VALUES ('7', 'Call awarded and being delivered in an established channel', 'The user is assigned an incoming call that is being connected to an already-established call channel.'); +INSERT INTO isdn_error_codes VALUES ('7F', 'Internetworking, unspecified', 'An event occurred, but the network does not provide causes for the action that it takes. The precise problem is unknown.'); + diff --git a/raddb/mods-config/sql/main/postgresql/extras/voip-postpaid.conf b/raddb/mods-config/sql/main/postgresql/extras/voip-postpaid.conf new file mode 100644 index 0000000..9f1449c --- /dev/null +++ b/raddb/mods-config/sql/main/postgresql/extras/voip-postpaid.conf @@ -0,0 +1,70 @@ +# -*- text -*- +## +## voip-postpaid.conf -- PostgreSQL configuration for H323 VoIP billingx +## (cisco_h323_db_schema.sql) +## +## $Id$ + + + ####################################################################### + # Query config: Username + ####################################################################### + # This is the username that will get substituted, escaped, and added + # as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used below + # everywhere a username substitution is needed so you you can be sure + # the username passed from the client is escaped properly. + # + # Uncomment the next line, if you want the sql_user_name to mean: + # + # Use Stripped-User-Name, if it's there. + # Else use User-Name, if it's there, + # Else use hard-coded string "none" as the user name. + # + #sql_user_name = "%{%{Stripped-User-Name}:-%{%{User-Name}:-none}}" + # + sql_user_name = "%{User-Name}" + + accounting { + reference = "%{tolower:type.%{Acct-Status-Type}.query}" + + # Write SQL queries to a logfile. This is potentially useful for bulk inserts + # when used with the rlm_sql_null driver. +# logfile = ${logdir}/accounting.sql + + type { + start { + query = "INSERT INTO ${....acct_table1}%{h323-call-type} \ + (RadiusServerName, UserName, NASIPAddress, AcctTime, CalledStationId, \ + CallingStationId, AcctDelayTime, h323gwid, h323callorigin, \ + h323setuptime, H323ConnectTime, callid) \ + VALUES(\ + '${radius_server_name}', '%{SQL-User-Name}', \ + '%{NAS-IP-Address}', now(), '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', '%{%{Acct-Delay-Time}:-0}', '%{h323-gw-id}', \ + '%{h323-call-origin}', strip_dot('%{h323-setup-time}'), \ + strip_dot('%{h323-connect-time}'), pick_id('%{h323-conf-id}', \ + '%{call-id}'))" + } + + stop { + query = "INSERT INTO $....acct_table2}%{h323-call-type} \ + (RadiusServerName, UserName, NASIPAddress, AcctTime, \ + AcctSessionTime, AcctInputOctets, AcctOutputOctets, CalledStationId, \ + CallingStationId, AcctDelayTime, H323RemoteAddress, H323VoiceQuality, \ + CiscoNASPort, h323callorigin, callid, h323connecttime, \ + h323disconnectcause, h323disconnecttime, h323gwid, h323setuptime) \ + VALUES(\ + '${radius_server_name}', '%{SQL-User-Name}', '%{NAS-IP-Address}', \ + NOW(), '%{%{Acct-Session-Time}:-0}', \ + '%{%{Acct-Input-Octets}:-0}', '%{%{Acct-Output-Octets}:-0}', \ + '%{Called-Station-Id}', '%{Calling-Station-Id}', \ + '%{%{Acct-Delay-Time}:-0}', NULLIF('%{h323-remote-address}', '')::inet, \ + NULLIF('%{h323-voice-quality}','')::integer, \ + NULLIF('%{Cisco-NAS-Port}', ''), \ + '%{h323-call-origin}', pick_id('%{h323-conf-id}', '%{call-id}'), \ + strip_dot('%{h323-connect-time}'), '%{h323-disconnect-cause}', \ + strip_dot('%{h323-disconnect-time}'), '%{h323-gw-id}', \ + strip_dot('%{h323-setup-time}'))" + } + } + } diff --git a/raddb/mods-config/sql/main/postgresql/process-radacct.sql b/raddb/mods-config/sql/main/postgresql/process-radacct.sql new file mode 100644 index 0000000..7a70724 --- /dev/null +++ b/raddb/mods-config/sql/main/postgresql/process-radacct.sql @@ -0,0 +1,278 @@ +# -*- text -*- +# +# main/postgresql/process-radacct.sql -- Schema extensions for processing radacct entries +# +# $Id$ + +-- --------------------------------- +-- - Per-user data usage over time - +-- --------------------------------- +-- +-- An extension to the standard schema to hold per-user data usage statistics +-- for arbitrary periods. +-- +-- The data_usage_by_period table is populated by periodically calling the +-- fr_new_data_usage_period stored procedure. +-- +-- This table can be queried in various ways to produce reports of aggregate +-- data use over time. For example, if the fr_new_data_usage_period SP is +-- invoked one per day just after midnight, to produce usage data with daily +-- granularity, then a reasonably accurate monthly bandwidth summary for a +-- given user could be obtained by queriing this table with: +-- +-- SELECT +-- TO_CHAR(CURRENT_TIMESTAMP, 'YYYY-Month') AS month, +-- TRUNC(SUM(acctinputoctets)/1000/1000/1000,9) AS gb_in, +-- TRUNC(SUM(acctoutputoctets)/1000/1000/1000,9) AS gb_out +-- FROM +-- data_usage_by_period +-- WHERE +-- username='bob' AND +-- period_end IS NOT NULL +-- GROUP BY +-- month; +-- +-- month | gb_in | gb_out +-- ----------------+-------------+-------------- +-- 2019-July | 5.782279231 | 50.545664824 +-- 2019-August | 4.230543344 | 48.523096424 +-- 2019-September | 4.847360599 | 48.631835488 +-- 2019-October | 6.456763254 | 51.686231937 +-- 2019-November | 6.362537735 | 52.385710572 +-- 2019-December | 4.301524442 | 50.762240277 +-- 2020-January | 5.436280545 | 49.067775286 +-- (7 rows) +-- +CREATE TABLE data_usage_by_period ( + username text, + period_start timestamp with time zone, + period_end timestamp with time zone, + acctinputoctets bigint, + acctoutputoctets bigint +); +ALTER TABLE data_usage_by_period ADD CONSTRAINT data_usage_by_period_pkey PRIMARY KEY (username, period_start); +CREATE INDEX data_usage_by_period_pkey_period_end ON data_usage_by_period(period_end); + + +-- +-- Stored procedure that when run with some arbitrary frequency, say +-- once per day by cron, will process the recent radacct entries to extract +-- time-windowed data containing acct{input,output}octets ("data usage") per +-- username, per period. +-- +-- Each invocation will create new rows in the data_usage_by_period tables +-- containing the data used by each user since the procedure was last invoked. +-- The intervals do not need to be identical but care should be taken to +-- ensure that the start/end of each period aligns well with any intended +-- reporting intervals. +-- +-- It can be invoked by running: +-- +-- SELECT fr_new_data_usage_period(); +-- +-- +CREATE OR REPLACE FUNCTION fr_new_data_usage_period () +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE v_start timestamp; +DECLARE v_end timestamp; +BEGIN + + SELECT COALESCE(MAX(period_end) + INTERVAL '1 SECOND', TO_TIMESTAMP(0)) INTO v_start FROM data_usage_by_period; + SELECT DATE_TRUNC('second',CURRENT_TIMESTAMP) INTO v_end; + + -- + -- Add the data usage for the sessions that were active in the current + -- period to the table. Include all sessions that finished since the start + -- of this period as well as those still ongoing. + -- + INSERT INTO data_usage_by_period (username, period_start, period_end, acctinputoctets, acctoutputoctets) + SELECT * + FROM ( + SELECT + username, + v_start, + v_end, + SUM(acctinputoctets) AS acctinputoctets, + SUM(acctoutputoctets) AS acctoutputoctets + FROM + radacct + WHERE + acctstoptime > v_start OR + acctstoptime IS NULL + GROUP BY + username + ) AS s + ON CONFLICT ON CONSTRAINT data_usage_by_period_pkey + DO UPDATE + SET + acctinputoctets = data_usage_by_period.acctinputoctets + EXCLUDED.acctinputoctets, + acctoutputoctets = data_usage_by_period.acctoutputoctets + EXCLUDED.acctoutputoctets, + period_end = v_end; + + -- + -- Create an open-ended "next period" for all ongoing sessions and carry a + -- negative value of their data usage to avoid double-accounting when we + -- process the next period. Their current data usage has already been + -- allocated to the current and possibly previous periods. + -- + INSERT INTO data_usage_by_period (username, period_start, period_end, acctinputoctets, acctoutputoctets) + SELECT * + FROM ( + SELECT + username, + v_end + INTERVAL '1 SECOND', + NULL::timestamp, + 0 - SUM(acctinputoctets), + 0 - SUM(acctoutputoctets) + FROM + radacct + WHERE + acctstoptime IS NULL + GROUP BY + username + ) AS s; + +END +$$; + + +-- ------------------------------------------------------ +-- - "Lightweight" Accounting-On/Off strategy resources - +-- ------------------------------------------------------ +-- +-- The following resources are for use only when the "lightweight" +-- Accounting-On/Off strategy is enabled in queries.conf. +-- +-- Instead of bulk closing the radacct sessions belonging to a reloaded NAS, +-- this strategy leaves them open and records the NAS reload time in the +-- nasreload table. +-- +-- Where applicable, the onus is on the administator to: +-- +-- * Consider the nas reload times when deriving a list of +-- active/inactive sessions, and when determining the duration of sessions +-- interrupted by a NAS reload. (Refer to the view below.) +-- +-- * Close the affected sessions out of band. (Refer to the SP below.) +-- +-- +-- The radacct_with_reloads view presents the radacct table with two additional +-- columns: acctstoptime_with_reloads and acctsessiontime_with_reloads +-- +-- Where the session isn't closed (acctstoptime IS NULL), yet it started before +-- the last reload of the NAS (radacct.acctstarttime < nasreload.reloadtime), +-- the derived columns are set based on the reload time of the NAS (effectively +-- the point in time that the session was interrupted.) +-- +CREATE VIEW radacct_with_reloads AS +SELECT + a.*, + COALESCE(a.AcctStopTime, + CASE WHEN a.AcctStartTime < n.ReloadTime THEN n.ReloadTime END + ) AS AcctStopTime_With_Reloads, + COALESCE(a.AcctSessionTime, + CASE WHEN a.AcctStopTime IS NULL AND a.AcctStartTime < n.ReloadTime THEN + EXTRACT(EPOCH FROM (n.ReloadTime - a.AcctStartTime)) + END + ) AS AcctSessionTime_With_Reloads +FROM radacct a +LEFT OUTER JOIN nasreload n USING (nasipaddress); + + +-- +-- It may be desirable to periodically "close" radacct sessions belonging to a +-- reloaded NAS, replicating the "bulk close" Accounting-On/Off behaviour, +-- just not in real time. +-- +-- The fr_radacct_close_after_reload SP will set radacct.acctstoptime to +-- nasreload.reloadtime, calculate the corresponding radacct.acctsessiontime, +-- and set acctterminatecause to "NAS reboot" for interrupted sessions. It +-- does so in batches, which avoids long-lived locks on the affected rows. +-- +-- It can be invoked as follows: +-- +-- CALL fr_radacct_close_after_reload(); +-- +-- Note: This SP requires PostgreSQL >= 11 which was the first version to +-- introduce PROCEDUREs which permit transaction control. This allows COMMIT +-- to be called to incrementally apply successive batch updates prior to the +-- end of the procedure. Prior to version 11 there exists only FUNCTIONs that +-- execute atomically. You can convert this procedure to a function, but by +-- doing so you are really no better off than performing a single, +-- long-running bulk update. +-- +-- Note: This SP walks radacct in strides of v_batch_size. It will typically +-- skip closed and ongoing sessions at a rate significantly faster than +-- 500,000 rows per second and process batched updates faster than 25,000 +-- orphaned sessions per second. If this isn't fast enough then you should +-- really consider using a custom schema that includes partitioning by +-- nasipaddress or acct{start,stop}time. +-- +CREATE OR REPLACE PROCEDURE fr_radacct_close_after_reload () +LANGUAGE plpgsql +AS $$ + +DECLARE v_a bigint; +DECLARE v_z bigint; +DECLARE v_updated bigint DEFAULT 0; +DECLARE v_last_report bigint DEFAULT 0; +DECLARE v_now bigint; +DECLARE v_last boolean DEFAULT false; +DECLARE v_rowcount integer; + +-- +-- This works for many circumstances +-- +DECLARE v_batch_size CONSTANT integer := 2500; + +BEGIN + + SELECT MIN(RadAcctId) INTO v_a FROM radacct WHERE AcctStopTime IS NULL; + + LOOP + + v_z := NULL; + SELECT RadAcctId INTO v_z FROM radacct WHERE RadAcctId > v_a ORDER BY RadAcctId OFFSET v_batch_size LIMIT 1; + + IF v_z IS NULL THEN + SELECT MAX(RadAcctId) INTO v_z FROM radacct; + v_last := true; + END IF; + + UPDATE radacct a + SET + AcctStopTime = n.reloadtime, + AcctSessionTime = EXTRACT(EPOCH FROM (n.ReloadTime - a.AcctStartTime)), + AcctTerminateCause = 'NAS reboot' + FROM nasreload n + WHERE + a.NASIPAddress = n.NASIPAddress + AND RadAcctId BETWEEN v_a AND v_z + AND AcctStopTime IS NULL + AND AcctStartTime < n.ReloadTime; + + GET DIAGNOSTICS v_rowcount := ROW_COUNT; + v_updated := v_updated + v_rowcount; + + COMMIT; -- Make the update visible + + v_a := v_z + 1; + + -- + -- Periodically report how far we've got + -- + SELECT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) INTO v_now; + IF v_last_report != v_now OR v_last THEN + RAISE NOTICE 'RadAcctID: %; Sessions closed: %', v_z, v_updated; + v_last_report := v_now; + END IF; + + EXIT WHEN v_last; + + END LOOP; + +END +$$; + diff --git a/raddb/mods-config/sql/main/postgresql/queries.conf b/raddb/mods-config/sql/main/postgresql/queries.conf new file mode 100644 index 0000000..d410562 --- /dev/null +++ b/raddb/mods-config/sql/main/postgresql/queries.conf @@ -0,0 +1,742 @@ +# -*- text -*- +# +# main/postgresql/queries.conf -- PostgreSQL configuration for default schema (schema.sql) +# +# $Id$ + +# Use the driver specific SQL escape method. +# +# If you enable this configuration item, the "safe_characters" +# configuration is ignored. FreeRADIUS then uses the PostgreSQL escape +# functions to escape input strings. The only downside to making this +# change is that the PostgreSQL escaping method is not the same the one +# used by FreeRADIUS. So characters which are NOT in the +# "safe_characters" list will now be stored differently in the database. +# +#auto_escape = yes + +# Safe characters list for sql queries. Everything else is replaced +# with their mime-encoded equivalents. +# The default list should be ok +# Using 'auto_escape' is preferred +# safe_characters = "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /" + +####################################################################### +# Query config: Username +####################################################################### +# This is the username that will get substituted, escaped, and added +# as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used +# below everywhere a username substitution is needed so you you can +# be sure the username passed from the client is escaped properly. +# +# Uncomment the next line, if you want the sql_user_name to mean: +# +# Use Stripped-User-Name, if it's there. +# Else use User-Name, if it's there, +# Else use hard-coded string "none" as the user name. +# +#sql_user_name = "%{%{Stripped-User-Name}:-%{%{User-Name}:-none}}" + +sql_user_name = "%{User-Name}" + +####################################################################### +# Query config: Event-Timestamp +####################################################################### +# event_timestamp_epoch is the basis for the time inserted into +# accounting records. Typically this will be the Event-Timestamp of the +# accounting request, which is usually provided by a NAS. +# +# Uncomment the next line, if you want the timestamp to be based on the +# request reception time recorded by this server, for example if you +# distrust the provided Event-Timestamp. +#event_timestamp_epoch = "%l" + +event_timestamp_epoch = "%{%{integer:Event-Timestamp}:-%l}" + +# event_timestamp is the SQL snippet for converting an epoch timestamp +# to an SQL date. + +event_timestamp = "TO_TIMESTAMP(${event_timestamp_epoch})" + +####################################################################### +# Query config: Class attribute +####################################################################### +# +# 3.0.22 and later have a "class" column in the accounting table. +# +# However, we do NOT want to break existing configurations by adding +# the Class attribute to the default queries. If we did that, then +# systems using newer versions of the server would fail, because +# there is no "class" column in their accounting tables. +# +# The solution to that is the following "class" subsection. If your +# database has a "class" column for the various tables, then you can +# uncomment the configuration items here. The queries below will +# then automatically insert the Class attribute into radacct, +# radpostauth, etc. +# +class { + # + # Delete the '#' character from each of the configuration + # items in this section. This change puts the Class + # attribute into the various tables. Leave the double-quoted + # string there, as the value for the configuration item. + # + # See also policy.d/accounting, and the "insert_acct_class" + # policy. You will need to list (or uncomment) + # "insert_acct_class" in the "post-auth" section in order to + # create a Class attribute. + # + column_name = # ", Class" + packet_xlat = # ", '%{Class}'" + reply_xlat = # ", '%{reply:Class}'" +} + +####################################################################### +# Default profile +####################################################################### +# This is the default profile. It is found in SQL by group membership. +# That means that this profile must be a member of at least one group +# which will contain the corresponding check and reply items. +# This profile will be queried in the authorize section for every user. +# The point is to assign all users a default profile without having to +# manually add each one to a group that will contain the profile. +# The SQL module will also honor the User-Profile attribute. This +# attribute can be set anywhere in the authorize section (ie the users +# file). It is found exactly as the default profile is found. +# If it is set then it will *overwrite* the default profile setting. +# The idea is to select profiles based on checks on the incoming +# packets, not on user group membership. For example: +# -- users file -- +# DEFAULT Service-Type == Outbound-User, User-Profile := "outbound" +# DEFAULT Service-Type == Framed-User, User-Profile := "framed" +# +# By default the default_user_profile is not set +# +# default_user_profile = "DEFAULT" + +####################################################################### +# Open Query +####################################################################### +# This query is run whenever a new connection is opened. +# It is commented out by default. +# +# If you have issues with connections hanging for too long, uncomment +# the next line, and set the timeout in milliseconds. As a general +# rule, if the queries take longer than a second, something is wrong +# with the database. +#open_query = "set statement_timeout to 1000" + +####################################################################### +# NAS Query +####################################################################### +# This query retrieves the radius clients +# +# 0. Row ID (currently unused) +# 1. Name (or IP address) +# 2. Shortname +# 3. Type +# 4. Secret +# 5. Server +####################################################################### + +client_query = "\ + SELECT id, nasname, shortname, type, secret, server \ + FROM ${client_table}" + +####################################################################### +# Authorization Queries +####################################################################### +# These queries compare the check items for the user +# in ${authcheck_table} and setup the reply items in +# ${authreply_table}. You can use any query/tables +# you want, but the return data for each row MUST +# be in the following order: +# +# 0. Row ID (currently unused) +# 1. UserName/GroupName +# 2. Item Attr Name +# 3. Item Attr Value +# 4. Item Attr Operation +####################################################################### + +# +# Use these for case insensitive usernames. WARNING: Slower queries! +# +#authorize_check_query = "\ +# SELECT id, UserName, Attribute, Value, Op \ +# FROM ${authcheck_table} \ +# WHERE LOWER(UserName) = LOWER('%{SQL-User-Name}') \ +# ORDER BY id" + +#authorize_reply_query = "\ +# SELECT id, UserName, Attribute, Value, Op \ +# FROM ${authreply_table} \ +# WHERE LOWER(UserName) = LOWER('%{SQL-User-Name}') \ +# ORDER BY id" + +authorize_check_query = "\ + SELECT id, UserName, Attribute, Value, Op \ + FROM ${authcheck_table} \ + WHERE Username = '%{SQL-User-Name}' \ + ORDER BY id" + +authorize_reply_query = "\ + SELECT id, UserName, Attribute, Value, Op \ + FROM ${authreply_table} \ + WHERE Username = '%{SQL-User-Name}' \ + ORDER BY id" + +# +# Use these for case insensitive usernames. WARNING: Slower queries! +# +#authorize_group_check_query = "\ +# SELECT \ +# ${groupcheck_table}.id, ${groupcheck_table}.GroupName, ${groupcheck_table}.Attribute, \ +# ${groupcheck_table}.Value, ${groupcheck_table}.Op \ +# FROM ${groupcheck_table}, ${usergroup_table} \ +# WHERE LOWER(${usergroup_table}.UserName) = LOWER('%{SQL-User-Name}') \ +# AND ${usergroup_table}.GroupName = ${groupcheck_table}.GroupName \ +# ORDER BY ${groupcheck_table}.id" + +#authorize_group_reply_query = "\ +# SELECT \ +# ${groupreply_table}.id, ${groupreply_table}.GroupName, \ +# ${groupreply_table}.Attribute, ${groupreply_table}.Value, ${groupreply_table}.Op \ +# FROM ${groupreply_table}, ${usergroup_table} \ +# WHERE LOWER(${usergroup_table}.UserName) = LOWER('%{SQL-User-Name}') \ +# AND ${usergroup_table}.GroupName = ${groupreply_table}.GroupName \ +# ORDER BY ${groupreply_table}.id" + +authorize_group_check_query = "\ + SELECT id, GroupName, Attribute, Value, op \ + FROM ${groupcheck_table} \ + WHERE GroupName = '%{${group_attribute}}' \ + ORDER BY id" + +authorize_group_reply_query = "\ + SELECT id, GroupName, Attribute, Value, op \ + FROM ${groupreply_table} \ + WHERE GroupName = '%{${group_attribute}}' \ + ORDER BY id" + +####################################################################### +# Simultaneous Use Checking Queries +####################################################################### +# simul_count_query - query for the number of current connections +# - If this is not defined, no simultaneous use checking +# - will be performed by this module instance +# simul_verify_query - query to return details of current connections for verification +# - Leave blank or commented out to disable verification step +# - Note that the returned field order should not be changed. +####################################################################### + +simul_count_query = "\ + SELECT COUNT(RadAcctId) \ + FROM ${acct_table1} a \ + LEFT OUTER JOIN nasreload n USING (NASIPAddress) \ + WHERE UserName='%{SQL-User-Name}' \ + AND AcctStopTime IS NULL \ + AND (a.AcctStartTime > n.ReloadTime OR n.ReloadTime IS NULL)" + +simul_verify_query = "\ + SELECT RadAcctId, AcctSessionId, UserName, NASIPAddress, NASPortId, FramedIPAddress, CallingStationId, \ + FramedProtocol \ + FROM ${acct_table1} a \ + LEFT OUTER JOIN nasreload n USING (nasipaddress) \ + WHERE UserName='%{SQL-User-Name}' \ + AND AcctStopTime IS NULL \ + AND (a.AcctStartTime > n.reloadtime OR n.reloadtime IS NULL)" + +####################################################################### +# Group Membership Queries +####################################################################### +# group_membership_query - Check user group membership +####################################################################### + +# Use these for case insensitive usernames. WARNING: Slower queries! +#group_membership_query = "\ +# SELECT GroupName \ +# FROM ${usergroup_table} \ +# WHERE LOWER(UserName) = LOWER('%{SQL-User-Name}') \ +# ORDER BY priority" + +group_membership_query = "\ + SELECT GroupName \ + FROM ${usergroup_table} \ + WHERE UserName='%{SQL-User-Name}' \ + ORDER BY priority" + +####################################################################### +# Accounting and Post-Auth Queries +####################################################################### +# These queries insert/update accounting and authentication records. +# The query to use is determined by the value of 'reference'. +# This value is used as a configuration path and should resolve to one +# or more 'query's. If reference points to multiple queries, and a query +# fails, the next query is executed. +# +# Behaviour is identical to the old 1.x/2.x module, except we can now +# fail between N queries, and query selection can be based on any +# combination of attributes, or custom 'Acct-Status-Type' values. +####################################################################### + +accounting { + reference = "%{tolower:type.%{%{Acct-Status-Type}:-%{Request-Processing-Stage}}.query}" + + # Write SQL queries to a logfile. This is potentially useful for bulk inserts + # when used with the rlm_sql_null driver. +# logfile = ${logdir}/accounting.sql + + column_list = "\ + AcctSessionId, \ + AcctUniqueId, \ + UserName, \ + Realm, \ + NASIPAddress, \ + NASPortId, \ + NASPortType, \ + AcctStartTime, \ + AcctUpdateTime, \ + AcctStopTime, \ + AcctSessionTime, \ + AcctAuthentic, \ + ConnectInfo_start, \ + ConnectInfo_Stop, \ + AcctInputOctets, \ + AcctOutputOctets, \ + CalledStationId, \ + CallingStationId, \ + AcctTerminateCause, \ + ServiceType, \ + FramedProtocol, \ + FramedIpAddress, \ + FramedIpv6Address, \ + FramedIpv6Prefix, \ + FramedInterfaceId, \ + DelegatedIpv6Prefix \ + ${..class.column_name}" + + type { + + accounting-on { + + # + # "Bulk update" Accounting-On/Off strategy. + # + # Immediately terminate all sessions associated with a + # given NAS. + # + # Note: If a large number of sessions require closing + # then the bulk update may be take a long time to run + # and lock an excessive number of rows. See the + # strategy below for an alternative approach that does + # not touch the radacct session data. + # + query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctStopTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = (${....event_timestamp_epoch} - EXTRACT(EPOCH FROM(AcctStartTime))), \ + AcctTerminateCause = '%{%{Acct-Terminate-Cause}:-NAS-Reboot}' \ + WHERE AcctStopTime IS NULL \ + AND NASIPAddress= '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND AcctStartTime <= ${....event_timestamp}" + + # + # "Lightweight" Accounting-On/Off strategy. + # + # Record the reload time of the NAS and let the + # administrator actually close the sessions in radacct + # out-of-band, if desired. + # + # Implementation advice, together with a stored + # procedure for closing sessions and a view showing + # the effective stop time of each session is provided + # in process-radacct.sql. + # + # To enable this strategy, just change the previous + # query to "-query", and this one to "query". The + # previous one will be ignored, and this one will be + # enabled. + # + -query = "\ + INSERT INTO nasreload (NASIPAddress, ReloadTime) \ + VALUES ('%{NAS-IP-Address}', ${....event_timestamp}) \ + ON CONFLICT ON (NASIPAddress) \ + DO UPDATE SET \ + ReloadTime = ${....event_timestamp}" + + } + + accounting-off { + query = "${..accounting-on.query}" + } + + # + # Implement the "sql_session_start" policy. + # See raddb/policy.d/accounting for more details. + # + # You also need to fix the other queries as + # documented below. Look for "sql_session_start". + # + post-auth { + query = "\ + INSERT INTO ${....acct_table1} \ + (${...column_list}) \ + VALUES(\ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + NULLIF('%{Realm}', ''), \ + '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}', \ + NULLIF('%{%{NAS-Port-ID}:-%{NAS-Port}}', ''), \ + '%{NAS-Port-Type}', \ + ${....event_timestamp}, \ + NULL, \ + NULL, \ + 0, \ + '', \ + '%{Connect-Info}', \ + NULL, \ + 0, \ + 0, \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + NULL, \ + '%{Service-Type}', \ + '', \ + NULL, \ + NULL, \ + NULL, \ + NULL, \ + NULL \ + ${....class.reply_xlat})" + + query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctStartTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + ConnectInfo_start = '%{Connect-Info}', \ + AcctSessionId = '%{Acct-Session-Id}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}' \ + AND AcctStopTime IS NULL" + } + + start { + query = "\ + INSERT INTO ${....acct_table1} \ + (${...column_list}) \ + VALUES(\ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + NULLIF('%{Realm}', ''), \ + '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}', \ + NULLIF('%{%{NAS-Port-ID}:-%{NAS-Port}}', ''), \ + '%{NAS-Port-Type}', \ + ${....event_timestamp}, \ + ${....event_timestamp}, \ + NULL, \ + 0, \ + '%{Acct-Authentic}', \ + '%{Connect-Info}', \ + NULL, \ + 0, \ + 0, \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + NULL, \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + NULLIF('%{Framed-IP-Address}', '')::inet, \ + NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + NULLIF('%{Framed-Interface-Id}', ''), \ + NULLIF('%{Delegated-IPv6-Prefix}', '')::inet \ + ${....class.packet_xlat} ) \ + ON CONFLICT (AcctUniqueId) \ + DO UPDATE \ + SET \ + AcctStartTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + ConnectInfo_start = '%{Connect-Info}' \ + WHERE ${....acct_table1}.AcctUniqueId = '%{Acct-Unique-Session-Id}' \ + AND ${....acct_table1}.AcctStopTime IS NULL" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', '')::inet, \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', '')::inet, \ + AcctStartTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp} \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}' \ + AND AcctStopTime IS NULL" + + # and again where we don't have "AND AcctStopTime IS NULL" + query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctStartTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + ConnectInfo_start = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}'" + } + + interim-update { + query = "\ + UPDATE ${....acct_table1} \ + SET \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', '')::inet, \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', '')::inet, \ + AcctSessionTime = %{%{Acct-Session-Time}:-NULL}, \ + AcctInterval = (${....event_timestamp_epoch} - EXTRACT(EPOCH FROM (COALESCE(AcctUpdateTime, AcctStartTime)))), \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctInputOctets = (('%{%{Acct-Input-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Input-Octets}:-0}'::bigint), \ + AcctOutputOctets = (('%{%{Acct-Output-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Output-Octets}:-0}'::bigint) \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}' \ + AND AcctStopTime IS NULL" + + query = "\ + INSERT INTO ${....acct_table1} \ + (${...column_list}) \ + VALUES(\ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + NULLIF('%{Realm}', ''), \ + '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}', \ + NULLIF('%{%{NAS-Port-ID}:-%{NAS-Port}}', ''), \ + '%{NAS-Port-Type}', \ + ${....event_timestamp}, \ + ${....event_timestamp}, \ + NULL, \ + %{%{Acct-Session-Time}:-NULL}, \ + '%{Acct-Authentic}', \ + '%{Connect-Info}', \ + NULL, \ + (('%{%{Acct-Input-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Input-Octets}:-0}'::bigint), \ + (('%{%{Acct-Output-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Output-Octets}:-0}'::bigint), \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + NULL, \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + NULLIF('%{Framed-IP-Address}', '')::inet, \ + NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + NULLIF('%{Framed-Interface-Id}', ''), \ + NULLIF('%{Delegated-IPv6-Prefix}', '')::inet \ + ${....class.packet_xlat}) \ + ON CONFLICT (AcctUniqueId) \ + DO NOTHING" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', '')::inet, \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', '')::inet, \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = COALESCE(%{%{Acct-Session-Time}:-NULL}, \ + (${....event_timestamp_epoch} - EXTRACT(EPOCH FROM(AcctStartTime)))), \ + AcctInputOctets = (('%{%{Acct-Input-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Input-Octets}:-0}'::bigint), \ + AcctOutputOctets = (('%{%{Acct-Output-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Output-Octets}:-0}'::bigint) \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}' \ + AND AcctStopTime IS NULL" + } + + stop { + query = "\ + UPDATE ${....acct_table2} \ + SET \ + AcctStopTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = COALESCE(%{%{Acct-Session-Time}:-NULL}, \ + (${....event_timestamp_epoch} - EXTRACT(EPOCH FROM(AcctStartTime)))), \ + AcctInputOctets = (('%{%{Acct-Input-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Input-Octets}:-0}'::bigint), \ + AcctOutputOctets = (('%{%{Acct-Output-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Output-Octets}:-0}'::bigint), \ + AcctTerminateCause = '%{Acct-Terminate-Cause}', \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', '')::inet, \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', '')::inet, \ + ConnectInfo_stop = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}' \ + AND AcctStopTime IS NULL" + + query = "\ + INSERT INTO ${....acct_table1} \ + (${...column_list}) \ + VALUES(\ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + NULLIF('%{Realm}', ''), \ + '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}', \ + NULLIF('%{%{NAS-Port-ID}:-%{NAS-Port}}', ''), \ + '%{NAS-Port-Type}', \ + TO_TIMESTAMP(${....event_timestamp_epoch} - %{%{Acct-Session-Time}:-0}), \ + ${....event_timestamp}, \ + ${....event_timestamp}, \ + NULLIF('%{Acct-Session-Time}', '')::bigint, \ + '%{Acct-Authentic}', \ + '%{Connect-Info}', \ + NULL, \ + (('%{%{Acct-Input-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Input-Octets}:-0}'::bigint), \ + (('%{%{Acct-Output-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Output-Octets}:-0}'::bigint), \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '%{Acct-Terminate-Cause}', \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + NULLIF('%{Framed-IP-Address}', '')::inet, \ + NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + NULLIF('%{Framed-Interface-Id}', ''), \ + NULLIF('%{Delegated-IPv6-Prefix}', '')::inet \ + ${....class.packet_xlat}) \ + ON CONFLICT (AcctUniqueId) \ + DO NOTHING" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', '')::inet, \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', '')::inet, \ + AcctStopTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = COALESCE(%{%{Acct-Session-Time}:-NULL}, \ + (${....event_timestamp_epoch} - EXTRACT(EPOCH FROM(AcctStartTime)))), \ + AcctInputOctets = (('%{%{Acct-Input-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Input-Octets}:-0}'::bigint), \ + AcctOutputOctets = (('%{%{Acct-Output-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Output-Octets}:-0}'::bigint), \ + AcctTerminateCause = '%{Acct-Terminate-Cause}', \ + ConnectInfo_stop = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}' \ + AND AcctStopTime IS NULL" + + # and again where we don't have "AND AcctStopTime IS NULL" + query = "\ + UPDATE ${....acct_table2} \ + SET \ + AcctStopTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = COALESCE(%{%{Acct-Session-Time}:-NULL}, \ + (${....event_timestamp_epoch} - EXTRACT(EPOCH FROM(AcctStartTime)))), \ + AcctInputOctets = (('%{%{Acct-Input-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Input-Octets}:-0}'::bigint), \ + AcctOutputOctets = (('%{%{Acct-Output-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Output-Octets}:-0}'::bigint), \ + AcctTerminateCause = '%{Acct-Terminate-Cause}', \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', '')::inet, \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', '')::inet, \ + ConnectInfo_stop = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}'" + } + + # + # No Acct-Status-Type == ignore the packet + # + accounting { + query = "SELECT true" + } + } +} + + +####################################################################### +# Authentication Logging Queries +####################################################################### +# postauth_query - Insert some info after authentication +####################################################################### + +post-auth { + # Write SQL queries to a logfile. This is potentially useful for bulk inserts + # when used with the rlm_sql_null driver. +# logfile = ${logdir}/post-auth.sql + + query = "\ + INSERT INTO ${..postauth_table} \ + (username, pass, reply, authdate ${..class.column_name}) \ + VALUES(\ + '%{User-Name}', \ + '%{%{User-Password}:-%{Chap-Password}}', \ + '%{reply:Packet-Type}', \ + '%S.%M' \ + ${..class.reply_xlat})" +} diff --git a/raddb/mods-config/sql/main/postgresql/schema.sql b/raddb/mods-config/sql/main/postgresql/schema.sql new file mode 100644 index 0000000..518bc5d --- /dev/null +++ b/raddb/mods-config/sql/main/postgresql/schema.sql @@ -0,0 +1,178 @@ +/* + * $Id$ + * + * PostgreSQL schema for FreeRADIUS + * + */ + +/* + * Table structure for table 'radacct' + * + */ +CREATE TABLE IF NOT EXISTS radacct ( + RadAcctId bigserial PRIMARY KEY, + AcctSessionId text NOT NULL, + AcctUniqueId text NOT NULL UNIQUE, + UserName text, + Realm text, + NASIPAddress inet NOT NULL, + NASPortId text, + NASPortType text, + AcctStartTime timestamp with time zone, + AcctUpdateTime timestamp with time zone, + AcctStopTime timestamp with time zone, + AcctInterval bigint, + AcctSessionTime bigint, + AcctAuthentic text, + ConnectInfo_start text, + ConnectInfo_stop text, + AcctInputOctets bigint, + AcctOutputOctets bigint, + CalledStationId text, + CallingStationId text, + AcctTerminateCause text, + ServiceType text, + FramedProtocol text, + FramedIPAddress inet, + FramedIPv6Address inet, + FramedIPv6Prefix inet, + FramedInterfaceId text, + DelegatedIPv6Prefix inet, + Class text +); +-- This index may be useful.. +-- CREATE UNIQUE INDEX radacct_whoson on radacct (AcctStartTime, nasipaddress); + +-- For use by update-, stop- and simul_* queries +CREATE INDEX radacct_active_session_idx ON radacct (AcctUniqueId) WHERE AcctStopTime IS NULL; + +-- Add if you you regularly have to replay packets +-- CREATE INDEX radacct_session_idx ON radacct (AcctUniqueId); + +-- For backwards compatibility +-- CREATE INDEX radacct_active_user_idx ON radacct (AcctSessionId, UserName, NASIPAddress) WHERE AcctStopTime IS NULL; + +-- For use by onoff- +CREATE INDEX radacct_bulk_close ON radacct (NASIPAddress, AcctStartTime) WHERE AcctStopTime IS NULL; + +-- and for common statistic queries: +CREATE INDEX radacct_start_user_idx ON radacct (AcctStartTime, UserName); + +-- and, optionally +-- CREATE INDEX radacct_stop_user_idx ON radacct (acctStopTime, UserName); + +-- and for Class +CREATE INDEX radacct_calss_idx ON radacct (Class); + + +/* + * Table structure for table 'radcheck' + */ +CREATE TABLE IF NOT EXISTS radcheck ( + id serial PRIMARY KEY, + UserName text NOT NULL DEFAULT '', + Attribute text NOT NULL DEFAULT '', + op VARCHAR(2) NOT NULL DEFAULT '==', + Value text NOT NULL DEFAULT '' +); +create index radcheck_UserName on radcheck (UserName,Attribute); +/* + * Use this index if you use case insensitive queries + */ +-- create index radcheck_UserName_lower on radcheck (lower(UserName),Attribute); + +/* + * Table structure for table 'radgroupcheck' + */ +CREATE TABLE IF NOT EXISTS radgroupcheck ( + id serial PRIMARY KEY, + GroupName text NOT NULL DEFAULT '', + Attribute text NOT NULL DEFAULT '', + op VARCHAR(2) NOT NULL DEFAULT '==', + Value text NOT NULL DEFAULT '' +); +create index radgroupcheck_GroupName on radgroupcheck (GroupName,Attribute); + +/* + * Table structure for table 'radgroupreply' + */ +CREATE TABLE IF NOT EXISTS radgroupreply ( + id serial PRIMARY KEY, + GroupName text NOT NULL DEFAULT '', + Attribute text NOT NULL DEFAULT '', + op VARCHAR(2) NOT NULL DEFAULT '=', + Value text NOT NULL DEFAULT '' +); +create index radgroupreply_GroupName on radgroupreply (GroupName,Attribute); + +/* + * Table structure for table 'radreply' + */ +CREATE TABLE IF NOT EXISTS radreply ( + id serial PRIMARY KEY, + UserName text NOT NULL DEFAULT '', + Attribute text NOT NULL DEFAULT '', + op VARCHAR(2) NOT NULL DEFAULT '=', + Value text NOT NULL DEFAULT '' +); +create index radreply_UserName on radreply (UserName,Attribute); +/* + * Use this index if you use case insensitive queries + */ +-- create index radreply_UserName_lower on radreply (lower(UserName),Attribute); + +/* + * Table structure for table 'radusergroup' + */ +CREATE TABLE IF NOT EXISTS radusergroup ( + id serial PRIMARY KEY, + UserName text NOT NULL DEFAULT '', + GroupName text NOT NULL DEFAULT '', + priority integer NOT NULL DEFAULT 0 +); +create index radusergroup_UserName on radusergroup (UserName); +/* + * Use this index if you use case insensitive queries + */ +-- create index radusergroup_UserName_lower on radusergroup (lower(UserName)); + +-- +-- Table structure for table 'radpostauth' +-- + +CREATE TABLE IF NOT EXISTS radpostauth ( + id bigserial PRIMARY KEY, + username text NOT NULL, + pass text, + reply text, + CalledStationId text, + CallingStationId text, + authdate timestamp with time zone NOT NULL default now(), + Class text +); +CREATE INDEX radpostauth_username_idx ON radpostauth (username); +CREATE INDEX radpostauth_class_idx ON radpostauth (Class); + +/* + * Table structure for table 'nas' + */ +CREATE TABLE IF NOT EXISTS nas ( + id serial PRIMARY KEY, + nasname text NOT NULL, + shortname text NOT NULL, + type text NOT NULL DEFAULT 'other', + ports integer, + secret text NOT NULL, + server text, + community text, + description text +); +create index nas_nasname on nas (nasname); + +/* + * Table structure for table 'nasreload' + */ +CREATE TABLE IF NOT EXISTS nasreload ( + NASIPAddress inet PRIMARY KEY, + ReloadTime timestamp with time zone NOT NULL +); diff --git a/raddb/mods-config/sql/main/postgresql/setup.sql b/raddb/mods-config/sql/main/postgresql/setup.sql new file mode 100644 index 0000000..def5531 --- /dev/null +++ b/raddb/mods-config/sql/main/postgresql/setup.sql @@ -0,0 +1,58 @@ +/* + * setup.sql -- PostgreSQL commands for creating the RADIUS user. + * + * WARNING: You should change 'localhost' and 'radpass' + * to something else. Also update raddb/mods-available/sql + * with the new RADIUS password. + * + * $Id$ + */ + +/* + * Create default administrator for RADIUS + * + */ +CREATE USER radius WITH PASSWORD 'radpass'; + +/* + * The server can read the authorisation data + * + */ +GRANT SELECT ON radcheck TO radius; +GRANT SELECT ON radreply TO radius; +GRANT SELECT ON radusergroup TO radius; +GRANT SELECT ON radgroupcheck TO radius; +GRANT SELECT ON radgroupreply TO radius; + +/* + * The server can write accounting and post-auth data + * + */ +GRANT SELECT, INSERT, UPDATE on radacct TO radius; +GRANT SELECT, INSERT, UPDATE on radpostauth TO radius; + +/* + * The server can read the NAS data + * + */ +GRANT SELECT ON nas TO radius; + +/* + * In the case of the "lightweight accounting-on/off" strategy, the server also + * records NAS reload times + * + */ +GRANT SELECT, INSERT, UPDATE ON nasreload TO radius; + +/* + * Grant permissions on sequences + * + */ +GRANT USAGE, SELECT ON SEQUENCE radcheck_id_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE radreply_id_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE radusergroup_id_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE radgroupcheck_id_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE radgroupreply_id_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE radacct_radacctid_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE radpostauth_id_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE nas_id_seq TO radius; |