summaryrefslogtreecommitdiffstats
path: root/doc/antora/modules/howto/pages/protocols/dhcp
diff options
context:
space:
mode:
Diffstat (limited to 'doc/antora/modules/howto/pages/protocols/dhcp')
-rw-r--r--doc/antora/modules/howto/pages/protocols/dhcp/enable.adoc213
-rw-r--r--doc/antora/modules/howto/pages/protocols/dhcp/index.adoc35
-rw-r--r--doc/antora/modules/howto/pages/protocols/dhcp/policy.adoc14
-rw-r--r--doc/antora/modules/howto/pages/protocols/dhcp/policy_common_options.adoc80
-rw-r--r--doc/antora/modules/howto/pages/protocols/dhcp/policy_device_options.adoc310
-rw-r--r--doc/antora/modules/howto/pages/protocols/dhcp/policy_ippool_access.adoc54
-rw-r--r--doc/antora/modules/howto/pages/protocols/dhcp/policy_ippool_creation.adoc112
-rw-r--r--doc/antora/modules/howto/pages/protocols/dhcp/policy_network_options.adoc237
-rw-r--r--doc/antora/modules/howto/pages/protocols/dhcp/policy_subnet_options.adoc184
-rw-r--r--doc/antora/modules/howto/pages/protocols/dhcp/prepare.adoc59
-rw-r--r--doc/antora/modules/howto/pages/protocols/dhcp/test.adoc143
11 files changed, 1441 insertions, 0 deletions
diff --git a/doc/antora/modules/howto/pages/protocols/dhcp/enable.adoc b/doc/antora/modules/howto/pages/protocols/dhcp/enable.adoc
new file mode 100644
index 0000000..2824bd0
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/dhcp/enable.adoc
@@ -0,0 +1,213 @@
+== Enabling the DHCP service
+
+A major difference between configuring FreeRADIUS as a DHCP server versus most
+other DHCP software such as ISC DHCP is that other software typically uses a
+single monolithic configuration file whereas FreeRADIUS has a collection of
+configuration files. This reflects the modularity of FreeRADIUS; attempting to
+put the entire configuration in a single file would result in a very difficult
+to read configuration.
+
+The root of the FreeRADIUS configuration may be in a different location on the
+filesystem depending on how FreeRADIUS has been installed. This directory will
+be referred to as `<raddb>` below. The sample configuration files are well
+commented describing what each configuration option does.
+
+FreeRADIUS compiled from source will default to `/usr/local/etc/raddb`.
+Pre-built packages will default to either `/etc/raddb` or
+`/etc/freeradius`.
+
+
+=== Enable the DHCP virtual server
+
+The FreeRADIUS configuration separates each network service that it provides
+into "virtual servers". A number of sample virtual server definitions are
+provided in `<raddb>/sites-available`, one of which is the sample
+configuration for a DHCP service.
+
+Sites may be added to the working configuration by either creating a symlink to
+them or copying them to `<conf>/sites-enabled` depending on how you wish to
+manage future upgrades.
+
+[TIP]
+====
+As with other package-managed configuration files, package upgrades will not
+automatically replace files that you have edited but you will need to resolve
+any local differences. Creating copies avoids the need to resolve conflicts
+during a package upgrade.
+====
+
+Add the DHCP virtual server to the active configuration:
+
+[source,shell]
+----
+cd <raddb>/sites-enabled
+ln -s ../sites-available/dhcp .
+----
+
+or:
+
+[source,shell]
+----
+cd <raddb>/sites-enabled
+cp ../sites-available/dhcp .
+----
+
+The sample configuration has been set up in such a way that it is initially
+safe. It will not actually take over live DHCP serving on the network when it
+is simply enabled until it is configured to do so. Rather is set up for testing
+prior to going live.
+
+The virtual server begins with a `listen` section. In this section your need to
+modify the following configuration items:
+
+`ipaddr`:: The IP address to listen on.
+`src_ipaddr`:: The source IP for unicast packets.
+`port`:: The port to listen on. Setting this to `67` will make the DHCP service live on the network.
+`interface`:: The network interface to listen on.
+`broadcast`:: Allow broadcast packets. For most live systems this will need to be set to `yes`.
+
+Below the `listen` section, there are sections that define how to respond to
+each of the DHCP packet types. Most installations will require that you review
+the settings for `DHCP-Discover` and `DHCP-Request`.
+
+Their contents contain directives in the FreeRADIUS policy language, "unlang".
+Many examples are provided which have been carefully described.
+
+
+=== Enable SQL and IP pool modules
+
+FreeRADIUS has many modules to support different aspects of the functionality
+required for the network protocols it can process. The two of most significance
+for DHCP are `dhcp_sql` and `dhcp_sqlippool`. As with virtual servers, a
+number of example module configurations are available in
+`<raddb>/mods-available`.
+These should be symlinked or copied into `<raddb>/mods-enabled` in order to
+enable them.
+
+
+==== Configure the `dhcp_sql` module
+
+Add the `dhcp_sql` module to the active configuration:
+
+[source,shell]
+----
+cd <raddb>/mods-enabled
+ln -s ../mods-available/dhcp_sql .
+----
+
+or:
+
+[source,shell]
+----
+cd <raddb>/mods-enabled
+cp ../mods-available/dhcp_sql .
+----
+
+The `dhcp_sql` module should be configured with the connection parameters for
+whichever database is to be used. The key configuration items are:
+
+`dialect`:: Which SQL dialect is in use.
+`driver`:: Which driver to use to access the database. For most databases this
+ is `rlm_sql_<dialect>`, however Microsoft SQL Server has a choice of
+ drivers.
+
+Then, there are configuration options that are unique to each database,
+including connection details. For most databases these are:
+
+`server`:: The host name or IP address of the database server.
+`port`:: The port to connect to the database server on.
+`login`:: The user name used to connect to the database.
+`password`:: The password for authenticating to the database.
+`radius_db`:: The name of the database.
+
+[NOTE]
+====
+SQLite does not use these connection options, rather the `filename`
+option within the `sqlite` section is used to determine where the database
+will be stored.
+====
+
+
+==== Configure the `dhcp_sqlippool` module
+
+Add the `dhcp_sqlippool` module to the active configuration:
+
+[source,shell]
+----
+cd <raddb>/mods-enabled
+ln -s ../mods-available/dhcp_sqlippool .
+----
+
+or
+
+[source,shell]
+----
+cd <raddb>/mods-enabled
+cp ../mods-available/dhcp_sqlippool .
+----
+
+The `dhcp_sqlippool` module must be configured. The key configuration
+items are:
+
+`dialect`:: Set this to the same SQL dialect as in the `sql` module.
+`offer_duration`:: How long an IP is offered to the client in a DHCP OFFER.
+`lease_duration`:: How long an IP is leased to the client in a DHCP ACK.
+
+
+=== Provision the database
+
+You should provision your database by creating a user for FreeRADIUS (matching
+the configuration that you have previously provided) and then loading the
+schema. The procedure for doing this will vary according to the database
+server.
+
+The schema, stored procedure definition and any additional setup scripts for
+your database are in `<raddb>/mods-config/sql/ippool-dhcp/{dialect}/`.
+
+=== Test FreeRADIUS startup
+
+Once you have provisioned your schema, created a user account and granted
+access to the user, you should be able to start FreeRADIUS.
+
+If FreeRADIUS has been configured correctly then the output of `ss` will
+contain a line showing that FreeRADIUS is listening for DHCP packets on the
+designated interface on port 67:
+
+.Example of FreeRADIUS listening on `<interface>` for DHCP packets
+==================================================================
+ # ss -lunp
+ Netid Recv-Q Send-Q Local Address:Port ...
+ udp 0 0 0.0.0.0%<interface>:67 ... users:(("radiusd",...))
+==================================================================
+
+Note that if the database is inaccessible then FreeRADIUS will normally refuse
+to start.
+
+The FreeRADIUS wiki contains extensive information about debugging FreeRADIUS
+startup issues that we do not repeat in any detail here.
+
+Essentially, stop your init system from repeatedly trying to launch FreeRADIUS:
+
+[source,shell]
+----
+service radiusd stop
+----
+
+Then start FreeRADIUS manually in debug mode:
+
+[source,shell]
+----
+radiusd -X
+----
+
+Carefully read the output since this will tell you why FreeRADIUS was unable to
+start.
+
+Once you have fixed the issue start FreeRADIUS as normal:
+
+[source,shell]
+----
+service radiusd start
+----
+
+Now xref:protocols/dhcp/test.adoc[test the DHCP service] to ensure that it is responding to requests.
diff --git a/doc/antora/modules/howto/pages/protocols/dhcp/index.adoc b/doc/antora/modules/howto/pages/protocols/dhcp/index.adoc
new file mode 100644
index 0000000..fde2202
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/dhcp/index.adoc
@@ -0,0 +1,35 @@
+= FreeRADIUS DHCP server
+
+This guide describes how FreeRADIUS can be used in place of ISC DHCP or ISC Kea
+to provide a significantly more performant and, above all, more flexible DHCP
+server.
+
+This guide provides a suggested configuration that should be somewhat familiar
+to anyone who has previously implemented DHCP using the most frequently used
+features of other DHCP server software.
+
+The modular design of FreeRADIUS means that there is no one "right" way to
+implement the DHCP service. FreeRADIUS allows you to put together a "mix and
+match" approach.
+
+For example you can manage the leases in an SQL database. You might then hard
+code certain DHCP reply parameters within configuration and then look up
+additional parameters using a datastore such as:
+
+ * a local file such as a structured text file or an SQLite database
+ * an organisational LDAP directory
+ * an SQL or "no SQL" database
+ * a remote endpoint such as a RESTful HTTP API
+
+The policy language and modular configuration of FreeRADIUS is sufficiently
+powerful and that almost any aspect of the server's behaviour can be customised
+to implement even the most sophisticated DHCP configurations.
+
+== Sections in this guide
+
+This guide is organised into four parts that should be read in order:
+
+1. xref:protocols/dhcp/prepare.adoc[Preparation]
+2. xref:protocols/dhcp/enable.adoc[Enabling the DHCP service]
+3. xref:protocols/dhcp/test.adoc[Testing the DHCP service]
+4. xref:protocols/dhcp/policy.adoc[Defining the DHCP policy]
diff --git a/doc/antora/modules/howto/pages/protocols/dhcp/policy.adoc b/doc/antora/modules/howto/pages/protocols/dhcp/policy.adoc
new file mode 100644
index 0000000..d8f1bcb
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/dhcp/policy.adoc
@@ -0,0 +1,14 @@
+== Defining the DHCP policy
+
+Now that FreeRADIUS is successfully running as a DHCP server it is necessary to
+configure a DHCP policy so that it returns correctly formed responses to the DHCP
+requests that it receives.
+
+This involves a number of steps:
+
+ * xref:protocols/dhcp/policy_ippool_creation.adoc[Defining the IP address pools.]
+ * xref:protocols/dhcp/policy_common_options.adoc[Defining the options that are common to all replies.]
+ * xref:protocols/dhcp/policy_network_options.adoc[Defining the options for the network from which the request originates and ensuring that IP addresses are allocated from the correct pool.]
+ * xref:protocols/dhcp/policy_subnet_options.adoc[Defining the options for the subnet to which this issued IP address belongs.]
+ * xref:protocols/dhcp/policy_device_options.adoc[Defining the device, class and group based options specific to the device.]
+ * xref:protocols/dhcp/policy_ippool_access.adoc[Using device properties to restrict access to certain pools.]
diff --git a/doc/antora/modules/howto/pages/protocols/dhcp/policy_common_options.adoc b/doc/antora/modules/howto/pages/protocols/dhcp/policy_common_options.adoc
new file mode 100644
index 0000000..949868d
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/dhcp/policy_common_options.adoc
@@ -0,0 +1,80 @@
+== Configure common reply options
+
+FreeRADIUS includes a powerful xref:unlang/index.adoc[policy language] called
+"unlang".
+
+Statements in unlang may be used to call further policies, update attribute
+lists and invoke modules. There are also control flow statements (if,
+switch, etc.) typical of most imperative languages.
+
+FreeRADIUS has a number attribute lists that it maintains as it processes
+packets within the virtual server sections. Most relevant to DHCP are
+`request`, `control` and `reply`.
+
+The DHCP options from the current request packet are provided in the
+`request` list. This includes fixed DHCP parameters such as
+`DHCP-Client-Hardware-Address`, optional parameters such as
+`DHCP-Requested-IP-Address`, and parameters synthesised by FreeRADIUS such as
+`DHCP-Message-Type` and `DHCP-Network-Subnet`.
+
+DHCP options can be set by updating their value in the `reply` list. This
+forms the basis of the packet returned to the client.
+
+In the default DHCP server configuration, a "policy" (akin to a subroutine) is
+used to set common options for reply packets. The policy is found in
+`<raddb>/policy.d/dhcp`.
+
+Look at the contents of the `dhcp_common` section and set any global options
+applicable to all clients in this policy.
+
+[source,unlang]
+----
+dhcp_common {
+ update reply {
+ &DHCP-Domain-Name-Server := 8.8.8.8
+ &DHCP-Domain-Name-Server += 8.8.4.4
+ &DHCP-Subnet-Mask := 255.255.255.0
+ &DHCP-Router-Address := 192.0.2.1
+ ...
+ }
+}
+----
+
+Note, FreeRADIUS has four main operators for assigning values to attributes:
+
+`=`:: Add the attribute to the list, if and only if an attribute of the same
+ name is not already present in that list.
+`:=`:: Add the attribute to the list. If any attribute of the same name is
+ already present in that list it is replaced with the new one.
+`+=`:: Add the attribute to the tail of the list, even if attributes of the
+ same name are already present in the list.
+`^=`:: Add the attribute to the head of the list, even if attributes of the
+ same name are already present in the list.
+
+These operators allow for attributes to be set to default values and then
+overwritten, e.g. setting a default lease time, but then overwriting it for
+a particular group of clients.
+
+Attributes in the `control` list are not returned in the DHCP reply packets
+but instead govern aspects of server's behaviour.
+
+To use an SQL backend for either static or dynamic IP allocation, un-comment
+the block:
+
+[source,unlang]
+----
+update control {
+ &Pool-Name := "local"
+}
+dhcp_sqlippool
+----
+
+The `Pool-Name` control attribute is used in looking up addresses in the
+database. The line containing `dhcp_sqlippool` is a call to invoke an
+instance of a module with that name. This module is responsible for assigning a
+free IP address into the `DHCP-Your-IP-Address` reply attribute from the pool
+identified by `Pool-Name`.
+
+Here `Pool-Name` is being set to a constant value (`local`) indicating
+that a single pool is to be used. If you have multiple pools, then replace this
+`update` block with logic to map clients to the correct pool, as described below.
diff --git a/doc/antora/modules/howto/pages/protocols/dhcp/policy_device_options.adoc b/doc/antora/modules/howto/pages/protocols/dhcp/policy_device_options.adoc
new file mode 100644
index 0000000..05845ea
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/dhcp/policy_device_options.adoc
@@ -0,0 +1,310 @@
+== Configure "device", "class" and "group" options
+
+Beyond the global, network and subnet options already described, most sites
+will have a number of group or class based options, and have a requirement for
+setting reply parameters against individual devices.
+
+In general, FreeRADIUS does not differentiate between "classes" (memberships
+defined by some attribute of the DHCP request) and "groups" (memberships
+defined by some manually aggregation related devices, typically based on lists
+of MAC address).
+
+The sample DHCP configuration provided with FreeRADIUS makes use of an internal
+attribute `DHCP-Group-Name` to support the setting of different options for
+different groups of devices.
+
+In general the groups to which a device belongs is determined during the
+processing of a request and these are added as instances of the
+`DHCP-Group-Name` attribute. This may be by performing a test on one or more
+request parameters (akin to a "class"), hash-based lookup of up all of part of
+an attribute in a local list (akin to a "subclass"), or doing the same using a
+remote datastore (SQL, LDAP, REST API, etc).
+
+FreeRADIUS can then iterate over `DHCP-Group-Name` to set group-specific
+options.
+
+We describe some of these options in more detail.
+
+=== Directly in Policy
+
+Simple class options can be written directly into policy. This is most
+suited to those options that rarely change and are based on attributes in the
+request such as the `User-Class`.
+
+Consider the ISC DHCP configuration snippet:
+
+[source,iscdhcp]
+----
+filename "undionly.kpxe";
+class "pxeclient" {
+ match option substring(user-class,0,4);
+}
+subclass "pxeclient" "iPXE" {
+ filename "http://my.web.server/boot_script.php";
+}
+----
+
+Or the equivalent Kea configuration:
+
+[source,isckea]
+----
+"Dhcp4": {
+ "option-data": [
+ { "name": "boot-file-name", "data": "undionly.kpxe" }
+ ],
+ "client-classes": [
+ {
+ "name": "pxeclient",
+ "test": "substring(option[77],0,4) == 'iPXE'",
+ "option-data": [
+ {
+ "name": "boot-file-name",
+ "data": "http://my.web.server/boot_script.php"
+ }
+ ]
+ }
+ ]
+ ...
+}
+----
+
+These define the "filename" DHCP option differently based on whether or not the
+supplied "user-class" option begins with "iPXE".
+
+FreeRADIUS provides multiple ways for this to be configured.
+
+For example, the following "unlang" policy implements the class options defined
+above:
+
+[source,unlang]
+----
+if (&DHCP-User-Class && "%{substring:&DHCP-User-Class 0 4}" == "iPXE") {
+ update reply {
+ &DHCP-Boot-Filename := "http://my.web.server/boot_script.php"
+ }
+} else {
+ update reply {
+ &DHCP-Boot-Filename := "undionly.kpxe"
+ }
+}
+----
+
+Policy-based configuration of DHCP options is also useful for complex matching.
+For example, the following Unlang sets the DHCP-Boot-Filename parameter based
+on the request's DHCP-Client-Identifier using regular expression captures,
+provided that it matches the given format:
+
+[source,unlang]
+----
+if (&DHCP-Client-Identifier && \
+ "%{string:DHCP-Client-Identifier}" =~ /^RAS([0-9])-site([A-Z])$/) {
+ update reply {
+ &DHCP-Boot-Filename := "rasboot-%{1}-%{2}.kpxe"
+ }
+}
+----
+
+=== In Text Files
+
+The `files` module that has already been described for global, network and
+subnet options can also be used to apply options to groups of clients.
+
+Firstly we must defined a mapping from a set of clients clients to their
+respective groups. One option for this is to use the `passwd` module, for
+which a sample configuration is included.
+
+Firstly symlink or copy the module configuration
+`<raddb>/mods-available/dhcp_passwd` into `<raddb>/mods-enabled/`. The
+suggested configuration expects the group membership file to be in
+`<raddb>/mods-config/files/dhcp_groups` and take the form of:
+
+[source,config]
+----
+<group1 name>|<hardware address>,<hardware address>,<hardware address>
+<group2 name>|<hardware address>,<hardware address>
+----
+
+i.e. one line for each group starting with the group name followed by a pipe
+character and then a comma-separated list of hardware addresses.
+
+The `allow_multiple_keys` option allows for a host to be a member of
+more than one group.
+
+Sample configuration for looking up group options is contained in
+`<raddb>/policy.d/dhcp` in the `dhcp_group_options` policy and in
+`<raddb>/mods-available/dhcp_files` as the `dhcp_set_group_options` instance.
+
+The same data file `<raddb>/mods-config/files/dhcp` is used to lookup
+group options as was used for global and network options. In this instance,
+add entries with the group name as the key such as:
+
+[source,config]
+----
+group1
+ DHCP-Log-Server := 10.10.0.100,
+ DHCP-LPR-Server := 10.10.0.200
+
+group2
+ DHCP-LPR-Server := 192.168.20.200
+----
+
+=== In the SQL Database
+
+Policy and files are both read during startup and editing them while
+FreeRADIUS is running will not result in any changes in behaviour. If
+you require regular changes to DHCP options, then storing them in
+an SQL database provides greater flexibility since the queries will be run in
+response to each DHCP packet rather than requiring the server to be restarted.
+
+DHCP reply options for devices (including network-specific options) can be
+fetched from SQL using an arbitrary lookup key. This can be performed multiple
+times as necessary using different contexts, for example to first set
+subnet-specific options and then to set group-specific options.
+
+The default schema contains three tables to support this:
+
+"dhcpreply" contains reply options for a given identifier (e.g. MAC Address):
+
+.dhcpreply table
+|===
+|Identifier |Attribute |Op |Value |Context
+
+|`02:01:aa:bb:cc:dd` |`DHCP-Log-Server` |`:=` |`192.0.2.10` |`by-mac`
+|`02:01:aa:bb:cc:dd` |`DHCP-LPR-Server` |`:=` |`192.0.2.11` |`by-mac`
+|`02:01:aa:bb:cc:dd` |`Fall-Through` |`:=` |`Yes` |`by-mac`
+|===
+
+"dhcpgroup" maps identifiers to a group of options that can be shared:
+
+.dhcpgroup table
+|===
+|Identifier |GroupName |Priority |Context
+
+|`02:01:aa:bb:cc:dd` |`salesdept` |`10` |`by-mac`
+|===
+
+"dhcpgroupreply" contains reply options for each group:
+
+.dhcpgroupreply table
+|===
+|GroupName |Attribute |Op |Value |Context
+
+|`salesdept` |`DHCP-NTP-Servers` |`:=` |`192.0.2.20` |`by-mac`
+|`salesdept` |`DHCP-Log-Server` |`+=` |`192.0.2.21` |`by-mac`
+|`salesdept` |`DHCP-LPR-Server` |`^=` |`192.0.2.22` |`by-mac`
+|===
+
+Within the context of assigning options directly to devices, as well as to
+manually-curated groups of devices keyed by their MAC address:
+
+ - Place device-specific options in the "dhcpreply" table.
+ - Add `Fall-Through := Yes` to the options in the "dhcpreply" table in order
+ to trigger group lookups, which are disabled by default.
+ - Place entries in the "dhcpgroup" `identifier = <MAC-Address>, groupname = <group>, priority =
+ <priority>` in the "dhcpgroup" table to map a device to its groups by
+ priority.
+ - Place the grouped options in the "dhcpgroupreply" table.
+ - For each of the above, set `Context` to something by which the option
+ lookup is referred to in the policy, for example `Context = 'by-mac'`.
+
+For the above example you would add the following to the DHCP virtual server to
+perform reply option lookup using the device's MAC address against the `by-mac`
+context:
+
+[source,unlang]
+----
+update control {
+ &DHCP-SQL-Option-Context := "by-mac"
+ &DHCP-SQL-Option-Identifier := &request:DHCP-Client-Hardware-Address
+}
+dhcp_sql.authorize
+----
+
+In the above, the DHCP reply options would be assigned to a device with MAC
+address 02:01:aa:bb:cc:dd as follows:
+
+ - Firstly, the `DHCP-Log-Server` option would be set to `192.0.2.10` and the
+ `DHCP-LPR-Server` option set to `192.0.2.11`.
+ - `Fall-Through` is set, so the group mapping is then queried which
+ determines that the device belongs to a single `salesdept` group.
+ - Finally, the options for the `salesdept` group are now merged, setting a
+ `DHCP-NTP-Servers` option to `192.0.2.20`, appending an additional
+ `DHCP-Log-Server` option set to `192.0.2.21`, and prepending an additional
+ `DHCP-LPR-Server` option set to `192.0.2.22`.
+
+If instead you wanted to perform a "subclass" lookup based on the first three
+octets of the device's MAC address then with tables containing the following
+sample data you could invoke an SQL lookup as shown:
+
+."dhcpreply" table:
+|===
+|Identifier |Attribute |Op |Value |Context
+
+|`000393` |`Fall-Through` |`:=` |`Yes` |`class-vendor`
+|`000a27` |`Fall-Through` |`:=` |`Yes` |`class-vendor`
+|`f40304` |`Fall-Through` |`:=` |`Yes` |`class-vendor`
+|===
+
+."dhcpgroup" table:
+|===
+|Identifier |GroupName |Priority |Context
+
+|`000393` |`apple` |`10` |`class-vendor`
+|`000a27` |`apple` |`10` |`class-vendor`
+|`f40304` |`google` |`10` |`class-vendor`
+|===
+
+."dhcpgroupreply" table:
+|===
+|GroupName |Attribute |Op |Value |Context
+
+|`apple` |`DHCP-Boot-Filename` |`:=` |`apple.efi` |`class-vendor`
+|`google` |`DHCP-Boot-Filename` |`:=` |`google.efi` |`class-vendor`
+|===
+
+
+[source,unlang]
+----
+update control {
+ &DHCP-SQL-Option-Context := "class-vendor"
+ &DHCP-SQL-Option-Identifier := \
+ "%{substring:%{hex:&DHCP-Client-Hardware-Address} 0 6}"
+}
+dhcp_sql.authorize
+----
+
+The file `policy.d/dhcp` contains a policy named `dhcp_policy_sql` which
+provides further worked examples for different types of option lookups.
+
+=== Testing "device", "class" and "group" options
+
+You should now test that any device-related options that you have configured
+using the various methods available are applied successfully by generating
+packets containing those parameters based upon which the reply options are set.
+
+For example, to test the iPXE user class example above you might want to
+generate a request as follows:
+
+[source,shell]
+----
+cat <<EOF > dhcp-packet-ipxe-boot.txt
+DHCP-Message-Type := DHCP-Discover
+DHCP-Client-Hardware-Address := 02:01:aa:bb:cc:dd
+DHCP-User-Class := "iPXE-class-abc"
+EOF
+----
+
+To which you would expect to see a response such as:
+
+.Example output from dhcpclient
+===============================
+ dhcpclient: ...
+ ----------------------------------------------------------------------
+ Waiting for DHCP replies for: 5.000000
+ ----------------------------------------------------------------------
+ ...
+ DHCP-Message-Type = DHCP-Offer
+ DHCP-Your-IP-Address = 1.2.3.4
+ DHCP-Boot-Filename := "http://my.web.server/boot_script.php"
+ ...
+===============================
diff --git a/doc/antora/modules/howto/pages/protocols/dhcp/policy_ippool_access.adoc b/doc/antora/modules/howto/pages/protocols/dhcp/policy_ippool_access.adoc
new file mode 100644
index 0000000..40b8e30
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/dhcp/policy_ippool_access.adoc
@@ -0,0 +1,54 @@
+== Configure access restrictions for pools
+
+We can combine what we have learned in the preceeding sections to provide pools
+whose access is restricted in some way, for example to a particular class.
+
+Consider the ISC DHCP configuration snippet:
+
+[source,iscdhcp]
+----
+subnet 10.99.99.0 netmask 255.255.255.0 {
+ pool {
+ range 10.99.99.200 10.99.99.250;
+ allow members of "printers";
+ }
+ option routers 10.99.99.1;
+}
+----
+
+Or the equivalent Kea configuration:
+
+[source,isckea]
+----
+"Dhcp4": {
+ "subnet4": [{
+ "subnet": "10.99.99.0/24",
+ "pools": [
+ {
+ "pool": "10.99.99.200 - 10.99.99.250",
+ "client-class": "printers"
+ }
+ ],
+ "option-data": [
+ { "name": "routers", "data": "10.10.0.1" }
+ ]
+ }],
+ ...
+}
+----
+
+These define a subnet containing a single pool that is restricted to members of
+the "printers" class. (The definition for this class is omitted.)
+
+In FreeRADIUS, to filter access to this pool entries such as the following
+should included in the `<raddb>/mods-config/files/dhcp` configuration file:
+
+[source,config]
+----
+network DHCP-Network-Subnet < 10.99.99.0/24, \
+ DHCP-Group-Name == "printers", Pool-Name := "printers-pool"
+ DHCP-Router-Address := 10.99.99.1
+----
+
+Note that any number of additional filters can be added to the initial "check"
+line to restrict matches to the network block.
diff --git a/doc/antora/modules/howto/pages/protocols/dhcp/policy_ippool_creation.adoc b/doc/antora/modules/howto/pages/protocols/dhcp/policy_ippool_creation.adoc
new file mode 100644
index 0000000..e976873
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/dhcp/policy_ippool_creation.adoc
@@ -0,0 +1,112 @@
+=== Determine the IP pool plan
+
+Except for cases where all IP allocation is performed using a mapping from the
+device MAC address to a fixed IP address, the DHCP configuration will involve
+the use of one or more IP address pools.
+
+FreeRADIUS stores all the IP addresses in its pools in whichever database has
+been chosen. An instance of the `sqlippools` module is used to manage all pools
+within a single table (normally `dhcpippool`). Each row of this table
+corresponds to an IP address that is a member of some pool. The pools are
+distinguished by name, so the table has a column (`pool_name`) that denotes
+this.
+
+Each pool in this table should be composed of a set of equally valid IP
+addresses for the devices that are designated to be members of the pool.
+
+Firstly, consider the network locations to which distinct ranges of IP
+addresses must be allocated and provisionally assign a pool to each.
+
+Next, consider that many networks support multiple co-existing subnets without
+VLAN separation. We will call this a "shared-network" to use the original ISC
+DHCP parlance. In Microsoft DHCP contexts this is often referred to as a
+"multinet".
+
+Often in a shared-network the policy has no regard for which of the network's
+devices is allocated to which subnet. In this case we must create a single,
+combined pool containing all of the IP addresses from each subnet in that
+network. Since all addresses in a pool are treated equally this will mean that
+any IP address may be allocated to a device that is making a DHCP request from
+that network. The appropriate DHCP parameters for the subnet to which the IP
+address belongs is determined after allocation.
+
+There are sometimes shared-networks (or even single subnets) for which IP
+addresses belonging to any subnet may be technically suitable for any device,
+however some local policy wants to assigning them to a particular subnet, for
+example to provide loose segregation between classes of device. In this case we
+define multiple pools, one for each range of IP addresses whose devices needs to
+be differentiated.
+
+The choice of pool is ordinarily determined based on the network from which the
+request originates using a mapping from Layer 2 networks to the pool name
+provided by the user. The indicator for the originating network can be
+overridden when this alone is insufficient to implement the required pool
+selection policy such as when you need to differentiate the pool's users with
+more granularity that their Layer 2 network, such as by considering device
+attributes ("class" membership in ISC parlance) or Option 82 circuit data.
+
+
+=== Populate the IP Pools
+
+By this stage you should have derived a list of pools, the IP address ranges
+contained therein, and the means of selecting the pool to use based on the
+originating network and/or some additional criteria from the request.
+
+A helper Perl script is provided with FreeRADIUS that can be used to populate
+the pools provide that you are using the default schema.
+
+[source,shell]
+----
+rlm_sqlippool_tool -p <pool_name> -s <range_start> -e <range_end> \
+ -t <table_name> (-d <sql_dialect> | -f <raddb_dir> [ -i <instance> ]) \
+ [ -c <capacity> ] [ -x <existing_ips_file> ]
+----
+
+If, for example, you had a range configured in ISC DHCP as:
+
+[source,iscdhcp]
+----
+range 10.0.0.5 10.0.0.199
+----
+
+and you are using PostgreSQL as your database, and you wish to refer to this pool
+using the name `local`, this could be prepared with:
+
+[source,shell]
+----
+rlm_sqlippool_tool -p local -s 10.0.0.5 -e 10.0.0.199 -t dhcpippool -d postgresql
+----
+
+If the SQL module of FreeRADIUS is already configured then this can
+be referenced so that the tool is able to use the configured connection
+parameters to connect to the database and populate the pool:
+
+[source,shell]
+----
+rlm_sqlippool_tool -p local -s 10.0.0.5 -e 10.0.0.199 -t dhcpippool -f /etc/raddb
+----
+
+For installations that require multiple pools, `rlm_sqlippool_tool` can
+be called referencing a YAML file defining the pools. Comments at the
+head of `rlm_sqlippool_tool` explain the options in more detail.
+
+If static leases are required then these should be set up in the database
+such that the MAC address of the client should be set as the `pool_key`
+against the corresponding address and the `status` column of the row
+representing the address set to `static`. A helper perl script,
+`rlm_iscfixed2ippool` can be used to read an ISC DHCP config file and produce
+SQL to perform these changes or directly update the database:
+
+[source,shell]
+----
+rlm_iscfixed2ippool -c <dhcpd.<raddb> -t <table_name> -k <mac|id> \
+ (-d <sql_dialect> | -f <raddb_dir> [-i <instance>])
+----
+
+For example, to read /etc/dhcp/dhcpd.conf and populate the configured
+FreeRADIUS database, using the mac as the identifier:
+
+[source,shell]
+----
+rlm_iscfixed2ippool -c /etc/dhcp/dhcpd.conf -t dhcpippool -k mac -f /usr/local/etc/raddb
+----
diff --git a/doc/antora/modules/howto/pages/protocols/dhcp/policy_network_options.adoc b/doc/antora/modules/howto/pages/protocols/dhcp/policy_network_options.adoc
new file mode 100644
index 0000000..e2657a8
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/dhcp/policy_network_options.adoc
@@ -0,0 +1,237 @@
+== Configure network-specific options and IP pool selection
+
+In an environment where multiple networks (often VLANs) are in use, it is
+necessary to identify which network a client belongs to in order to assign an
+address from the correct pool.
+
+Consider the ISC DHCP configuration snippet:
+
+[source,iscdhcp]
+----
+option domain-name "example.org";
+
+subnet 10.10.0.0 netmask 255.255.0.0 {
+ range 10.10.1.10 10.10.10.254;
+ range 10.10.100.10 10.10.110.254;
+ option routers 10.10.0.1;
+ option domain-name-servers 10.10.0.2, 10.10.0.3;
+ default-lease-time 7200;
+}
+----
+
+Or the equivalent Kea configuration:
+
+[source,isckea]
+----
+"Dhcp4": {
+ "option-data": [
+ { "name": "domain-name", "data": "example.org" }
+ ],
+ "subnet4": [{
+ "subnet": "10.10.0.0/16",
+ "pools": [ { "pool": "10.10.1.10 - 10.10.10.254" },
+ { "pool": "10.10.100.10 - 10.10.110.254" }
+ ],
+ "option-data": [
+ { "name": "routers", "data": "10.10.0.1" },
+ { "name": "domain-name-servers", "data": "10.10.0.2, 10.10.0.3" }
+ ],
+ "valid-lifetime": 7200
+ }],
+ ...
+}
+----
+
+These define a network consisting of a single subnet 10.10.0.0/16 containing two
+IP address pools 10.10.1.10 - 10.10.10.254 and 10.10.100.10 - 10.10.110.254.
+Requests that are determined to have originated from this network (e.g. because
+their `giaddr` belongs within the subnet) will be assigned the specified DHCP
+parameters and allocated an address from one of its ranges.
+
+To provide equivalent functionality, FreeRADIUS must identify the correct DHCP
+reply parameters as well as the name of the pool to be used for IP address
+assignment, based on the originating network of the request.
+
+The definition for this pool (the addresses contained within it, corresponding
+to the `range` statement in ISC DHCP and Kea) is specified entirely in the
+database: It is precisely the rows in the `dhcpippool` table with a particular
+`pool_name`.
+
+[TIP]
+====
+As described previously, in FreeRADIUS a pool is a set of IP addresses that are
+equally valid with respect to the network policy; therefore, unlike ISC DHCP
+and ISC Kea, FreeRADIUS does not differentiate between the two `range`s.
+Instead we should have previously populated a single pool containing all of the
+IP addresses from both ranges.
+====
+
+FreeRADIUS derives a request attribute called `DHCP-Network-Subnet` which
+honours the standard DHCP process for designating the choice of network, in
+order of preference:
+
+ 1. Link Selection Suboption of Option 82
+ 2. IPv4 Subnet Selection Option
+ 3. Gateway IP Address ("giaddr")
+ 4. Client IP Address ("ciaddr", only set for unicast packets)
+
+If `DHCP-Network-Subnet` contains an IP address then this should be used as
+the basis of choosing a network. When there is no address in this attribute it
+can be assumed that the packet has been received from a client on the local
+LAN.
+
+The `files` module in FreeRADIUS provides a simple method to map
+`DHCP-Network-Subnet` to the corresponding pool based on its network
+membership, setting the appropriate options to return to clients. It can also
+set the global options.
+
+[TIP]
+====
+In the case where an instance of the `files` module is used to get global
+default parameters, the `dhcp_common` policy becomes redundant so the
+statement calling the policy (by name) can be commented out in
+`<raddb>/sites-enabled/dhcp`.
+====
+
+To use the provided example `files` module instance for DHCP, symlink or copy
+`<raddb>/mods-available/dhcp_files` into `<raddb>/mods-enabled/` and then
+uncomment the calls to `dhcp_network` in `<raddb>/sites-enabled/dhcp`.
+
+A template configuration file `<raddb>/mods-config/files/dhcp` is also
+provided which should be adapted to suit your network topology.
+
+For the configuration above you may deduce the following configuration, which
+has been extended to include an initial default section for requests originating
+from directly-connected clients on the local LAN (192.168.20/24):
+
+[source,config]
+----
+network Pool-Name := "local"
+ DHCP-Domain-Name := "example.org",
+ DHCP-Subnet-Mask := 255.255.255.0,
+ DHCP-Router-Address := 192.168.20.1,
+ DHCP-Domain-Name-Server := 192.168.20.2,
+ Fall-Through := yes
+
+network DHCP-Network-Subnet < 10.10.0.0/16, Pool-Name := "remote"
+ DHCP-Subnet-Mask := 255.0.0.0,
+ DHCP-Router-Address := 10.10.0.1,
+ DHCP-Domain-Name-Server := 10.10.0.2,
+ DHCP-Domain-Name-Server += 10.10.0.3,
+ DHCP-IP-Address-Lease-Time := 7200
+----
+
+Each block in the file starts with a line beginning with the key to be matched.
+In this case the keyword of `network` (defined earlier in `dhcp_networks`
+configuration) is used for each block, so each of the above blocks is a
+candidate during the search.
+
+There may be further filtering of the candidates in the form of `<Attribute>
+<op> <Value>`. In the case of the second block we match the
+`DHCP-Network-Subnet` to an enclosing subnet with
+`DHCP-Network-Subnet < <subnet>`. Additional filters could be added as
+required, comma separated.
+
+Following the filters on the first line, attributes in the `control` list can
+be set using the syntax of `<Attribute> := <Value>`. In this example this is
+used to specify the `Pool-Name` for choosing the appropriate IP pool to
+allocate an address from.
+
+Subsequent indented lines are attribute assignments for values in the `reply`
+list. Note that, apart from the last line, they are all terminated with a
+comma.
+
+The special option `Fall-Through` determines whether, following a match,
+other records are checked for a match. All lookups will match the entry
+with a key of `network` and no further filtering, so `Fall-Through`
+is set on that record in order that the other records will be tested
+to find subnet matches.
+
+=== Example packet processing
+
+For our example, we consider a request arriving from a DHCP relay within
+10.10.0.0/16. In the absence of any specific DHCP subnet selection options in
+the request, the `DHCP-Network-Subnet` attribute is calculated to be the
+relay's IP address, say 10.10.0.1.
+
+The request is matched against the first block, setting an initial pool name to
+"local", domain name to "example.org" and setting some additional global
+default parameters. By virtue of `Fall-Through` being set, the next block is
+considered.
+
+Since the network identifier is within the specified subnet (i.e. `10.10.0.1 <
+10.10.0.0/16`) this second block is matched. This block overrides the pool name
+setting it to "remote", overrides some other global defaults and sets the lease
+time to 7200 seconds. `Fall-Through` is not set, so we are now done with
+deriving the pool name and network options.
+
+When the `dhcp_sqlippool` module is called during DHCP DISCOVER processing (in
+`<raddb>/sites-enabled/dhcp`) the `remote` pool will be used for IP address
+allocation.
+
+The assigned IP address and network parameters will subsequently be returned in
+the DHCP reply.
+
+=== Testing the pool operation and network-specific options
+
+Before proceeding further, you should test the operation of the IP pools and
+ensure that any network-specific reply attributes that you have configured are
+correctly set in replies.
+
+For example, if you have a single, flat pool you should test using sample
+packets for devices with different MAC addresses and/or Client Identifiers.
+
+[source,shell]
+----
+cat <<EOF > dhcp-packet-1.txt
+DHCP-Message-Type := DHCP-Discover
+DHCP-Client-Hardware-Address := 02:01:11:11:11:11
+DHCP-Client-Identifier := device1
+EOF
+----
+
+[source,shell]
+----
+cat <<EOF > dhcp-packet-2.txt
+DHCP-Message-Type := DHCP-Discover
+DHCP-Client-Hardware-Address := 02:01:22:22:22:22
+DHCP-Client-Identifier := device2
+EOF
+----
+
+Generate these packets as show previously using the dhcpclient tool and look
+for `DHCP-Your-IP-Address` in the DHCP responses to determine the IP address
+that has been offered.
+
+Ensure that the DHCP Offer responses contain unique IP addresses. Ensure that
+when these requests are resent within the lifetime of the initial offer that
+the reponses to the subsequent replies contain the original IP address that was
+in the initial offer to the device.
+
+Additionally, ensure that the DHCP Offers contain any network-specific
+parameters that you have specified.
+
+In the case that the policy contains multiple IP pools and network definitions
+for clients belonging to different Layer 2 networks (or indeed belonging to the
+same network but segregated according to some local policy) you should ensure
+that the devices are being mapped to the correct definition.
+
+For a typical policy that selects the IP pool and network options based on the
+originating network for the DHCP packet, explicitly specifying a network by
+including a `DHCP-Subnet-Selection-Option` parameter may avoid the need to test
+from a host within each individual network:
+
+[source,shell]
+----
+cat <<EOF > dhcp-packet-network-10.10.10.0.txt
+DHCP-Message-Type := DHCP-Discover
+DHCP-Client-Hardware-Address := 02:01:aa:bb:cc:dd
+DHCP-Client-Identifier := abc123
+DHCP-Subnet-Selection-Option := 10.10.10.0
+EOF
+----
+
+For policies where the IP pool and network option selection is based on some
+custom criteria it is necessary to include different variations for the
+parameters on which the policy makes the decision. The testing example for the
+class-specific options later in this document provides such an example.
diff --git a/doc/antora/modules/howto/pages/protocols/dhcp/policy_subnet_options.adoc b/doc/antora/modules/howto/pages/protocols/dhcp/policy_subnet_options.adoc
new file mode 100644
index 0000000..1980e89
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/dhcp/policy_subnet_options.adoc
@@ -0,0 +1,184 @@
+== Configure subnet-specific options for shared networks
+
+In the case that shared-networks are in use, with the pool containing
+equally-valid IP addresses from multiple subnets, it is necessary to set the
+subnet-specific parameters such as `DHCP-Router-Address`, `DHCP-Subnet-Mask`
+and `DHCP-Broadcast-Address` based on the IP address that has been allocated.
+
+Consider the ISC DHCP configuration snippet:
+
+[source,iscdhcp]
+----
+option domain-name "example.org";
+
+shared-network bigdept {
+
+ option domain-name-servers 10.10.0.2, 10.10.0.3;
+ default-lease-time 7200;
+
+ subnet 10.30.10.0 netmask 255.255.255.0 {
+ option routers 10.30.10.1;
+ }
+ subnet 10.30.20.0 netmask 255.255.255.0 {
+ option routers 10.30.20.1;
+ }
+ range 10.30.10.10 10.30.10.254;
+ range 10.30.20.10 10.30.20.254;
+
+}
+----
+
+Or the equivalent Kea configuration:
+
+[source,kea]
+----
+"Dhcp4": {
+ "option-data": [
+ { "name": "domain-name", "data": "example.org" }
+ ],
+ "shared-networks": [{
+ "name": "bigdept",
+ "option-data": [
+ { "name": "domain-name-servers", "data": "10.10.0.2, 10.10.0.3" }
+ ],
+ "valid-lifetime": 7200,
+ "subnet4": [{
+ "subnet": "10.30.10.0/24",
+ "pools": [ { "pool": "10.30.10.10 - 10.30.10.254" } ],
+ "option-data": [
+ { "name": "routers", "data": "10.30.10.1" }
+ ]
+ }],
+ "subnet4": [{
+ "subnet": "10.30.20.0/24",
+ "pools": [ { "pool": "10.30.20.10 - 10.30.20.254" } ],
+ "option-data": [
+ { "name": "routers", "data": "10.30.20.1" }
+ ]
+ }]
+ }],
+ ...
+}
+----
+
+As with the network to pool lookup, an instance of the `files` modules can be
+employed (this time after the allocation of an IP address) to set the correct
+reply parameters based on the subnet membership of the assigned address.
+
+To do this, we can use this section of `<raddb>/mods-available/dhcp_files`:
+
+[source,config]
+----
+files dhcp_subnets {
+ filename = ${modconfdir}/files/dhcp
+ key = "subnet"
+}
+----
+
+Additionally, uncomment the `dhcp_subnets` policy in `<raddb>/policy.d/dhcp`.
+This policy wraps the call to the `dhcp_subnets` files module with code that
+"tightens" the `DHCP-Network-Subnet` attribute by setting it to the
+just-allocated IP address.
+
+The relevant entries in the `<raddb>/mods-config/files/dhcp` configuration
+file might then look something like this:
+
+[source,config]
+----
+network
+ DHCP-Domain-Name := "example.org",
+ Fall-Through := yes
+
+network DHCP-Network-Subnet < 10.30.0.0/16, Pool-Name := "bigdept"
+ DHCP-Domain-Name-Server := 10.10.0.2,
+ DHCP-Domain-Name-Server += 10.10.0.3,
+ DHCP-IP-Address-Lease-Time := 7200
+
+subnet DHCP-Network-Subnet < 10.30.10.0/24
+ DHCP-Router-Address := 10.30.10.1
+
+subnet DHCP-Network-Subnet < 10.30.20.0/24
+ DHCP-Router-Address := 10.30.20.1
+----
+
+=== Example packet processing
+
+For our example, we consider a request arriving from a DHCP relay within
+10.30.10.0/24. In the absence of any specific DHCP subnet selection options in
+the request, the `DHCP-Network-Subnet` attribute is calculated to be the
+relay's IP address, say 10.30.10.1.
+
+The request is matched against the first "network" block, setting the domain
+name to "example.org". By virtue of `Fall-Through` being set, the next "network"
+block is considered.
+
+Since the network identifier is within the specified subnet (i.e. `10.30.10.1 <
+10.30.0.0/16`) this second "network" block is matched. This block sets the pool
+name to "bigdept", sets some network-specific DNS resolvers and sets the lease
+time to 7200 seconds. `Fall-Through` is not set, so we are now done with
+deriving the pool name and network options.
+
+When the `dhcp_sqlippool` module is called during DHCP DISCOVER processing (in
+`<raddb>/sites-enabled/dhcp`) the `bigdept` pool will be used for IP address
+allocation.
+
+After IP allocation the `dhcp_subnet` policy and files instance are called.
+Before the subnet options are looked up the `DHCP-Network-Subnet`
+attribute is tightened to match the assigned IP address, say 10.30.20.123.
+
+The request does not match the first subnet block since 10.30.20.123 is not
+within 10.30.10.0/24. However, the request does match the second subnet block
+since `10.30.20.123 < 10.30.20.0/24`. This block sets the default gateway
+reply parameter. `Fall-Through` is not set, so we are now done with deriving
+the pool name and network options.
+
+The assigned IP address, network and subnet parameters will subsequently be
+returned in the DHCP reply.
+
+=== Testing the subnet-specific options
+
+If you have set any subnet-specific reply parameters then you should test these
+before proceeding further.
+
+For example, in the case that you have a single, large pool spanning two IP
+subnets you might want to test by repeatedly allocating addresses using sample
+packets with different MAC addresses, each time checking to ensure that the
+DHCP parameters correspond to the IP address that has been offered.
+
+.Example output from dhcpclient showing a response
+==================================================
+ dhcpclient: ...
+ ...
+ ----------------------------------------------------------------------
+ Waiting for DHCP replies for: 5.000000
+ ----------------------------------------------------------------------
+ ...
+ DHCP-Your-IP-Address = 10.0.10.50
+ DHCP-Router-Address = 10.0.10.1
+ DHCP-Broadcast-Address = 10.0.10.255
+ DHCP-Subnet-Mask = 255.255.255.255
+==================================================
+
+
+.Example output from dhcpclient showing a response
+==================================================
+ dhcpclient: ...
+ ...
+ ----------------------------------------------------------------------
+ Waiting for DHCP replies for: 5.000000
+ ----------------------------------------------------------------------
+ ...
+ DHCP-Your-IP-Address = 10.99.99.50
+ DHCP-Router-Address = 10.99.99.1
+ DHCP-Broadcast-Address = 10.99.99.255
+ DHCP-Subnet-Mask = 255.255.255.255
+==================================================
+
+
+[TIP]
+====
+If the subnets are large then you might want to temporarily reduce their
+size by setting the `status` field of the majority of the rows for each subnet
+to "`disabled`" to cause offers to be made more readily with IP addresses in
+different subnets.
+====
diff --git a/doc/antora/modules/howto/pages/protocols/dhcp/prepare.adoc b/doc/antora/modules/howto/pages/protocols/dhcp/prepare.adoc
new file mode 100644
index 0000000..aa43530
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/dhcp/prepare.adoc
@@ -0,0 +1,59 @@
+== Preparation
+
+It is necessary to consider the requirements for the installation in order to
+devise an efficient and manageable set up.
+
+=== Understand the network topology
+
+When multiple networks (VLANs) are in use consideration must be given to how
+the correct "pool" (IP address ranges) from which to allocate addresses is
+identified.
+
+The policy for setting specific DHCP options (e.g. lease time, default gateway,
+time server and vendor-specific parameters) for different groups of hosts,
+based on their network or some device attributes either supplied in the DHCP
+requests or determined by dynamic lookup, should be well defined and
+understood.
+
+Other DHCP servers may implement implicit assumptions about the requirement of
+your network topology and silently define particular behaviours, such as the
+selection of IP address pool for a request based on a relay address. Some of
+these behaviours must be specifed explicitly when using FreeRADIUS.
+
+=== Choose a database backend
+
+FreeRADIUS stores its leases in an SQL database, so one of the key decisions to
+make is which database to use.
+
+FreeRADIUS supports:
+
+ * SQLite
+ * PostgreSQL
+ * MySQL / MariaDB
+ * Microsoft SQL Server
+ * Oracle
+
+In most configurations the SQL database is likely to be the limiting component
+that restricts the IP allocation throughput of the overall system. Each
+database server has its own performance characteristics and unique approach to
+features such as high-availability.
+
+The choice of database should be made carefully based on the performance and
+high-availability requirements of the system, as well as any prior experience.
+
+[TIP]
+====
+SQLite is an in-process database that uses the local file system, is simple to
+configure and is suitable for smaller installations. However, users with larger
+address pools or high availability requirements should choose one of the other
+standalone databases based on criteria such as performance, features,
+familiarity and your need for commercial support.
+====
+
+FreeRADIUS ships with a default database schema and set of queries for each
+supported database. These are sufficient for most DHCP deployments but can be
+reviewed and modified as required to suit a particular situation, for example
+to customise the IP allocation policy such as by disabling address
+"stickiness".
+
+Now xref:protocols/dhcp/enable.adoc[enable the DHCP service].
diff --git a/doc/antora/modules/howto/pages/protocols/dhcp/test.adoc b/doc/antora/modules/howto/pages/protocols/dhcp/test.adoc
new file mode 100644
index 0000000..322de08
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/dhcp/test.adoc
@@ -0,0 +1,143 @@
+== Testing the DHCP service
+
+We can verify that FreeRADIUS is providing a DHCP service using the
+`dhcpclient` tool that is included with the FreeRADIUS distribution.
+
+Temporarily configure FreeRADIUS to issue a single static IP address to all
+clients by updating the `dhcp DHCP-Discover` section in the `dhcp` virtual
+server to include the following:
+
+[source,unlang]
+----
+update reply {
+ &DHCP-Your-IP-Address := 1.2.3.4
+}
+----
+
+Define a sample DHCP packet as follows:
+
+[source,shell]
+----
+cat <<EOF > dhcp-packet.txt
+DHCP-Message-Type := DHCP-Discover
+DHCP-Client-Hardware-Address := 02:01:aa:bb:cc:dd
+DHCP-Client-Identifier := abc123
+EOF
+----
+
+We can now generate this packet by invoking one of the following commands based
+on the current circumstances...
+
+From the host that is running the FreeRADIUS DHCP server:
+
+[source,shell]
+----
+dhcpclient -i lo 255.255.255.255 -f dhcp-packet.txt -x auto
+----
+
+From a different host with an interface (eth0) in the same broadcast domain
+as the FreeRADIUS DHCP server:
+
+[source,shell]
+----
+dhcpclient -i eth0 255.255.255.255 -f dhcp-packet.txt -x auto
+----
+
+If all of the DHCP broadcast traffic in other Layer 2 networks is converted to
+unicast by DHCP relay agents then it is not necessary for FreeRADIUS to listen
+on a broadcast address. In this case you can test DHCP using a unicast request:
+
+[source,shell]
+----
+dhcpclient 192.0.2.10 -f dhcp-packet.txt -x auto
+----
+
+[NOTE]
+====
+In order for the returned, unicast DHCP OFFER to be received it is necessary to
+ensure that the `DHCP-Your-IP-Address` parameter set by FreeRADIUS matches an
+address on the interface used by the dhcpclient tool to send the Discover
+packet.
+====
+
+When one of the above commands is run, the tool with generate output such as
+the following which shows that the packet was sent and that it is now waiting
+for replies:
+
+.Example output from dhcpclient showing the request
+===================================================
+ dhcpclient: ...
+ ----------------------------------------------------------------------
+ DHCP-Opcode = 0x01
+ DHCP-Hardware-Type = 0x01
+ DHCP-Hardware-Address-Length = 0x06
+ DHCP-Hop-Count = 0x00
+ DHCP-Transaction-Id = 0x5e0bbfab
+ DHCP-Number-of-Seconds = 0x0000
+ DHCP-Flags = 0x0000
+ DHCP-Client-IP-Address = 0x00000000
+ DHCP-Your-IP-Address = 0x00000000
+ DHCP-Server-IP-Address = 0x00000000
+ DHCP-Gateway-IP-Address = 0x00000000
+ ...
+ ----------------------------------------------------------------------
+ Waiting for DHCP replies for: 5.000000
+ ----------------------------------------------------------------------
+===================================================
+
+
+Each received DHCP response will generate output such as the following:
+
+.Example output from dhcpclient showing a response
+==================================================
+ ...
+ ----------------------------------------------------------------------
+ DHCP-Opcode = Server-Message
+ DHCP-Hardware-Type = Ethernet
+ DHCP-Hardware-Address-Length = 6
+ DHCP-Hop-Count = 0
+ DHCP-Transaction-Id = 1577828267
+ DHCP-Number-of-Seconds = 0
+ DHCP-Flags = 0
+ DHCP-Client-IP-Address = 0.0.0.0
+ DHCP-Your-IP-Address = 1.2.3.4
+ DHCP-Server-IP-Address = 192.0.2.10
+ DHCP-Gateway-IP-Address = 0.0.0.0
+ DHCP-Client-Hardware-Address = 02:42:0a:00:00:0b
+ DHCP-Message-Type = DHCP-Offer
+ DHCP-Client-Identifier = 0x616263313233
+ Waiting for additional DHCP replies for: 4.999429
+ ...
+==================================================
+
+Examine the DHCP response to ensure that it has the correct message type
+(`DHCP-Offer`, in this case), contains the temporary IP address that you
+configured earlier, i.e. `DHCP-Your-IP-Address = 1.2.3.4`, and any other
+expected reply parameters (which we configure later). You should also carefully
+examine the output of a FreeRADIUS debug session (`radius -X`) to ensure that
+the policy is being executed in the way that you expect and that no warnings
+are being generated.
+
+You can now change the content of the sample DHCP request by editing the
+`dhcp-packet.txt` file and re-run the above command to see the server's reply.
+You should examine the DHCP dictionary distrubuted with FreeRADIUS (usually
+`/usr/share/freeradius/dictionary.dhcp`) which provides the list of all of the
+DHCP parameters ("attributes") understood by FreeRADIUS.
+
+[WARNING]
+====
+When you are done **remember** to remove the temporary edit that was made to
+the `dhcp` virtual server that provides the static IP assignment.
+====
+
+=== Testing the DHCP policy
+
+The remainder of this guide describes how to configure the IP address plan,
+setup the IP pools and define a DHCP policy. You should develop your policy by
+making small, incremental changes to the provided configuration and then test
+those changes with the approach described above, using `dhcpclient` and `radius -X`,
+modifying the sample DHCP packet as required. If you break the policy then
+revert the last change, attempt to understand what went wrong, and try
+something else.
+
+Now xref:protocols/dhcp/policy.adoc[define the DHCP policy].