summaryrefslogtreecommitdiffstats
path: root/doc/antora
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 14:11:00 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 14:11:00 +0000
commitaf754e596a8dbb05ed8580c342e7fe02e08b28e0 (patch)
treeb2f334c2b55ede42081aa6710a72da784547d8ea /doc/antora
parentInitial commit. (diff)
downloadfreeradius-af754e596a8dbb05ed8580c342e7fe02e08b28e0.tar.xz
freeradius-af754e596a8dbb05ed8580c342e7fe02e08b28e0.zip
Adding upstream version 3.2.3+dfsg.upstream/3.2.3+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'doc/antora')
-rw-r--r--doc/antora/antora.yml18
-rw-r--r--doc/antora/modules/ROOT/assets/images/favicon.pngbin0 -> 4105 bytes
-rw-r--r--doc/antora/modules/ROOT/assets/images/favicon.svg104
-rw-r--r--doc/antora/modules/ROOT/assets/images/networkradius.pngbin0 -> 7126 bytes
-rw-r--r--doc/antora/modules/ROOT/nav.adoc1
-rw-r--r--doc/antora/modules/ROOT/pages/directories.adoc69
-rw-r--r--doc/antora/modules/ROOT/pages/index.adoc137
-rw-r--r--doc/antora/modules/howto/nav.adoc19
-rw-r--r--doc/antora/modules/howto/pages/index.adoc17
-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
-rw-r--r--doc/antora/modules/howto/pages/protocols/proxy/enable_proxy_protocol.adoc114
-rw-r--r--doc/antora/modules/howto/pages/protocols/proxy/enable_radsec.adoc188
-rw-r--r--doc/antora/modules/howto/pages/protocols/proxy/index.adoc126
-rw-r--r--doc/antora/modules/howto/pages/protocols/proxy/radsec_client.adoc181
-rw-r--r--doc/antora/modules/howto/pages/protocols/proxy/radsec_with_haproxy.adoc134
-rw-r--r--doc/antora/modules/howto/pages/protocols/proxy/radsec_with_traefik.adoc128
-rw-r--r--doc/antora/modules/installation/nav.adoc5
-rw-r--r--doc/antora/modules/installation/pages/dependencies.adoc58
-rw-r--r--doc/antora/modules/installation/pages/index.adoc15
-rw-r--r--doc/antora/modules/installation/pages/packages.adoc22
-rw-r--r--doc/antora/modules/installation/pages/source.adoc199
-rw-r--r--doc/antora/modules/installation/pages/upgrade.adoc737
-rw-r--r--doc/antora/modules/unlang/.gitignore1
-rw-r--r--doc/antora/modules/unlang/nav.adoc51
-rw-r--r--doc/antora/modules/unlang/pages/attr.adoc77
-rw-r--r--doc/antora/modules/unlang/pages/break.adoc28
-rw-r--r--doc/antora/modules/unlang/pages/case.adoc44
-rw-r--r--doc/antora/modules/unlang/pages/condition/and.adoc21
-rw-r--r--doc/antora/modules/unlang/pages/condition/cmp.adoc104
-rw-r--r--doc/antora/modules/unlang/pages/condition/eq.adoc30
-rw-r--r--doc/antora/modules/unlang/pages/condition/index.adoc85
-rw-r--r--doc/antora/modules/unlang/pages/condition/not.adoc19
-rw-r--r--doc/antora/modules/unlang/pages/condition/operands.adoc37
-rw-r--r--doc/antora/modules/unlang/pages/condition/or.adoc21
-rw-r--r--doc/antora/modules/unlang/pages/condition/para.adoc19
-rw-r--r--doc/antora/modules/unlang/pages/condition/regex.adoc180
-rw-r--r--doc/antora/modules/unlang/pages/condition/return_codes.adoc35
-rw-r--r--doc/antora/modules/unlang/pages/default.adoc47
-rw-r--r--doc/antora/modules/unlang/pages/else.adoc30
-rw-r--r--doc/antora/modules/unlang/pages/elsif.adoc43
-rw-r--r--doc/antora/modules/unlang/pages/foreach.adoc40
-rw-r--r--doc/antora/modules/unlang/pages/group.adoc39
-rw-r--r--doc/antora/modules/unlang/pages/if.adoc29
-rw-r--r--doc/antora/modules/unlang/pages/index.adoc162
-rw-r--r--doc/antora/modules/unlang/pages/keywords.adoc78
-rw-r--r--doc/antora/modules/unlang/pages/list.adoc72
-rw-r--r--doc/antora/modules/unlang/pages/load-balance.adoc32
-rw-r--r--doc/antora/modules/unlang/pages/module.adoc86
-rw-r--r--doc/antora/modules/unlang/pages/module_builtin.adoc42
-rw-r--r--doc/antora/modules/unlang/pages/module_method.adoc27
-rw-r--r--doc/antora/modules/unlang/pages/redundant-load-balance.adoc39
-rw-r--r--doc/antora/modules/unlang/pages/redundant.adoc42
-rw-r--r--doc/antora/modules/unlang/pages/return.adoc36
-rw-r--r--doc/antora/modules/unlang/pages/return_codes.adoc17
-rw-r--r--doc/antora/modules/unlang/pages/switch.adoc83
-rw-r--r--doc/antora/modules/unlang/pages/type/all_types.adoc80
-rw-r--r--doc/antora/modules/unlang/pages/type/double.adoc39
-rw-r--r--doc/antora/modules/unlang/pages/type/index.adoc117
-rw-r--r--doc/antora/modules/unlang/pages/type/ip.adoc15
-rw-r--r--doc/antora/modules/unlang/pages/type/numb.adoc11
-rw-r--r--doc/antora/modules/unlang/pages/type/string/backticks.adoc38
-rw-r--r--doc/antora/modules/unlang/pages/type/string/double.adoc68
-rw-r--r--doc/antora/modules/unlang/pages/type/string/escaping.adoc14
-rw-r--r--doc/antora/modules/unlang/pages/type/string/single.adoc19
-rw-r--r--doc/antora/modules/unlang/pages/type/string/unquoted.adoc21
-rw-r--r--doc/antora/modules/unlang/pages/update.adoc160
-rw-r--r--doc/antora/modules/unlang/pages/xlat/alternation.adoc24
-rw-r--r--doc/antora/modules/unlang/pages/xlat/attribute.adoc54
-rw-r--r--doc/antora/modules/unlang/pages/xlat/builtin.adoc891
-rw-r--r--doc/antora/modules/unlang/pages/xlat/character.adoc80
-rw-r--r--doc/antora/modules/unlang/pages/xlat/index.adoc56
-rw-r--r--doc/antora/modules/unlang/pages/xlat/module.adoc18
-rw-r--r--doc/antora/modules/unlang/partials/rcode_table.adoc39
83 files changed, 7153 insertions, 0 deletions
diff --git a/doc/antora/antora.yml b/doc/antora/antora.yml
new file mode 100644
index 0000000..e345e9c
--- /dev/null
+++ b/doc/antora/antora.yml
@@ -0,0 +1,18 @@
+#
+# Metadata for the freeradius-server component
+# Examples of other components are the PAM module,
+# apache module, and the client library.
+#
+name: freeradius-server
+title: The FreeRADIUS Server
+version: '3.2.3'
+start_page: ROOT:index.adoc
+nav:
+- modules/ROOT/nav.adoc
+- modules/installation/nav.adoc
+- modules/concepts/nav.adoc
+- modules/howto/nav.adoc
+- modules/tutorials/nav.adoc
+- modules/unlang/nav.adoc
+- modules/developers/nav.adoc
+- modules/raddb/nav.adoc
diff --git a/doc/antora/modules/ROOT/assets/images/favicon.png b/doc/antora/modules/ROOT/assets/images/favicon.png
new file mode 100644
index 0000000..8c71104
--- /dev/null
+++ b/doc/antora/modules/ROOT/assets/images/favicon.png
Binary files differ
diff --git a/doc/antora/modules/ROOT/assets/images/favicon.svg b/doc/antora/modules/ROOT/assets/images/favicon.svg
new file mode 100644
index 0000000..7476355
--- /dev/null
+++ b/doc/antora/modules/ROOT/assets/images/favicon.svg
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 160 160" style="enable-background:new 0 0 160 160;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:none;stroke:#FFD62B;stroke-width:3;stroke-miterlimit:10;}
+ .st1{fill:#B0C3C6;}
+ .st2{fill:none;stroke:#DEE7E8;stroke-miterlimit:10;}
+ .st3{fill:#00A6E2;}
+ .st4{fill:none;stroke:#B0C3C6;stroke-width:2;stroke-miterlimit:10;}
+ .st5{fill:#666666;}
+ .st6{fill:none;stroke:#B0C3C6;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:10;}
+ .st7{fill:#00C9ED;}
+ .st8{fill:#FFFFFF;stroke:#B0C3C6;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:10;}
+ .st9{fill:#303030;}
+ .st10{opacity:0.4;fill:#F6F6F6;}
+ .st11{fill:none;stroke:#999999;stroke-width:2;stroke-miterlimit:10;}
+ .st12{fill:#999999;}
+ .st13{fill:#FFFFFF;}
+ .st14{clip-path:url(#SVGID_2_);fill:#2F3537;}
+ .st15{opacity:0.3;fill:none;stroke:#9FB1B3;stroke-width:2;stroke-miterlimit:10;}
+ .st16{fill:none;stroke:#FFFFFF;stroke-miterlimit:10;}
+ .st17{fill:#FFFFFF;stroke:#FFFFFF;stroke-miterlimit:10;}
+ .st18{fill:none;stroke:#303030;stroke-miterlimit:10;}
+ .st19{opacity:0.8;fill:#B0C3C6;}
+ .st20{opacity:0.7;}
+ .st21{opacity:0.8;clip-path:url(#SVGID_4_);fill:#00A6E2;}
+ .st22{opacity:0.8;fill:#00A6E2;}
+ .st23{opacity:0.8;clip-path:url(#SVGID_6_);fill:#00A6E2;}
+ .st24{clip-path:url(#SVGID_8_);}
+ .st25{clip-path:url(#SVGID_10_);}
+ .st26{fill:none;stroke:#B0C3C6;stroke-width:3;stroke-linejoin:round;stroke-miterlimit:10;}
+ .st27{opacity:0.3;fill:none;stroke:#9FB1B3;stroke-width:3;stroke-miterlimit:10;}
+ .st28{fill:#FFFFFF;stroke:#B0C3C6;stroke-width:3;stroke-linejoin:round;stroke-miterlimit:10;}
+ .st29{clip-path:url(#SVGID_12_);fill:#2F3537;}
+ .st30{clip-path:url(#SVGID_14_);fill:#B0C3C6;}
+ .st31{fill:#33B8E8;}
+ .st32{fill:#238DB4;}
+ .st33{fill:#E2E7E8;}
+ .st34{clip-path:url(#SVGID_16_);}
+ .st35{fill:#FFFFFF;stroke:#B0C3C6;stroke-width:2;stroke-miterlimit:10;}
+ .st36{fill:#B4CBCE;}
+ .st37{fill:#003147;}
+ .st38{fill:#FFD62B;}
+ .st39{fill:#00B78E;}
+ .st40{fill:#FF7824;}
+ .st41{fill:#FF3223;}
+ .st42{fill:#7955DF;}
+ .st43{fill:none;stroke:#FF3223;stroke-width:2;stroke-miterlimit:10;}
+ .st44{fill:none;stroke:#00A6E2;stroke-width:2;stroke-miterlimit:10;}
+ .st45{clip-path:url(#SVGID_18_);fill:#303030;}
+ .st46{fill:#F5C81F;}
+ .st47{fill:#F49F90;}
+ .st48{fill:#F3EEDE;}
+ .st49{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
+ .st50{fill:#00131F;}
+ .st51{clip-path:url(#SVGID_20_);fill:#303030;}
+ .st52{fill:none;stroke:#FFFFFF;stroke-width:2;stroke-miterlimit:10;}
+ .st53{clip-path:url(#SVGID_22_);fill:#303030;}
+ .st54{clip-path:url(#SVGID_24_);fill:#303030;}
+</style>
+<path class="st0" d="M72.3,25.6c0,0-2.3-3.2-6-2.3"/>
+<g>
+ <polygon class="st50" points="79.2,73 20.3,37.9 86.2,19.3 145.1,54.4 "/>
+ <g>
+ <g>
+ <g>
+ <g>
+ <path class="st3" d="M126.2,113.6c-4.4,0-8.2-3-9.3-7.1c2.8,1,5.7,1.6,8.8,1.6c4.4,0,7.9-3.5,7.9-7.9c0-4.4-3.5-7.9-7.9-7.9
+ c-5.3,0-9.9-4.3-9.9-9.6c0-0.7,0-2.1,0-2.1v-6.1c0-19.4-15.7-35.2-35.2-35.2c-19.4,0-35.2,15.7-35.2,35.2v6.1c0,0,0,1.3,0,2.1
+ c0,5.3-4.6,9.6-9.9,9.6c-4.4,0-7.9,3.5-7.9,7.9c0,4.4,3.5,7.9,7.9,7.9c3.5,0,6.8-0.7,9.8-2c-1,4.3-4.8,7.5-9.4,7.5
+ c-4.4,0-7.9,3.5-7.9,7.9c0,4.4,3.5,7.9,7.9,7.9c6.9,0,13.2-2.8,17.8-7.3v10.4c0,4.4,3.5,7.9,7.9,7.9c4.4,0,7.9-3.5,7.9-7.9
+ v-14.8h3.1v18.7c0,4.4,3.5,7.9,7.9,7.9c4.4,0,7.9-3.5,7.9-7.9v-18.7h3.1v14.8c0,4.4,3.5,7.9,7.9,7.9c4.4,0,7.9-3.5,7.9-7.9
+ v-11.4c4.7,5.1,11.3,8.2,18.7,8.2c4.4,0,7.9-3.5,7.9-7.9C134.1,117.1,130.5,113.6,126.2,113.6z"/>
+ <g>
+ <circle class="st13" cx="80" cy="76.9" r="17.5"/>
+ <g>
+ <defs>
+ <circle id="SVGID_19_" cx="80" cy="76.9" r="17.5"/>
+ </defs>
+ <use xlink:href="#SVGID_19_" style="overflow:visible;fill:#FFFFFF;"/>
+ <clipPath id="SVGID_2_">
+ <use xlink:href="#SVGID_19_" style="overflow:visible;"/>
+ </clipPath>
+ <path style="clip-path:url(#SVGID_2_);fill:#303030;" d="M73.3,85.5c-6.7,0-12.2-5.5-12.2-12.2c0-6.7,5.5-12.2,12.2-12.2
+ s12.2,5.5,12.2,12.2C85.5,80,80.1,85.5,73.3,85.5z"/>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ <g>
+ <path class="st50" d="M46,65.8c0-19.1,15.5-29.7,34.6-29.7s34.6,10.6,34.6,29.7c0,0-9.6-9.8-34.2-9.8S46,65.8,46,65.8z"/>
+ </g>
+ <g>
+ <path class="st0" d="M66.3,23.3c0,0-10.7,2.8-10.2,9.7v20.8"/>
+ <line class="st0" x1="56" y1="53.7" x2="59.3" y2="66"/>
+ <line class="st0" x1="56" y1="53.7" x2="54" y2="64.7"/>
+ <line class="st0" x1="56" y1="53.7" x2="62" y2="59.2"/>
+ <line class="st0" x1="56" y1="53.7" x2="50.8" y2="60.4"/>
+ </g>
+</g>
+</svg>
diff --git a/doc/antora/modules/ROOT/assets/images/networkradius.png b/doc/antora/modules/ROOT/assets/images/networkradius.png
new file mode 100644
index 0000000..bf1ea80
--- /dev/null
+++ b/doc/antora/modules/ROOT/assets/images/networkradius.png
Binary files differ
diff --git a/doc/antora/modules/ROOT/nav.adoc b/doc/antora/modules/ROOT/nav.adoc
new file mode 100644
index 0000000..3d92412
--- /dev/null
+++ b/doc/antora/modules/ROOT/nav.adoc
@@ -0,0 +1 @@
+* xref:index.adoc[Introduction]
diff --git a/doc/antora/modules/ROOT/pages/directories.adoc b/doc/antora/modules/ROOT/pages/directories.adoc
new file mode 100644
index 0000000..9b16249
--- /dev/null
+++ b/doc/antora/modules/ROOT/pages/directories.adoc
@@ -0,0 +1,69 @@
+= Directories
+
+The directories in the server source are laid out ad follows:
+
+== Documentation
+
+[width="100%",cols="50%,50%",options="header",]
+|===
+| Directory | Description
+| `doc/` | Various snippets of documentation.
+| `doc/introduction/` | Concepts and introduction to FreeRADIUS.
+| `doc/raddb/` | HTML versions of the configuration files.
+| `doc/developers/` | Developer documentation for internal APIs
+| `doc/unlang/` | The unlang processing language.
+| `doc/upgrade/` | How to upgrade from version 3 to version 4.
+| `doc/rfc/` | Copies of the RFC’s. If you have Perl, do a `make` in
+ that directory, and look at the HTML output.
+| `doc/antora/` | Metadata and documentation source files to build
+ an Antora based documentation site.
+| `doc/doxygen/` | Files to build a Doxygen site from the source code.
+| `man/` | Unix Manual pages for the server, configuration files,
+ and associated utilities.
+|===
+
+== Utility
+
+[cols=",",options="header",]
+|===
+|Directory | Description
+| `mibs/` | SNMP Mibs for the server.
+| `scripts/` | Sample scripts for startup and maintenance.
+|===
+
+== Configuration
+
+[width="100%",cols="50%,50%",options="header",]
+|===
+| Directory | Description
+| `raddb/` | Sample configuration files for the server.
+| `raddb/mods-available` | Module configuration files.
+| `raddb/mods-enabled` | Directory containing symlinks to `raddb/mods-available`.
+ Controls which modules are enabled.
+| `raddb/sites-available` | Virtual servers.
+| `raddb/sites-enabled` | Directory containing symlinks to `raddb/sites-available`.
+ Control which virtual servers are enabled.
+|===
+
+== Packaging
+
+[cols=",",options="header",]
+|===
+|Directory | Description
+| `debian/` | Files to build a `freeradius` Debian Linux package.
+| `redhat/` | Additional files for a RedHat Linux system.
+| `suse/` | Additional files for a SuSE (UnitedLinux) system.
+|===
+
+== Source
+
+[cols=",",options="header",]
+|===
+|Directory | Description
+| `src/` | Source code.
+| `src/bin/` | Source code for the daemon and associated utilities.
+| `src/lib/` | Source code for various utility libraries.
+| `src/include/` | Header files.
+| `src/protocols/` | Dynamic frontend plug-in modules.
+| `src/modules/` | Dynamic backend plug-in modules.
+|===
diff --git a/doc/antora/modules/ROOT/pages/index.adoc b/doc/antora/modules/ROOT/pages/index.adoc
new file mode 100644
index 0000000..e9bc7a0
--- /dev/null
+++ b/doc/antora/modules/ROOT/pages/index.adoc
@@ -0,0 +1,137 @@
+= Introduction
+
+This is the documentation for FreeRADIUS, version 3. The documentation
+is available under the Creative Commons Non-Commercial license, as given
+in the `LICENSE` file in this directory.
+
+FreeRADIUS is a complex piece of software with many configuration
+options. However, we have taken great care to make the default
+configuration work in most circumstances. The result is that for most
+simple systems, it is trivial to install and configure the server. For
+those situations, this documentation will serve to answer basic
+questions about functionality, configuration, etc.
+
+For more complex requirements, FreeRADIUS can be difficult to
+configure. The reason for this difficulty is that the server can do
+almost anything, which means that there are a near-infinite number of
+ways to configure it. The question for an administrator, then, is what
+piece of the configuration to change, and how to change it.
+
+This documentation will answer those questions. The FreeRADIUS team has
+put substantial effort into writing the documentation for this release.
+Everything in the server is fully documented, and there are many
+`how-to` guides available.
+
+The documentation is split into sections by subject area, oganized by
+desired outcome. At a high level, the subject areas describe:
+
+* xref:concepts:index.adoc[Concepts] and introduction for newcomers.
+* xref:installation:index.adoc[Installing] and xref:installation:upgrade.adoc[upgrading] FreeRADIUS.
+* The syntax of the xref:unlang:index.adoc[unlang] processing language.
+* The xref:raddb:index.adoc[configuration files] located in `/etc/raddb/`, or `/etc/freeradius/`
+* Various xref:howto:index.adoc[how-to] guides.
+* xref:developers:index.adoc[Developer documentation].
+
+This organization means that for example, the `ldap` module will have
+documention located in multiple places. We feel that organizing the
+documentation by desired _goal_ is better than the alternatives.
+
+Within each section, the documentation is split into small pages, which
+are generally no more than a few screens worth of information. We feel
+that having multiple small pages with cross-links is more helpful than
+having a smaller number of enormous pages. This division ensures that
+(for example) the `how-to` guides are split into a series of small
+steps, each of which can be performed quickly.
+
+We hope that this extended documentation will address any lingering
+concerns about the quality of the FreeRADIUS documentation.
+
+== Changes From Earlier Versions
+
+Administrators who have version 2 and wish to upgrade to version 3
+should read the xref:installation:upgrade.adoc[upgrading] documentation.
+That documentation explains the differences between the two versions, and
+how an existing configuration can be reproduced in the latest
+release. We do _not_ recommend using version 2 configuration files
+with version 3. The configuration files are _not_ compatible across a
+major version upgrade.
+
+== Getting Started with FreeRADIUS
+
+FreeRADIUS can be installed using the pre-built packages available
+from http://packages.networkradius.com[Network RADIUS,
+window="_blank"]. That page contains packages for all common OS
+distributions. New packages are available as soon as a new version
+has been released. Packages for older releases are also available for
+historical purposes.
+
+FreeRADIUS can also be installed from the source code. Please see the
+xref:installation:index.adoc[installation guide] for instructions.
+
+WARNING: Many Operating System distributions ship versions of FreeRADIUS
+which are years out of date. Those versions may contain bugs which have
+been fixed in newer releases. We recommend using the
+http://packages.networkradius.com[Network RADIUS, window="_blank"] packages where
+possible.
+
+Administrators who are new to FreeRADIUS should read the
+xref:concepts:index.adoc[concepts section] as it describes the concepts behind
+FreeRADIUS. It is vital for newcomers to understand these concepts, as the rest
+of the documentation assumes familiarity with them.
+
+A detailed xref:unlang:index.adoc[unlang] reference guide is also available.
+This section describes the syntax and functionality of the keywords,
+data types, etc. used in the `unlang` processing language.
+
+All of the xref:raddb:index.adoc[configuration files] are available in
+hypertext format. In can often be easier to read the configuration files
+in a nicely formatted version, instead of as a fixed-width font in a
+text editor.
+
+For specific problem solving, we recommend the xref:howto:index.adoc[how-to]
+guides. These guides give instructions for reaching high-level goals, or
+for configuring and testing individual xref:howto:modules/index.adoc[modules].
+
+There is also xref:developers:index.adoc[developer documentation]. This section
+documents the APIs for developers. Most people can ignore it.
+
+== Debugging
+
+If you have ANY problems, concerns, or surprises when running the
+server, the the server should be run in debugging mode as root, from the
+command line:
+
+```
+# radiusd -X
+```
+
+It will produce a large number of messages. The answers to many
+questions, and the solution to many problems, can usually be found in
+these messages. When run in a terminal window, error messages will be
+shown in red text, and warning messages will be shown in yellow text.
+
+For other use-cases, please look for `ERROR` or `WARNING` in the
+debug output. In many cases, those messages describe exactly what is
+going wrong, and how to fix it.
+
+For further details, about the debug output see the
+http://wiki.freeradius.org/radiusd-X[radiusd-X, window="_blank"] page on the
+http://wiki.freeradius.org[wiki, window="_blank"].
+
+== Getting Help
+
+We also recommend joining the
+http://lists.freeradius.org/mailman/listinfo/freeradius-users[mailing
+list] in order to ask questions and receive answers. The developers are
+not on Stack Overflow, IRC, or other web sites. While the FreeRADIUS
+source is available on
+https://github.com/FreeRADIUS/freeradius-server/[GitHub, window="_blank"], questions
+posted there will not be answered.
+
+Before posting to the list, please read the
+http://wiki.freeradius.org/list-help[list help, window="_blank"] page. That page explains
+how to run the server in debugging mode; how to understand the debug
+output; and what information to post to the list.
+
+Commercial support for FreeRADIUS is available from
+https://networkradius.com/freeradius-support/[Network RADIUS, window="_blank"].
diff --git a/doc/antora/modules/howto/nav.adoc b/doc/antora/modules/howto/nav.adoc
new file mode 100644
index 0000000..351200b
--- /dev/null
+++ b/doc/antora/modules/howto/nav.adoc
@@ -0,0 +1,19 @@
+* xref:index.adoc[Howto Guides]
+** Protocols
+**** xref:protocols/dhcp/index.adoc[DHCP]
+***** xref:protocols/dhcp/prepare.adoc[Preparation]
+***** xref:protocols/dhcp/enable.adoc[Enabling the DHCP service]
+***** xref:protocols/dhcp/test.adoc[Testing the DHCP service]
+***** xref:protocols/dhcp/policy.adoc[Defining the DHCP policy]
+****** xref:protocols/dhcp/policy_ippool_creation.adoc[IP pool creation]
+****** xref:protocols/dhcp/policy_common_options.adoc[Common options]
+****** xref:protocols/dhcp/policy_network_options.adoc[Network options and IP pool selection]
+****** xref:protocols/dhcp/policy_subnet_options.adoc[Subnet options]
+****** xref:protocols/dhcp/policy_device_options.adoc[Device, class and group options]
+****** xref:protocols/dhcp/policy_ippool_access.adoc[IP pool access restriction]
+**** xref:protocols/proxy/index.adoc[PROXY Protocol]
+***** xref:protocols/proxy/enable_radsec.adoc[Enabling RadSec]
+***** xref:protocols/proxy/radsec_client.adoc[Configuring a test RadSec client]
+***** xref:protocols/proxy/radsec_with_haproxy.adoc[Proxying RadSec with HAproxy]
+***** xref:protocols/proxy/radsec_with_traefik.adoc[Proxying RadSec with Traefik]
+***** xref:protocols/proxy/enable_proxy_protocol.adoc[Enabling PROXY Protocol for RadSec]
diff --git a/doc/antora/modules/howto/pages/index.adoc b/doc/antora/modules/howto/pages/index.adoc
new file mode 100644
index 0000000..47a5146
--- /dev/null
+++ b/doc/antora/modules/howto/pages/index.adoc
@@ -0,0 +1,17 @@
+= Howto Guides
+
+The documents in this section describe how to perform various common tasks with
+FreeRADIUS. They also provide worked examples on using the various modules in
+common deployment scenarions.
+
+If you have a topic you'd like to see included in the list of howtos, contact
+the developers on the
+link:http://lists.freeradius.org/mailman/listinfo/freeradius-users[User's
+mailing list].
+
+Some of the documents here started life as pages on
+link:http://wiki.freeradius.org[wiki.freeradius.org]. If you've just been
+through a particularly arduous service configuration and deployment, and would
+like to help your fellow users, then please create a new how to on the wiki.
+If it's popular enough, we'll include it in the official documentation for the
+next release.
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].
diff --git a/doc/antora/modules/howto/pages/protocols/proxy/enable_proxy_protocol.adoc b/doc/antora/modules/howto/pages/protocols/proxy/enable_proxy_protocol.adoc
new file mode 100644
index 0000000..b689824
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/proxy/enable_proxy_protocol.adoc
@@ -0,0 +1,114 @@
+== Enabling PROXY Protocol
+
+Now that we have a working configuration which used RadSec and HAproxy
+or Traefik, we are finally ready to enable PROXY Protocol.
+
+Configure FreeRADIUS on the `radsecsvr` host to expect the PROXY
+Protocol for RadSec connections. This is done by editing the `listen
+{}` section of the `tls` virtual server to include a reference to the
+proxy protocol:
+
+.Enabling PROXY Protocol in a FreeRADIUS virtual server
+=======================================================
+
+ listen {
+ ...
+ proxy_protocol = true
+ ...
+ }
+
+=======================================================
+
+Now restart the debugging session:
+[source,shell]
+----
+radiusd -fxxl /dev/stdout
+----
+
+
+For HAproxy, you should enable the PROXY Protocol on connections to
+the RadSec backend, by editing the `backend` definition to add a
+`send-proxy` argument:
+
+.Example HAproxy backend configuration with PROXY Protocol
+==========================================================
+
+ backend radsec_be
+ mode tcp
+ balance roundrobin
+ server radsecsvr 172.23.0.3:2083 send-proxy
+
+==========================================================
+
+Note the `send-proxy` argument in the `server` definition.
+
+Now reload the HAproxy service:
+
+[source,shell]
+----
+service haproxy reload
+---
+
+
+For Traefik, enable the PROXY Protocol on connections to the RadSec
+backend by editing the `radsec-service` definition to add a reference
+to the proxy protocol"
+
+.Example Traefik service configuration with PROXY Protocol
+==========================================================
+
+ radsec-service:
+ loadBalancer:
+ servers:
+ - address: "172.23.0.3:2083"
+ proxyProtocol:
+ version: 1
+
+==========================================================
+
+Note the `proxyProtocol` and `version: 1` directives.
+
+Traefik should automatically detect the updates and reconfigure the
+service.
+
+
+=== Testing RadSec connectivity via a proxy using PROXY Protocol
+
+Finally, with your test client configured to use the proxy, perform a
+test authentication:
+
+[source,shell]
+----
+ echo "User-Name = bob" | radclient 127.0.0.1 auth testing123
+----
+
+You should expect to see the familiar output:
+
+.Example output from radclient
+==============================
+
+ Sent Access-Request Id 252 from 0.0.0.0:50118 to 127.0.0.1:1812 length 27
+ Received Access-Accept Id 252 from 127.0.0.1:1812 to 127.0.0.1:50118 length 39
+
+==============================
+
+Now examine the FreeRADIUS debug output on the RadSec server:
+
+.Expected output from `radiusd -X` with PROXY Protocol
+======================================================
+
+ ...
+ (0) (TLS) Received PROXY protocol connection from client \
+ 172.23.0.2:55343 -> 172.23.0.4:2083, via proxy 172.23.0.4:40268 -> 0.0.0.0:2083
+ ...
+ (0) Received Access-Request Id 227 from 172.23.0.2:55343 to 172.23.0.4:2083 length 49
+ (0) Sent Access-Accept Id 227 from 172.23.0.4:2083 to 172.23.0.2:55343 length 0
+ ...
+
+======================================================
+
+The output indicates that FreeRADIUS is receiving the originating
+connection information from the PROXY Protocol. FreeRADIUS then
+handles the RadSec requests as though they have been received directly
+from the originating client.
+
diff --git a/doc/antora/modules/howto/pages/protocols/proxy/enable_radsec.adoc b/doc/antora/modules/howto/pages/protocols/proxy/enable_radsec.adoc
new file mode 100644
index 0000000..f5e7603
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/proxy/enable_radsec.adoc
@@ -0,0 +1,188 @@
+== Enabling RadSec with FreeRADIUS
+
+Our first task is to set up a RadSec server by configuring an instance of
+FreeRADIUS to accept RADIUS over TLS requests.
+
+The following steps should be performed on the host which will be the
+RadSec server, we will call it `radsecsvr`.
+
+You can install FreeRADIUS using the NetworkRADIUS packages by
+following the instructions provided here:
+
+<https://networkradius.com/packages/>
+
+Before making any configuration changes, you should stop the radiusd
+service:
+
+[source,shell]
+----
+ service radiusd stop
+----
+
+Then, enable the `tls` virtual server:
+
+[source,shell]
+----
+cd /etc/raddb/sites-enabled
+ln -s ../sites-available/tls
+----
+
+The FreeRADIUS distribution contains an example Certificate Authority
+that will have generated the necessary CA, server and client
+certificates and keys during package installation. You can use this
+CA, or you can use your own CA and certificates.
+
+[TIP]
+====
+If the example certificates are not present (for example if FreeRADIUS was
+installed from source) then FreeRADIUS will fail to start. The files can be
+regenerated by running `make` in the `/etc/raddb/certs` directory.
+====
+
+Edit the `tls` virtual server configuration, in order to add
+definitions for the clients by extending the `clients radsec {}` section:
+
+.Example radsec client definitions in `/etc/raddb/sites-available/tls`
+====
+
+ clients radsec {
+ ...
+ # Direct connections from the test client
+ client radseccli {
+ ipaddr = 172.23.0.2
+ proto = tls
+ virtual_server = default
+ }
+ # Connections via HAproxy
+ client haproxy {
+ ipaddr = 172.23.0.4
+ proto = tls
+ virtual_server = default
+ }
+ # Connections via Traefik
+ client traefik {
+ ipaddr = 172.23.0.5
+ proto = tls
+ virtual_server = default
+ }
+ }
+
+====
+
+The client `ipaddr` configuration item is used to match the source IP
+address of incoming connections. You must add client definitions for
+each of the clients which will connect.
+
+For RadSec, you can just list the IP address of the RadSec client.
+This client definition is used for processing RADIUS packets from the
+RadSec client.
+
+[NOTE]
+====
+A `secret` does not have to be specified for RadSec clients, as the
+default is `radsec`. If you specify a secret, then that will be used
+instead of `radsec`.
+====
+
+When the PROXY protocol is used, you must _also_ define a client which
+matches the IP address of the proxy (haproxy, etc). This client is
+only used to check that the source IP is permitted to connect to the
+server. Fields other than `ipaddr` can be specified (and in some
+cases may be required). However, all other fields will be ignored.
+
+For testing purposes, we want to amend the `default` virtual server so
+that it accepts all authentication reqeusts and immediately responds
+to accounting requests.
+
+Edit the `/etc/raddb/sites-enabled/default` file so that the beginning of
+the `authorize` and `preacct` sections looks as follows:
+
+.Example default virtual server modification to unconditionally accept Access-Requests
+====
+
+ authorize {
+ accept
+ ...
+ }
+ ...
+ preacct {
+ handled
+ ...
+ }
+
+====
+
+This change makes the `authorize` section always "accept" the user,
+and makes the `preacct` section always say "we handled the accounting
+request". These changes are only for testing, and should never be
+used in production.
+
+Start the FreeRADIUS service in the foreground with debugging enabled:
+
+[source,shell]
+----
+radiusd -fxxl /dev/stdout
+----
+
+Examine the output from FreeRADIUS to ensure that it is now listening for
+RadSec connection on TCP/2083:
+
+.Example output from running `radiusd -fxxl /dev/stdout`
+====
+
+ FreeRADIUS Version 3.0.24
+ Copyright (C) 1999-2021 The FreeRADIUS server project and contributors
+ ...
+ ... : Debug: Listening on auth+acct proto tcp address * port 2083 (TLS) bound to server default
+ ... : Debug: Listening on auth address * port 1812 bound to server default
+ ... : Debug: Listening on acct address * port 1813 bound to server default
+ ... : Debug: Listening on auth address :: port 1812 bound to server default
+ ... : Debug: Listening on acct address :: port 1813 bound to server default
+ ...
+ ... : Info: Ready to process requests
+
+====
+
+FreeRADIUS is now ready to process RadSec traffic.
+
+For testing, we first test normal RADIUS over UDP functionality, then
+the RadSec connection using a test client, then introduce a proxy
+server, and finally we enable PROXY Protocol. Doing the tests in this
+way ensures that we know that all previous steps work before trying
+the next step. This process allows us to quickly narrow down
+problems, and gets us to the final goal _faster_ than just "doing
+everything all at once".
+
+=== Testing the RADIUS policy
+
+Before moving on, verify that the FreeRADIUS policy is able to
+authenticate a local test RADIUS Access-Request over UDP:
+
+[source,shell]
+----
+echo "User-Name = terry" | radclient 127.0.0.1 auth testing123
+----
+
+Due to the `accept` we added in the `authorize` section, the expected
+output should be an Access-Accept:
+
+.Expected output from radclient
+===============================
+
+ Sent Access-Request Id 157 from 0.0.0.0:36850 to 127.0.0.1:1812 length 27
+ Received Access-Accept Id 157 from 127.0.0.1:1812 to 127.0.0.1:36850 length 20
+
+===============================
+
+Any other output indicates that there is a problem with the FreeRADIUS
+configuration which *must* be solved before testing RadSec. Carefully verify that
+you have carried out each of the above steps correctly and examine the debug
+output from FreeRADIUS, which will usually tell you what is wrong.
+
+See [how to read the debug
+output](http://wiki.freeradius.org/radiusd-X) for instructions on
+reading amd understanding the debug output.
+
+The next step is to xref:protocols/proxy/radsec_client.adoc[configure
+FreeRADIUS as a RadSec test client] so that we can verify that our
+RadSec server is working.
diff --git a/doc/antora/modules/howto/pages/protocols/proxy/index.adoc b/doc/antora/modules/howto/pages/protocols/proxy/index.adoc
new file mode 100644
index 0000000..5100635
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/proxy/index.adoc
@@ -0,0 +1,126 @@
+= Proxying RadSec and enabling PROXY Protocol
+
+This guide shows how to set up FreeRADIUS to serve RadSec connections, fronted
+by either HAproxy or Traefik as Layer 4 proxies that pass on the original
+client connection information using PROXY Protocol.
+
+It is not a comprehensive guide to using RadSec with FreeRADIUS. It presents a
+basic configuration that uses an example CA and does not validate certificate
+attributes or perform revokation status.
+
+
+== Introduction
+
+FreeRADIUS supports receiving RADIUS requests over TLS-enabled TCP connections
+and supports proxying of requests over TCP connections to another TLS-enabled
+homeserver. The protocol for RADIUS over TLS is called "RadSec" and is defined
+in RFC 6614.
+
+FreeRADIUS is a capable and performant application-aware ("Layer 7") proxy /
+load-balancer for RadSec and other forms of RADIUS traffic.
+
+
+=== Layer 4 proxying
+
+Rather than use an application-aware proxy it is sometimes better to reduce the
+performance impact incurred by re-encoding an application protocol by using a
+"Layer 4" proxy that operates at the level of individual connections without
+regard for the application protocol. Such a proxy is more of a "bump in the
+wire" than a request buffer and minimises the latency incurred due to proxying.
+
+It is common to see software such as HAproxy and Traefik used in Layer 4 mode
+in place of FreeRADIUS for purposes such as connection load balancing. In
+addition to improved performance, these tools have the benefit that they
+typically support dynamic service discovery and "hitless" reloads to
+automatically adapt their connection routing based on changes to backend
+services such as the introduction of new nodes with even a momentary loss of
+service.
+
+
+=== Loss of connection information
+
+When TCP connections are relayed through Layer 4 proxies the information
+about the originating source of the connection is no longer known to the
+backend service, unless it is otherwise made available. Identifying the
+originator of connections is often necessary for security purposes and for
+request processing.
+
+Whilst many application protcols support headers that allow proxies to preserve
+connection information these are not helpful in the context of Layer 4
+proxying: The process of populating headers requires knowledge of the
+application protocol to re-encode requests as they are transmitted between the
+frontend and backend connections.
+
+
+=== PROXY Protocol
+
+PROXY Protocol overcomes this limitation by allowing the original connection
+information to be provided to the backend at the start of the TCP connection.
+After this initial data is encoded the remainder of the conversation then
+proceeds as normal. However now that the connection information is known to the
+backend server it is able to process requests made on the connection as though
+the connection were being made directly by the client and not via the proxy.
+
+PROXY Protocol is specified in this document:
+http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
+
+
+== Requirements
+
+PROXY Protocol Version 1 is supported by FreeRADIUS v3.0.24 and later versions.
+
+You will require the following set of VMs or containers, each with their own
+IP address:
+
+[cols="1,1,1"]
+|===
+|Hostname|IP address|Purpose
+
+|radseccli
+|172.23.0.2
+|FreeRADIUS configured to provide a RadSec test client
+
+|radsecsvr
+|172.23.0.3
+|FreeRADIUS configured as a RadSec server
+
+|haproxy
+|172.23.0.4
+|HAproxy in Layer 4 mode to the FreeRADIUS RadSec backend
+|===
+
+Optionally you may want to configure a host to run Traefik within a Docker
+container using host mode networking, perhaps configured by Docker Compose,
+however the installation is beyond the scope of this guide:
+
+[cols="1,1,1"]
+|===
+|traefik
+|172.23.0.5
+|Traefik configured as a TCP router with TLS passthrough to the FreeRADIUS RadSec backend
+|===
+
+The hostnames and IP addresses provided above are for examples purposes and are
+used throughout the remainder of this guide. This guide provides commands and
+output for CentOS. Other distributions will have minor differences, including
+the location of the FreeRADIUS configuration (the "raddb").
+
+[NOTE]
+====
+You can choose to use your own hostname, IP addresses and OS distribution. You
+could also use official Docker images provided by the respecitive projects,
+however these prescribe methods for configuring and managing the services
+that are not typical for a normal package installation which would provide a
+distraction if used for by guide.
+====
+
+
+== Sections in this guide
+
+This guide is organised into four parts that should be read in order:
+
+1. xref:protocols/proxy/enable_radsec.adoc[Enabling RadSec]
+2. xref:protocols/proxy/radsec_client.adoc[Configuring a test RadSec client]
+3. xref:protocols/proxy/radsec_with_haproxy.adoc[Proxying RadSec with HAproxy]
+4. xref:protocols/proxy/radsec_with_traefik.adoc[Proxying RadSec with Traefik]
+5. xref:protocols/proxy/enable_proxy_protocol.adoc[Enabling PROXY Protocol for RadSec]
diff --git a/doc/antora/modules/howto/pages/protocols/proxy/radsec_client.adoc b/doc/antora/modules/howto/pages/protocols/proxy/radsec_client.adoc
new file mode 100644
index 0000000..d92345e
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/proxy/radsec_client.adoc
@@ -0,0 +1,181 @@
+== Configuring FreeRADIUS as a RadSec test client
+
+Unfortunately, the `radclient` program does not support RadSec. We
+must therefore configure an instance of FreeRADIUS as a "transport
+converter" which proxies UDP-based RADIUS requests to a RadSec
+destination of our choice.
+
+The following steps should be performed on a client system, which we
+will call `radseccli`. This system should be a new system, with a
+different IP address. That is, you shoudl not edit the configuration
+on the `radsecsvr` host. Doing so will break the RadSec configuration.
+
+Install FreeRADIUS using the NetworkRADIUS packages by following the
+instructions provided here:
+
+<https://networkradius.com/packages/>
+
+Before making any configuration changes, you should stop the radiusd
+service:
+
+[source,shell]
+----
+ service radiusd stop
+----
+
+Add a new `tls` home server definition, which will point to the RadSec
+server. We do this by creating a file
+`/etc/raddb/sites-enabled/radsec-homeserver` with the following
+contents:
+
+.Example homeserver, pool and realm definitions for the RadSec service
+====
+
+ home_server tls {
+ ipaddr = 172.23.0.3 # IP address of our RadSec server
+ port = 2083
+ type = auth+acct
+ proto = tcp
+ tls {
+ private_key_password = whatever
+ private_key_file = ${certdir}/client.pem
+ certificate_file = ${certdir}/client.pem
+ ca_file = ${cadir}/ca.pem
+ }
+ }
+ home_server_pool tls {
+ type = fail-over
+ home_server = tls
+ }
+ realm tls {
+ auth_pool = tls
+ acct_pool = tls
+ }
+
+====
+
+[TIP]
+====
+Complete descriptions of each of the above configuration items can be found in the
+`[raddb]/sites-available/tls` example configuration file. For simple tests, however,
+we can omit all of the comments from the file.
+====
+
+To use this `tls` home server, we change the `default` virtual server to proxy
+all authentication and accounting requests to it.
+
+Edit the `/etc/raddb/sites-enabled/default` file so that the beginning of
+the `authorize` and `preacct` sections looks as follows:
+
+.Example default virtual server modification to proxy requests to a RadSec proxy server
+====
+
+ authorize {
+ update control {
+ &Proxy-To-Realm := tls
+ }
+ handled
+ ...
+ }
+ ...
+ preacct {
+ update control {
+ &Proxy-To-Realm := tls
+ }
+ handled
+ ...
+ }
+
+====
+
+These changes make the `tls` virtual server always proxy packets.
+These changes are only for testing, and should never be used in
+production.
+
+We must now copy the example CA certificate as well as the client
+certificate and key files which are on the `radsecsrv` host to this
+test client.
+
+Replace the following files on `radseccli` with the equivalent files from
+`radsecsrv`:
+
+[cols="1,1,1"]
+|===
+|File|Corresponding configuration item|Purpose
+
+|/etc/raddb/certs/ca.pem
+|`ca_file`
+|CA certificate which is used to authenticate the server certificate presented by the RadSec server to the client.
+
+|/etc/raddb/certs/client.pem
+|`certificate_file`
+|Client certificate (signed by the CA certificate) that is presented by the test client to the RadSec server.
+
+|/etc/raddb/certs/client.pem
+|`private_key_file` and `private_key_password`
+|Private key corresponding to the client certificate
+|===
+
+Note that the client certificate and key are typically bundled into a single file.
+
+[CAUTION]
+====
+If you do not correctly replace the CA, client certificate, and key
+material on the test client then the RadSec client and RadSec server
+will fail to mutually authenticate each other as they do not share a
+trusted CA. If you see messages like `unknown CA`, then you know that
+the certificates have not been set up correctly.
+====
+
+Start the FreeRADIUS service in debug mode:
+
+[source,shell]
+----
+radiusd -X
+----
+
+
+=== Testing RadSec connectivity
+
+At this stage you should be able to cause the test client to send RadSec
+requests directly to the RadSec server.
+
+Run the following to send a RADUS (UDP) Access-Request to the local FreeRADIUS
+instance. It should then proxy the request over RadSec connection to
+the remote RadSec server:
+
+[source,shell]
+----
+ echo "User-Name = bob" | radclient 127.0.0.1 auth testing123
+----
+
+If the test client is able to successfully establish the RadSec
+connection, and the RadSec server replies with an Access-Accept
+response, then the output will be as follows:
+
+.Expected output from radclient
+===============================
+
+ Sent Access-Request Id 252 from 0.0.0.0:50118 to 127.0.0.1:1812 length 27
+ Received Access-Accept Id 252 from 127.0.0.1:1812 to 127.0.0.1:50118 length 39
+
+===============================
+
+Lack of response or an Access-Reject response indicates that the RadSec
+connection is not being established successfully.
+
+There may be serveral reasons for broken connectivity including:
+
+ * The client not accepting the certificate presented by the server.
+ * The server not accepting the certificate presented by the client.
+
+Look at the debug output generated by both the test client and the RadSec
+server. In many cases it will tell you exactly what the problem is.
+
+Do not proceed with any further steps until direct connections between the
+RadSec client and Radsec Server are working properly.
+
+Once things are working we are ready to
+xref:protocols/proxy/radsec_with_haproxy.adoc[configure HAproxy to proxy RadSec
+connections] or to xref:protocols/proxy/radsec_with_traefik.adoc[configure
+Traefik to proxy RadSec connections].
diff --git a/doc/antora/modules/howto/pages/protocols/proxy/radsec_with_haproxy.adoc b/doc/antora/modules/howto/pages/protocols/proxy/radsec_with_haproxy.adoc
new file mode 100644
index 0000000..e58abfe
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/proxy/radsec_with_haproxy.adoc
@@ -0,0 +1,134 @@
+== Proxying RadSec with HAproxy
+
+This section shows how to configure HAproxy to proxy RadSec connections.
+
+The following steps should be performed on the `haproxy` host, unless otherwise
+stated.
+
+Install the HAproxy package supplied with the OS distribution:
+
+[source,shell]
+----
+ yum install haproxy
+----
+
+Stop the haproxy service:
+
+[source,shell]
+----
+ service haproxy stop
+----
+
+Modify the haproxy configuration (typically `/etc/haproxy/haproxy.conf`) so
+that it includes new frontend and backend configuration for the radsec service:
+
+.Example minimal HAproxy configuration
+======================================
+
+ global
+ maxconn 100
+ defaults
+ mode tcp
+ timeout connect 10s
+ timeout client 30s
+ timeout server 30s
+ frontend radsec_fe
+ bind *:2083
+ default_backend radsec_be
+ backend radsec_be
+ balance roundrobin
+ server radsecsvr 172.23.0.3:2083
+
+======================================
+
+Note the `mode tcp` directive which tells HAproxy to act as a Layer 4
+proxy, so that it doesn't attempt to perform SSL termination or
+decode the RADIUS protocol.
+
+[NOTE]
+====
+The above example is a minimal configuration. In practise you will want to
+retain many of the HAproxy configuration items already present in the
+configuration (e.g. `log`, `chroot`, `user`, `group`), but these vary across
+distributions. Other HTTP-related options that may already exist in the
+configuration will conflict with `mode tcp` (Layer 4 proxying) and should be
+removed if HAproxy complains about them.
+
+However, you should first get things working with the minimal
+configuration which is known to work, and then make customisations.
+If you start off with a complex configuration, then there may be a
+large number of things which are broken, and debugging them all will
+be difficult. Start simple, and then add complexity!
+====
+
+Restart the haproxy service in foreground mode for debugging purposes:
+
+[source,shell]
+----
+haproxy -f /etc/haproxy/haproxy.cfg -db
+----
+
+
+=== Testing RadSec connectivity via HAproxy
+
+Now edit the test RadSec client, so that instead of making connections directly
+to the RadSec server it makes connections to the HAproxy server.
+
+On `radseccli` edit the `/etc/raddb/sites-enabled/tls` file, and set
+the IP address to the address of the `haproxy` host.
+
+.Example updated test client homeserver configuration
+=====================================================
+
+ home_server tls {
+ ipaddr = 172.23.0.4 # Updated from radsecsvr to haproxy
+ ...
+ }
+
+=====================================================
+
+Restart the debug mode session:
+
+[source,shell]
+----
+radiusd -X
+----
+
+Perform a test authentication:
+
+[source,shell]
+----
+ echo "User-Name = bob" | radclient 127.0.0.1 auth testing123
+----
+
+If the test client is able to successfully establish the RadSec
+connection via HAproxy, and the RadSec server replies with an
+Access-Accept response, then the output will be as follows:
+
+.Expected output from radclient
+===============================
+
+ Sent Access-Request Id 252 from 0.0.0.0:50118 to 127.0.0.1:1812 length 27
+ Received Access-Accept Id 252 from 127.0.0.1:1812 to 127.0.0.1:50118 length 39
+
+===============================
+
+HAproxy should also log a message that indicates that the connection was
+proxied, such as the following:
+
+.Expected output from HAproxy
+=============================
+
+ <150>...: Connect from 172.23.0.2:50087 to 172.23.0.4:2083 (radius_fr/TCP)
+
+=============================
+
+Any other output from radclient or HAproxy indicates that there is a
+problem with the HAproxy configuration, or that FreeRADIUS is not
+accepting connection from the `haproxy` host, which must be solved
+before continuing.
+
+Once proxied connections are working we are ready to
+xref:protocols/proxy/enable_proxy_protocol.adoc[enable the PROXY
+Protocol] on both HAproxy and the RadSec server.
+
diff --git a/doc/antora/modules/howto/pages/protocols/proxy/radsec_with_traefik.adoc b/doc/antora/modules/howto/pages/protocols/proxy/radsec_with_traefik.adoc
new file mode 100644
index 0000000..11030e9
--- /dev/null
+++ b/doc/antora/modules/howto/pages/protocols/proxy/radsec_with_traefik.adoc
@@ -0,0 +1,128 @@
+== Proxying RadSec with Traefik
+
+This section shows how to configure Traefik to proxy RadSec connections. You
+should skip this section if you are not using Traefik as your proxy.
+
+Installing Traefik is beyond the scope of this guide. It is typically installed
+as a service mesh router within a Docker or Kubernetes environment using
+offical Docker images.
+
+Traefik configuration has two components of interest:
+
+ * Static configuration: Defines "entrypoints" on which Traefik listens for connections.
+ * Dynamic configuration: Defines backend service components and the routing policy.
+
+Traefik supports a number of providers of dynamic configuration data for the
+router and service definitions. For demonstration purposes the files provider
+is used here, however you can switch to another provide once you have things
+working using this method.
+
+The static configuration can be provided by starting Traefik with the following
+arguments:
+
+.Example Traefik static configuration
+=====================================
+
+ traefik \
+ --log.level=DEBUG \
+ --providers.file.filename=/etc/traefik/dynamic_config.yml
+ --providers.file.watch=true
+ --entryPoints.radsec.address=:2083
+
+=====================================
+
+Note that a `radsec` entrypoint is defined to listen on port 2083 and that a
+static `file` provider is used to defined the dynamic services.
+
+The backend for RadSec should be defined in this file as follows:
+
+.Example Traefik dynamic configuration
+======================================
+
+ tcp:
+ routers:
+ radsec-router:
+ entryPoints:
+ - radsec
+ rule: "HostSNI(`*`)"
+ service: "radsec-service"
+ tls:
+ passthrough: true
+ services:
+ radsec-service:
+ loadBalancer:
+ servers:
+ - address: "172.23.0.3:2083"
+
+======================================
+
+Note the `passthrough: true` directive under `tls:` which tells Treafik not to
+attempt TLS termination which it would otherwise perform for all incoming TLS
+connections. We require that the connection is passed through from the RadSec
+client to the RadSec server without being reterminated since the end client's
+certificate is authenticated by the RadSec server and many be used for
+policy decisions.
+
+
+=== Testing RadSec connectivity via Traefik
+
+Now amend the test RadSec client so that instead of making connections directly
+to the RadSec server it makes them via Traefik.
+
+On `radseccli` amend `/etc/raddb/sites-enabled/tls` and set the IP address to
+that of the `traefik` host.
+
+.Example updated test client homeserver configuration
+=====================================================
+
+ home_server tls {
+ ipaddr = 172.23.0.5 # Updated from radsecsvr to traefik
+ ...
+ }
+
+=====================================================
+
+Restart the debug mode session:
+
+[source,shell]
+----
+radiusd -X
+----
+
+Perform a test authentication:
+
+[source,shell]
+----
+ echo "User-Name = bob" | radclient 127.0.0.1 auth testing123
+----
+
+If the test client is able to successfully establish the RadSec connection via
+Traefik and the RadSec server replies with an Access-Accept response then the
+output will be as follows:
+
+.Example output from radclient
+==============================
+
+ Sent Access-Request Id 252 from 0.0.0.0:50118 to 127.0.0.1:1812 length 27
+ Received Access-Accept Id 252 from 127.0.0.1:1812 to 127.0.0.1:50118 length 39
+
+==============================
+
+Traefik should also log a message that indicates that the connection was
+proxied, such as the following:
+
+.Example output from Traefik
+============================
+
+ time="..." level=debug msg="Handling connection from 172.23.0.2:57367"
+
+============================
+
+Any other output from radclient or Traefik indicates that there is a problem
+with the Traefik configuration or that FreeRADIUS is not accepting connection
+from the `traefik` host, which must be solved before continuing.
+
+Once proxied connections are working we are ready to
+xref:protocols/proxy/enable_proxy_protocol.adoc[enable the PROXY Protocol] on
+both Traefik and the RadSec server.
+
diff --git a/doc/antora/modules/installation/nav.adoc b/doc/antora/modules/installation/nav.adoc
new file mode 100644
index 0000000..26ce32e
--- /dev/null
+++ b/doc/antora/modules/installation/nav.adoc
@@ -0,0 +1,5 @@
+* xref:index.adoc[Installing and upgrading]
+** xref:packages.adoc[Install from packages]
+** xref:dependencies.adoc[Dependencies]
+** xref:source.adoc[Build from source]
+** xref:upgrade.adoc[Upgrading to v3]
diff --git a/doc/antora/modules/installation/pages/dependencies.adoc b/doc/antora/modules/installation/pages/dependencies.adoc
new file mode 100644
index 0000000..e910e76
--- /dev/null
+++ b/doc/antora/modules/installation/pages/dependencies.adoc
@@ -0,0 +1,58 @@
+= FreeRADIUS Dependencies
+
+Some external dependencies must be installed before building or
+running FreeRADIUS. The core depends on two mandatory libraries:
+`libtalloc` for memory management and `libkqueue` for event
+handling.
+
+Many of the modules also have optional dependencies. For example,
+the LDAP module requires LDAP client libraries to be installed
+and database modules need their respective database client
+libraries.
+
+If building from source code, the configure stage will check for
+the optional dependencies. Any missing libraries will cause that
+particular module to be skipped.
+
+== Libraries
+
+=== libtalloc
+
+Talloc is a memory allocation library available at
+https://talloc.samba.org/talloc/doc/html/index.html
+
+*OSX*
+
+`# brew install talloc`
+
+*Debian, Ubuntu and `dpkg`-based systems*
+
+`# apt-get install libtalloc-dev`
+
+*RedHat or CentOS*
+
+```
+# subscription-manager repos --enable rhel-7-server-optional-rpms
+# yum install libtalloc-dev
+```
+
+=== kqueue
+
+Kqueue is an event / timer API originally written for BSD systems.
+It is _much_ simpler to use than third-party event libraries. A
+library, `libkqueue`, is available for Linux systems.
+
+*OSX*
+
+_kqueue is already available, there is nothing to install._
+
+*Debian, Ubuntu and `dpkg`-based systems*
+
+`# apt-get install libkqueue-dev`
+
+*RedHat or CentOS*
+
+```
+# subscription-manager repos --enable rhel-7-server-optional-rpms
+# yum install libkqueue-dev
+```
diff --git a/doc/antora/modules/installation/pages/index.adoc b/doc/antora/modules/installation/pages/index.adoc
new file mode 100644
index 0000000..b810078
--- /dev/null
+++ b/doc/antora/modules/installation/pages/index.adoc
@@ -0,0 +1,15 @@
+== Installation
+
+FreeRADIUS is available from multiple sources:
+
+* Official xref:packages.adoc[Network RADIUS packages]
+* xref:source.adoc[Source code]
+* Many Operating System distributions
+
+We highly recommend using the official packages from Network
+RADIUS, where available.
+
+The documents in this section cover details of the above
+installation methods, as well as instructions on building
+packages locally.
+
diff --git a/doc/antora/modules/installation/pages/packages.adoc b/doc/antora/modules/installation/pages/packages.adoc
new file mode 100644
index 0000000..ffc52cd
--- /dev/null
+++ b/doc/antora/modules/installation/pages/packages.adoc
@@ -0,0 +1,22 @@
+== Install from packages
+
+Network RADIUS provide pre-built binary packages of FreeRADIUS for
+common Linux distributions. This is the recommended installation
+method when packages are available for your system.
+
+The official http://packages.networkradius.com[Network RADIUS
+packages] page contains recent FreeRADIUS packages and
+installation instructions.
+
+=== Distribution-supplied packages
+
+While many Operating System distributions ship FreeRADIUS
+packages, the versions they include are often years out of date.
+As well as missing out on the latest bug fixes and features, this
+also means that it is very hard to know if an issue encountered is
+still a problem or if it is fixed in the latest release.
+
+Therefore, whilst the distribution-supplied packages can often be
+the most convenient to install, we do not usually recommend using
+them.
+
diff --git a/doc/antora/modules/installation/pages/source.adoc b/doc/antora/modules/installation/pages/source.adoc
new file mode 100644
index 0000000..cf40a79
--- /dev/null
+++ b/doc/antora/modules/installation/pages/source.adoc
@@ -0,0 +1,199 @@
+== Building from Source
+
+We recommend xref:packages.adoc[installing from packages] if
+possible. Full instructions on building and installing from source
+code follow.
+
+The mandatory xref:installation:dependencies.adoc[dependencies]
+must be installed before FreeRADIUS can be built. These dependencies
+are `libtalloc` and `libkqueue`, which FreeRADIUS uses for memory
+management, and platform-independent event handling.
+
+Per-module dependencies that enable support for external services
+such as LDAP, SQL, etc, are optional. They must be installed for
+any modules that are to be used. The FreeRADIUS `./configure` step
+will automatically detect if each module has its dependencies met
+and automatically enable support for them. If the features you
+require are not enabled you should inspect the configure script
+output to figure out which additional development libraries need
+to be installed.
+
+The FreeRADIUS source may be obtained from a number of locations:
+
+* Download the latest version of the FreeRADIUS source from
+ https://www.freeradius.org/releases/[the FreeRADIUS web site]; or
+* download directly from the
+ ftp://ftp.freeradius.org/pub/freeradius/[FreeRADIUS FTP site]; or
+* download from
+ https://github.com/FreeRADIUS/freeradius-server/[GitHub].
+
+The file wil be name something like: `freeradius-server-3.0.22.tar.gz`.
+Later versions will be `3.0.23`, or `4.0.0`, etc. PGP signatures are
+also provided for official releases from the FTP site; these are
+named e.g. `freeradius-server-3.0.22.tar.gz.sig`.
+
+Un-tar the file, and change to the FreeRADIUS directory (where
+`VERSION` below is the version of the server that you have
+downloaded).
+
+[source,shell]
+----
+tar -zxf freeradius-server-VERSION.tar.gz
+cd freeradius-server-VERSION
+----
+
+Take the following steps to build and install the server from source:
+
+[source,shell]
+----
+./configure
+make
+sudo make install
+----
+
+=== Custom build
+
+FreeRADIUS has GNU autoconf support. This means you have to run
+`./configure`, and then run `make`. To see which configuration
+options are supported, run `./configure --help`, and read its output.
+
+The `make install` stage will install the binaries, the "man" pages,
+and _may_ install the configuration files. If you have not installed a
+RADIUS server before, then the configuration files for FreeRADIUS will
+be installed.
+
+If you already have a RADIUS server installed, then *FreeRADIUS
+WILL NOT over-write your current configuration.*
+
+The `make install` process will warn you about the files it could not
+install.
+
+If you see a warning message about files that could not be
+installed, then you *must* ensure that the new server is using the
+new configuration files and not the old configuration files, as
+this may cause undesired behavior and failure to operate correctly.
+
+The initial output from running in debugging mode (`radiusd -X`)
+will tell you which configuration files are being used. See
+xref:installation:upgrade.adoc[Upgrading] for information about
+upgrading from older versions. There _may_ be changes in the
+dictionary files which are required for a new version of the
+software. These files will not be installed over your current
+configuration, so you *must* verify and install any problem files by
+hand, for example using `diff(1)` to check for changes.
+
+When installing from source, it is _extremely_ helpful to read the
+output of `./configure`, `make`, and `make install`. If a
+particular module you expected to be installed was not installed,
+then the output will tell you why that module was not installed.
+The most likely reason is that required libraries (including their
+development header files) are not available.
+
+Please do _not_ post questions to the FreeRADIUS users list
+without first carefully reading the output of this process as it
+often contains the information needed to resolve a problem.
+
+== Upgrading To A New Minor Release
+
+The installation process will not over-write your existing configuration
+files. It will, however, warn you about the files it did not install.
+These will require manual integration with the existing files.
+
+It is not possible to re-use configurations between different major
+versions of the server.
+
+For details on what has changed between the version, see the
+xref:installation:upgrade.adoc[upgrade] guide.
+
+We _strongly_ recommend that new major versions be installed in a
+different location than any existing installations. Any local policies
+can then be migrated gradually to the configuration format of the new
+major version. The number of differences in the new configuration mean
+that is is both simpler and safer to migrate your configurations rather
+than to try and just get the old configuration to work.
+
+== Running the server
+
+If the server builds and installs, but doesn’t run correctly, then
+you should first use debugging mode (`radiusd -X`) to figure out
+the problem.
+
+This is your best hope for understanding the problem. Read _all_
+of the messages which are printed to the screen, the answer to
+your problem will often be in a warning or error message.
+
+We really cannot emphasize that last sentence enough. Configuring
+a RADIUS server for complex local authentication isn’t a trivial
+task. Your _best_ and _only_ method for debugging it is to read
+the debug messages, where the server will tell you exactly what
+it’s doing, and why. You should then compare its behaviour to what
+you intended, and edit the configuration files as appropriate.
+
+If you don’t use debugging mode, and ask questions on the mailing
+list, then the responses will all tell you to use debugging mode.
+The server prints out a lot of information in this mode, including
+suggestions for fixes to common problems. Look especially for
+`WARNING` and `ERROR` messages in the output, and read the related
+messages.
+
+Since the main developers of FreeRADIUS use debugging mode to
+track down their configuration problems with the server, it’s a
+good idea for you to use it, too. If you don’t, there is little
+hope for you to solve any configuration problem related to the
+server.
+
+To start the server in debugging mode, do:
+
+[source,shell]
+----
+radiusd -X
+----
+
+You should see a lot of text printed on the screen as it starts up. If
+you don’t, or if you see error messages, please read the FAQ:
+
+https://wiki.freeradius.org/guide/FAQ
+
+If the server says `Ready to process requests.`, then it is running
+properly. From another shell (or another window), type
+
+[source,shell]
+----
+radtest test test localhost 0 testing123
+----
+
+You should see the server print out more messages as it receives the
+request, and responds to it. The `radtest` program should receive the
+response within a few seconds. It doesn’t matter if the authentication
+request is accepted or rejected, what matters is that the server
+received the request, and responded to it.
+
+You can now edit the configuration files for your local system. You will
+usually want to start with `sites-enabled/default` for main
+configurations. To set which NASes (clients) can communicate with this
+server, edit `raddb/clients.conf`. Please read the configuration files
+carefully, as many configuration options are only documented in comments
+in the file.
+
+Note that is is _highly_ recommended that you use some sort of version
+control system to manage your configuration, such as git or Subversion.
+You should then make small changes to the configuration, checking in and
+testing as you go. When a config change causes the server to stop
+working, you will be able to easily step back and find out what update
+broke the configuration.
+
+It is also considered a best practice to maintain a staging or
+development environment. This allows you to test and integrate your
+changes without impacting your active production environment. You should
+make the appropirate investment in order to properly support a critical
+resource such as your authentication servers.
+
+Configuring and running the server MAY be complicated. Many modules have
+`man` pages. See `man rlm_pap`, or `man rlm_*` for information.
+Please read the documentation in the doc/ directory. The comments in the
+configuration files also contain a lot of documentation.
+
+If you have any additional issues, the FAQ is also a good place to
+start.
+
+https://wiki.freeradius.org/guide/FAQ
diff --git a/doc/antora/modules/installation/pages/upgrade.adoc b/doc/antora/modules/installation/pages/upgrade.adoc
new file mode 100644
index 0000000..67874c8
--- /dev/null
+++ b/doc/antora/modules/installation/pages/upgrade.adoc
@@ -0,0 +1,737 @@
+= Upgrading from v2 to v3
+
+The configuration for 3.0 is *largely* compatible with the 2.x.x
+configuration. However, it is NOT possible to simply use the 2.x.x
+configuration as-is. Instead, you should re-create it.
+
+== Security
+
+A number of configuration items have moved into the "security" subsection of
+radiusd.conf. If you use these, you should move them. Otherwise, they can
+be ignored.
+
+The list of moved options is:
+
+* chroot
+* user
+* group
+* allow_core_dumps
+* reject_delay
+* status_server
+
+These entries should be moved from "radiusd.conf" to the "security"
+subsection of that file.
+
+== Naming
+
+Many names used by configuration items were inconsistent in earlier
+versions of the server. These names have been unified in version 3.0.
+
+If a file is being referenced or created the config item `filename`
+is used.
+
+If a file is being created, the initial permissions are set by the
+`permissions` config item.
+
+If a directory hierarchy needs to be created, the permissions are set
+by `dir_permissions`.
+
+If an external host is referenced in the context of a module the
+`server` config item is used.
+
+Unless the config item is a well recognised portmanteau
+(as `filename` is for example), it must be written as multiple
+distinct words separated by underscores `_`.
+
+The configuration items `file`, `script_file`, `module`,
+`detail`, `detailfile`, `attrsfile`, `perm`, `dirperm`,
+`detailperm`, and `hostname` are deprecated. As well as any false
+portmanteaus, and configuration items that used hyphens as word
+delimiters. e.g. `foo-bar` has been changed to `foo_bar`. Please
+update your module configuration to use the new syntax.
+
+In most cases the server will tell you the replacement config item to
+use. As always, run the server in debugging mode to see these
+messages.
+
+== Modules Directory
+
+As of version 3.0, the `modules/` directory no longer exists.
+
+Instead, all "example" modules have been put into the
+`mods-available/` directory. Modules which can be loaded by the
+server are placed in the `mods-enabled/` directory. All of the
+modules in that directory will be loaded. This means that the
+`instantiate` section of radiusd.conf is less important. The only
+reason to list a module in the `instantiate` section is to force
+ordering when the modules are loaded.
+
+Modules can be enabled by creating a soft link. For module `foo`, do:
+
+[source,shell]
+----
+cd raddb/mods-enabled
+ln -s ../mods-available/foo
+----
+
+To create "local" versions of the modules, we suggest copying the file
+instead. This leaves the original file (with documentation) in the
+`mods-available/` directory. Local changes should go into the
+`mods-enabled/` directory.
+
+Module-specific configuration files are now in the `mods-config/`
+directory. This change allows for better organization, and means that
+there are fewer files in the main `raddb` directory. See
+`mods-config/README.rst` for more details.
+
+== Changed Modules
+
+The following modules have been changed in this version.
+
+=== rlm_sql
+
+The SQL configuration has been moved from `sql.conf` to
+`mods-available/sql`. The `sqlippool.conf` file has also been
+moved to `mods-available/sqlippool`.
+
+The SQL module configuration has been changed. The old connection
+pool options are no longer accepted:
+
+----
+num_sql_socks
+connect_failure_retry_delay
+lifetime
+max_queries
+----
+
+Instead, a connection pool configuration is used. This configuration
+contains all of the functionality of the previous configuration, but
+in a more generic form. It also is used in multiple modules, meaning
+that there are fewer different configuration items. The mapping
+between the configuration items is:
+
+----
+num_sql_socks -> pool { max }
+connect_failure_retry_delay -> pool { retry_delay }
+lifetime -> pool { lifetime }
+max_queries -> pool { uses }
+----
+
+The pool configuration adds a number of new configuration options,
+which allow the administrator to better control how FreeRADIUS uses
+SQL connection pools.
+
+The following parameters have been changed:
+
+----
+trace -> removed
+tracefile -> logfile
+----
+
+The logfile is intended to log SQL queries performed. If you need to
+debug the server, use debugging mode. If `logfile` is set, then
+*all* SQL queries will go to `logfile`.
+
+You can now use a NULL SQL database:
+
+.Example
+----
+driver = rlm_sql_null
+----
+
+This is an empty driver which will always return "success". It is
+intended to be used to replace the `sql_log` module, and to work in
+conjunction with the `radsqlrelay` program. Simply take your normal
+configuration for raddb/mods-enabled/sql, and set:
+
+.Example
+----
+driver = rlm_sql_null
+...
+logfile = ${radacctdir}/sql.log
+----
+
+All of the SQL queries will be logged to that file. The connection
+pool does not need to be configured for the `null` SQL driver. It
+can be left as-is, or deleted from the SQL configuration file.
+
+
+=== rlm_sql_sybase
+
+The `rlm_sql_sybase` module has been renamed to `rlm_sql_freetds`
+and the old `rlm_sql_freetds` module has been removed.
+
+`rlm_sql_sybase` used the newer ct-lib API, and `rlm_sql_freetds`
+used an older API and was incomplete.
+
+The new `rlm_sql_freetds` module now also supports database
+selection on connection startup so `use` statements no longer
+have to be included in queries.
+
+
+=== sql/dialup.conf
+
+Queries for post-auth and accounting calls have been re-arranged. The
+SQL module will now expand the 'reference' configuration item in the
+appropriate sub-section, and resolve this to a configuration
+item. This behaviour is similar to rlm_linelog. This dynamic
+expansion allows for a dynamic mapping between accounting types and
+SQL queries. Previously, the mapping was fixed. Any "new" accounting
+type was ignored by the module. Now, support for any accounting type
+can be added by just adding a new target, as below.
+
+Queries from v2.x.x may be manually copied to the new v3.0
+`dialup.conf` file (`raddb/sql/main/<dialect>/queries.conf`).
+When doing this you may also need to update references to the
+accounting tables, as their definitions will now be outside of
+the subsection containing the query.
+
+The mapping from old "fixed" query to new "dynamic" query is as follows:
+
+----
+accounting_onoff_query -> accounting.type.accounting-on.query
+accounting_update_query -> accounting.type.interim-update.query
+accounting_update_query_alt +> accounting.type.interim-update.query
+accounting_start_query -> accounting.type.start.query
+accounting_start_query_alt +> accounting.type.start.query
+accounting_stop_query -> accounting.type.stop.query
+accounting_stop_query_alt +> accounting.type.stop.query
+postauth_query -> post-auth.query
+----
+
+Alternatively a 2.x.x config may be patched to work with the
+3.0 module by adding the following:
+
+.Example
+[source,unlang]
+----
+ accounting {
+ reference = "%{tolower:type.%{Acct-Status-Type}.query}"
+ type {
+ accounting-on {
+ query = "${....accounting_onoff_query}"
+ }
+ accounting-off {
+ query = "${....accounting_onoff_query}"
+ }
+ start {
+ query = "${....accounting_start_query}"
+ query = "${....accounting_start_query_alt}"
+ }
+ interim-update {
+ query = "${....accounting_update_query}"
+ query = "${....accounting_update_query_alt}"
+ }
+ stop {
+ query = "${....accounting_stop_query}"
+ query = "${....accounting_stop_query_alt}"
+ }
+ }
+ }
+ post-auth {
+ query = "${..postauth_query}"
+ }
+----
+
+In general, it is safer to migrate the configuration rather than
+trying to "patch" it, to make it look like a v2 configuration.
+
+Note that the sub-sections holding the queries are labelled
+`accounting-on`, and not `accounting_on`. The reason is that the
+names of these sections are taken directly from the
+`Accounting-Request` packet, and the `Acct-Status-Type` field.
+The `sql` module looks at the value of that field, and then looks
+for a section of that name, in order to find the query to use.
+
+That process means that the server can be extended to support any new
+value of `Acct-Status-Type`, simply by adding a named sub-section,
+and a query. This behavior is preferable to that of v2, which had
+hard-coded queries for certain `Acct-Status-Type` values, and was
+ignored all other values.
+
+=== rlm_ldap
+
+The LDAP module configuration has been substantially changed. Please
+read `raddb/mods-available/ldap`. It now uses a connection pool,
+just like the SQL module.
+
+Many of the configuration items remain the same, but they have been
+moved into subsections. This change is largely cosmetic, but it makes
+the configuration clearer. Instead of having a large set of random
+configuration items, they are now organized into logical groups.
+
+You will need to read your old LDAP configuration, and migrate it
+manually to the new configuration. Simply copying the old
+configuration WILL NOT WORK.
+
+Users upgrading from 2.x.x who used to call the ldap module in
+`post-auth` should now set `edir_autz = yes`, and remove the `ldap`
+module from the `post-auth` section.
+
+=== rlm_ldap and LDAP-Group
+
+In 2.x.x the registration of the `LDAP-Group` pair comparison was done
+by the last instance of rlm_ldap to be instantiated. In 3.0 this has
+changed so that only the default `ldap {}` instance registers
+`LDAP-Group`.
+
+If `<instance>-LDAP-Group` is already used throughout your configuration
+no changes will be needed.
+
+=== rlm_ldap authentication
+
+In 2.x.x the LDAP module had a `set_auth_type` configuration item,
+which forced `Auth-Type := ldap`. This was removed in 3.x.x as it
+often did not work, and was not consistent with the rest of the
+server. We generally recommend that LDAP should be used as a
+database, and that FreeRADIUS should do authentication.
+
+The only reason to use `Auth-Type := ldap` is when the LDAP server
+will not supply the "known good" password to FreeRADIUS, *and* where
+the Access-Request contains User-Password. This situation happens
+only for Active Directory. If you think you need to force `Auth-Type
+:= ldap` in other situations, you are very likely to be wrong.
+
+The following is an example of what should be inserted into the
+`authorize {}` and `authenticate {}` sections of the relevant
+virtual-servers, to get functionality equivalent to v2.x:
+
+.Example
+[source,unlang]
+----
+authorize {
+ ...
+ ldap
+ if ((ok || updated) && User-Password) {
+ update control {
+ Auth-Type := ldap
+ }
+...
+}
+authenticate {
+ ...
+ Auth-Type ldap {
+ ldap
+ }
+...
+}
+----
+
+=== rlm_eap
+
+The EAP configuration has been moved from `eap.conf` to
+`mods-available/eap`. A new `pwd` subsection has been added for
+EAP-PWD.
+
+
+=== rlm_expiration & rlm_logintime
+
+The rlm_expiration and rlm_logintime modules no longer add a `Reply-Message`,
+the same behaviour can be achieved checking the return code of the module and
+adding the `Reply-Message` with unlang:
+
+.Example
+[source,unlang]
+----
+expiration
+if (userlock) {
+ update reply {
+ Reply-Message := "Your account has expired"
+ }
+}
+----
+
+
+=== rlm_unix
+
+The `unix` module does not have an `authenticate` section. So you
+cannot set `Auth-Type := System`. The `unix` module has also been
+deleted from the examples in `sites-available/`. Listing it there
+has been deprecated for many years.
+
+The PAP module can do crypt authentication. It should be used instead
+of Unix authentication.
+
+The Unix module still can pull the passwords from `/etc/passwd`, or
+`/etc/shadow`. This is done by listing it in the `authorize`
+section, as is done in the examples in `sites-available/`. However,
+some systems using NIS or NSS will not supply passwords to the
+`unix` module. For those systems, we recommend putting users and
+passwords into a database, instead of relying on `/etc/passwd`.
+
+
+=== rlm_preprocess
+
+In 2.x.x `huntroups` and `users` files were loaded from default locations
+without being configured explicitly. Since 3.x.x you need to set
+`huntgroups` and `users` configuration item(s) in module section in order
+to get them being processed.
+
+
+== New Modules
+
+=== rlm_date
+
+Instances of rlm_date register an xlat method which can translate
+integer and date values to an arbitrarily formatted date time
+string, or an arbitrarily formated time string to an integer,
+depending on the attribute type passed.
+
+
+=== rlm_rest
+
+The `rest` module is used to translate RADIUS requests into
+RESTfull HTTP requests. Currently supported body types are JSON
+and POST.
+
+
+=== rlm_unpack
+
+The `unpack` module is used to turn data buried inside of binary
+attributes. e.g. if we have `Class = 0x00000001020304` then:
+
+.Example
+[source,unlang]
+----
+Tmp-Integer-0 := "%{unpack:&Class 4 short}"
+----
+
+will unpack octets 4 and 5 as a "short", which has value 0x0304.
+All integers are assumed to be in network byte order.
+
+
+=== rlm_yubikey
+
+The `yubikey` module can be used to forward yubikey OTP token
+values to a Yubico validation server, or decrypt the token
+using a PSK.
+
+
+== Deleted Modules
+
+The following modules have been deleted, and are no longer supported
+in Version 3. If you are using one of these modules, your
+configuration can probably be changed to not need it. Otherwise email
+the freeradius-devel list, and ask about the module.
+
+
+=== rlm_acct_unique
+
+This module has been replaced by the "acct_unique" policy. See
+raddb/policy.d/accounting.
+
+The method for calculating the value of acct_unique has changed.
+However, as this method was configurable, this change should not
+matter. The only issue is in having a v2 and v3 server writing to the
+same database at the same time. They will calculate different values
+for Acct-Unique-Id.
+
+
+=== rlm_acctlog
+
+You should use rlm_linelog instead. That module has a superset of the
+acctlog functionality.
+
+
+=== rlm_attr_rewrite
+
+The attr_rewrite module looked for an attribute, and then re-wrote it,
+or created a new attribute. All of that can be done in "unlang".
+
+A sample configuration in "unlang" is:
+
+.Example
+[source,unlang]
+----
+if (request:Calling-Station-Id) {
+ update request {
+ Calling-Station-Id := "...."
+ }
+}
+----
+
+We suggest updating all uses of attr_rewrite to use unlang instead.
+
+
+=== rlm_checkval
+
+The checkval module compared two attributes. All of that can be done in "unlang":
+
+.Example
+[source,unlang]
+----
+if (&request:Calling-Station-Id == &control:Calling-Station-Id) {
+ ok
+}
+----
+
+We suggest updating all uses of checkval to use unlang instead.
+
+
+=== rlm_dbm
+
+No one seems to use it. There is no sample configuration for it.
+There is no speed advantage to using it over the "files" module.
+Modern systems are fast enough that 10K entries can be read from the
+"users" file in about 10ms. If you need more users than that, use a
+real database such as SQL.
+
+
+=== rlm_fastusers
+
+No one seems to use it. It has been deprecated since Version 2.0.0.
+The "files" module was rewritten so that the "fastusers" module was no
+longer necessary.
+
+
+=== rlm_policy
+
+No one seems to use it. Almost all of its functionality is available
+via `unlang`.
+
+
+=== rlm_sim_files
+
+The rlm_sim_files module has been deleted. It was never marked "stable",
+and was never used in a production environment. There are better ways
+to test EAP.
+
+If you want similar functionality, see rlm_passwd. It can read CSV
+files, and create attributes from them.
+
+
+=== rlm_sql_log
+
+This has been replaced with the "null" sql driver. See
+`raddb/mods-available/sql` for an example configuration.
+
+The main SQL module has more functionality than rlm_sql_log, and
+results in less code in the server.
+
+== Other Functionality
+
+The following is a list of new / changed functionality.
+
+=== RadSec
+
+RadSec (or RADIUS over TLS) is now supported. RADIUS over bare TCP
+is also supported, but is recommended only for secure networks.
+
+See `sites-available/tls` for complete details on using TLS. The server
+can both receive incoming TLS connections, and also originate outgoing
+TLS connections.
+
+The TLS configuration is taken from the old EAP-TLS configuration. It
+is largely identical to the old EAP-TLS configuration, so it should be
+simple to use and configure. It re-uses much of the EAP-TLS code,
+so it is well-tested and reliable.
+
+Once RadSec is enabled, normal debugging mode will not work. This is
+because the TLS code requires threading to work properly. Instead of doing:
+
+.Example
+[source,shell]
+----
+radiusd -X
+----
+
+you will need to do:
+
+.Example
+[source,shell]
+----
+radiusd -fxx -l stdout
+----
+
+That's the price to pay for using RadSec. This limitation may be
+lifted in a future version of the server.
+
+
+=== PAP and User-Password
+
+From version 3.0 onwards the server no longer supports authenticating
+against a cleartext password in the 'User-Password' attribute. Any
+occurences of this (for instance, in the users file) should now be changed
+to 'Cleartext-Password' instead.
+
+e.g. change entries like this:
+
+----
+bob User-Password == "hello"
+----
+
+to ones like this:
+
+----
+bob Cleartext-Password := "hello"
+----
+
+If this is not done, authentication will likely fail. The server will
+also print a helpful message in debugging mode.
+
+If it really is impossible to do this, the following unlang inserted above
+the call to the pap module may be used to copy User-Password to the correct
+attribute:
+
+.Example
+[source,unlang]
+----
+if (!control:Cleartext-Password && control:User-Password) {
+ update control {
+ Cleartext-Password := "%{control:User-Password}"
+ }
+}
+----
+
+However, this should only be seen as a temporary, not permanent, fix.
+It is better to fix your databases to use the correct configuration.
+
+
+== Unlang
+
+
+The unlang policy language is compatible with v2, but has a number of
+new features. See `man unlang` for complete documentation.
+
+
+=== Errors
+
+Many more errors are caught when the server is starting up. Syntax
+errors in `unlang` are caught, and a helpful error message is
+printed. The error message points to the exact place where the error
+occurred:
+
+----
+ ./raddb/sites-enabled/default[230]: Parse error in condition
+ ERROR: if (User-Name ! "bob") {
+ ERROR: ^ Invalid operator
+----
+
+`update` sections are more generic. Instead of doing `update
+reply`, you can do the following:
+
+.Example
+[source,unlang]
+----
+update {
+ reply:Class := 0x0000
+ control:Cleartext-Password := "hello"
+}
+----
+
+This change means that you need fewer `update` sections.
+
+
+=== Comparisons
+
+Attribute comparisons can be done via the `&` operator. When you
+needed to compare two attributes, the old comparison style was:
+
+.Example
+[source,unlang]
+----
+if (User-Name == "%{control:Tmp-String-0}") {
+----
+
+This syntax is inefficient, as the `Tmp-String-0` attribute would be
+printed to an intermediate string, causing unnecessary work. You can
+now instead compare the two attributes directly:
+
+.Example
+[source,unlang]
+----
+if (&User-Name == &control:Tmp-String-0) {
+----
+
+See `man unlang` for more details.
+
+=== Casts
+
+Casts are now permitted. This allows you to force type-specific
+comparisons:
+
+.Example
+[source,unlang]
+----
+if (<ipaddr>"%{sql: SELECT...}" == 127.0.0.1) {
+----
+
+This forces the string returned by the SELECT to be treated as an IP
+address, and compare to `127.0.0.1`. Previously, the comparison
+would have been done as a simple string comparison.
+
+
+=== Networks
+
+IP networks are now supported:
+
+ if (127.0.0.1/32 == 127.0.0.1) {
+
+Will be `true`. The various comparison operators can be used to
+check IP network membership::
+
+.Example
+[source,unlang]
+----
+if (127/8 > 127.0.0.1) {
+----
+
+Returns `true`, because `127.0.0.1` is within the `127/8`
+network. However, the following comparison will return `false`::
+
+.Example
+[source,unlang]
+----
+if (127/8 > 192.168.0.1) {
+----
+
+because `192.168.0.1` is outside of the `127/8` network.
+
+
+=== Optimization
+
+As `unlang` is now pre-compiled, many compile-time optimizations are
+done. This means that the debug output may not be exactly the same as
+what is in the configuration files:
+
+ if (0 && (User-Name == "bob')) {
+
+The result will always be `false`, as the `if 0` prevents the
+following `&& ...` from being evaluated.
+
+Not only that, but the entire contents of that section will be ignored
+entirely:
+
+.Example
+[source,unlang]
+----
+if (0) {
+ this_module_does_not_exist
+ and_this_one_does_not_exist_either
+}
+----
+
+In v2, that configuration would result in a parse error, as there is
+no module called `this_module_does_not_exist`. In v3, that text is
+ignored. This ability allows you to have dynamic configurations where
+certain parts are used (or not) depending on compile-time configuration.
+
+Similarly, conditions which always evaluate to `true` will be
+optimized away:
+
+
+.Example
+[source,unlang]
+----
+if (1) {
+ files
+}
+----
+
+That configuration will never show the `if (1)` output in debugging mode.
+
+=== Dialup_admin
+
+The dialup_admin directory has been removed. No one stepped forward
+to maintain it, and the code had not been changed in many years.
+
diff --git a/doc/antora/modules/unlang/.gitignore b/doc/antora/modules/unlang/.gitignore
new file mode 100644
index 0000000..c5722d7
--- /dev/null
+++ b/doc/antora/modules/unlang/.gitignore
@@ -0,0 +1 @@
+!*.adoc
diff --git a/doc/antora/modules/unlang/nav.adoc b/doc/antora/modules/unlang/nav.adoc
new file mode 100644
index 0000000..77be328
--- /dev/null
+++ b/doc/antora/modules/unlang/nav.adoc
@@ -0,0 +1,51 @@
+* xref:index.adoc[Unlang Policy Language]
+
+** xref:list.adoc[Attribute Lists]
+** xref:attr.adoc[Attribute References]
+** xref:return_codes.adoc[Return Codes]
+
+** xref:keywords.adoc[Keywords]
+*** xref:break.adoc[break]
+*** xref:case.adoc[case]
+*** xref:else.adoc[else]
+*** xref:elsif.adoc[elsif]
+*** xref:foreach.adoc[foreach]
+*** xref:group.adoc[group]
+*** xref:if.adoc[if]
+*** xref:load-balance.adoc[load-balance]
+*** xref:redundant-load-balance.adoc[redundant-load-balance]
+*** xref:redundant.adoc[redundant]
+*** xref:return.adoc[return]
+*** xref:switch.adoc[switch]
+*** xref:update.adoc[update]
+
+** xref:module.adoc[Modules]
+*** xref:module_method.adoc[Module Methods]
+*** xref:module_builtin.adoc[Built-in Modules]
+
+** xref:type/index.adoc[Data Types]
+*** xref:type/index.adoc[List of Data Types]
+*** xref:type/ip.adoc[IP Addresses]
+*** xref:type/numb.adoc[Numbers]
+*** xref:type/string/single.adoc[Single Quoted Strings]
+*** xref:type/string/double.adoc[Double Quoted Strings]
+*** xref:type/string/backticks.adoc[Backtick-quoted string]
+*** xref:type/string/unquoted.adoc[Unquoted Strings]
+
+** xref:condition/index.adoc[Conditional Expressions]
+*** xref:condition/cmp.adoc[Comparisons]
+*** xref:condition/operands.adoc[Operands]
+*** xref:condition/return_code.adoc[The Return Code Operator]
+*** xref:condition/eq.adoc[The '==' Operator]
+*** xref:condition/and.adoc[The '&&' Operator]
+*** xref:condition/or.adoc[The '||' Operator]
+*** xref:condition/not.adoc[The '!' Operator]
+*** xref:condition/para.adoc[The '( )' Operator]
+*** xref:condition/regex.adoc[Regular Expressions]
+
+** xref:xlat/index.adoc[String Expansion]
+*** xref:xlat/alternation.adoc[Alternation Syntax]
+*** xref:xlat/builtin.adoc[Built-in Expansions]
+*** xref:xlat/character.adoc[Single Letter Expansions]
+*** xref:xlat/attribute.adoc[Attribute References]
+*** xref:xlat/module.adoc[Module References]
diff --git a/doc/antora/modules/unlang/pages/attr.adoc b/doc/antora/modules/unlang/pages/attr.adoc
new file mode 100644
index 0000000..70afce4
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/attr.adoc
@@ -0,0 +1,77 @@
+= &Attribute References
+
+.Syntax
+[source,unlang]
+----
+[&]Attribute-Name
+----
+
+The `&Attribute-Name` operator returns a reference to the named
+attribute.
+
+When used as an existence check in a condition, the condition
+evaluates to `true` if the attribute exists. Otherwise, the condition
+evaluates to `false`.
+
+When used elsewhere, such as in xref:switch.adoc[switch], it returns
+the value of the named attribute.
+
+.Examples
+[source,unlang]
+----
+&User-Name
+&NAS-IP-Address
+----
+
+== Lists
+
+The attribute reference can also be qualified with a
+xref:list.adoc[list] reference. When no list is given, the server
+looks in the input packet list for the named attribute.
+
+.Examples
+
+[source,unlang]
+----
+&request:User-Name
+&reply:NAS-IP-Address
+----
+
+== Array References
+
+.Syntax
+[source,unlang]
+----
+&Attribute-Name[<integer>]
+----
+
+When an attribute appears multiple times in a list, this syntax allows
+you to address the attributes as if they were array entries. The
+`<integer>` value defines which attribute to address. The `[0]` value
+refers to the first attributes, `[1]` refers to the second attribute,
+etc.
+
+.Examples
+[source,unlang]
+----
+&EAP-Message[1]
+&reply:NAS-IP-Address[2]
+----
+
+== Removing ambuguity from the configuration files
+
+The server does not use the `&` character to distinguish attribute names
+from other strings.
+
+Without the `&`, it is more difficult to parse the configuration file
+clearly. You could interpret a string as `hello-there`
+either as a literal string "hello-there", or as a reference to an
+attribute named `hello-there`.
+
+Adding the leading `&` character means that attribute references are
+now easily distinguishable from literal strings. The use of the leading
+`&` character is highly recommended.
+
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/break.adoc b/doc/antora/modules/unlang/pages/break.adoc
new file mode 100644
index 0000000..01783ea
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/break.adoc
@@ -0,0 +1,28 @@
+= The break statement
+
+.Syntax
+[source,unlang]
+----
+break
+----
+
+The `break` statement is used to exit an enclosing
+xref:foreach.adoc[foreach] loop. The `break` statement only be used
+inside of a xref:foreach.adoc[foreach] loop.
+
+.Example
+[source,unlang]
+----
+foreach &Class {
+ if (&Foreach-Variable-0 == 0xabcdef) {
+ break
+ }
+
+ update reply {
+ Reply-Message += "Contains %{Foreach-Variable-0}"
+ }
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/case.adoc b/doc/antora/modules/unlang/pages/case.adoc
new file mode 100644
index 0000000..ba2b5fe
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/case.adoc
@@ -0,0 +1,44 @@
+= The case Statement
+
+.Syntax
+[source,unlang]
+----
+case [ <match> ] {
+ [ statements ]
+}
+----
+
+The `case` statement is used to match data inside of a
+xref:switch.adoc[switch] statement. The `case` statement cannot be used
+outside of a xref:switch.adoc[switch] statement.
+
+
+The `<match>` text can be an attribute reference such as `&User-Name`,
+or it can be a xref:type/string/index.adoc[string]. If the match
+text is a dynamically expanded string, then the match is performed on
+the output of the string expansion.
+
+If no `<match>` text is given, it means that the `case` statement is
+the "default" and will match all which is not matched by another
+`case` statement inside of the same xref:switch.adoc[switch].
+
+.Example
+[source,unlang]
+----
+switch &User-Name {
+ case "bob" {
+ reject
+ }
+
+ case &Filter-Id {
+ reject
+ }
+
+ case {
+ ok
+ }
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/condition/and.adoc b/doc/antora/modules/unlang/pages/condition/and.adoc
new file mode 100644
index 0000000..50b3deb
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/condition/and.adoc
@@ -0,0 +1,21 @@
+= The && Operator
+
+.Syntax
+[source,unlang]
+----
+(condition-1 && condition-2)
+----
+
+The `&&` operator performs a short-circuit "and" evaluation of the
+two conditions. This operator evaluates _condition-1_ and returns
+`false` if _condition-1_ returns `false`. Only if _condition-1_
+returns `true` is _condition-2_ evaluated and its result returned.
+
+.Examples
+[source,unlang]
+----
+if (&User-Name && &EAP-Message) { ...
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/condition/cmp.adoc b/doc/antora/modules/unlang/pages/condition/cmp.adoc
new file mode 100644
index 0000000..4138b86
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/condition/cmp.adoc
@@ -0,0 +1,104 @@
+= Comparisons
+
+.Syntax
+[source,unlang]
+----
+lhs OP rhs
+----
+
+The most common use-case for conditions is to perform comparisons.
+The `lhs` and `rhs` of a conditional comparison can be
+xref:attr.adoc[&Attribute-Name] or xref:type/index.adoc[data]. The
+the `OP` is an operator, commonly `==` or `\<=`. It is used to
+control how the two other portions of the condition are compared.
+
+== The Comparison Operators
+
+The comparison operators are given below.
+
+[options="header"]
+|=====
+| Operator | Description
+| < | less than
+| \<= | less than or equals
+| == | equals
+| != | not equals
+| >= | greater than or equals
+| > | greater than
+| xref:condition/regex.adoc[=~] | regular expression matches
+| xref:condition/regex.adoc[!~] | regular expression does not match
+|=====
+
+The comparison operators perform _type-specific_ comparisons. The
+only exceptions are the xref:condition/regex.adoc[regular expression] operators,
+which interpret the `lhs` as a printable string, and the `rhs` as a
+regular expression.
+
+== IP Address Comparisons
+
+The type-specific comparisons operate as expected for most data types.
+The only exception is data types that are IP addresses or IP address
+prefixes. For those data types, the comparisons are done via the
+following rules:
+
+* Any unqualified IP address is assumed to have a /32 prefix (IPv4)
+ or a /128 prefix (IPv6).
+
+* If the prefixes of the left and right sides are equal, then the comparisons
+ are performed on the IP address portion.
+
+* If the prefixes of the left and right sides are not equal, then the
+ comparisons are performed as _set membership checks_.
+
+The syntax allows conditions such as `192.0.2.1 < 192.0.2/24`. This
+condition will return `true`, as the IP address `192.0.2.1' is within
+the network `192.0.2/24`.
+
+== Casting
+
+In some situations, it is useful to force the left side to be
+interpreted as a particular data type.
+
+[NOTE]
+The data types used by the cast *must* be a type taken from the RADIUS
+dictionaries, e.g., `ipaddr`, `integer`, etc. These types are not the
+same as the xref:type/index.adoc[data types] used in the
+configuration files.
+
+.Syntax
+[source,unlang]
+----
+<cast>lhs OP rhs
+----
+
+The `cast` text can be any one of the standard RADIUS dictionary data
+types, as with the following example:
+
+.Example
+[source,unlang]
+----
+<ipaddr>&Class == 127.0.0.1
+----
+
+In this example, the `Class` attribute is treated as if it was an IPv4
+address and is compared to the address `127.0.0.1`
+
+Casting is most useful when the left side of a comparison is a
+dynamically expanded string. The cast ensures that the comparison is
+done in a type-safe manner, instead of performing a string comparison.
+
+.Example
+[source,unlang]
+----
+<integer>`/bin/echo 00` == 0
+----
+
+In this example, the string output of the `echo` program is interpreted as an
+integer. It is then compared to the right side via integer
+comparisons. Since the integer `00` is equivalent to the integer `0`,
+the comparison will match. If the comparison had been performed via
+string equality checks, then the comparison would fail, because the
+strings `00` and `0` are different.
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/condition/eq.adoc b/doc/antora/modules/unlang/pages/condition/eq.adoc
new file mode 100644
index 0000000..d9e51f3
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/condition/eq.adoc
@@ -0,0 +1,30 @@
+= The == Operator
+
+.Syntax
+`(data-1 == data-2)`
+
+The `==` operator compares the result of evaluating `data-1` and
+`data-2`. As discussed in xref:type/index.adoc[Data Types], the `data-1`
+field may be interpreted as a reference to an attribute.
+
+The `data-2` field is interpreted in a type-specific manner. For
+example, if `data-1` refers to an attribute of type `ipaddr`, then
+`data-2` is evaluated as an IP address. If `data-1` refers to an
+attribute of type `integer`, then `data-2` is evaluated as an integer
+or as a named enumeration defined by a `VALUE` statement in a
+dictionary. Similarly, if `data-1` refers to an attribute of type
+`date`, then `data-2` will be interpreted as a date string.
+
+If the resulting data evaluates to be the same, then the operator
+returns `true`; otherwise, it returns `false`.
+
+.Example
+[source,unlang]
+----
+if (User-Name == "bob") {
+ ...
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/condition/index.adoc b/doc/antora/modules/unlang/pages/condition/index.adoc
new file mode 100644
index 0000000..b9d9d5f
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/condition/index.adoc
@@ -0,0 +1,85 @@
+= Conditional Expressions
+
+Conditions are evaluated when parsing xref:if.adoc[if] and
+xref:elsif.adoc[elsif] statements. These conditions allow the server to
+make complex decisions based on one of a number of possible criteria.
+
+.Syntax
+[source,unlang]
+----
+if ( condition ) { ...
+
+elsif ( condition ) { ...
+----
+
+Conditions are expressed using the following syntax:
+
+[options="header"]
+|=====
+| Syntax | Description
+| xref:attr.adoc[&Attribute-Name] | Check for attribute existence.
+| xref:condition/return_code.adoc[rcode] | Check return code of a previous module.
+| xref:condition/operands.adoc[data] | Check value of data.
+| xref:condition/cmp.adoc[lhs OP rhs] | Compare two kinds of data.
+| xref:condition/para.adoc[( condition )] | Check sub-condition
+| xref:condition/not.adoc[! condition] | Negate a conditional check
+| xref:condition/and.adoc[( condition ) && ...] | Check a condition AND the next one
+| xref:condition/or.adoc[( condition ) \|\| ...] | Check a condition OR the next one
+|=====
+
+
+.Examples
+[source,unlang]
+----
+if ( &User-Name == "bob" ) {
+ ...
+}
+
+if ( &Framed-IP-Address == 127.0.0.1 ) {
+ ...
+}
+
+if ( &Calling-Station-Id == "%{sql:SELECT ...}" ) {
+ ...
+}
+----
+
+== Load-time Syntax Checks
+
+The server performs a number of checks when it loads the configuration
+files. Unlike version 2, all of the conditions are syntax checked
+when the server loads. This checking greatly aids in creating
+configurations that are correct. Where the configuration is
+incorrect, a descriptive error is produced.
+
+This error contains the filename and line number of the syntax error.
+In addition, it will print out a portion of the line that caused the
+error and will point to the exact character where the error was seen.
+These descriptive messages mean that most errors are easy to find and fix.
+
+== Load-time Optimizations
+
+The server performs a number of optimizations when it loads the
+configuration files. Conditions that have static values are
+evaluated and replaced with the result of the conditional comparison.
+
+.Example
+[source,unlang]
+----
+if ( 0 == 1 ) {
+ ...
+}
+----
+
+The condition `0 == 1` is static and will evaluate to `false`. Since
+it evaluates to `false`, the configuration inside of the `if`
+statement is ignored. Any modules referenced inside of the `if`
+statement will not be loaded.
+
+This optimization is most useful for creating configurations that
+selectively load (or not) certain policies. If the condition above
+was used in version 2, then the configuration inside of the `if` statement
+would be loaded, even though it would never be used.
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/condition/not.adoc b/doc/antora/modules/unlang/pages/condition/not.adoc
new file mode 100644
index 0000000..bde038e
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/condition/not.adoc
@@ -0,0 +1,19 @@
+= The ! Operator
+
+.Syntax
+[source,unlang]
+----
+! condition
+----
+
+The `!` operator negates the result of the following condition. It
+returns `true` when _condition_ returns `false`. It returns `false`
+when _condition_ returns `true`.
+
+.Examples
+
+`(! (foo == bar))` +
+`! &User-Name`
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/condition/operands.adoc b/doc/antora/modules/unlang/pages/condition/operands.adoc
new file mode 100644
index 0000000..4a2d00b
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/condition/operands.adoc
@@ -0,0 +1,37 @@
+= Operands
+
+.Syntax
+[source,unlang]
+----
+string
+integer
+"double-quoted string"
+'single-quoted string'
+`back-quoted string`
+----
+
+Any text not matching xref:attr.adoc[&Attribute-Name] or
+xref:condition/return_code.adoc[Return Code] is interpreted as a value for a
+particular xref:type/index.adoc[data type].
+
+Double-quoted strings and back-quoted strings are dynamically expanded
+before the condition is evaluated. Single-quoted strings are static
+literals and are not dynamically expanded.
+
+When used as an existence check, the condition evaluates to `true` if
+the data is non-zero. Otherwise, the condition evaluates to `false`.
+
+For integer existence checks, `0` is `false`; all other values are `true`.
+
+For string existence checks, an empty string is `false`. All other
+strings are `true`.
+
+All other data types are disallowed in existence checks.
+
+.Examples
+
+`"hello there"` +
+`5`
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/condition/or.adoc b/doc/antora/modules/unlang/pages/condition/or.adoc
new file mode 100644
index 0000000..80c2cb4
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/condition/or.adoc
@@ -0,0 +1,21 @@
+= The || Operator
+
+.Syntax
+[source,unlang]
+----
+(expression-1 || expression-2)
+----
+
+The `||` operator performs a short-circuit "or" evaluation of the two
+expressions. This operator evaluates _condition-1_ and returns `true`
+if _condition-1_ returns true. Only if _condition-1_ returns `false`
+is _condition-2_ evaluated and its result returned.
+
+.Examples
+[source,unlang]
+----
+if (&User-Name || &NAS-IP-Address) { ...
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/condition/para.adoc b/doc/antora/modules/unlang/pages/condition/para.adoc
new file mode 100644
index 0000000..bdb3f01
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/condition/para.adoc
@@ -0,0 +1,19 @@
+= The ( ) Operator
+
+.Syntax
+[source,unlang]
+----
+( condition )
+----
+
+The `( )` operator returns the result of evaluating the given
+`condition`. It is used to clarify policies or to explicitly define
+conditional precedence.
+
+.Examples
+
+`(foo)` +
+`(bar || (baz && dub))`
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/condition/regex.adoc b/doc/antora/modules/unlang/pages/condition/regex.adoc
new file mode 100644
index 0000000..038faa6
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/condition/regex.adoc
@@ -0,0 +1,180 @@
+= Regular Expressions
+
+.Syntax
+====
+[source,unlang]
+----
+(<subject> =~ /<pattern>/)
+(<subject> =~ /<pattern>/[imsux])
+
+(<subject> !~ /<pattern>/)
+(<subject> !~ /<pattern>/[imsux])
+----
+====
+
+== Matching
+The regular expression operators perform regular expression matching
+on the data. The `<subject>` field can be an attribute reference or data,
+as with the other xref:condition/cmp.adoc[comparison] operators. The `/<pattern>/`
+field must be a valid regular expression.
+
+The `=~` operator evaluates to `true` when `data` matches the
+`/<pattern>/`. Otherwise, it evaluates to `false`.
+
+The `!~` operator evaluates to `true` when `data` does not match the
+`/<pattern>/`. Otherwise, it evaluates to `true`.
+
+The regular expression comparison is performed on the _string representation_
+of the left side of the comparison. That is, if the left side is an
+xref:type/numb.adoc[integer], the regular expression will behave is if the
+value `0` was the literal string `"0"`. Similarly, if the left side is an
+xref:attr.adoc[&Attribute-Name], then the regular expression will behave is if
+the attribute was printed to a string, and the match was performed on the
+resulting string.
+
+.Checking if the `User-Name` attribute contains a realm of example.com
+====
+[source,unlang]
+----
+if (&User-Name =~ /@example\.com$/) {
+ ...
+}
+----
+====
+
+== Dialects
+
+The syntax of the regular expression is defined by the regular
+expression library available on the local system.
+
+FreeRADIUS currently supports:
+
+* link:https://www.pcre.org/original/doc/html/[libpcre] and
+link:https://www.pcre.org/current/doc/html/[libpcre2] both of which
+provide
+link:https://en.wikipedia.org/wiki/Perl_Compatible_Regular_Expressions[Perl
+Compatible Regular expressions].
+* Regex support provided by the local libc implementation, usually
+link:http://en.wikipedia.org/wiki/Regular_expression#POSIX_basic_and_extended[
+Posix regular expressions].
+
+[TIP]
+====
+Use the output of `radiusd -Xxv` to determine which regular expression library is in use.
+
+....
+...
+Debug : regex-pcre : no
+Debug : regex-pcre2 : yes
+Debug : regex-posix : no
+Debug : regex-posix-extended : no
+Debug : regex-binsafe : yes
+...
+Debug : pcre2 : 10.33 (2019-04-16) - retrieved at build time
+....
+====
+
+[WARNING]
+====
+Depending on the regular expression library or libc implementation the server
+was built against, the pattern matching function available may not be binary
+safe (see `regex-binsafe` in the output of `radiusd -Xxv`).
+
+If a binary safe regex match function is not available, and a match is
+attempted against a subject that contains one or more `NUL` ('\0') bytes, the
+match will be aborted, any condition that uses the result will evaluate to false,
+and a warning will be emitted.
+====
+
+== Flags
+
+The regular expression `/<pattern>/` may be followed by one or more flag
+characters. Again, which flags are available depends on the regular expression
+library the server was built with. Multiple flags may be specified per
+`/pattern/`.
+
+.Flags and their uses
+
+[options="header"]
+|=====
+| Flag Character | Available with | Effect
+| `i` | All | Enable case-insensitive matching.
+| `m` | All | '^' and '$' match newlines within the subject.
+| `s` | libpcre[2] | '.' matches anything, including newlines.
+| `u` | libpcre[2] | Treats subjects as UTF8. Invalid UTF8
+ sequences will result in the match failing.
+ |`x` | libpcre[2] | Allows comments in expressions by ignoring
+ whitespace, and text between '#' and the next
+ newline character.
+|=====
+
+== Subcapture groups
+
+When the `=~` or `!~` operators are used, then parentheses in the regular
+expression will sub capture groups, which contain part of the subject string.
+
+The special expansion `%{0}` expands to the portion of the subject that
+matched. The expansions +
+`%{1}`..`%{32}` expand to the contents of any subcapture groups.
+
+When using libpcre[2], named capture groups may also be accessed using the
+built-in expansion +
+`%{regex:<named capture group>}`.
+
+Please see the xref:xlat/builtin.adoc#_0_32[xlat documentation] for
+more information on regular expression matching.
+
+.Extracting the 'user' portion of a realm qualified string
+====
+[source,unlang]
+----
+if (&User-Name =~ /^(.*)@example\.com$/) {
+ update reply {
+ Reply-Message := "Hello %{1}"
+ }
+}
+----
+====
+
+== Pre-Compiled vs Runtime Compiled
+
+When the server starts any regular expressions comparisons it finds will be
+pre-compiled, and if support is available, JIT'd (converted to machine code)
+to ensure fast execution.
+
+If a pattern contains a xref:xlat/index.adoc[string expansion], the pattern
+cannot be compiled on startup, and will be compiled at runtime each time the
+expression is evaluated. The server will also turn off JITing for runtime
+compiled expressions, as the overhead is greater than the time that would be
+saved during evaluation.
+
+.A runtime compiled regular expression
+====
+[source,unlang]
+----
+if (&User-Name =~ /^@%{Tmp-String-0}$/) {
+ ...
+}
+----
+====
+
+To ensure optimal performance you should limit the number of patterns
+containing xref:xlat/index.adoc[string expansions], and if using PCRE, combine
+multiple expressions operating on the same subject into a single expression
+using the PCRE alternation '|' operator.
+
+.Using multiple string expansions and the PCRE alternation operator
+====
+[source,unlang]
+----
+if (&User-Name =~ /^@(%{Tmp-String-0}|%{Tmp-String-1})$/) {
+ ...
+}
+----
+====
+
+
+// Licenced under CC-by-NC 4.0.
+// Copyright (C) 2020 Network RADIUS SAS.
+// Copyright (C) 2019 Arran Cudbard-Bell <a.cudbardb@freeradius.org>
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/condition/return_codes.adoc b/doc/antora/modules/unlang/pages/condition/return_codes.adoc
new file mode 100644
index 0000000..ebc49ed
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/condition/return_codes.adoc
@@ -0,0 +1,35 @@
+= The return code Operator
+
+.Syntax
+[source,unlang]
+----
+rcode
+----
+
+The Unlang interpreter tracks the return code of any module, string expansion
+or keyword that has been called.
+
+This return code can be checked in any condition. If the saved return code
+matches the `code` given here, then the condition evaluates to `true`.
+Otherwise, it evaluates to `false`.
+
+rcodes cannot be set in a condition. rcodes cannot be compared with anything else.
+
+The list of valid return codes is as follows:
+
+.Return Codes
+
+include::partial$rcode_table.adoc[]
+
+.Examples
+
+[source,unlang]
+----
+sql
+if (notfound) {
+ ...
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/default.adoc b/doc/antora/modules/unlang/pages/default.adoc
new file mode 100644
index 0000000..3b298f6
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/default.adoc
@@ -0,0 +1,47 @@
+= The case Statement
+
+.Syntax
+[source,unlang]
+----
+case [ <match> ] {
+ [ statements ]
+}
+----
+
+The `case` statement is used to match data inside of a
+xref:switch.adoc[switch] statement. The `case` statement cannot be used
+outside of a xref:switch.adoc[switch] statement.
+
+
+The `<match>` text can be an attribute reference such as `&User-Name`,
+or it can be a xref:type/string/index.adoc[string]. If the match
+text is a dynamically expanded string, then the match is performed on
+the output of the string expansion.
+
+The keyword `default` can be used to specify the default action to
+take inside of a xref:switch.adoc[switch] statement.
+
+If no `<match>` text is given, it means that the `case` statement is
+the "default" and will match all which is not matched by another
+`case` statement inside of the same xref:switch.adoc[switch].
+
+.Example
+[source,unlang]
+----
+switch &User-Name {
+ case "bob" {
+ reject
+ }
+
+ case &Filter-Id {
+ reject
+ }
+
+ default {
+ ok
+ }
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/else.adoc b/doc/antora/modules/unlang/pages/else.adoc
new file mode 100644
index 0000000..a795d0e
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/else.adoc
@@ -0,0 +1,30 @@
+= The else Statement
+
+.Syntax
+[source,unlang]
+----
+if (condition) {
+ [ statements ]
+}
+else {
+ [ statements ]
+}
+----
+
+An xref:if.adoc[if] statement can have an `else` clause. If _condition_
+evaluates to `false`, the statements in the xref:if.adoc[if] subsection are skipped
+and the statements within the `else` subsection are executed.
+
+.Example
+[source,unlang]
+----
+if (&User-Name == "bob") {
+ reject
+}
+else {
+ ok
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/elsif.adoc b/doc/antora/modules/unlang/pages/elsif.adoc
new file mode 100644
index 0000000..ff5799c
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/elsif.adoc
@@ -0,0 +1,43 @@
+= The elsif Statement
+
+.Syntax
+[source,unlang]
+----
+if (condition-1) {
+ [ statements-1 ]
+}
+elsif (condition-2) {
+ [ statements-2 ]
+}
+else {
+ [ statements-3 ]
+}
+----
+
+An `elsif` statement is used to evaluate a subsequent
+xref:condition/index.adoc[condition] after a preceding xref:if.adoc[if] statement
+evaluates to `false`. In the example above, when _condition-1_
+evaluates to false, then _statements-1_ are skipped and _condition-2_
+is checked. When _condition-2_ evaluates true, then _statements-2_
+are executed. When _condition-2_ evaluates false, then
+_statements-2_ are skipped and _statements-3_ are executed.
+
+As with xref:if.adoc[if], an `elsif` clause does not need to be followed by
+an xref:else.adoc[else] statement. However, any xref:else.adoc[else] statement
+must be the last statement in an `elsif` chain. An arbitrary number of
+`elsif` statements can be chained together to create a series of
+conditional checks and statements.
+
+.Example
+[source,unlang]
+----
+if (&User-Name == "bob") {
+ reject
+}
+elsif (&User-Name == "doug") {
+ ok
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/foreach.adoc b/doc/antora/modules/unlang/pages/foreach.adoc
new file mode 100644
index 0000000..6ed3ddf
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/foreach.adoc
@@ -0,0 +1,40 @@
+= The foreach Statement
+
+.Syntax
+[source,unlang]
+----
+foreach <attribute-reference> {
+ [ statements ]
+}
+----
+
+The `foreach` statement loops over a set of attributes as given by
+`<attribute-reference>`. The loop can be exited early by using the
+xref:break.adoc[break] keyword.
+
+<attribute-reference>::
+
+The xref:attr.adoc[attribute reference] which will will be looped
+over. The reference can be to one attribute, to an array, a child, or
+be a subset.
+
+Inside of the `foreach` block, the attribute that is being looped over
+can be referenced as `Foreach-Variable-0`, through
+`Foreach-Variable-9`. The last digit is the depth of the loop,
+starting at "0". The loops can be nested up to eight (8) deep, though
+this is not recommended.
+
+The attributes being looped over cannot be modified or deleted.
+
+.Example
+[source,unlang]
+----
+foreach &Class {
+ update reply {
+ Reply-Message += "Contains %{Foreach-Variable-0}"
+ }
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/group.adoc b/doc/antora/modules/unlang/pages/group.adoc
new file mode 100644
index 0000000..98801fd
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/group.adoc
@@ -0,0 +1,39 @@
+= The group Statement
+
+.Syntax
+[source,unlang]
+----
+group {
+ [ statements ]
+}
+----
+
+The `group` statement collects a series of statements into a list.
+The default processing sections of the server (`authorize`,
+`accounting`, etc.) are also `group` statements. Those sections are
+given different name for management reasons, but they behave
+internally exactly like a `group`.
+
+[ statements ]:: The `unlang` commands which will be executed.
+
+All of the statements inside of the `group` are executed in sequence.
+The `group` statement is not normally used, as the statements within
+it can just be placed inside of the enclosing section. However, the
+`group` statement is included in the `unlang` syntax for completeness.
+
+.Examples
+
+[source,unlang]
+----
+group {
+ sql
+ ldap
+ file
+ if (updated) {
+ ...
+ }
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/if.adoc b/doc/antora/modules/unlang/pages/if.adoc
new file mode 100644
index 0000000..ea549ef
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/if.adoc
@@ -0,0 +1,29 @@
+= The if Statement
+
+.Syntax
+[source,unlang]
+----
+if (condition) {
+ [ statements ]
+}
+----
+
+.Description
+The `if` statement evaluates a xref:condition/index.adoc[condition]. When the
+_condition_ evaluates to `true`, the statements within the subsection
+are executed. When the _condition_ evaluates to `false`, those
+statements are skipped.
+
+An `if` statement can optionally be followed by an xref:else.adoc[else] or
+an xref:elsif.adoc[elsif] statement.
+
+.Example
+[source,unlang]
+----
+if (&User-Name == "bob") {
+ reject
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/index.adoc b/doc/antora/modules/unlang/pages/index.adoc
new file mode 100644
index 0000000..fc812f8
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/index.adoc
@@ -0,0 +1,162 @@
+= Unlang Policy Language
+
+The server supports a simple processing language called "Unlang",
+which is short for "un-language". The original intention of using an
+"un-language" was to avoid creating yet another programming language.
+Instead, the `unlang` syntax allows for simple _if / then / else_
+checks, and attribute editing. It does not support recursion,
+subroutines, or more complex flow controls.
+
+Where more complicated functionality is required, we recommend using
+the `lua`, `perl` or `python` modules. Those modules allow the insertion of
+full-featured scripts at any point in the packet processing.
+
+NOTE: The documentation is this directory is _reference_
+documentation. That is, it describes the syntax of `unlang` keywords,
+using minimal examples. The reference documentation does not,
+however, describe _when_ to use the keywords, or _how_ to create
+policies. Please see the xref:howto:index.adoc[howto] directory for
+more in-depth "how to" guides.
+
+The documentation is organized so that each item is on its own page.
+The page beings with a description of the item, followed by some text
+explaining what the item does. The page concludes with one or more
+examples of using the item in `unlang` policies.
+
+The `unlang` processing can be split into some high-level concepts.
+
+== Keywords
+
+xref:keywords.adoc[Keywords], which are the basic statements of the
+language, e.g., xref:load-balance.adoc[load-balance],
+xref:if.adoc[if], xref:else.adoc[else], etc.
+
+.Example
+[source,unlang]
+----
+load-balance {
+ sql1
+ sql2
+ sql3
+}
+----
+
+== Conditional Expressions
+
+xref:condition/index.adoc[Conditional expressions], which are used to check
+if conditions evaluate to _true_ or _false_. Conditional expressions
+can be used to control the flow of processing.
+
+.Example
+[source,unlang]
+----
+if ((&User-Name == "bob") && (&Calling-Station-Id == "00:01:03:04:05")) {
+ ...
+}
+----
+
+== Update Statements
+
+xref:update.adoc[update] statements are used to edit attributes and
+lists of attributes.
+
+Most request packets will result in reply packets that contain
+additional information for the requestor. The `update` section allows
+policies to add attributes to requests, replies, or to other places.
+
+.Example
+[source,unlang]
+----
+update reply {
+ &Session-Timeout := 3600
+ &Framed-IP-Address := 192.0.2.4
+}
+----
+
+== String Expansions
+
+xref:xlat/index.adoc[String expansion] Using `%{...}` to perform dynamic
+string expansions. (also known as xref:xlat/index.adoc[xlat])
+
+String expansions are usually performed in order to get additional
+information which is not immediately available to the policy. This
+information can be taken from almost any source, including other
+attributes, databases, and scripts.
+
+.Example
+[source,unlang]
+----
+update reply {
+ &Framed-IP-Address := "%{sql:SELECT static_ip from table WHERE user = '%{User-Name}'}"
+}
+----
+
+== Data Types
+
+Each attribute used by the server has an associated
+xref:type/index.adoc[data type]. The `unlang` interpreter enforces
+restrictions on assignments, so that only valid data types can be
+assigned to an attribute. Invalid assignments result in a run-time
+error.
+
+.Example
+[source,unlang]
+----
+update reply {
+ &Framed-IP-Address := 192.0.2.4
+ &Session-Timeout := 5
+ &Reply-Message := "hello"
+}
+----
+
+== Design Goals of Unlang
+
+The goal of `unlang` is to allow simple policies to be written with
+minimal effort. Conditional checks can be performed by the policies,
+which can then update the request or response attributes based on the
+results of those checks. `unlang` can only be used in a processing
+section, it cannot be used anywhere else, including in configuration
+sections for a client or a module. The reason for this limitation is
+that the language is intended to perform specific actions on requests
+and responses. The client and module sections contain definitions for
+a client or module; they do not define how a request is processed.
+
+`unlang` uses the same the basic syntax as the configuration files.
+The syntax of the configuration file for lines, comments, sections,
+sub-section, etc., all apply to `unlang`.
+
+Where `unlang` differs from the basic configuration file format is in
+complexity and operation. The normal configuration files are
+_declarative_ and they are _static_. That is, they declare variables
+and values for those variables. Those values do not change when the
+server is running.
+
+In contrast, `unlang` performs run-time processing of requests.
+Conditional statements such as xref:if.adoc[if] are evaluated for every
+packet that is received. Attribute editing statements such as
+xref:update.adoc[update] can be used to edit attribute contents or lists
+of attributes.
+
+.Example
+[source,unlang]
+----
+# First, the keyword 'if'
+
+# followed by condition which checks that the User-Name
+# attribute has value "bob"
+
+if (&User-Name == "bob") {
+ # keyword "update"
+
+ # followed by instructions to add the Reply-Message
+ # attribute to the "reply" list, with contents
+ # "Hello, bob"
+
+ update reply {
+ Reply-Message := "Hello, bob"
+ }
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/keywords.adoc b/doc/antora/modules/unlang/pages/keywords.adoc
new file mode 100644
index 0000000..e6411ea
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/keywords.adoc
@@ -0,0 +1,78 @@
+= Keywords
+
+The following tables list the keywords used in `Unlang`. These keywords
+implement the "flow control" of the policies.
+
+== Flow Control Keywords
+
+The _flow control_ keywords allow _if / then / else_ checks, simple
+looping, etc.
+
+.Flow Control
+[options="header"]
+[cols="30%,70%"]
+|=====
+| Keyword | Description
+| xref:break.adoc[break] | Exit early from a `foreach` loop.
+| xref:case.adoc[case] | Match inside of a `switch`.
+| xref:else.adoc[else] | Do something when an `if` does not match.
+| xref:elsif.adoc[elsif] | Check for condition when a previous `if` does not match.
+| xref:foreach.adoc[foreach] | Loop over a list of attributes.
+| xref:if.adoc[if] | Check for a condition, and execute a sub-policy if it matches.
+| xref:return.adoc[return] | Immediately stop processing a section.
+| xref:switch.adoc[switch] | Check for multiple values.
+|=====
+
+== Attribute Editing Keywords
+
+The _attribute editing_ keywords allow policies to add, delete, and
+modify attributes in any list or packet.
+
+.Attribute Editing
+[options="header"]
+[cols="30%,70%"]
+|=====
+| Keyword | Description
+| xref:update.adoc[update] | Add or filter attributes to a list
+|=====
+
+== Grouping Keywords
+
+The _grouping_ keywords allow policies to be organized into groups,
+including load-balancing.
+
+.Grouping
+[options="header"]
+[cols="30%,70%"]
+|=====
+| Keyword | Description
+| xref:group.adoc[group] | Group a series of statements.
+| xref:load-balance.adoc[load-balance] | Define a load balancing group without fail-over.
+| xref:redundant.adoc[redundant] | Define a redundant group with fail-over.
+| xref:redundant-load-balance.adoc[redundant-load-balance] | Define a redundant group with fail-over and load balancing.
+|=====
+
+== Module Keywords
+
+The _module_ keywords refer pre-packaged libraries that implement
+specific functionality, such as connecting to SQL, LDAP, etc. The
+name used here is not the literal string `module`. Instead, it is the
+name of an instance of a pre-packaged module such as `sql`, or `ldap`, or
+`eap`.
+
+The documentation below describes how to reference modules. That is,
+how to use `sql`, etc. in the policies. Please see
+`raddb/mods-available/` for configuration examples for each module.
+
+.Modules
+[options="header"]
+[cols="30%,70%"]
+|=====
+| Keyword | Description
+| xref:module.adoc[<module>] | Execute a named module, e.g., `sql`.
+| xref:module_method.adoc[<module>.<method>] | Execute a particular method of a named module, e.g., `pap.authorize`
+| xref:module_builtin.adoc[reject] | Built-in modules, e.g., `reject`, or `ok`, etc.
+|=====
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/list.adoc b/doc/antora/modules/unlang/pages/list.adoc
new file mode 100644
index 0000000..a55a54f
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/list.adoc
@@ -0,0 +1,72 @@
+= Attribute Lists
+
+An attribute list contains a set of attributes. The allowed lists
+are:
+
+`request`:: Attributes in the incoming request packet.
+
+`reply`:: Attributes in the outgoing reply packet.
+
+`control`:: Attributes in the internal "control" list that is
+associated with the request.
++
+The `control` attributes are used to manage how the request is
+processed. These attributes are never sent in any packet.
+
+`session-state`:: Attributes which are maintained across multi-packet
+exchanges.
+
+`proxy-request`:: Attributes in the proxied request packet to a home server.
+
+`proxy-reply`:: Attributes in the reply packet from the home server.
+
+`coa`:: Attributes in a CoA-Request packet which is sent to a home server.
+
+`disconnect`:: Attributes in a Disconnect-Request packet which is sent to a home server.
+
+There must be a colon `:` after the list name and before the attribute name.
+This syntax helps the server to distinguish between list names and attribute
+names.
+
+With the exception of `session-state`, all of the above lists are
+ephemeral. That is, they exist for one packet exchange, and only one
+packet exchange. When a reply is sent for a request, the above lists
+and all attributes are deleted. There is no way to reference an
+attribute from a previous packet. We recommend using a database to
+track complex state.
+
+The `coa` and `disconnect` lists can only be used when the server
+receives an Access-Request or Accounting-Request. Use `request` and
+`reply` instead of `coa` when the server receives a CoA-Request or
+Disconnect-Request packet.
+
+Adding one or more attributes to either of the `coa` or `disconnect`
+list causes server to originate a CoA-Request or Disconnect-Request
+packet. That packet is sent when the current Access-Request or
+Accounting-Request has been finished, and a reply sent to the NAS.
+See `raddb/sites-available/originate-coa` for additional information.
+
+In some cases, requests are associated a multi-packet exchange. For
+those situations, the `session-state` list is automatically saved when
+a reply is sent, and it is automatically restored when the next packet
+in sequence comes in. Once the packet exchange has been finished, the
+`session-state` list is deleted.
+
+In some cases, there is a parent-child relationship between requests.
+In those situations, it is possible for the policy rules in the child
+to refer to attributes in the parent. This reference can be made by
+prefixing the _<list>_ name with the `parent` qualifier. The key word
+`outer` is also a synonym for `parent`. If there are multiple
+parent-child relationships, the `parent` qualifier can be repeated.
+
+There is, however, no way for the parent to refer to the child. When
+the child is running, the parent is suspended. Once the child
+finishes, it is deleted, and is no longer accessible to the parent.
+
+.Examples
+`&parent.request.User-Name` +
+`&parent.reply.Reply-Message` +
+`&parent.parent.session-state.Filter-Id`
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/load-balance.adoc b/doc/antora/modules/unlang/pages/load-balance.adoc
new file mode 100644
index 0000000..d64b161
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/load-balance.adoc
@@ -0,0 +1,32 @@
+= The load-balance Statement
+
+.Syntax
+[source,unlang]
+----
+load-balance {
+ [ statements ]
+}
+----
+
+The `load-balance` section is similar to the `redundant` section
+except that only one module in the subsection is ever called.
+
+In general, the
+xref:redundant-load-balance.adoc[redundant-load-balance] statement is
+more useful than this one.
+
+[ statements ]:: One or more `unlang` commands. Only one of the
+statements is executed.
+
+.Examples
+
+[source,unlang]
+----
+load-balance &User-Name {
+ sql1
+ sql2
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/module.adoc b/doc/antora/modules/unlang/pages/module.adoc
new file mode 100644
index 0000000..fd18f2f
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/module.adoc
@@ -0,0 +1,86 @@
+= Modules
+
+.Syntax
+[source,unlang]
+----
+<module>
+----
+
+The `<module>` statement is a reference to the named module. Common
+module names include `pap`, `chap`, `files`, `eap`, and `sql`. The
+`modules { ... }` subsection of `radiusd.conf` contains all of the
+valid modules.
+
+When processing reaches a named module, the pre-compiled module is
+called. The module may succeed or fail and will return a status code
+to the `unlang` interpreter detailing success or failure.
+
+.Example
+[source,unlang]
+----
+chap
+sql
+----
+
+== Module Return Codes
+
+When a module is called, it returns one of the following codes to
+the interpreter; the meaning of each code is detailed to the right of
+the source, below:
+
+.Module Return Codes
+
+The below table describes the purpose of the rcodes that may be returned
+by a module call. In this case the 'operation' referenced in the table is
+the current module call.
+
+include::partial$rcode_table.adoc[]
+
+These return codes can be used in a subsequent
+xref:condition/index.adoc[conditional expression] thus allowing policies to
+perform different actions based on the behaviour of the modules.
+
+.Example
+[source,unlang]
+----
+sql
+if (notfound) {
+ update reply {
+ Reply-Message = "We don't know who you are"
+ }
+ reject
+}
+----
+
+== Module Return Code priority overrides
+
+In the case of modules, rcodes can be modified on a per-call basis.
+
+Module priority overrides are specified in a block inline with the module call.
+The format of an override is `<rcode> = (0+|<rcode>|return)` - That is,
+a number greater than or equal to 0, the priority of another rcode, or the special
+priority `return` which causes the current section to immediately exit.
+
+[source, unlang]
+----
+ldap { <1>
+ fail = 1 <2>
+ ok = handled <3>
+ reject = return <4>
+}
+----
+
+<1> Call to the `ldap` module.
+<2> Sets the priority of the `fail` rcode to be `1`. If the priority
+ of the rcode for the request is `0`, then the request request rcode
+ will be set to `fail` if the module returns `fail`.
+<3> Sets the priority of the `ok` rcode to be whatever the default is for
+ `handled` in this section. As the default for `handled` is usually
+ `return`. If `ok` is returned, the current section will immediately
+ exit.
+<4> Sets the priority of `reject` to be `return`. If the module returns
+ `reject`, the current section will immediately exit.
+
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/module_builtin.adoc b/doc/antora/modules/unlang/pages/module_builtin.adoc
new file mode 100644
index 0000000..f21a128
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/module_builtin.adoc
@@ -0,0 +1,42 @@
+= Built-in Modules
+
+In some cases, it is useful to reject a request immediately or perform another
+action on it. The built-in modules can be used to perform these actions. These
+modules are named for the return codes given in the xref:module.adoc[module]
+section.
+
+In practice, these modules are implemented by the `always` module and
+exist so that a success or failure can be forced during the processing
+of a policy.
+
+The names and behaviours of these modules are given below:
+
+`fail`::
+Causes the request to be treated as if a database failure had
+occurred.
+
+`noop`::
+Do nothing. This also serves as an instruction to the
+configurable failover tracking that nothing was done in the current
+section.
+
+`ok`::
+Instructs the server that the request was processed properly. This keyword can be used to over-ride earlier failures if the local
+administrator determines that the failures are not catastrophic.
+
+`reject`::
+Causes the request to be immediately rejected.
+
+.Example
+[source,unlang]
+----
+if (!&User-Name) {
+ update reply {
+ Reply-Message := "We don't know who you are"
+ }
+ reject
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/module_method.adoc b/doc/antora/modules/unlang/pages/module_method.adoc
new file mode 100644
index 0000000..98cd375
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/module_method.adoc
@@ -0,0 +1,27 @@
+= Module methods
+
+.Syntax
+[source,unlang]
+----
+<module>.<method>
+----
+
+This variant of xref:module.adoc[<module>] is used in one processing
+section. It calls a module using the method of another processing
+section. For example, it can be used to call a module's `authorize`
+method while processing the `post-auth` section.
+
+The `<module>` portion must refer to an existing module; the
+`<method>` portion must refer to processing method supported by that
+module. Typically, the names of the processing method will be the
+same as the processing sections.
+
+.Example
+[source,unlang]
+----
+sql.recv.Accounting-Request
+files.recv.Access-Request
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/redundant-load-balance.adoc b/doc/antora/modules/unlang/pages/redundant-load-balance.adoc
new file mode 100644
index 0000000..2322f72
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/redundant-load-balance.adoc
@@ -0,0 +1,39 @@
+= The redundant-load-balance Statement
+
+.Syntax
+[source,unlang]
+----
+redundant-load-balance {
+ [ statements ]
+}
+----
+
+The `redundant-load-balance` section operates as a combination of the
+xref:redundant.adoc[redundant] and xref:load-balance.adoc[load-balance]
+sections.
+
+[ statements ]:: One or more `unlang` commands.
++
+If the selected statement succeeds, then the server stops processing
+the `redundant-load-balance` section. If, however, that statement fails,
+then the next statement in the list is chosen (wrapping around to the
+top). This process continues until either one statement succeeds or all
+of the statements have failed.
++
+All of the statements in the list should be modules, and of the same
+type (e.g., `ldap` or `sql`). All of the statements in the list should
+behave identically, otherwise different requests will be processed
+through different modules and will give different results.
+
+.Example
+[source,unlang]
+----
+redundant-load-balance &User-Name {
+ sql1
+ sql2
+ sql3
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/redundant.adoc b/doc/antora/modules/unlang/pages/redundant.adoc
new file mode 100644
index 0000000..e837d1f
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/redundant.adoc
@@ -0,0 +1,42 @@
+= The redundant Statement
+
+.Syntax
+[source,unlang]
+----
+redundant {
+ [ statements ]
+}
+----
+
+The `redundant` section executes a series of statements in sequence.
+As soon as one statement succeeds, the rest of the section is skipped.
+
+[ statements ]:: One or more `unlang` commands. Processing starts
+from the first statement in the list.
++
+If the selected statement succeeds, then the server stops processing
+the `redundant` section. If, however, that statement fails, then the
+next statement in the list is chosen. This process continues until
+either one statement succeeds or all of the statements have failed.
++
+All of the statements in the list should be modules, and of the same
+type (e.g., `ldap` or `sql`). All of the statements in the list should
+behave identically, otherwise different requests will be processed
+through different modules and will give different results.
+
+In general, we recommend using the
+xref:redundant-load-balance.adoc[redundant-load-balance] statement
+instead of `redundant`.
+
+.Example
+[source,unlang]
+----
+redundant {
+ sql1
+ sql2
+ sql3
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/return.adoc b/doc/antora/modules/unlang/pages/return.adoc
new file mode 100644
index 0000000..aea1bc2
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/return.adoc
@@ -0,0 +1,36 @@
+= The return Statement
+
+.Syntax
+[source,unlang]
+----
+return
+----
+
+The `return` statement is used to exit a processing section such as
+`authorize`. It behaves similarly to the
+xref:break.adoc[break] statement, except that it is not limited to
+being used inside of a xref:foreach.adoc[foreach] loop.
+
+The `return` statement is not strictly necessary. It is mainly used
+to simplify the layout of `unlang` policies. If the `return`
+statement did not exist, then every xref:if.adoc[if] statement might need
+to have a matching xref:else.adoc[else] statement.
+
+The `return` statement will also exit a policy which is defined in the
+`policy { ... } ` subsection. This behavior allows policies to be
+treated as a function call. Any `return` inside of the policy section
+will only return from that policy. The `return` will _not_ return
+from the enclosing processing section which called the policy.
+
+.Example
+[source,unlang]
+----
+sql
+if (&reply.Filter-Id == "hello") {
+ return
+}
+...
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/return_codes.adoc b/doc/antora/modules/unlang/pages/return_codes.adoc
new file mode 100644
index 0000000..3b09c2d
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/return_codes.adoc
@@ -0,0 +1,17 @@
+= Return codes
+
+Many operations in the server produce a return code (rcode).
+The different rcodes give a course indication of whether a particular operation
+(a module call, string expansion, or keyword) was successful.
+
+Unlike return values in other languages, FreeRADIUS' rcodes are are always taken
+from a fixed compiled-in set.
+
+include::partial$rcode_table.adoc[]
+
+Return codes propagate through nested unlang sections based on their priority.
+If a rcode returned by an operation has a higher priority than the current rcode
+associated with the request, then the request rcode is overwritten.
+
+Return code priorities are assigned by the section the module call, expansion or
+keyword was used in.
diff --git a/doc/antora/modules/unlang/pages/switch.adoc b/doc/antora/modules/unlang/pages/switch.adoc
new file mode 100644
index 0000000..deff703
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/switch.adoc
@@ -0,0 +1,83 @@
+= The switch Statement
+
+.Syntax
+[source,unlang]
+----
+switch <expansion> {
+ case <match-1> {
+ [ statements-1 ]
+ }
+ case <match-2> {
+ [ statements-2 ]
+ }
+ case {
+ [ statements-3 ]
+ }
+}
+----
+
+A `switch` statement causes the server to evaluate _expansion_, which
+can be an xref:attr.adoc[&Attribute-Name] or
+xref:condition/operands.adoc[data]. The result is compared against _match-1_
+and _match-2_ to find a match. If no string matches, then the server
+looks for the default xref:case.adoc[case] statement, which has no
+associated match.
+
+The matching is done via equality. The `switch` statement is mostly
+syntactic sugar and is used to simplify the visual form of the
+configuration. It is mostly equivalent to the following use of
+xref:if.adoc[if] statements:
+
+.Nearly equivalent syntax
+[source,unlang]
+----
+if (<expansion> == <match-1>) {
+ [ statements-1 ]
+}
+elsif (<expansion> == <match-2>) {
+ [ statements-2 ]
+}
+else {
+ [ statements-3 ]
+}
+----
+
+The only difference between the two forms is that for a `switch`
+statement, the _expansion_ is evaluated only once. For the equivalent
+xref:if.adoc[if] statement, the _expansion_ is evaluated again for every
+xref:if.adoc[if].
+
+If a matching xref:case.adoc[case] is found, the statements within
+that xref:case.adoc[case] are evaluated. If no matching
+xref:case.adoc[case] is found, the `case` section with no "match" is
+evaluated. If there is no such `case { ...}` subsection, then the
+`switch` statement behaves as if the `case {...}` section was empty.
+
+////
+For compatibility with version 3, and empty `case` statement can also
+be used instead of `default`.
+////
+
+The _match_ text for the xref:case.adoc[case] statement can be an
+xref:attr.adoc[&Attribute-Name] or xref:type/index.adoc[data].
+
+No statement other than xref:case.adoc[case] can appear in a `switch`
+statement, and the xref:case.adoc[case] statement cannot appear outside of a
+`switch` statement.
+
+.Example
+[source,unlang]
+----
+switch &User-Name {
+ case "bob" {
+ reject
+ }
+
+ case {
+ ok
+ }
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/type/all_types.adoc b/doc/antora/modules/unlang/pages/type/all_types.adoc
new file mode 100644
index 0000000..0bace01
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/type/all_types.adoc
@@ -0,0 +1,80 @@
+= List of Data Types
+
+The server support a wide range of data types, both in `unlang` and in
+the dictionaries. This page outlines the names and functionality of
+those data types.
+
+== Basic Data Types
+
+There are a number of "basic" data types. These data types are
+fixed-size, and encapsulate simple concepts such as "integer" or "IP
+address".
+
+Basic data types can be used in `unlang`, as they contain simple
+values which can be compared, or assigned to one attribute. In most
+cases, it is not necessary to know the name of the data type. It is
+possible to write values in the format you expect, The server will do
+"the right thing" when interpreting the values.
+
+.Basic Data Types
+[options="header"]
+[cols="15%,85%"]
+|=====
+| Data Type | Description
+| bool | boolean
+| date | calendar date
+| ethernet | Ethernet address
+| float32 | 32-bit floating point number
+| float64 | 64-bit floating point number
+| ifid | interface ID
+| int8 | 8-bit signed integer
+| int16 | 16-bit signed integer
+| int32 | 32-bit signed integer
+| int64 | 64-bit signed integer
+| ipaddr | IPv4 address
+| ipv6addr | IPv6 address
+| ipv4prefix | IPv4 network with address and prefix length
+| ipv6prefix | IPv6 network with address and prefix length
+| octets | raw binary, printed as hex strings
+| string | printable strings
+| time_delta | difference between two calendar dates
+| uint8 | 8-bit unsigned integer
+| uint16 | 16-bit unsigned integer
+| uint32 | 32-bit unsigned integer
+| uint64 | 64-bit unsigned integer
+|=====
+
+=== Structural Data Types
+
+The following data types are "structural", in that they form
+parent-child relationships between attributes. These data types can
+only be used in the dictionaries. They cannot be used in `unlang`
+statements.
+
+.Structural Data Types
+[options="header"]
+[cols="15%,85%"]
+|=====
+| Data Type | Description
+| struct | structure which contains fixed-width fields
+| tlv | type-length-value which contains other attributes
+| vsa | Encapsulation of vendor-specific attributes
+|=====
+
+=== Protocol-Specific Data Types
+
+The following data types are used only in certain protocols. These
+data types can be used only in the dictionaries. They cannot be used
+in `unlang` statements.
+
+.Protocol Specific Data Types
+[options="header"]
+[cols="15%,15%,70%"]
+|=====
+| Data Type | Protocol | Description
+| abinary | RADIUS | Ascend binary filters
+| extended | RADIUS | attributes which "extend" the number space
+|=====
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS
diff --git a/doc/antora/modules/unlang/pages/type/double.adoc b/doc/antora/modules/unlang/pages/type/double.adoc
new file mode 100644
index 0000000..6c3e708
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/type/double.adoc
@@ -0,0 +1,39 @@
+= Double-Quoted Strings
+
+.Syntax
+`"string"`
+
+A double-quoted string is interpreted via the usual rules in
+programming languages for double quoted strings. The double-quote
+character can be placed in a string by escaping it with a backslash.
+Carriage returns and line-feeds can also be used via the usual `\r` and
+`\n` syntax.
+
+The main difference between the single and double quoted strings is
+that the double quoted strings can be dynamically expanded. The syntax
+`${...}` is used for parse-time expansion and `%{...}` is used for
+run-time expansion. The difference between the two methods is that the
+`${...}` form is expanded when the server loads the configuration
+files and is valid anywhere in the configuration files. The `%{...}`
+link:xlat.adoc[string expansion] form is valid only in conditional
+expressions and attribute assignments.
+
+The output of the dynamic expansion can be interpreted as a string,
+a number, or an IP address, depending on its context.
+
+Note that the interpretation of text _strongly_ depends on the
+context. The text `"0000"` can be interpreted as a data type
+"integer", having value zero, or a data type "string", having value
+`"0000"`. In general when a particular piece of text is used, it is
+used with the context of a known attribute. That attribute has a
+link:data.adoc[data type], and the text will be interpreted as that
+data type.
+
+.Examples
+
+`"word"` +
+`"a string"` +
+`"this has embedded\ncharacters"`
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/type/index.adoc b/doc/antora/modules/unlang/pages/type/index.adoc
new file mode 100644
index 0000000..7d0d70f
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/type/index.adoc
@@ -0,0 +1,117 @@
+= Data Types
+
+Unlang supports a number of data types. These data types are used in
+conditional expressions or when assigning a value to an attribute.
+
+== Using Data Types
+
+The server support a wide range of data types, as given in the
+xref:unlang/type/all_types.adoc[list of data types] page. The choice
+of which data type applies is determined by the context in which that
+data type is used. This context is usually taken from an attribute
+which is being assigned a value.
+
+The `unlang` interpreter uses pre-defined attributes which are defined
+in dictionaries. The dictionaries define both a name, and a data type
+for the attributes. In the interpreter, then, attributes can be
+assigned a value or compared to a value, without specifying the data
+type. The interpreter knows how to parse the value by using the data
+type assigned to the attribute.
+
+The result is that in most cases, it is not necessary to know the name
+of the data types. It is possible to write values in the format you
+expect, and he server will do "the right thing" when interpreting the
+values.
+
+.Attributes with Different Data Types
+[source,unlang]
+----
+Framed-IP-Address = 192.0.2.1
+Framed-IPv6-Address = 2001:db8::
+Reply-Message = "This is a reply"
+Port-Limit = 5
+Boolean = true
+Octets-Thing = 0xabcdef0102030405
+MAC-Address = 00:01:02:03:04:05
+----
+
+== Parsing Data Types
+
+The interpreter is flexible when parsing data types. So long as the
+value can be parsed as the given data type without error, the value
+will be accepted.
+
+For example, a particular attribute may be of data type `ipaddr` in
+order to store IPv4 addresses. The interpreter will then accept the
+following strings as valid IPv4 addresses:
+
+`192.168.0.2`:: xref:type/string/unquoted.adoc[Unquoted text], interpreted as the data type
+
+`'192.168.0.2'`:: xref:type/string/single.adoc[Single-quoted string], the contents of the string are parsed as the data type.
++
+The single-quoted string form is most useful when the data type
+contains special characters that may otherwise confuse the parser.
+
+`"192.168.0.2"`:: xref:type/string/double.adoc[Double-quoted string].
++
+The contents of the string are dynamically expanded as described in
+the xref:unlang/xlat/index.adoc[dynamic expansion] page. The
+resulting output is then interpreted as the given data type.
+
+`{backtick}/bin/echo 192.168.0.2{backtick}`:: xref:type/string/backticks.adoc[backtick-quoted string].
+Run a script, and parse the resulting string as the data type.
+
+Similar processing rules are applied when parsing assignments and
+comparisons, for all attributes and data types.
+
+=== Casting Data Types
+
+In some cases, it is necessary to parse values which do not refer to
+attributes. This situation usually occurs when two values need to be
+compared, as in the following example:
+
+[source,unlang]
+----
+if ("%{sql:SELECT ipaddress FROM table WHERE user=%{User-Name}}" == 192.0.2.1) }
+ ....
+}
+----
+
+Since there is no attribute on either side of the `==` operator, the
+interpreter has no way of knowing that the string `192.0.2.1` is an IP
+address. There is unfortunately no way of automatically parsing
+strings in order to determine the data type to use. Any such
+automatic parsing would work most of the time, but it would have
+error cases where the parsing was incorrect.
+
+The solution is to resolve these ambiguities by allowing the values to
+be cast to a particular type. Casting a value to a type tells the
+interpreter how that value should be parsed. Casting is done by
+prefixing a value with the type name, surrounded by angle brackets;
+`<...>`.
+
+.Syntax
+----
+<...>value
+----
+
+We can add a cast to the above example, as follows:
+
+[source,unlang]
+----
+if ("%{sql:SELECT ipaddress FROM table WHERE user=%{User-Name}}" == <ipaddr>192.0.2.1) }
+ ....
+}
+----
+
+In this example, we prefix the IP address with the string `<ipaddr>`.
+The interpreter then knows that the value `192.0.2.` should be
+interpreted as the data type `ipaddr`, and not as the literal string
+`"192.0.2."`.
+
+For a full list of data types which can be used in a cast, please see
+the xref:unlang/type/all_types.adoc[list of data types] page, and the
+"Basic Type Types" section.
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/type/ip.adoc b/doc/antora/modules/unlang/pages/type/ip.adoc
new file mode 100644
index 0000000..fc25ae8
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/type/ip.adoc
@@ -0,0 +1,15 @@
+= IP Addresses
+
+.Examples
+
+`192.0.2.16` +
+`::1` +
+`example.com`
+
+Depending on the context, a "simple word", as above, may be
+interpreted as an IPv4 or an IPv6 address. This interpretation is
+usually done when the string is used in the context of an attribute,
+or to compare two addresses or assign an address to an attribute.
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/type/numb.adoc b/doc/antora/modules/unlang/pages/type/numb.adoc
new file mode 100644
index 0000000..284cf81
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/type/numb.adoc
@@ -0,0 +1,11 @@
+= Numbers
+
+.Examples
+
+`0` +
+`563`
+
+Numbers are unsigned integers that are composed of decimal digits.
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/type/string/backticks.adoc b/doc/antora/modules/unlang/pages/type/string/backticks.adoc
new file mode 100644
index 0000000..9372b4c
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/type/string/backticks.adoc
@@ -0,0 +1,38 @@
+= Backtick-quoted string
+
+.Syntax
+`{backtick}string{backtick}`
+
+The backtick operator is used to perform a run-time expansion
+similar to what is done with the Unix shell. The contents of the string
+are split into one or more sub-strings, based on intermediate
+whitespace. Each substring is then expanded as described above for
+double quoted strings. The resulting set of strings is used to execute a
+program with the associated arguments.
+
+The output of the program is recorded, and the resulting data is
+used in place of the input string value. Where the output is composed of
+multiple lines, any carriage returns and line feeds are replaced by
+spaces.
+
+For safety reasons, the full path to the executed program should be
+given. In addition, the string is split into arguments _before_ the
+substrings are dynamically expanded. This step is done both to allow
+the substrings to contain spaces, and to prevent spaces in the
+expanded substrings from affecting the number of command-line
+arguments.
+
+For performance reasons, we recommend that the use of back-quoted
+strings be kept to a minimum. Executing external programs is
+relatively expensive, and executing a large number of programs for
+every request can quickly use all of the CPU time in a server. If many
+programs need to be executed, it is suggested that alternative ways to
+achieve the same result be found. In some cases, using a real
+programming language such as `lua`, `perl` or `python` may be better.
+
+.Examples
+
+`{backtick}/bin/echo hello{backtick}`
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/type/string/double.adoc b/doc/antora/modules/unlang/pages/type/string/double.adoc
new file mode 100644
index 0000000..ea87bc5
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/type/string/double.adoc
@@ -0,0 +1,68 @@
+= Double Quoted Strings
+
+.Syntax
+`"string"`
+
+A double-quoted string allows escape sequences and xref:xlat/index.adoc[dynamic
+expansions]. As with xref:type/string/single.adoc[single-quoted strings], text
+within double quotes can include spaces.
+
+The main difference between the single and double quoted strings is
+that the double quoted strings can be dynamically expanded. The syntax
+`${...}` is used for parse-time expansion and `%{...}` is used for
+run-time expansion. The difference between the two methods is that the
+`${...}` form is expanded when the server loads the configuration
+files and is valid anywhere in the configuration files. The `%{...}`
+xref:xlat/index.adoc[string expansion] form is valid only in conditional
+expressions and attribute assignments.
+
+The output of the dynamic expansion can be interpreted as a string,
+a number, or an IP address, depending on its context.
+
+Note that the interpretation of text _strongly_ depends on the
+context. The text `"0000"` can be interpreted as a data type
+"integer", having value zero, or a data type "string", having value
+`"0000"`. In general when a particular piece of text is used, it is
+used with the context of a known attribute. That attribute has a
+xref:type/index.adoc[data type], and the text will be interpreted as that
+data type.
+
+NOTE: Most values retrieved from external datastores will be treated implicitly
+as double-quoted strings.
+
+== Escape sequences
+
+Escape sequences allow the inclusion of characters that may be difficult to
+represent in datastores, or the FreeRADIUS configuration files.
+
+.Escape sequences and their descriptions
+[options="header", cols="15%,85%"]
+|=====
+| Escape sequence | Character represented
+| `\\` | Literal backslash (0x5c)
+| `\r` | Carriage return (0x0d)
+| `\n` | Line feed (0x0a)
+| `\t` | Horizontal tab (0x09)
+| `\"` | Double quote (0x22)
+| `\x<hex><hex>` | A byte whose numerical value is given by `<hex><hex>` interpreted as a hexadecimal number.
+| `\x<oct><oct><oct>` | A byte whose numerical value is given by `<oct><oct><oct>` interpreted as an octal number.
+|=====
+
+.Examples
+
+`"word"` +
+`"a string"' +
+`"foo\"bar\""` +
+`"this is a long string"` +
+`"this has embedded\ncharacters"` +
+`"attribute\tvalue\nusername\t%{User-Name}\nreply-message\t%{reply.Reply-Message}"`
+`"The result of 'SELECT * FROM foo WHERE 1' is: %{sql:SELECT * FROM foo WHERE 1}"`
+
+// Licenced under CC-by-NC 4.0.
+// Copyright (C) 2019 Arran Cudbard-Bell <a.cudbardb@freeradius.org>
+// Copyright (C) 2019 The FreeRADIUS project.
+// Copyright (C) 2020 Network RADIUS SAS.
+
+
+
+
diff --git a/doc/antora/modules/unlang/pages/type/string/escaping.adoc b/doc/antora/modules/unlang/pages/type/string/escaping.adoc
new file mode 100644
index 0000000..e63a498
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/type/string/escaping.adoc
@@ -0,0 +1,14 @@
+= Character Escaping
+
+The quotation characters in the above string data types can be
+escaped by using the backslash, or `\,` character. The backslash
+character itself can be created by using `\\`. Carriage returns and
+line feeds can be created by using `\n` and `\r`.
+
+.Examples
+
+`"I say \"hello\" to you"` +
+`"This is split\nacross two lines"`
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/type/string/single.adoc b/doc/antora/modules/unlang/pages/type/string/single.adoc
new file mode 100644
index 0000000..fa2ac05
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/type/string/single.adoc
@@ -0,0 +1,19 @@
+= Single Quoted Strings
+
+.Syntax
+`'string'`
+
+A single-quoted string is interpreted without any dynamic string
+expansion. The quotes allow the string to contain spaces, among other
+special characters. The single quote character can be placed in such a
+string by escaping it with a backslash.
+
+.Examples
+
+`'hello'` +
+`'foo bar`' +
+`'foo\\'bar'` +
+`'this is a long string'`
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/type/string/unquoted.adoc b/doc/antora/modules/unlang/pages/type/string/unquoted.adoc
new file mode 100644
index 0000000..9dd6e55
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/type/string/unquoted.adoc
@@ -0,0 +1,21 @@
+= Unquoted Strings
+
+Where a series of characters cannot be parsed as a decimal number,
+they are interpreted as a simple string composed of one word. This
+word is delimited by spaces, or by other tokens, such as `)` in
+conditional expressions.
+
+This unquoted text is interpreted as simple strings and are generally
+equivalent to placing the string in single quotes.
+
+The interpretation of the text depends on the context, which is
+usually defined by an attribute which has a xref:type/index.adoc[data type].
+
+.Examples
+
+`Hello` +
+`192.168.0.1` +
+`00:01:02:03:04:05`
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/update.adoc b/doc/antora/modules/unlang/pages/update.adoc
new file mode 100644
index 0000000..645f4d8
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/update.adoc
@@ -0,0 +1,160 @@
+= The update Statement
+
+.Syntax
+[source,unlang]
+----
+update [ <list> ] {
+ <server-attribute> <op> <value>
+ ...
+}
+----
+
+The `update` statement adds attributes to, or edits the attributes in,
+the named _<list>_.
+
+The `update` statement consists of the following syntax elements:
+
+<list>:: The attribute list which will be updated. The list is
+usually `request`, `reply`, or `control`.
++
+If the _<list>_ qualifier is omitted, then each entry inside of the
+`update` section *must* be prefixed with a list name. For example,
+`&request.User-Name ...`
+
+<server-attribute>:: The server attribute which is assigned the
+_<value>_.
+
+<op>:: The operator such as `=`, `:=`, etc.
+
+<value>:: The value which is assigned to the attribute. If the field
+is a double-quoted string, it undergoes xref:xlat/index.adoc[string
+expansion], and the resulting value is assigned to the attribute.
+
+The update process is atomic, in that either all of the attributes are
+modified, or none of them are modified. If the `update` fails for any
+reason, then all of the results are discarded, and the `update` does
+not affect any server attributes.
+
+.Example
+[source,unlang]
+----
+update reply {
+ &Reply-Message := "Hello!"
+ &Framed-IP-Address := 192.0.2.4
+}
+----
+
+== Lists
+
+The _<list>_ field sets the attribute list that will be updated. If
+the _<list>_ qualifier is omitted, then each entry inside of the
+`update` section *must* be prefixed with a list name. For example,
+`&request.User-Name ...`
+
+Please see the xref:list.adoc[list] page for valid list names.
+
+== Server Attributes
+
+The _<server-attribute>_ field is an attribute name, such as
+`&Reply-Message`. The attribute name may also be prefixed with a
+_<list>_ qualifier, which overrides the _<list>_ given at the start
+of the `update` section.
+
+NOTE: In version 3, the leading `&` is optional but recommended.
+
+== Editing Operators
+
+The `<op>` field is used to define how the attribute is processed.
+Different operators allow attributes to be added, deleted, or
+replaced, as defined below.
+
+.Editing Operators
+[options="header"]
+[cols="10%,90%"]
+|=====
+| Operator | Description
+| = | 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, its value is replaced with the
+value of the current attribute.
+| += | 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.
+| -= | Remove all attributes from the list that match _<value>_.
+| !* | Delete all occurances of the attribute, no matter what the value.
+|=====
+
+== Filtering Operators
+
+The following operators may also be used in addition to the ones
+listed above. These operators use the _<server-attribute>_ and
+_<value>_ fields to enforce limits on all attributes in the given
+_<list>_, and to edit attributes which have a matching
+_<server-attribute>_ name. All other attributes are ignored.
+
+.Filtering Operators
+[options="header]
+[cols="10%,90%"]
+|=====
+| Operator | Description
+| == | Keep only the attributes in the list that match _<value>_
+| < | Keep only the attributes in the list that have values less than _<value>_.
+| \<= | Keep only the attributes in the list that have values less than or equal to _<value>_.
+| > | Keep only the attributes in the list that have values greater than _<value>_.
+| >= | Keep only the attributes in the list that have values greater than or equal to _<value>_.
+| =~ | Keep only the attributes in the list which match the regular expression given in _<value>_.
+| !~ | Keep only the attributes in the list which do not match the regular expression given in _<value>_.
+|=====
+
+The `==` operator is very different from the `=` operator listed
+above. The `=` operator is used to add new attributes to the list,
+while the `==` operator removes all attributes that do not match the
+given value.
+
+The comparison operators `<`, `<=`, `>`, and `>=` have some additional
+side effects. Any non-matching value is replaced by the _<value>_
+given here. If no attribute exists, it is created with the given
+_<value>_.
+
+For IP addresses, the operators `>`, `>=`, `<`, and `\<=` check for
+membership in a network. The _<value>_ field should then be a IP
+network, given in `address/mask` format.
+
+.Example
+[source,unlang]
+----
+update reply {
+ &Session-timeout := 86400
+}
+----
+
+.Example
+[source,unlang]
+----
+update reply {
+ &Reply-Message += "Rejected: Also, realm does not end with ac.uk"
+}
+----
+
+== Values
+
+The _<value>_ field is the value which is assigned to the
+_<server-attribute>_. The interpretation of the _<value>_ field
+depends on the data type of the contents. For example, if the string
+`"192.0.2.1"` is assigned to an attribute of the `string` data type,
+then the result is an ASCII string containing that value. However, if
+the same string is assigned to an attribute of the `ipaddr` data type,
+then the result is a 32-bit IPv4 address, with binary value `0xc0000201`.
+
+.Example
+[source,unlang]
+----
+update reply {
+ &Session-Timeout <= 3600
+}
+----
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/xlat/alternation.adoc b/doc/antora/modules/unlang/pages/xlat/alternation.adoc
new file mode 100644
index 0000000..adb7604
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/xlat/alternation.adoc
@@ -0,0 +1,24 @@
+= Alternation Syntax
+
+Alternation syntax similar to that used in Unix shells may also be
+used:
+
+`%{%{Foo}:-bar}`
+
+This code returns the value of `%{Foo}`, if it has a value.
+Otherwise, it returns a literal string bar.
+
+`%{%{Foo}:-%{Bar}}`
+
+This code returns the value of `%{Foo}`, if it has a value.
+Otherwise, it returns the expansion of `%{Bar}`.
+
+These conditional expansions can be nested to almost any depth, such
+as with `%{%{One}:-%{%{Two}:-%{Three}}}`.
+
+.Examples
+`%{%{Stripped-User-Name}:-%{User-Name}}` +
+`%{%{Framed-IP-Address}:-<none>}`
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/xlat/attribute.adoc b/doc/antora/modules/unlang/pages/xlat/attribute.adoc
new file mode 100644
index 0000000..a3ee29b
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/xlat/attribute.adoc
@@ -0,0 +1,54 @@
+= Attribute References
+
+Attributes in a list may be referenced via one of the following two
+syntaxes:
+
+`%{Attribute-Name}` +
+`%{<list>:Attribute-Name}`
+
+The `<list>:` prefix is optional. If given, it must be a valid
+reference to an xref:list.adoc[attribute list].
+
+If the `<list>:` prefix is omitted, then the `request` list is
+assumed.
+
+For EAP methods with tunneled authentication sessions (i.e. PEAP and
+EAP-TTLS), the inner tunnel session can refer to a list for the outer
+session by prefixing the list name with `outer.` ; for example,
+`outer.request`.
+
+When a reference is encountered, the given list is examined for an
+attribute of the given name. If found, the variable reference in the
+string is replaced with the value of that attribute. Otherwise, the
+reference is replacedd with an empty string.
+
+.Examples
+
+`%{User-Name}` +
+`%{request.User-Name} # same as above` +
+`%{reply.User-Name}` +
+`%{outer.request.User-Name} # from inside of a TTLS/PEAP tunnel`
+
+Examples of using references inside of a string:
+
+`"Hello %{User-Name}"` +
+`"You, %{User-Name} are not allowed to use %{NAS-IP-Address}"`
+
+== Additional Variations
+
+`%{Attribute-Name[#]}`::
+Returns an integer containing the number of named attributes
+
+`%{Attribute-Name[0]}`::
+
+When an attribute appears multiple times in a list, this syntax allows
+you to address the attributes as with array entries. `[0]` refers to
+the first attributes, `[1]` refers to the second attribute, etc.
+
+`%{Attribute-Name[*]}`::
+
+Returns a comma-separated string containing all values for the named
+attributes.
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/xlat/builtin.adoc b/doc/antora/modules/unlang/pages/xlat/builtin.adoc
new file mode 100644
index 0000000..f236a57
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/xlat/builtin.adoc
@@ -0,0 +1,891 @@
+= Built-In Expansions
+
+In addition to storing attribute references, the server has a number
+of built-in expansions. These expansions act largely as functions
+which operate on inputs, and produce an output.
+
+
+
+== Attribute Manipulation
+
+=== %{length: ... }
+
+The `length` expansion returns the size of the input as an integer.
+When the input is a string, then the output is identical to the
+`strlen` expansion.
+
+When the input is an attribute reference, the output is the size of
+the attributes data as encoded "on the wire".
+
+.Return: _size_
+
+.Determining the length of fixed and variable length attributes
+====
+[source,unlang]
+----
+update control {
+ &Tmp-String-0 := "Caipirinha"
+ &Framed-IP-Address := 192.0.2.1
+}
+
+update reply {
+ &Reply-Message := "The length of %{control:Tmp-String-0} is %{length:&control:Tmp-String-0}"
+ &Reply-Message += "The length of %{control:Framed-IP-Address} is %{length:&control:Framed-IP-Address}"
+}
+----
+
+.Output
+....
+The length of Caipirinha is 10
+The length of 192.168.0.2 is 4
+....
+====
+
+`length` is built in to the server core.
+
+
+
+=== %{integer:<&ref>}
+
+Print the value of the attribute an integer.
+
+In normal operation, `integer` attributes are printed using the name
+given by a `VALUE` statement in a dictionary. Similarly, date
+attributes are printed as dates, i.e., "January 1 2010.
+
+The `integer` expansion applies only to attributes which can be
+converted to an integer. For all other inputs, it returns `0`.
+
+A common usage is to find the difference between two dates.
+
+For example, if a request contains `Service-Type = Login-User`, the
+expansion of `%{integer:&Service-Type}` will yield `1`, which is the
+value associated with the `Login-User` name. Using
+`%{integer:&Event-Timestamp}` will return the event timestamp as an
+unsigned 32-bit number.
+
+.Return: _string_
+
+.Determining the integer value of an enumerated attribute
+====
+[source,unlang]
+----
+update {
+ &control:Service-Type := Login-User
+}
+update reply {
+ &Reply-Message := "The value of Service-Type is %{integer:&control:Service-Type}"
+}
+----
+
+.Output
+
+```
+The value of Service-Type is 1
+```
+====
+
+`integer` is built in to the server core.
+
+
+
+=== %{rand:<number>}
+
+Generate random number from `0` to `<number>-1`.
+
+.Return: _uint64_
+
+.Generating a random number between 0 and 511
+====
+[source,unlang]
+----
+update reply {
+ &Reply-Message := "The random number is %{rand:512}"
+}
+----
+
+.Output
+
+```
+The random number is 347
+```
+====
+
+`rand` is provided by the `rlm_expr` module.
+
+
+
+=== %{tag:<attribute ref>}
+
+CAUTION: This expansion is deprecated and will likely be removed.
+
+Returns a list of tags for any attributes found using ``<attribute ref>``.
+
+.Return: _int8_
+
+.Determining the tag value of the second instance of the `radius.Tunnel-Server-Endpoint` attribute
+====
+[source,unlang]
+----
+update request {
+ &Tunnel-Server-Endpoint := '192.0.1.1'
+ &Tunnel-Server-Endpoint:1 := '192.0.5.2'
+ &Tunnel-Server-Endpoint:1 += '192.0.3.8'
+ &Tunnel-Server-Endpoint:2 := '192.0.2.1'
+ &Tunnel-Server-Endpoint:2 += '192.0.3.4'
+}
+
+update reply {
+ &Reply-Message := "The tag value of the second instance of Tunnel-Server-Enpoint is %{request:Tunnel-Server-Endpoint[1]}"
+}
+----
+
+.Output
+
+```
+The tag value of the second instance of Tunnel-Server-Enpoint is 192.0.5.2
+```
+====
+
+`tag` is built in to the server core.
+
+
+
+=== %{string:<data>}
+
+Convert input to a string (if possible). For _octets_ type attributes, this
+means interpreting the data as a UTF8 string, and inserting octal escape
+sequences where appropriate.
+
+For other types, this means printing the value in its _presentation_ format,
+i.e. dotted quads for IPv4 addresses, link:https://en.wikipedia.org/wiki/ISO_8601[ISO 8601]
+time for date types, enumeration values for attributes such as `radius.Service-Type` etc.
+
+.Return: _string_
+
+.String interpolation using the raw octets value of Tmp-Octets-0, and the stringified version
+====
+[source,unlang]
+----
+update control {
+ &Tmp-Octets-0 := 0x7465737431
+}
+update reply {
+ &Reply-Message := "The string value of %{control:Tmp-Octets-0} is %{string:%{control:Tmp-Octets-0}}"
+}
+----
+====
+
+.Output
+
+```
+The string value of 0x7465737431 is test1
+```
+
+`string` is built in to the server core.
+
+
+
+== Server Manipulation
+
+=== %{config:<key>}
+
+Refers to a variable in the configuration file. See the documentation
+on configuration file references.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+"Server installed in %{config:prefix}"
+"Module rlm_exec.shell_escape = %{config:modules.exec.shell_escape}"
+----
+
+.Output
+
+```
+Server installed in /opt/freeradius
+Module rlm_exec.shell_escape = yes
+```
+
+`config` is built in to the server core.
+
+
+
+=== %{client:<key>}
+
+Refers to a variable that was defined in the client section for the
+current client. See the sections `client { ... }` in `clients.conf`.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+"The client ipaddr is %{client:ipaddr}"
+----
+
+.Output
+
+```
+The client ipaddr is 192.168.5.9
+```
+
+`client` is built in to the server core.
+
+
+
+=== %{debug:<level>}
+
+Dynamically change the debug level to something high, recording the old level.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+authorize {
+ if (&request:User-Name == "bob") {
+ "%{debug:4}"
+ } else {
+ "%{debug:0}"
+ }
+ ...
+}
+----
+
+.Output (_extra informations only for that condition_)
+
+```
+...
+(0) authorize {
+(0) if (&request:User-Name == "bob") {
+(0) EXPAND %{debug:4}
+(0) --> 2
+(0) } # if (&request:User-Name == "bob") (...)
+(0) filter_username {
+(0) if (&State) {
+(0) ...
+(0) }
+...
+```
+
+`debug` is built in to the server core.
+
+
+
+=== %{debug_attr:<list:[index]>}
+
+Print to debug output all instances of current attribute, or all attributes in a list.
+expands to a zero-length string.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+authorize {
+ if (&request:User-Name == "bob") {
+ "%{debug_attr:request[*]}"
+ }
+ ...
+}
+----
+
+.Output
+
+```
+...
+(0) authorize {
+(0) if (&request:User-Name == "bob") {
+(0) Attributes matching "request[*]"
+(0) &request:User-Name = bob
+(0) &request:User-Password = hello
+(0) &request:NAS-IP-Address = 127.0.1.1
+(0) &request:NAS-Port = 1
+(0) &request:Message-Authenticator = 0x9210ee447a9f4c522f5300eb8fc15e14
+(0) EXPAND %{debug_attr:request[*]}
+(0) } # if (&request:User-Name == "bob") (...)
+...
+```
+
+`debug_attr` is built in to the server core.
+
+
+
+== String manipulation
+
+=== %{lpad:<&ref> <val> <char>}
+
+Left-pad a string.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+update control {
+ &Tmp-String-0 := "123"
+}
+update reply {
+ &Reply-Message := "Maximum should be %{lpad:&control:Tmp-String-0 11 0}"
+}
+----
+
+.Output
+
+```
+Maximum should be 00000000123
+```
+
+`lpad` is provided by the `rlm_expr` module.
+
+
+
+=== %{rpad:<&ref> <val> <char>}
+
+Right-pad a string.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+update control {
+ &Tmp-String-0 := "123"
+}
+update reply {
+ &Reply-Message := "Maximum should be %{rpad:&control:Tmp-String-0 11 0}"
+}
+----
+
+.Output
+
+```
+Maximum should be 12300000000
+```
+
+`rpad` is provided by the `rlm_expr` module.
+
+
+
+=== %{pairs:<&list:[*]>}
+
+Serialize attributes as comma-delimited string.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+update {
+ &control:Tmp-String-0 := "This is a string"
+ &control:Tmp-String-0 += "This is another one"
+}
+
+update reply {
+ &Reply-Message := "Serialize output: %{pairs:&control[*]}"
+}
+----
+
+.Output
+
+```
+Serialize output: Tmp-String-0 = \"This is a string\"Tmp-String-0 = \"This is another one\"
+```
+
+`pairs` is provided by the `rlm_expr` module.
+
+
+
+=== %{randstr: ...}
+
+Get random string built from character classes.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+update reply {
+ &Reply-Message := "The random string output is %{randstr:aaaaaaaa}"
+}
+----
+
+.Output
+
+```
+The random string output is 4Uq0gPyG
+```
+
+`randstr` is provided by the `rlm_expr` module.
+
+
+
+=== %{strlen: ... }
+
+Length of given string.
+
+.Return: _integer_
+
+.Example
+
+[source,unlang]
+----
+update control {
+ &Tmp-String-0 := "Caipirinha"
+}
+update reply {
+ &Reply-Message := "The length of %{control:Tmp-String-0} is %{strlen:&control:Tmp-String-0}"
+}
+----
+
+.Output
+
+```
+The length of Caipirinha is 21
+```
+
+`strlen` is built in to the server core.
+
+
+
+=== %{tolower: ... }
+
+Dynamically expands the string and returns the lowercase version of
+it. This definition is only available in version 2.1.10 and later.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+update control {
+ &Tmp-String-0 := "CAIPIRINHA"
+}
+update reply {
+ &Reply-Message := "tolower of %{control:Tmp-String-0} is %{tolower:%{control:Tmp-String-0}}"
+}
+----
+
+.Output
+
+```
+tolower of CAIPIRINHA is caipirinha
+```
+
+`tolower` is provided by the `rlm_expr` module.
+
+
+
+=== %{toupper: ... }
+
+Dynamically expands the string and returns the uppercase version of
+it. This definition is only available in version 2.1.10 and later.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+update control {
+ &Tmp-String-0 := "caipirinha"
+}
+update reply {
+ &Reply-Message := "toupper of %{control:Tmp-String-0} is %{toupper:%{control:Tmp-String-0}}"
+}
+----
+
+.Output
+
+```
+toupper of caipirinha is CAIPIRINHA
+```
+
+`toupper` is provided by the `rlm_expr` module.
+
+
+
+== String Conversion
+
+=== %{base64: ... }
+
+Encode a string using Base64.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+update control {
+ &Tmp-String-0 := "Caipirinha"
+}
+update reply {
+ &Reply-Message := "The base64 of %{control:Tmp-String-0} is %{base64:%{control:Tmp-String-0}}"
+}
+----
+
+.Output
+
+```
+The base64 of foo is Q2FpcGlyaW5oYQ==
+```
+
+`base64` is provided by the `rlm_expr` module.
+
+
+
+=== %{base64tohex: ... }
+
+Decode a base64 string (e.g. previously encoded using `base64`) to
+hex.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+update control {
+ &Tmp-String-0 := "Q2FpcGlyaW5oYQ=="
+}
+update reply {
+ &Reply-Message := "The base64tohex of %{control:Tmp-String-0} is %{base64tohex:%{control:Tmp-String-0}}"
+}
+----
+
+.Output
+
+```
+The base64decode of Q2FpcGlyaW5oYQ== is 436169706972696e6861
+```
+
+`base64tohex` is provided by the `rlm_expr` module.
+
+
+
+=== %{hex: ... }
+
+Convert to hex.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+update control {
+ &Tmp-String-0 := "12345"
+}
+update reply {
+ &Reply-Message := "The value of %{control:Tmp-String-0} in hex is %{hex:%{control:Tmp-String-0}}"
+}
+----
+
+.Output
+
+```
+The value of 12345 in hex is 3132333435
+```
+
+`hex` is built in to the server core.
+
+
+
+=== %{urlquote: ... }
+
+Quote URL special characters.
+
+.Return: _string_.
+
+.Example
+
+[source,unlang]
+----
+update {
+ &control:Tmp-String-0 := "http://example.org/"
+}
+update reply {
+ &Reply-Message += "The urlquote of %{control:Tmp-String-0} is %{urlquote:%{control:Tmp-String-0}}"
+}
+----
+
+.Output
+
+```
+The urlquote of http://example.org/ is http%3A%2F%2Fexample.org%2F
+```
+
+`urlquote` is provided by the `rlm_expr` module.
+
+
+
+=== %{urlunquote: ... }
+
+Unquote URL special characters.
+
+.Return: _string_.
+
+.Example
+
+[source,unlang]
+----
+update {
+ &control:Tmp-String-0 := "http%%3A%%2F%%2Fexample.org%%2F" # Attention for the double %.
+}
+update reply {
+ &Reply-Message += "The urlunquote of %{control:Tmp-String-0} is %{urlunquote:%{control:Tmp-String-0}}"
+}
+----
+
+.Output
+
+```
+The urlunquote of http%3A%2F%2Fexample.org%2F is http://example.org/
+```
+
+`urlunquote` is provided by the `rlm_expr` module.
+
+
+
+== Hashing and Encryption
+
+=== %{hmacmd5:<shared_key> <string>}
+
+Generate `HMAC-MD5` of string.
+
+.Return: _octal_
+
+.Example
+
+[source,unlang]
+----
+update {
+ &control:Tmp-String-0 := "mykey"
+ &control:Tmp-String-1 := "Caipirinha"
+}
+update {
+ &control:Tmp-Octets-0 := "%{hmacmd5:%{control:Tmp-String-0} %{control:Tmp-String-1}}"
+}
+
+update reply {
+ &Reply-Message := "The HMAC-MD5 of %{control:Tmp-String-1} in octets is %{control:Tmp-Octets-0}"
+ &Reply-Message += "The HMAC-MD5 of %{control:Tmp-String-1} in hex is %{hex:control:Tmp-Octets-0}"
+}
+----
+
+.Output
+
+```
+The HMAC-MD5 of Caipirinha in octets is \317}\264@K\216\371\035\304\367\202,c\376\341\203
+The HMAC-MD5 of Caipirinha in hex is 636f6e74726f6c3a546d702d4f63746574732d30
+```
+
+`hmacmd5` is provided by the `rlm_expr` module.
+
+
+
+=== %{hmacsha1:<shared_key> <string>}
+
+Generate `HMAC-SHA1` of string.
+
+.Return: _octal_
+
+.Example
+
+[source,unlang]
+----
+update {
+ &control:Tmp-String-0 := "mykey"
+ &control:Tmp-String-1 := "Caipirinha"
+}
+update {
+ &control:Tmp-Octets-0 := "%{hmacsha1:%{control:Tmp-String-0} %{control:Tmp-String-1}}"
+}
+
+update reply {
+ &Reply-Message := "The HMAC-SHA1 of %{control:Tmp-String-1} in octets is %{control:Tmp-Octets-0}"
+ &Reply-Message += "The HMAC-SHA1 of %{control:Tmp-String-1} in hex is %{hex:control:Tmp-Octets-0}"
+}
+----
+
+.Output
+
+```
+The HMAC-SHA1 of Caipirinha in octets is \311\007\212\234j\355\207\035\225\256\372ʙ>R\"\341\351O)
+The HMAC-SHA1 of Caipirinha in hex is 636f6e74726f6c3a546d702d4f63746574732d30
+```
+
+`hmacsha1` is provided by the `rlm_expr` module.
+
+
+
+=== %{md5: ... }
+
+Dynamically expands the string and performs an MD5 hash on it. The
+result is binary data.
+
+.Return: _binary data_
+
+.Example
+
+[source,unlang]
+----
+update control {
+ &Tmp-String-0 := "Caipirinha"
+}
+update reply {
+ &Reply-Message := "md5 of %{control:Tmp-String-0} is octal=%{md5:%{control:Tmp-String-0}}"
+ &Reply-Message := "md5 of %{control:Tmp-String-0} is hex=%{hex:%{md5:%{control:Tmp-String-0}}}"
+}
+----
+
+.Output
+
+```
+md5 of Caipirinha is octal=\024\204\013md||\230\243\3472\3703\330n\251
+md5 of Caipirinha is hex=14840b6d647c7c98a3e732f833d86ea9
+```
+
+`md5` is provided by the `rlm_expr` module.
+
+
+
+== Miscellaneous Expansions
+
+=== +%{0}+..+%{32}+
+
+`%{0}` expands to the portion of the subject that matched the last regular
+expression evaluated. `%{1}`..`%{32}` expand to the contents of any capture
+groups in the pattern.
+
+Every time a regular expression is evaluated, whether it matches or not,
+the numbered capture group values will be cleared.
+
+
+
+=== +%{regex:<named capture group>}+
+
+Return named subcapture value from the last regular expression evaluated.
+
+Results of named capture groups are available using the `%{regex:<named capture
+group>}` expansion. They will also be accessible using the numbered expansions
+described xref:builtin.adoc#_0_32[above].
+
+Every time a regular expression is evaluated, whether it matches or not,
+the named capture group values will be cleared.
+
+[NOTE]
+====
+This expansion is only available if the server is built with libpcre or libpcre2.
+Use the output of `radiusd -Xxv` to determine which regular expression library in use.
+
+....
+...
+Debug : regex-pcre : no
+Debug : regex-pcre2 : yes
+Debug : regex-posix : no
+Debug : regex-posix-extended : no
+Debug : regex-binsafe : yes
+...
+Debug : pcre2 : 10.33 (2019-04-16) - retrieved at build time
+....
+====
+
+`regex` is built in to the server core.
+
+
+
+=== +%{nexttime:<time>}+
+
+Calculate number of seconds until next n hour(`s`), day(`s`), week(`s`), year(`s`).
+
+.Return: _string_
+
+.Example:
+
+With the current time at 16:18, `%{nexttime:1h}` will expand to `2520`.
+
+[source,unlang]
+----
+update reply {
+ &Reply-Message := "You should wait for %{nexttime:1h}s"
+}
+----
+
+.Output
+
+```
+You should wait for 2520s
+```
+
+`nexttime` is provided by the `rlm_expr` module.
+
+
+
+=== +%{Packet-Type}+
+
+The packet type (`Access-Request`, etc.)
+
+
+
+=== +%{Packet-Src-IP-Address} and %{Packet-Src-IPv6-Address}+
+
+The source IPv4 or IPv6 address of the packet. See also the expansions
+`%{client:ipaddr}` and `%{client:ipv6addr}`. The two expansions
+should be identical, unless `%{client:ipaddr}` contains a DNS hostname.
+
+
+
+=== +%{Packet-Dst-IP-Address} and %{Packet-Dst-IPv6-Address}+
+
+The destination IPv4 or IPv6 address of the packet. See also the
+expansions `%{listen:ipaddr}` and `%{listen:ipv6addr}`. If the socket
+is listening on a "wildcard" address, then these two expansions will be
+different, as follows: the `%{listen:ipaddr}` will be the wildcard
+address and `%{Packet-DST-IP-Address}` will be the unicast address to
+which the packet was sent.
+
+
+
+=== +%{Packet-Src-Port} and %{Packet-Dst-Port}+
+
+The source/destination ports associated with the packet.
+
+.Return: _string_.
+
+.Example
+
+[source,unlang]
+----
+update control {
+ &Tmp-String-0 := "user@example.com"
+}
+
+if (&control:Tmp-String-0 =~ /^(?<login>(.*))@(?<domain>(.*))$/) {
+ update reply {
+ &Reply-Message := "The %{control:Tmp-String-0} { login=%{regex:login}, domain=%{regex:domain} }"
+ }
+}
+----
+
+.Output
+
+```
+The user@example.com { login=user, domain=example.com }
+```
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/xlat/character.adoc b/doc/antora/modules/unlang/pages/xlat/character.adoc
new file mode 100644
index 0000000..84a148c
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/xlat/character.adoc
@@ -0,0 +1,80 @@
+= Single Letter Expansions
+
+The following are single letter expansions. These expansions do not
+use the typical `%{...}` format. Instead, they are short-cuts for
+simple, common cases.
+
+== Miscellaneous
+
+`%%`::
+
+Returns `%`.
+
+
+== Current Time
+
+`%c`::
+
+The current Unix epoch time in seconds. This is an unsigned decimal number.
+It should be used with time-based calculations.
+
+`%C`::
+
+The microsecond component of the current epoch time. This is an unsigned
+decimal number. It should be used with time-based calculations.
+
+
+== Request Time
+
+`%l`::
+
+The Unix timestamp of when the request was received. This is an unsigned
+decimal number. It should be used with time-based calculations.
+
+`%Y`::
+
+Four-digit year when the request was received.
+
+`%m`::
+
+Numeric month when the request was received.
+
+`%d`::
+
+Numeric day of the month when the request was received.
+
+`%H`::
+
+Hour of the day when the request was received.
+
+`%G`::
+
+Minute component of the time when the request was received.
+
+`%e`::
+
+Second component of the time when the request was received.
+
+`%M`::
+
+Microsecond component of the time when the request was received.
+
+`%D`::
+
+Request date in the format `YYYYMMDD`.
+
+`%S`::
+
+Request timestamp in SQL format, `YYYY-mmm-ddd HH:MM:SS`.
+
+`%t`::
+
+Request timestamp in _ctime_ format, `Www Mmm dd HH:MM:SS YYYY`.
+
+`%T`::
+
+Request timestamp in ISO format, `YYYY-mm-ddTHH:MM:SS.000`.
+
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/xlat/index.adoc b/doc/antora/modules/unlang/pages/xlat/index.adoc
new file mode 100644
index 0000000..b42f725
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/xlat/index.adoc
@@ -0,0 +1,56 @@
+= String Expansion
+
+String expansion is a feature that allows strings to dynamically
+define their value at run time. For historical reasons, these string
+expansions are called "xlats".
+
+String expansion is performed via the following syntax:
+
+`%{...}`
+
+Where the `%{` signals the start of a dynamic expansion, and `}`
+signals the end of the dynamic expansion. The contents of the
+expansion can be many things:
+
+.String Expansions
+[options="header"]
+|=====
+| Keyword | Description
+| xref:xlat/attribute.adoc[attributes] | Expand the value of a named attribute.
+| xref:xlat/character.adoc[single character] | Single character expansions.
+| xref:xlat/module.adoc[modules] | Pass a string to a module such as `sql`.
+| xref:xlat/alternation.adoc[condition] | Conditionally expand a string.
+| xref:xlat/builtin.adoc[built-in expansions] | Such as string length, tolower, etc...
+|=====
+
+This feature is used to create policies which refer to concepts rather
+than to specific values. For example, a policy can be created that
+refers to the User-Name in a request, via:
+
+`%{User-Name}`
+
+This string expansion is done only for double-quoted strings and for
+the back-tick operator.
+
+== Caveats
+
+Unlike other languages, there is no way to define new variables. All
+of the string expansions must refer to attributes that already exist,
+or to modules that will return a string value.
+
+== Character Escaping
+
+Some characters need to be escaped within a dynamically expanded
+string `%{...}`. The `%` character is used for variable expansion, so a
+literal `%` character can be created by using `%%`.
+
+Other than within a dynamically expanded string, very little
+character escaping is needed. The rules of the enclosing string context
+determine whether or not a space or " character needs to be escaped.
+
+.Example
+
+`Reply-Message := "%{User-Name} with a literal %%`
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/pages/xlat/module.adoc b/doc/antora/modules/unlang/pages/xlat/module.adoc
new file mode 100644
index 0000000..3ce4322
--- /dev/null
+++ b/doc/antora/modules/unlang/pages/xlat/module.adoc
@@ -0,0 +1,18 @@
+= Module References
+
+Individual modules may be referenced via the following syntax:
+
+`%{module:string}`
+
+These references are allowed only by a small number of modules that
+usually perform database lookups. The module name is the actual name of
+the module, as described earlier. The string portion is specific to each
+module and is not documented here. It is, however, usually dynamically
+expanded to allow for additional flexibility.
+
+.Examples
+
+`%{sql:SELECT name FROM mytable WHERE username = %{User-Name}}`
+
+// Copyright (C) 2020 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// Development of this documentation was sponsored by Network RADIUS SAS.
diff --git a/doc/antora/modules/unlang/partials/rcode_table.adoc b/doc/antora/modules/unlang/partials/rcode_table.adoc
new file mode 100644
index 0000000..e114e74
--- /dev/null
+++ b/doc/antora/modules/unlang/partials/rcode_table.adoc
@@ -0,0 +1,39 @@
+[options="header"]
+[cols="15%,85%"]
+|=====
+| Return code | Description
+| `fail` | The operation failed. Usually as a result of an
+ external dependency like a database being unavailable
+ or an internal error.
+| `handled` | The request has been "handled", no further policies
+ in the current section should be called, and the section
+ should immediately exit.
+| `invalid` | The request, or operation, was invalid. In the case of
+ requests this usually indicates absent or malformed
+ attribute values.
+| `noop` | The operation did nothing.
+| `notfound` | A 'lookup' operation returned no results.
+| `ok` | Operation completed successfully but did not change any
+ attributes in the request.
+| `reject` | The operation indicates the current request should be
+ 'rejected'. What this actually means is different from
+ protocol to protocol. It usually means that access to
+ the requested resource should be denied, or that the
+ current request should be NAKd. Usually returned when
+ provided credentials were invalid.
+| `updated` | The operation completed successfully and updated one
+ or more attributes in the request.
+| `disallow` | Access to a particular resource is
+ denied. This is similar to `reject` but is the result
+ of an authorizational check failing, as opposed to
+ credentials being incorrect.
+| `yield` | Returned by an operation when execution of a request should
+ be suspended.
+|=====
+
+[NOTE]
+====
+In versions ≤ v3.0.x the `disallow` rcode was called `userlock`. `disallow` and
+`userlock` have an identical meaning. `disallow` will be returned in any
+instance where `userlock` was returned in v3.0.x
+====