]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
SQL IP Pools support for MS SQL
authorTerry Burton <tez@terryburton.co.uk>
Wed, 27 Nov 2019 16:00:49 +0000 (16:00 +0000)
committerAlan DeKok <aland@freeradius.org>
Wed, 27 Nov 2019 22:25:02 +0000 (17:25 -0500)
Tested using the FreeTDS driver against SQL Server 2017 on Linux Docker, mostly
focussing on the stored procedure which gives 350 allocs/sec; 425 reallocs/sec
(with no specific database server optimisation).

The queries are more complicated than with most other dialects since MS SQL
lacks a "SELECT FOR UPDATE" statement, and its table hints do not provide a
direct means to implement the row-level read lock needed to guarentee that
concurrent queries do not select the same Framed-IP-Address for allocation to
distinct users.

raddb/mods-config/sql/ippool/mssql/procedure.sql [new file with mode: 0644]
raddb/mods-config/sql/ippool/mssql/queries.conf [new file with mode: 0644]
raddb/mods-config/sql/ippool/mssql/schema.sql [new file with mode: 0644]

diff --git a/raddb/mods-config/sql/ippool/mssql/procedure.sql b/raddb/mods-config/sql/ippool/mssql/procedure.sql
new file mode 100644 (file)
index 0000000..157d72c
--- /dev/null
@@ -0,0 +1,139 @@
+--
+-- A stored procedure to reallocate a user's previous address, otherwise
+-- provide a free address.
+--
+-- Using this SP reduces the usual set dialogue of queries to a single
+-- query:
+--
+--   BEGIN TRAN; "SELECT FOR UPDATE"; UPDATE; COMMIT TRAN;  ->  EXEC sp
+--
+-- The stored procedure is executed on an database instance within a single
+-- round trip which often leads to reduced deadlocking and significant
+-- performance improvements especially on multi-master clusters, perhaps even
+-- by an order of magnitude or more.
+--
+-- To use this stored procedure the corresponding queries.conf statements must
+-- be configured as follows:
+--
+-- allocate_begin = ""
+-- allocate_find = "\
+--      EXEC fr_allocate_previous_or_new_framedipaddress \
+--              @v_pool_name = '%{control:${pool_name}}', \
+--              @v_username = '%{User-Name}', \
+--              @v_callingstationid = '%{Calling-Station-Id}', \
+--              @v_nasipaddress = '%{NAS-IP-Address}', \
+--              @v_pool_key = '${pool_key}', \
+--              @v_lease_duration = ${lease_duration} \
+--      "
+-- allocate_update = ""
+-- allocate_commit = ""
+--
+
+CREATE INDEX UserName_CallingStationId ON radippool(pool_name,UserName,CallingStationId)
+GO
+
+CREATE OR ALTER PROCEDURE fr_allocate_previous_or_new_framedipaddress
+       @v_pool_name VARCHAR(64),
+       @v_username VARCHAR(64),
+       @v_callingstationid VARCHAR(64),
+       @v_nasipaddress VARCHAR(15),
+       @v_pool_key VARCHAR(64),
+       @v_lease_duration INT
+AS
+       BEGIN
+
+               -- MS SQL lacks a "SELECT FOR UPDATE" statement, and its table
+               -- hints do not provide a direct means to implement the row-level
+               -- read lock needed to guarentee that concurrent queries do not
+               -- select the same Framed-IP-Address for allocation to distinct
+               -- users.
+               --
+               -- The "WITH cte AS ( SELECT ... ) UPDATE cte ... OUTPUT INTO"
+               -- patterns in this procedure body compensate by wrapping
+               -- the SELECT in a synthetic UPDATE which locks the row.
+
+               DECLARE @r_address_tab TABLE(id VARCHAR(15));
+               DECLARE @r_address VARCHAR(15);
+
+               BEGIN TRAN;
+
+               -- Reissue an existing IP address lease when re-authenticating a session
+               --
+               WITH cte AS (
+                       SELECT TOP(1) FramedIPAddress
+                       FROM radippool
+                       WHERE pool_name = @v_pool_name
+                               AND expiry_time > CURRENT_TIMESTAMP
+                               AND UserName = @v_username
+                               AND CallingStationId = @v_callingstationid
+               )
+               UPDATE cte WITH (rowlock, readpast)
+               SET FramedIPAddress = FramedIPAddress
+               OUTPUT INSERTED.FramedIPAddress INTO @r_address_tab;
+               SELECT @r_address = id FROM @r_address_tab;
+
+               -- Reissue an user's previous IP address, provided that the lease is
+               -- available (i.e. enable sticky IPs)
+               --
+               -- When using this SELECT you should delete the one above. You must also
+               -- set allocate_clear = "" in queries.conf to persist the associations
+               -- for expired leases.
+               --
+               -- WITH cte AS (
+               --      SELECT TOP(1) FramedIPAddress
+               --      FROM radippool
+               --      WHERE pool_name = @v_pool_name
+               --              AND UserName = @v_username
+               --              AND CallingStationId = @v_callingstationid
+               -- )
+               -- UPDATE cte WITH (rowlock, readpast)
+               -- SET FramedIPAddress = FramedIPAddress
+               -- OUTPUT INSERTED.FramedIPAddress INTO @r_address_tab;
+               -- SELECT @r_address = id FROM @r_address_tab;
+
+               -- If we didn't reallocate a previous address then pick the least
+               -- recently used address from the pool which maximises the likelihood
+               -- of re-assigning the other addresses to their recent user
+               --
+               IF @r_address IS NULL
+               BEGIN
+                       WITH cte AS (
+                               SELECT TOP(1) FramedIPAddress
+                               FROM radippool
+                               WHERE pool_name = @v_pool_name
+                                       AND ( expiry_time < CURRENT_TIMESTAMP OR expiry_time IS NULL )
+                               ORDER BY
+                                       expiry_time
+                       )
+                       UPDATE cte WITH (rowlock, readpast)
+                       SET FramedIPAddress = FramedIPAddress
+                       OUTPUT INSERTED.FramedIPAddress INTO @r_address_tab;
+                       SELECT @r_address = id FROM @r_address_tab;
+               END
+
+               -- Return nothing if we failed to allocated an address
+               --
+               IF @r_address IS NULL
+               BEGIN
+                       COMMIT TRAN;
+                       RETURN;
+               END
+
+               -- Update the pool having allocated an IP address
+               --
+               UPDATE radippool
+               SET
+                       NASIPAddress = @v_nasipaddress,
+                       pool_key = @v_pool_key,
+                       CallingStationId = @v_callingstationid,
+                       UserName = @v_username,
+                       expiry_time = DATEADD(SECOND,@v_lease_duration,CURRENT_TIMESTAMP)
+               WHERE framedipaddress = @r_address;
+
+               COMMIT TRAN;
+
+               -- Return the address that we allocated
+               SELECT @r_address;
+
+       END
+GO
diff --git a/raddb/mods-config/sql/ippool/mssql/queries.conf b/raddb/mods-config/sql/ippool/mssql/queries.conf
new file mode 100644 (file)
index 0000000..5b0dafd
--- /dev/null
@@ -0,0 +1,183 @@
+# -*- text -*-
+#
+#  ippool/mssql/queries.conf -- MSSQL queries for rlm_sqlippool
+#
+#  $Id$
+
+# MSSQL-specific syntax
+allocate_begin = "BEGIN TRAN"
+allocate_commit = "COMMIT TRAN"
+
+#
+#  This series of queries allocates an IP address
+#
+#allocate_clear = "\
+#      UPDATE ${ippool_table} \
+#      SET \
+#              NASIPAddress = '', \
+#              pool_key = 0, \
+#              CallingStationId = '', \
+#              UserName = '', \
+#              expiry_time = NULL \
+#      WHERE pool_key = '${pool_key}'"
+
+#
+#  (Note: If your pool_key is set to Calling-Station-Id and not NAS-Port
+#  then you may wish to delete the "AND nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}'
+#  from the WHERE clause)
+#
+allocate_clear = "\
+       UPDATE ${ippool_table} \
+       SET \
+               NASIPAddress = '', \
+               pool_key = 0, \
+               CallingStationID = '', \
+               UserName = '', \
+               expiry_time = NULL \
+       WHERE expiry_time <= DATEADD(SECOND,-1,CURRENT_TIMESTAMP) \
+       AND NASIPAddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}'"
+
+#
+#  The ORDER BY clause of this query tries to allocate the same IP-address
+#  which user had last session...
+#
+allocate_find = "\
+       WITH cte AS ( \
+               SELECT TOP(1) FramedIPAddress FROM ${ippool_table} \
+               WHERE pool_name = '%{control:${pool_name}}' \
+               AND ( expiry_time < CURRENT_TIMESTAMP OR expiry_time IS NULL ) \
+               OR ( NASIPAddress = '%{NAS-IP-Address}' AND pool_key = '${pool_key}' ) \
+               ORDER BY \
+                       CASE WHEN UserName = '%{User-Name}' THEN 0 ELSE 1 END, \
+                       CASE WHEN CallingStationId = '%{Calling-Station-Id}' THEN 0 ELSE 1 END, \
+                       expiry_time \
+       ) \
+       UPDATE cte WITH (rowlock, readpast) \
+       SET FramedIPAddress = FramedIPAddress \
+       OUTPUT INSERTED.FramedIPAddress"
+
+#
+#  If you prefer to allocate a random IP address every time, use this query instead.
+#  Note: This is very slow if you have a lot of free IPs.
+#
+#allocate_find = "\
+#      WITH cte AS ( \
+#              SELECT TOP(1) FramedIPAddress FROM ${ippool_table} \
+#              WHERE pool_name = '%{control:${pool_name}}' \
+#              AND ( \
+#                      expiry_time < CURRENT_TIMESTAMP OR expiry_time IS NULL \
+#              ) \
+#              ORDER BY \
+#                      newid() \
+#      ) \
+#      UPDATE cte WITH (rowlock, readpast) \
+#      SET FramedIPAddress = FramedIPAddress \
+#      OUTPUT INSERTED.FramedIPAddress"
+
+#
+#  If an IP could not be allocated, check to see if the pool exists or not
+#  This allows the module to differentiate between a full pool and no pool
+#  Note: If you are not running redundant pool modules this query may be
+#  commented out to save running this query every time an ip is not allocated.
+#
+pool_check = "\
+       SELECT TOP(1) id \
+       FROM ${ippool_table} \
+       WHERE pool_name='%{control:${pool_name}}'"
+
+#
+#  This is the final IP Allocation query, which saves the allocated ip details.
+#
+allocate_update = "\
+       UPDATE ${ippool_table} \
+       SET \
+               NASIPAddress = '%{NAS-IP-Address}', pool_key = '${pool_key}', \
+               CallingStationId = '%{Calling-Station-Id}', \
+               UserName = '%{User-Name}', expiry_time = DATEADD(SECOND,${lease_duration},CURRENT_TIMESTAMP) \
+       WHERE FramedIPAddress = '%I'"
+
+#
+#  Use a stored procedure to find AND allocate the address. Read and customise
+#  `procedure.sql` in this directory to determine the optimal configuration.
+#
+#allocate_begin = ""
+#allocate_find = "\
+#      EXEC fr_allocate_previous_or_new_framedipaddress \
+#              @v_pool_name = '%{control:${pool_name}}', \
+#              @v_username = '%{User-Name}', \
+#              @v_callingstationid = '%{Calling-Station-Id}', \
+#              @v_nasipaddress = '%{NAS-IP-Address}', \
+#              @v_pool_key = '${pool_key}', \
+#              @v_lease_duration = ${lease_duration} \
+#      "
+#allocate_update = ""
+#allocate_commit = ""
+
+#
+#  This series of queries frees an IP number when an accounting START record arrives.
+#
+start_update = "\
+       UPDATE ${ippool_table} \
+       SET \
+               expiry_time = DATEADD(SECOND,${lease_duration},CURRENT_TIMESTAMP) \
+       WHERE NASIPAddress = '%{NAS-IP-Address}' \
+       AND pool_key = '${pool_key}' \
+       AND UserName = '%{User-Name}' \
+       AND CallingStationId = '%{Calling-Station-Id}' \
+       AND FramedIPAddress = '%{${attribute_name}}'"
+
+#
+#  Free an IP when an accounting STOP record arrives
+#
+stop_clear = "\
+       UPDATE ${ippool_table} \
+       SET \
+               NASIPAddress = '', \
+               pool_key = 0, \
+               CallingStationId = '', \
+               UserName = '', \
+               expiry_time = NULL \
+       WHERE NASIPAddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}' \
+       AND pool_key = '${pool_key}' \
+       AND UserName = '%{User-Name}' \
+       AND CallingStationId = '%{Calling-Station-Id}' \
+       AND FramedIPAddress = '%{${attribute_name}}'"
+
+#
+#  Update the expiry time for an IP when an accounting ALIVE record arrives
+#
+alive_update = "\
+       UPDATE ${ippool_table} \
+       SET \
+               expiry_time = DATEADD(SECOND,${lease_duration},CURRENT_TIMESTAMP) \
+       WHERE NASIPAddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}' \
+       AND pool_key = '${pool_key}' \
+       AND UserName = '%{User-Name}' \
+       AND CallingStationId = '%{Calling-Station-Id}' \
+       AND FramedIPAddress = '%{${attribute_name}}'"
+
+#
+#  Frees all IPs allocated to a NAS when an accounting ON record arrives
+#
+on_clear = "\
+       UPDATE ${ippool_table} \
+       SET \
+               NASIPAddress = '', \
+               pool_key = 0, \
+               CallingStationId = '', \
+               UserName = '', \
+               expiry_time = NULL \
+       WHERE NASIPAddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}'"
+
+#
+#  Frees all IPs allocated to a NAS when an accounting OFF record arrives
+#
+off_clear = "\
+       UPDATE ${ippool_table} \
+       SET \
+               NASIPAddress = '', \
+               pool_key = 0, \
+               CallingStationId = '', \
+               UserName = '', \
+               expiry_time = NULL \
+       WHERE NASIPAddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}'"
diff --git a/raddb/mods-config/sql/ippool/mssql/schema.sql b/raddb/mods-config/sql/ippool/mssql/schema.sql
new file mode 100644 (file)
index 0000000..2f699b0
--- /dev/null
@@ -0,0 +1,25 @@
+--
+-- Table structure for table 'radippool'
+--
+CREATE TABLE radippool (
+  id                    int IDENTITY (1,1) NOT NULL,
+  pool_name             varchar(30) NOT NULL,
+  FramedIPAddress       varchar(15) NOT NULL default '',
+  NASIPAddress          varchar(15) NOT NULL default '',
+  CalledStationId       VARCHAR(32) NOT NULL,
+  CallingStationId      VARCHAR(30) NOT NULL,
+  expiry_time           DATETIME NULL default NULL,
+  UserName              varchar(64) NOT NULL default '',
+  pool_key              varchar(30) NOT NULL default '',
+  PRIMARY KEY (id)
+)
+GO
+
+CREATE INDEX poolname_expire ON radippool(pool_name, expiry_time)
+GO
+
+CREATE INDEX FramedIPAddress ON radippool(FramedIPAddress)
+GO
+
+CREATE INDEX NASIPAddress_poolkey_FramedIPAddress ON radippool(NASIPAddress, pool_key, FramedIPAddress)
+GO