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:
| Binding | Description |
|---|---|
radius.request.code | RADIUS packet code (string name or numeric code) |
radius.request.attr.<Attribute-Name> | Set a specific RADIUS attribute value |
radius.request.attr | Bulk 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:
| Mapping | Description |
|---|---|
radius.reply.code | RADIUS reply code (access-accept, access-reject, etc.) |
radius.reply.attr.<Attribute-Name> | Value of a specific attribute from the reply |
radius.reply.attr | Bulk copy all reply attributes |
radius.request.code | Echo of the sent request code |
radius.request.attr.<Attribute-Name> | Echo of a sent request attribute |
radius.request.attr | Bulk 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 Code | Action Result |
|---|---|
| Access-Accept | accept |
| Accounting-Response | accept |
| Disconnect-Ack | accept |
| CoA-Ack | accept |
| Access-Reject | reject |
| Disconnect-Nak | reject |
| CoA-Nak | reject |
| 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.,
26for 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)orremove(radius.VENDOR_SPECIFIC): Removes all Vendor-Specific attributes (type 26), regardless of vendorremove("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/: Matchescisco-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.