The unlang policy language is compatible with v2 but has a number of new features. See man unlang for complete documentation.


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, the following can be done:

update {
    reply:Class := 0x0000
    control:Cleartext-Password := "hello"

This change means that update sections are needed.


Attribute comparisons can be done via the & operator. When two attributes required comparison, the old style was as follows:

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. The two attributes can now be compared directly:

if (&User-Name = &control:Tmp-String-0) {

See man unlang for more details.


Casts are now permitted. This allows forced type-specific comparisons:

if ("%{sql: SELECT...}" = {

This forces the string returned by the SELECT to be treated as an IP address and to compare to Previously, the comparison would have been done as a simple string comparison.

IP Address Networks

IP networks are now supported:

if ( = {

Will be true. The various comparison operators can be used to check IP network membership:

if (127/8 > {

Returns true, because is within the 127/8 network. However, the following comparison will return false:

if (127/8 > {

because is outside of the 127/8 network.


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:

if (0) {

In v2, the above 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 for dynamic configurations where certain parts are used (or not) depending on compile-time configuration.

Similarly, conditions that always evaluate to true will be optimized away:

if (1) {

That configuration will never show the if (1) output in debugging mode.