query

In query mode, you define named queries that construct independent RADIUS requests. This allows you to:

  • Send RADIUS requests from any protocol handler (HTTP, TACACS+, etc.)
  • Issue multiple RADIUS requests within a single execution context
  • Build custom RADIUS requests with specific attributes

Defining queries

Add query blocks within the backend configuration:

backends {
    radius "RADSEC_BACKEND" {
        server "radius.example.org" {
          # ... server configuration ...
        }

        query "AUTHENTICATE_USER" {
            # Set request attributes before sending
            bindings {
                radius.request.code = radius.ACCESS_REQUEST;
                radius.request.attr.User-Name = vars.username;
                radius.request.attr.User-Password = vars.password;
            }

            # Extract values from the reply
            mapping {
                vars.reply_code = "%{radius.reply.code}";
                vars.filter_id = "%{radius.reply.attr.Filter-Id}";
            }
        }
    }
}

Query bindings

The bindings block sets values on the RADIUS request before sending:

BindingDescription
radius.request.codeRADIUS packet code (string name or numeric code)
radius.request.attr.<Attribute-Name>Set a specific RADIUS attribute value
radius.request.attrBulk copy all RADIUS attributes (see below)

Values can be literal strings or references to context variables.

The radius.request.code binding accepts string names ("access-request", "accounting-request", etc.), numeric packet type codes, or predefined constants (radius.ACCESS_REQUEST, radius.ACCOUNTING_REQUEST, etc.).

The left side is the new request context being constructed, while the right side provides the values from the current execution context.

Query mapping

The mapping block extracts values from the RADIUS reply into the current context:

MappingDescription
radius.reply.codeRADIUS reply code (access-accept, access-reject, etc.)
radius.reply.attr.<Attribute-Name>Value of a specific attribute from the reply
radius.reply.attrBulk copy all reply attributes
radius.request.codeEcho of the sent request code
radius.request.attr.<Attribute-Name>Echo of a sent request attribute
radius.request.attrBulk copy all request attributes

Query results

The RADIUS backend query action returns a result based on the RADIUS reply code received from the upstream server:

RADIUS Reply CodeAction Result
Access-Acceptaccept
Accounting-Responseaccept
Disconnect-Ackaccept
CoA-Ackaccept
Access-Rejectreject
Disconnect-Nakreject
CoA-Nakreject
No response (timeout)error

Bulk attribute copy

The radius.request.attrs and radius.reply.attrs paths can be used to copy and write all attributes. This preserves wire-format bytes for non-encrypted attributes and correctly handles re-encryption of encrypted attributes (e.g. User-Password) using the destination context's shared secret.

This enables RADIUS proxying through query mode:

query "PROXY_REQUEST" {
    bindings {
        # proxy the request code as-is
        radius.request.code = radius.request.code;
        # proxy all request attributes forward without modification
        radius.request.attrs  = radius.request.attrs;
    }

    mapping {
        # proxy the reply code as-is
        radius.reply.code = radius.reply.code;
        # proxy all reply attributes back without modification
        radius.reply.attrs = radius.reply.attrs;
    }
}

In the example above, all request attributes from the incoming RADIUS packet are copied into the query's outgoing request. After the upstream server responds, all reply attributes are mapped back into the current context. This approach is useful when you need proxy-like behavior from a query, for example when combining proxying with other backend queries in the same handler.

Calling queries from handlers

Use the backend block with name and query parameters:

aaa {
    policy "DEFAULT" {
        handler "AUTHENTICATION" {
            @execute {
                # Set up variables for the query
                modify {
                    vars.username = "alice";
                    vars.password = "secret123";
                }

                # Execute the RADIUS query
                backend {
                    name "RADSEC_BACKEND";
                    query "AUTHENTICATE_USER";
                }

                # Use the results
                if all {
                    vars.reply_code == radius.ACCESS_ACCEPT;
                } then {
                    accept;
                } else {
                    reject;
                }
            }
        }
    }
}

Query from HTTP handler

RADIUS queries can be issued from any protocol. Here is an example using an HTTP server:

servers {
    http "HTTP_SERVER" {
        listen {
            protocol tcp;
            ip 0.0.0.0;
            port 4000;
        }
        clients "HTTP_CLIENTS";
        policy "DEFAULT";
    }
}

backends {
    radius "RADIUS_AUTH" {
        server "radius.example.org" {
           # ... server configuration ...
        }

        query "CHECK_USER" {
            bindings {
                radius.request.code = radius.ACCESS_REQUEST;
                radius.request.attr.User-Name = vars.http_user;
                radius.request.attr.User-Password = vars.http_password;
            }

            mapping {
                vars.auth_result = radius.reply.code;
            }
        }
    }
}

aaa {
    policy "DEFAULT" {
        handler "HTTP_AUTH" {
            @execute {
                modify {
                    vars.http_user = aaa.identity;
                    vars.http_password = aaa.password;
                }

                backend {
                    name "RADIUS_AUTH";
                    query "CHECK_USER";
                }

                if all {
                    vars.auth_result == radius.ACCESS_ACCEPT;
                } then {
                    accept;
                } else {
                    reject;
                }
            }
        }
    }
}

Filtering attributes in queries

The remove and select filters can be used in query bindings and mappings to filter RADIUS attributes.

remove(<args...>)

Removes attributes matching any of the specified arguments from the attribute collection. Arguments can be:

  • Attribute name (string): Remove by dictionary name, e.g., "User-Password", "cisco-avpair"
  • Attribute type (integer): Remove by RADIUS attribute type number, e.g., 26 for all Vendor-Specific attributes
  • Dictionary reference: Remove by dictionary constant, e.g., radius.dict.Calling-Station-Id
  • Regex pattern: Remove attributes whose dictionary name matches the pattern, e.g., /cisco/
  • Constant: Remove by RADIUS constant, e.g., radius.VENDOR_SPECIFIC (type 26)
query "PROXY_REQUEST" {
    bindings {
        radius.request.code = radius.request.code;
        # Remove specific attributes before copying the rest
        radius.request.attrs = radius.request.attrs | remove("User-Password", "cisco-avpair");
    }

    mapping {
        radius.reply.code = radius.reply.code;
        # Remove Session-Timeout from the reply before mapping back
        radius.reply.attrs = radius.reply.attrs | remove("Session-Timeout");
    }
}

Remove all Vendor-Specific (type 26) attributes:

radius.request.attrs = radius.request.attrs | remove(radius.VENDOR_SPECIFIC);

Remove attributes matching regex patterns:

bindings {
    radius.request.attrs = radius.request.attrs | remove(/^cisco/, /^framed/);
}

select(<args...>)

Keeps only attributes matching any of the specified arguments, removing everything else. Arguments use the same format as remove.

query "PROXY_SELECTED" {
    bindings {
        radius.request.code = radius.request.code;
        # Keep only these attributes, remove all others
        radius.request.attrs = radius.request.attrs | select("User-Name", "Calling-Station-Id", "NAS-IP-Address", "Service-Type");
    }
}

Matching by attribute type vs name

When filtering by attribute type number (e.g., 26 or radius.VENDOR_SPECIFIC), all attributes of that type are matched regardless of vendor. When filtering by attribute name (e.g., "cisco-avpair"), only the specific attribute is matched.

  • remove(26) or remove(radius.VENDOR_SPECIFIC): Removes all Vendor-Specific attributes (type 26), regardless of vendor
  • remove("cisco-avpair"): Removes only the Cisco-AVPair attribute (vendor 9, type 1)
  • remove("Service-Type"): Removes only Service-Type attributes (type 6)

Regex matching behavior

Regex patterns use partial (unanchored) matching by default. The pattern /cisco/ matches any attribute whose dictionary name contains "cisco" anywhere in the string. Attribute names are always converted to lowercase before matching, so regex patterns are effectively case-insensitive for attribute names. Use anchors if you need exact positioning:

  • /cisco/: Matches cisco-avpair, Cisco-nas-port, etc. (substring match)
  • /^cisco/: Matches only names starting with "cisco"
  • /^cisco-avpair$/: Matches only the exact name "cisco-avpair"

Unknown attribute names

If a string argument does not match any known dictionary attribute name, the filter produces a runtime error. This includes attribute names passed via dynamic variables. When this happens the query fails and no reply is sent to the client, resulting in a timeout.

Filtering individual values of a multi-value attribute

The remove and select filters also work on individual values of a multi-value attribute. Use the [*] accessor to read all values, pipe them through the filter, and assign the result back:

query "PROXY_FILTER_AVPAIRS" {
    bindings {
        radius.request.code = radius.request.code;
        radius.request.attrs = radius.request.attrs;
        # Remove individual cisco-avpair values matching the regex
        radius.request.attr.cisco-avpair = radius.request.attr.cisco-avpair[*] | remove(/^audit-session-id=.+/);
    }

    mapping {
        radius.reply.code = radius.reply.code;
        radius.reply.attrs = radius.reply.attrs;
    }
}

In the example above, if the request contains two Cisco-AVPair values audit-session-id=0a1b2c3d and shell:priv-lvl=15, the remove(/^audit-session-id=.+/) filter discards the first value and keeps the second.

The select filter works the same way but keeps only matching values:

radius.request.attr.cisco-avpair = radius.request.attr.cisco-avpair[*] | select(/^shell:priv-lvl=.+/);

Regex and exact string arguments can be mixed. Exact string matching compares the full value content and is case-sensitive:

# Keep values matching the regex OR the exact string "shell:priv-lvl=15"
radius.request.attr.cisco-avpair = radius.request.attr.cisco-avpair[*] | select(/^audit-session-id/, "shell:priv-lvl=15");

When applied to individual attribute values, only regex patterns and exact string patterns are meaningful as arguments. Attribute type numbers and dictionary references are not applicable because the attribute type is already determined by the accessor.

For general information about filters, see the Filters article.