= SQL-Based IP Pool module.

== Introduction

The `sqlippool` module allocates IP addresses from one or more pools
of addresses.  It tracks addrwss to that it does not issue the same
leased IP address to multiple devices.

Using FreeRADIUS for IP address allocation has some advantages.
Centralized pool management means that the pools remains available
when one or more of the NASs becomes unavailable.  When pools are
managed on a NAS, the pools are down when the NAS is down.

Networks may have organisation-specific policies for the way that IP
addresses are assigned.  These policies are difficult to implement on
a NAS, but can be implemented with FreeRADIUS.  For example,
FreeRADIUS can assign pools of IP addresses to specific NASs or user
groups.  FreeRADIUS also control reallocation, in order to move users
from one pool to another, without disconnecting them.  This move
allows address ranges to be retired or repurposed without forcibly
disconnecting the users.

As with most FreeRADIUS modules, the `sqlippool` module ships with a
default configuration that "just works".  However, the module can also
be configured to use sophisticated IP allocation policies, via custom
SQL queries.

All of the SQL queries used by the module are configurable.  The
module simply provides for a few "hooks", in order to allocate an IP,
clear an IP, expire IPs, etc.  The result is that the IP pool
allocations works with all possible SQL databases, without requiring
code changes.  In addition, anyone familiar with SQL can customize the
allocation policies.  Minimal knowledge of FreeRADIUS is required.


[#operation]
== Operation

The SQL IP Pools module only perform an action when FreeRADIUS
receives a packet.  The entire state of the pools is held in the
`radippool` table.  The benefit of this approach is that the RADIUS
server can be restarted, replaced, or debugged without losing track of
any IP addresses.

One downside is that there is no method to perform actions at a
particular time.  We suggust using "cron" for periodic cleanups,
analysis, etc.

Another downside is that FreeRADIUS does not "know" the state of a
particular IP address.  The `sqlippool` module simply runs SQL
queries, which either return an IP address, or return "failed to
allocate IP".  Since the entire state of the pool is in the SQL
database, we recommend using standard SQL queries to find the state of
an individual IP address, MAC address, etc.

=== `Access-Request`: A device connects to the NAS with correct credentials

. FreeRADIUS receives an `Access-Request` from the NAS.  After some
processing (modules. `unlang`, etc.), the `IP-Pool.Name` attribute is
added to the `control` list.  This attribute indicates that the IP
address should be allocated from the named pool.  This attribute can
be added from any module, database, `unlang`, etc.

. When the `sqlippool` module is eventually run, it looks for the
`IP-Pool.Name` attribute, and runs the various SQL queries in order to
perform IP address allocation.  If the module is configured to place
address into the `Framed-IP-Address` attribute (as it is by default),
and that attribute already exists, the module does nothing, and
returns `noop`.

. The module then runs the `alloc_existing` query, which looks for
the IP address last assigned to the device from the pool indicated by
`IP-Pool.Name`.  The device is identified using the `pool_key`
configuration item (typically `NAS-Port`) and a NAS identifier (the
`NAS-IP-Address`).

. If no address was found using `alloc_existing`, the module then
runs the `alloc_find` query, which chooses a free IP address from
the pool indicated by `IP-Pool.Name`.  If `alloc_find` does not return
an IP address, the `pool_check` query is run in order to determine
why the allocation failed.  For example, either the requested pool is
empty (i.e.  no free addresses), or it is non-existent.  This
information is returned in the `sqlippool` module return code, as
`notfound`, or one of the other return codes.

. If an IP address has been found, the module runs the
`alloc_update` query.  This query assigns the IP address to the
device by updating the `radippool` row for the IP address with
information about the lease.  The default information includes `expiry
time` based on the configured `lease_duration`, a unique identifier
for the device as specified by the `pool_key` configuration item
(typically `NAS-Port`) and a NAS identifier (the `NAS-IP-Address`).
Additionally, the default schema and queries record the user
identifier (`User-Name`) and device identifier (`Calling-Station-Id`).
This information can be used to assign the same IP to the same user or
device, on subsequent allocation requests.  With some database backends
(PostgreSQL and MS SQLServer) the update can be incorporated into the
`alloc_existing` and `alloc_find` queries, reducing round trips
to the database and improving performance.

. The module returns `updated` to indicate that it was successfull in
allocating an IP address.

. After some additional processing (modules, `unlang`, etc.),
FreeRADIUS sends an Access-Accept containing a `Framed-IP-Address`
attribute holding the allocated IP address to the NAS.


=== `Accounting Start`: A device and a NAS successfully establish a session

. The NAS sends an `Accounting Start` request to FreeRADIUS.  After some
processing (modules. `unlang`, etc.), the `sqlippool` module is run.

. The `sqlippol` module runs the `update_update` query.  This query
uses the configured `pool_key` and NAS identifier in order to identify
which entry in the `radippool` table to update.  The `expiry_time` is
updated based on the configured `lease_duration`. This update extends
the initial lease to the configured `lease_duration` in case the event
that the Accounting Start request was delayed.

. The module returns `updated` to indicate that it was successfull in
updating the state of the IP address.

. After some additional processing (modules, `unlang`, etc.),
FreeRADIUS sends an Accounting-Response to the NAS.


=== `Accounting Interim-Update`: A NAS sends an Interim-Update notification for a device's session

. The NAS sends an `Accounting Interim-Update` request to FreeRADIUS.
After some processing (modules. `unlang`, etc.), the `sqlippool`
module is run.

. The `sqlippol` module runs the `update_update` query.  This query
uses the configured `pool_key` and NAS identifier in order to identify
which entry in the `radippool` table to update.  The `expiry_time` is
updated based on the configured `lease_duration`.

. The module returns `updated` to indicate that it was successfull in
updating the state of the IP address.

. After some additional processing (modules, `unlang`, etc.),
FreeRADIUS sends an Accounting-Response to the NAS.

=== `Accounting Stop`: A device disconnects or its session times out

. The NAS sends an `Accounting Stop` request to FreeRADIUS.  After
some processing (modules. `unlang`, etc.), the `sqlippool` module is
run.

. The `sqlippol` module runs the `release_clear` query.  This query uses
the configured `pool_key` and NAS identifier in order to identify
which entry in the `radippool` table to update.  The update "clears"
the IP address, and marks it as free for later allocation.  Note that
by default, this "clear" does not remove information about which user
or device was associated with that address.  The result is that on
subsequent allocations, it is possible to re-allocate the same IP
address to the same user or device.

. The module returns `updated` to indicate that it was successfull in
updating the state of the IP address.

. After some additional processing (modules, `unlang`, etc.),
FreeRADIUS sends an Accounting-Response to the NAS.


=== `Accounting On/Off`: A NAS indicates that all of its sessions have been cleared

. The NAS sends an `Accounting On` or `Accounting Off` request to
FreeRADIUS.  After some processing (modules. `unlang`, etc.), the
`sqlippool` module is run.

. The `sqlippol` module runs the `bulk_release_clear` query.  This query
uses the `NAS-IP-Address` in order to identify all leases in the
`radippool` table which belong to the NAS in question.  The leases are
cleared, and the IP addresses are immediately released for further
allocation.  This proces effectively returns all IP address occupied
by the dropped sessions back into the pool.

. The module returns `updated` to indicate that it was successfull in
updating the state of the IP addresses.

. After some additional processing (modules, `unlang`, etc.),
FreeRADIUS sends an Accounting-Response to the NAS.

== Step by step setup instructions

As with any FreeRADIUS configuration you are strongly recommended to
start with the default configuration for `sqlippools`, as it is known
to work.

Change the configuration, one thing at a time, testing after each
step, until you have successfully implemented your intended policy.
We also recommend using a revision control system such as `git`.  When
configuration changes are tracked, it becomes trivial to fix issues by
checking out a "known working" version of the configuration.  The
alternative is to try to track changes manually, or to manually
recreate what is believed to be a "working" configuration.

This guide provides essential step by step instructions for
configuring the `sqlippool` module.  These instructions begin with a
"default" configuration.  The actions described here are
straightforward, and will arrive at a working configuration.

However, the reasons for setting particular module configuration items
are complex, and require an understanding of the NAS and wider access
network.  We therefore discuss these reasons in a separate section, so
that the guide is simpler to follow.

TIP: If at any time you break your system then follow the
https://wiki.freeradius.org/guide/radiusd-X[guide to debugging
FreeRADIUS]. If anything goes wrong, it should be trivial to revert
any recent changes.  Then, consider what went wrong by examining the
debug output.  If the issues are still clear, then ask a question on
the
http://lists.freeradius.org/mailman/listinfo/freeradius-users[freeradius-users]
mailing list.  Please also read the
https://wiki.freeradius.org/list-help[list help] instructions, for
what information we need in order to help you.  This information is
typically an explantion of what you are trying to achieve, what
exactly isn't working and provide the full debugging output for a
relevant test run.


=== 1. Enable the module and call it during request processing

Firstly enable the module by creating its symlink.

[source,shell]
----
cd /etc/raddb/mods-enabled
ln -s ../mods-available/sqlippool
----

Ensure that the module is invoked during authentication and accounting request
processing by uncommenting any lines containing `sqlippool` in the send
`Access-Accept` and `Accounting-Response` sections of the `default` site.

.[raddb]/sites-enabled/default
[source,config]
----
send Access-Accept {
...
       sqlippool
...
}

...

send Accounting-Response {
...
       sqlippool
...
}
----

In the event that `sqlippool` is unable to allocate an IP address you may want
to record the reason for the failure in a `Reply-Message` attribute of an
`Access-Reject` response.

.Example of recording the allocation failure reason in `Reply-Message`
=============================================

.[raddb]/sites-enabled/default
[source,config]
----
send Access-Accept {
...
        group {
                sqlippool {
                        ok = return
                        noop = return
                }
                # On failure for any reason, reject
                &reply += {
			Reply-Message = Module-Failure-Message
		}
                reject
        }
...
}

----
=============================================

You must now configure the module configuration items in
`[raddb]/mods-enabled/sqlippool` as described below.


=== 2. Configure a database server

The performance requirements for a system with a many IP allocations
is likely to determine your choice of database software and
architecture.

Read and understand the considerations for <<database-performance,choosing a database
server>>.

Consider how these relate to your solution, then select an appropriate
backend database for the SQL IP Pools. If a database server is already
deployed in your environment, then any solution fit within the
existing limitations.  High load IP address allocation can severely
stress an SQL database.  Depending on many factos, performance can
very from dozens of IP allocations per second to thousands of
allocations per second.

The `sqlippools` module requires a connection to the database to be
provided, using an instance of the `sql` module. Configure this as
described in xref:modules/sql/index.adoc[the SQL module guide].

Set the `sql_module_instance` configuration item to the name of the `sql`
instance that you have configured and set the `dialect` configuration item to
the same as that of the `sql` module. This determines which version of the
`queries.conf` file is used.

.[raddb]/mods-enabled/sqlippool
[source,config]
----
sqlippool {
...
        sql_module_instance = "sql"
        dialect = "mysql"
...
}
----


WARNING: If the SQL IP Pools database is a multi-master cluster (such as
Galera) then you will have multiple instances of the `sql` module, one per
node. For redundancy, you should configure a `sqlipool` instance corresponding
to each `sql` instance for each master node in your cluster. However, you *must
not* use a load-balancing policy such as `load-balance` or
`redundant-load-balance` to spread SQL IP Pool load between cluster nodes for
the <<database-clusters,reasons explained here>>. Do not ignore this advice
unless you are certain that you have understood the implications. Instead you
should use the basic `redundant` policy which will direct all queries to the
first available instance and then be careful to ensure that you do not run out
of connections or that the module might routinely fail in a way that directs IP
allocation queries to multiple database instances simultaneously during normal
operation.

Load the SQL IP Pool schema into the database. Methods vary between databases,
but a typical example would be:

.[raddb]/mods-enabled/sqlippool
[source,shell]
----
mysql radius < /etc/raddb/mods-config/sql/ippool/mysql/schema.sql
----

NOTE: The above command assumes that passwordless login has been configured via
the user’s `~/.my.cnf` file, or otherwise.

For performance reasons, if you are using a database that supports `SELECT ...
FOR UPDATE SKIP LOCKED` then you should edit the
`[raddb]/mods-config/sql/ippools/<dialect>/queries.conf` file corresponding to
your database dialect to select the `SKIP LOCKED` variant of the
`alloc_find` query. This will allow the database to remain responsive under
concurrent load.


=== 3. Configure a unique device identifier

Read and understand the considerations for <<device-identifier,choosing a device identifier>>.

Set the `pool_key` configuration item to the chosen unique device identifier attribute or
combination of attributes.

.[raddb]/mods-enabled/sqlippool
[source,config]
----
sqlippool {
...
        pool_key = "%{NAS-Port-Id}:%{Calling-Station-Id}"
...
}
----


=== 4. Configure the lease duration

Read and understand the considerations for <<lease-duration,choosing a lease duration>>.

Set the `lease_duration` configuration item to the chosen lease duration in seconds.

.[raddb]/mods-enabled/sqlippool
[source,config]
----
sqlippool {
...
        lease_duration = 3600
...
}
----

WARNING: Remember to enable Interim-Updates on the NAS and configure the update
interval with respect to the chosen `lease_duration`, typically to less than
half the lease duration.

In some access networks there may be circumstances where the device and NAS do
not always finish establishing a connection after FreeRADIUS has issued an IP
address and sent it in an `Access-Accept`, i.e. the session never really
starts and FreeRADIUS does not receive an `Accounting Start` request. This may
be due to a network fault, the device or NAS rejecting the data sent in the
`Access-Accept` response, or some additional policy implemented elsewhere. In
the default configuration, when a successfully authenticated session does not
become fully established the IP address will have been allocated for the full
`lease_duration` despite the device not having actually connected. It will remain
like this since there will be no `Accounting Stop` request generated for a
session that does not become fully established.

[#short-initial-lease]
TIP: This waste of an address may be undesirable if the capacity of the pool is
a concern, especially if sticky IPs are not enabled and a device that is
repeatedly failing to establish a connection is able to continue to consume IP
addresses. With some consideration, this initial lease could be amended to a
short, fixed interval rather than the full lease duration. Replace
`lease_duration` with a fixed value in seconds in the `alloc_update` query
in `queries.conf` or in the stored procedure in `procedure.sql`, whichever is
in use. This fixed interval should be greater than the maximum time it could
take for an Accounting Start to be received for successful connections.

FreeRADIUS errs on the side of caution and initially allocates IP
addresses during authentication for the full lease duration for two reasons:
(1) We do not actually know how long it takes for a session to become
established and for the Accounting Start request to actually be sent by the
NAS. (2) We do not know that Accounting Start requests will always be reliably
generated by the NAS even when a device has established a connection.  These
should be considered if you decide to amend the default policy.


=== 5. Configure the IP address attribute

Set the `attribute_name` configuration item to whatever RADIUS attribute is
required by the NAS, for example:

.[raddb]/mods-enabled/sqlippool
[source,config]
----
sqlippool {
...
        attribute_name = radius.Framed-IP-Address
...
}
----

TIP: Consult your NAS documentation to determine what attribute is used to
indicate the assigned IP address *and* any additional attributes that are
required for the RADIUS reply, e.g. `Framed-IP-Netmask`.

TIP: If your NAS is allocating IPv6 prefixes to devices on the basis of a
Framed-IPv6-Prefix RADIUS attribute then you can put the IPv6 prefixes into the
`radippool` table and set `attribute_name = Framed-IPv6-Prefix`.


=== 6. Populate the pool

Populate the pool either manually using a text editor or database tool, or via a script.

See xref:modules/sqlippool/generating.adoc[Generating IPs for the
pools] for instructions on how to create lists of IPs for a pool.  And
then xref:modules/sqlippool/insert.adoc[Inserting IPs into SQL]


.Example shell command for populating the `radippool` table
=============================================
 for i in `seq 10 250`; do
     echo "INSERT INTO radippool (          \
               pool_name,                   \
               framedipaddress,             \
               pool_key,                    \
               nasipaddress,                \
               calledstationid,             \
               callingstationid,            \
               expiry_time                  \
           ) VALUES (                       \
               'internet',                  \
               '192.0.2.$i',                \
               '0',                         \
               '',                          \
               '',                          \
               '',                          \
               CURRENT_TIMESTAMP            \
           );"
 done | mysql radius

=============================================

CAUTION: If at any time you should accidentally remove an IP address from the
pool that is in use then to avoid issuing duplicate IP addresses you must
either wait for the existing session to expire (or terminate it manually. e.g.
CoA/Disconnect) or reintroduce the IP address with a future expiry date beyond
which any existing session will have closed (or at least beyond the accounting
interval).

=== 7. Enable a pool user

For a request to acquire an address from the pool you must set a `IP-Pool.Name`
check attribute, either directly in the virtual server configuration or via a
data source such as `sql` (for example using the `radcheck` table).

.Example of setting a IP-Pool.Name attribute using the `radcheck` table
=============================================

[source,shell]
----
echo "INSERT INTO radcheck (username,attribute,op,value)   \
      VALUES ('bob','IP-Pool.Name',':=','internet');"         \
     | mysql radius
----

=============================================


.Example of setting a IP-Pool.Name attribute using in the server config
=============================================

.[raddb]/sites-enabled/default
[source,config]
----
send Access-Accept {
...
        &control.IP-Pool.Name := 'internet'
        sqlippool
...
}
----

=============================================

NOTE: Recall that if `sqlippool` is invoked without a `IP-Pool.Name` check
attribute then no action is taken. Also recall that if `sqlippool` is invoked
whilst the attribute configured in the `attribute_name` configuration item
(e.g. `Framed-IP-Address`) already exists then no action is taken.


=== 8. Test the basic setup

Start by creating some text files holding the test request data based on the
actual form of the contents of packets originating from the NAS.

.Example RADIUS requests text files for testing SQL IP Pools
=============================================

.access-request.rad
[source,config]
----
User-Name = bob
User-Password = testing123
Calling-Station-Id = 00:53:00:11:22:33
NAS-Port = 12345
NAS-IP-Address = 192.0.2.5
----

.accounting-start.rad
[source,config]
----
Acct-Session-Id = 1000
Acct-Status-Id = Start
User-Name = bob
User-Password = testing123
Calling-Station-Id = 00:53:00:11:22:33
NAS-Port = 12345
NAS-IP-Address = 192.0.2.5
Framed-IP-Address = ???.???.???.???
----

.accounting-alive.rad
[source,config]
----
Acct-Session-Id = 1000
Acct-Status-Id = Interim-Update
User-Name = bob
User-Password = testing123
Calling-Station-Id = 00:53:00:11:22:33
NAS-Port = 12345
NAS-IP-Address = 192.0.2.5
Framed-IP-Address = ???.???.???.???
----

.accounting-stop.rad
[source,config]
----
Acct-Session-Id = 1000
Acct-Status-Id = Interim-Update
User-Name = bob
User-Password = testing123
Calling-Station-Id = 00:53:00:11:22:33
NAS-Port = 12345
NAS-IP-Address = 192.0.2.5
Framed-IP-Address = ???.???.???.???
----

.accounting-on.rad
[source,config]
----
Acct-Status-Id = On
NAS-IP-Address = 192.0.2.5
----

.accounting-off.rad
[source,config]
----
Acct-Status-Id = Off
NAS-IP-Address = 192.0.2.5
----

=============================================

Now run through a series of tests examining the effect on the `radippool`
tables at each stage to ensure that it matches the expected behaviour as
described in the <<operation,Operation section>>.


**Testing initial authentication**

Send the Access Request to FreeRADIUS.

[source,terminal]
----
cat access-request.rad | radclient -x 127.0.0.1 auth testing123
Sent Access-Request Id 1 from 0.0.0.0:2000 to 127.0.0.1:1812 length 81
	Password.Cleartext = "testing123"
	User-Name = "bob"
	User-Password = "testing123"
	Calling-Station-Id = "00:53:00:11:22:33"
	NAS-Port = 12345
	NAS-IP-Address = 192.0.2.5
Received Access-Accept Id 1 from 127.0.0.1:1812 to 0.0.0.0:2000 length 31
	User-Name = "bob"
	Framed-IP-Address = 192.0.2.10
----

Note that an IP address has been issued in the `Framed-IP-Address` of the
reply.

TIP: If you do no receive an IP address then start FreeRADIUS in debugging mode
to determine where the process if failing as described in the
https://wiki.freeradius.org/guide/radiusd-X[guide to debugging FreeRADIUS]. Do
not proceed until you have resolved the IP allocation issue.

Check the status of the `radippool` table.

[source,terminal]
----
echo "SELECT * FROM radippool WHERE username <> ''" | mysql radius
+----+-----------+-----------------+--------------+----------+-------------------+---------------------+
| id | pool_name | framedipaddress | nasipaddress | username | callingstationid  | expiry_time         |
+----+-----------+-----------------+--------------+----------+-------------------+---------------------+
| 1  | internet  | 192.0.2.10      | 192.0.2.5    | bob      | 00:53:00:11:22:33 | 2020-01-01 10:10:10 |
+----+-----------+-----------------+--------------+----------+-------------------+---------------------+
1 rows in set (0.0030 sec)
----

For the entry matching the given IP address ensure that the IP allocation has
been recorded correctly. Check that:

. The `pool_key` matches the expected value of the unique identifier that you
chose. Double check that values of this form will be unique across all of your
devices.

. The `expiry_time` is `lease_duration` seconds ahead of the time of the
request (or some fixed value that you chose for the initial lease if you
updated the default policy.)

. The `User-Name` and `Calling-Station-Id` attributes are populated correctly
where this information is given in the request.

. The `NAS-IP-Address` has been provided. If not then you may need to
reconfigure your NAS to provide this or instantiate this attribute from
`Net.Src.IP` using an unlang policy in FreeRADIUS. Otherwise when
the NAS reboots you will not be able to match the affected IP addresses to the
device.

Update your sample text files containing the accounting requests to include the
allocated IP Address *before proceeding* with accounting packet testing.


**Testing Accounting Start**

[source,terminal]
----
cat accounting-start.rad | radclient -x 127.0.0.1 acct testing123
----

Check the status of the `radippool` table.

For the entry matching the given IP address ensure that *initial lease
extension* is occurring by verifying that the `expiry_time` is in the future by
`lease_duration` seconds from the time of the request.


**Testing Accounting Interim-Update**

[source,terminal]
----
cat accounting-alive.rad | radclient -x 127.0.0.1 acct testing123
----

Check the status of the `radippool` table.

For the entry matching the given IP address ensure that *IP address renewal* is
occurring by verifying that the `expiry_time` is in the future by
`lease_duration` seconds from the time of the request.


**Testing Accounting Stop**

[source,terminal]
----
cat accounting-stop.rad | radclient -x 127.0.0.1 acct testing123
----

Check the status of the `radippool` table.

For the entry matching the given IP address ensure that *IP address release* is
occurring by verifying that the `expiry_time` is set prior to the current time
or is null.


**Additional tests**

* Repeat this authentication test with the same user to ensure that the same IP
address is allocated for a re-authentication of an ongoing session.

* Repeat the authentication test with multiple users to ensure that each user is
assigned a unique IP address.

* Test that `Accounting On/Off` packets clear all sessions owned by the NAS.

* If you have already enabled a sticky IP policy then ensure that user
and device data is not removed when an `Accounting Stop` (and `Accounting
On/Off`) request is received. Ensure that users receive their previous IP
address when they authenticate using a device whose recent session is
disconnected.


== Recommended additional configuration

Once the initial configuration is working there are some additional recommended
configuration steps to consider.

The basic configuration arrived at in the previous section will be functional
but it is likely to exhibit poor performance under concurrent load. It also
implements a simple, dynamic IP allocation policy that you may want to change.


=== Enable the IP allocation stored procedure

The stored procedure is provided as an efficient means to both find and
allocate an IP address with a single SQL call so that no locks are held across
rounds trips between FreeRADIUS and the database. Depending on the database
software and configuration the stored procedure may increase the IP allocation
performance by as much as 100x and ensure that the system doesn't fail due to
excessive locking under high load.

Load the IP allocation stored procedure in the `procedure.sql` file
corresponding to the database dialect.

Methods vary between databases, but a typical example would be:

[source,shell]
----
mysql radius < /etc/raddb/mods-config/sql/ippool/mysql/procedure.sql
----

NOTE: The above command assumes that passwordless login has been configured via
the user’s `~/.my.cnf` file, or otherwise.

Read the comments in the `procedure.sql` file which explain how to use
the stored procedure, then amend the `alloc_find`, and
`allocate_update` queries (as well as the corresponding start/end
transaction query-parts) in the dialect's `queries.conf` file,
*exactly as described*.


.Example queries.conf configuration to use the stored procedure with MySQL
=============================================

[raddb]/mods-config/sql/ipool/<dialect>/queries.conf
[source,config]
----
...
allocate_begin = ""

alloc_find = "\
      CALL fr_allocate_previous_or_new_framedipaddress( \
              '%{control.${pool_name}}', \
              '%{User-Name}', \
              '%{Calling-Station-Id}', \
              '%{NAS-IP-Address}', \
              '${pool_key}', \
              ${lease_duration} \
      )"

allocate_update = ""

allocate_commit = ""

...
----

=============================================

Now that the IP allocation stored procedure has been enabled you should re-test
your configuration.


=== Customise the IP allocation policy

The IP allocation policy is mainly configured by modifying the `alloc_find`
query, however it is likely that the other queries will also need to be
modified to get the results you want.

By default each user is likely to get a different IP address each time they
authenticate unless they re-authenticate before their existing session has
terminated (as is the case with many EAP-based implementations during "fast
re-authentication").

Often it is required that upon reconnection a device obtains its previous IP
address, sometimes referred to as a "sticky IPs" policy.

If you are using the base queries (rather than the recommended stored
procedure) then the `queries.conf` file for your database dialect contains
several example `alloc_find` queries for choosing either a dynamic or sticky
IP policy.

If you are using the recommended stored procedure then the `procedure.sql` file
which contains the definition of the stored procedure for your database dialect
has comments that explain how to amend the procedure to choose either a dynamic
or sticky IP policy.

With a sticky IP policy it is necessary to amend the default actions
of the `release_clear` and `bulk_release_clear` queries.  By default,
each of these queries clobbers the `User-Name` and
`Calling-Station-Id` attributes when sessions expire therefore erasing
the affinity information.

TIP: A performance benefit of sticky IP addresses derived from the fact that
reallocation of an existing IP address is generally much quicker than
allocating a new IP address since it generally involves an indexed lookup of a
single IP address based on the device's request attributes rather than a walk
of the table's indexes.

TIP: The default `radippool` table contains attributes for `User-Name` and
`Calling-Station` that can be used to set the affinity for a sticky IP policy.
With care, these can be modified and the queries adjusted accordingly so that
any RADIUS attribute that is available during authentication can be used.

.Example queries for use with a sticky IP policy
=============================================

.[raddb]/mods-config/sql/ipool/<dialect>/queries.conf
[source,config]
----
...
alloc_begin = ""
alloc_find = "\
        CALL fr_allocate_previous_or_new_framedipaddress( \
                '%{control.${pool_name}}', \
                '%{User-Name}', \
                '%{Calling-Station-Id}', \
                '%{NAS-IP-Address}', \
                '${pool_key}', \
                ${lease_duration} \
        )"
alloc_update = ""
alloc_commit = ""
...
release_clear = "\
        UPDATE ${ippool_table} \
        SET \
                nasipaddress = '', \
                pool_key = 0, \
                expiry_time = NOW() \
        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}}'"

bulk_release_clear = "\
        UPDATE ${ippool_table} \
        SET \
                nasipaddress = '', \
                pool_key = 0, \
                expiry_time = NOW() \
        WHERE nasipaddress = '%{&NAS-IP-Address || &NAS-IPv6-Address}'"

...
----

.[raddb]/mods-config/sql/ipool/<dialect>/procedure.sql
[source,sql]
----
...
        -- Reissue an user's previous IP address, provided that the lease
        -- is available (i.e. enable sticky IPs)
        --
        SELECT framedipaddress INTO r_address
        FROM radippool
        WHERE pool_name = v_pool_name
                AND username = v_username
                AND callingstationid = v_callingstationid
        LIMIT 1
        FOR UPDATE SKIP LOCKED;

        -- 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 THEN
                SELECT framedipaddress INTO r_address
                FROM radippool
                WHERE pool_name = v_pool_name
                        AND ( expiry_time < NOW() OR expiry_time IS NULL )
                ORDER BY
                        expiry_time
                LIMIT 1
                FOR UPDATE SKIP LOCKED;
        END IF;
...
----

=============================================

Now that you have amended your IP allocation policy you should re-test your
configuration carefully to validate each of the amended queries and exercise
all of the features of your policy.

If your testing passes then FreeRADIUS should now be ready to allocate IP
addresses.


== Detailed considerations

[#database-performance]
=== Choice of database server

The performance of the `sqlippool` module is much more sensitive to the choice
of backend database than is typical for other FreeRADIUS database workloads.

The `sqlippool` module allocates IP addresses by using a relational database to
implement the "accept" part of a "worker queue" pattern, where multiple workers
attempt to select a single, unique work item from an unordered queue. In our
case the queue is the IP pool and each worker is an authentication request
attempting to reserve a unique IP address.

The overall performance largely depends on how directly and optimally the
database server software supports this pattern.

In particular, the SQL standard introduces two pragmas that facilitate an
optimised implementation of a worker queue:

`SELECT FOR UPDATE ...`::
Selects rows whilst exclusively locking them to prevent simultaneous locking
reads and writes of those same rows. When connections are simultaneously
selecting an IP address from a pool this pragma is what provides the guarantee
of uniqueness of the IP selected for each authentication request.

`... SKIP LOCKED`::
This is a modifier for `SELECT FOR UPDATE` that optimises the select by
allowing each reader to disregard any locked rows in order to avoid blocking.
This pragma avoids the "thundering herd" problem that can cripple performance
during highly concurrent IP allocation: Several authentication requests wait a
lock to be release on a single IP address that is in the process of being
allocated to a device. When the lock is release all but one of these previously
blocked connections block waiting for the next free IP address to be allocated.
And so on until they have all been serviced or their query times out... In the
absence of this pragma concurrent authentication requests are unnecessarily
serialised whilst their IP addresses are allocated resulting in poor
performance.

Here are some comments regarding the main database engines in relation to this
issue:

PostgreSQL::
Since version 9.5 provides a direct implementation of the `SELECT ... FOR
UPDATE SKIP LOCKED` statement which means that it is a very efficient backend
for `sqlippool`.

MySQL::
Ensure that you use a storage engine what supports transactions such as InnoDB.
Do not use MyISAM. Since version 8 the InnoDB storage engine provides a direct
implementation of the `SELECT ... FOR UPDATE SKIP LOCKED` statement which means
that it is a very efficient backend for `sqlippool`. Previous versions do not
implement the `SKIP LOCKED` pragma and will therefore have poor performance
under concurrent load, _but see the tip concerning the alternative stored
procedure below_.

MariaDB::
Ensure that you use a storage engine what supports transactions such as InnoDB.
Do not use MyISAM or Aria. All versions of MariaDB lack `SKIP LOCKED` and
therefore will have poor performance under concurrent load, _but see the tip
concerning the alternative stored procedure below_. Given the choice between
MariaDB and a recent version of MySQL you should use MySQL for SQL IP Pools.

MS SQL Server::
Does not provide a `SELECT ... FOR UPDATE` implementation nor a direct means to
provide a row-level, exclusive lock that ensures uniqueness of the selected
row. However FreeRADIUS is able to use an artificial `UPDATE` of a selected row
along with non-standard "hints" to obtain the required `SELECT ... FOR UPDATE
SKIP LOCKED` semantics, but we pay a performance penalty by doing so. The
overall performance is moderate but it does not collapse under concurreent
load.

Oracle::
Provides a direct implementation of the `SELECT ... FOR UPDATE SKIP LOCKED`
statement. It should be very efficient but limitations of the query syntax
result in the need to use multiple subqueries for which pay a performance
penalty. The overall performance is moderate but it does not collapse under
concurreent load.

SQLite::
Does not provide a `SELECT ... FOR UPDATE` implementation due to its simplified
locking characteristics. The only locking that is available for this purpose is
an exclusive lock taken over the entire database. This makes it unsuitable for
anything other than managing a small number of devices.

[TIP]
.Alternative stored procedure for MariaDB and MySQL < 8.0
===================================================================================

The file `procedure_no_skip_locked.sql` contains an alternative stored
procedure that is intended exclusively for MySQL variants that do not support
the `SKIP LOCKED` pragma, i.e. all MariaDB versions and versions of MySQL prior
to 8.0.

It should be a lot faster than using the default stored procedure under highly
concurrent workloads and should not result in thread starvation. It can best be
described as a useful hack that should not be used except when the
better-performing `SKIP LOCKED` method is unavailable.

This procedure works by using "user locks" to provide skippable, row-level
locking that facilitates non-blocking selection of unique IP addresses from the
pool. We pay a performance penalty for managing the locks, however it is
nowhere near as bad as serialising all concurrent pool access.

Since user locks are local to the database server instance this procedure is
not suitable for a multi-master cluster.

===================================================================================

[#database-clusters]
[NOTE]
.Database clusters (e.g. MaxScale, Galera)
===================================================================================

It is usually the case that row-level locks issued by `SELECT ... FOR UPDATE`
do not propagate around database clusters which means that unique IP address
selection cannot always be guaranteed.

Ingress nodes (relays, proxies, etc.) typically balance reads across all
cluster nodes (master and slave) whilst sending writes to one or more masters.
For many workloads this guarantees an appropriate level of data coherency,
however for an IP pool allocation implementation this is insufficient since we
must guarantee that no two nodes may concurrently `SELECT` the same IP address
from the pool of free addresses.

With clusters the behaviour is normally that when multiple connections
initially select the same IP address, one of them will succeed and when the
remaining transactions attempt to finalise the lease of the already-allocated
IP address only then will the conflict be detected.

When this occurs the sane and correct behaviour of most clusters is for
deadlocks to be generated for all but the first transaction and for them to be
rolled back. However this will result in IP allocation failures, which is
likely to be reported as an `Access-Reject`.

CAUTION: If however the cluster does not abort the transactions but goes on to
erroneously commit them, duplicate IP addresses will be allocated! It is
therefore critical to understand how your database cluster manages this case.

Incorrectly configured clusters are likely to have generally good performance
under high volumes of serialised load but may exhibit a performance cliff-edge
once a moderate level of concurrency is reached. The main symptom of this would
be the sudden onset of deadlock. Test your cluster under conditions that
represent the maximum load on the system, such as all devices simultaneously
re-authenticating after some form of failure.

TIP: In some cases it may be possible to alleviate these kinds of issue by
using a `redundant` policy for the `sql` module instances rather than a
`load-balance` policy, or by adding routing hints to the SQL queries to direct
all SQL IP Pool related queries to a single cluster node.

===================================================================================


[#device-identifier]
=== Choice of unique device identifier

Prior to issuing IP addresses to devices it is necessary to determine how to
uniquely identify devices to which IP addresses will be assigned on the basis
of the available RADIUS attributes.

In many fixed-line configurations a unique identifier for a device may be the
`NAS-Port` or `NAS-Port-Id`. In other scenarios identifiers based on the NAS
port may be shared by several devices and are unsuitable for uniquely
identifying devices. In some cases a circuit-specific identifier such as
`Vendor-Specific.ADSL-Forum.Agent-Circuit-ID` may be present.

CAUTION: However, a circuit-specific identifier is unsuitable if there are
multiple hosts on the circuit with which you issue individual addresses (for
example when using PPPoE passthrough to provide unique sessions to firewalls
behind a seperate CPE modem.) In this case the "device" must be thought of not
as the circuit but the individual hosts behind it.

The `Calling-Station-Id` attribute may contain host specific information (such
as a hardware address). You should consider the provenance of the data and
whether you trust it, e.g. does it report a MAC address that is under an
untrusted user's control.  Sometimes a combination of attributes including one
that you trust (such as "%{Vendor-Specific.ADSL-Forum.Agent-Circuit-ID}:%{Calling-Station-Id}") may
be appropriate.

The `pool_key` is used to issue indexed lookups into the `radippool` table so for
performance reasons it should have differentiation in the leading characters.
You need to understand your network configuration and the available RADIUS
attributes to determine what is an acceptable `pool_key`. You may need to
reconfigure your NAS to provide a suitable unique device identifier.

TIP: Most NASs allow the content of the `Calling-Station-Id` to the customised
so that in includes device specific information and network-related information
(such as SSID in the case of wireless networks).

NOTE: When accounting is enabled it is essential that any `pool_key` attribute
is included in *both authentication and accounting requests*. Otherwise lease
renewal will fail and IP addresses will be prematurely returned to the pool
before sessions have disconnected, resulting in duplicate IP allocations.


[#lease-duration]
=== Choice of lease duration

`sqlippools` manages the status of the IP addresses (either leased or free)
using the `expiry_time` of each entry in the `radippool` table. If it is in the
future then the IP address is considered to be actively leased (in use by a
device), otherwise it is free to be re-allocated to a device.

The `lease_duration` configuration item is used to set the `expiry_time` for an
IP address lease both when it is allocated at the start of a session and renewed
during an ongoing session.  The `expiry_time` is set to `lease_duration`
seconds after the current time. `expiry_time` and `lease_duration` are used as
a means to manage the IP address pools. They are not communicated to the NAS or
device.

In access networks where the IP address pool is plentiful and sessions have a
defined maximum length (for example when `Session-Timeout` RADIUS attribute is
being honoured), setting a `lease_duration` that is a little longer than the
maximum permitted session length is acceptable.

Often either the session duration is not restricted or the capacity of the pool
is small (relative to the number of devices) so that it needs careful
management to continuously free IP addresses that are no longer in active use.

Normally *accounting must be enabled* on the NAS and *an accounting interim
interval set* for two reasons:

. `Accounting Interim-Update` requests indicate that an IP address is still in
use by an ongoing session. This causes the lease to be periodically extended
before it expires and prevents an IP address from being prematurely freed as
this would result in active sessions having duplicate IP addresses.

. `Accounting Stop` (and `Accounting On/Off`) requests indicate that an IP
address is no longer in use so that it can be immediately released. This
ensures that a IP address is not leases longer than necessary and avoids wasted
pool space when a group of devices reconnect after their sessions are
interrupted.

CAUTION: It is essential that Accounting Interim-Update requests are
received reliably. Many NASs will repeat accounting requests that are not
acknowledged by a RADIUS server or forward them elsewhere. You should
determine whether accounting requests are delivered reliably in your network.

If accounting requests are received reliably then `lease_duration` could be set a
little longer than *twice the accounting interim interval* to handle the
occasional lost interim-update. Otherwise the lease duration might need to be
set several times longer.
