ipmap
The ipmap backend provides an in-memory IP prefix map that performs longest-prefix matching lookups. Populate the map from any other backend, such as SQLite or PostgreSQL, then query it at runtime to resolve an IP address to one or more values associated with its longest matching network prefix.
Each map entry has one required network prefix and zero or more named values. Set the prefix with ipmap.network, and set values with arbitrary ipmap.* fields such as ipmap.name, ipmap.secret, ipmap.site, or ipmap.vlan. When a lookup matches an entry, the query mapping can read those fields by their names, for example name, secret, site, and vlan.
Typical use cases include:
- Classifying RADIUS requests by the network the NAS or client belongs to
- Dynamic RADIUS client resolution in
@pre-clienthooks (per-IP shared secrets) - Mapping IP addresses to site names, VLAN assignments, or policy identifiers
Example
Dynamic RADIUS client resolution using a @pre-client hook. This populates the ipmap from an SQLite database containing per-client /32 prefixes and their shared secrets, then resolves the secret for each incoming request based on the client IP address:
backends {
sqlite "SQLITE" {
filename "/var/lib/radiator/clients.sqlite";
query "POPULATE_CLIENTS" {
statement "SELECT network, name, secret FROM clients";
mapping {
ipmap.network = network;
ipmap.name = name;
ipmap.secret = secret;
}
}
}
ipmap "RADIUS_CLIENTS" {
# Load the map data immediately on server startup
prepopulate true;
# Re-run the @populate pipeline every 5 minutes to refresh the map
# with a random delay of up to 30 seconds to avoid thundering herd
interval {
duration 5m;
random 30s;
}
@populate {
backend {
name "SQLITE";
query "POPULATE_CLIENTS";
}
}
query "LOOKUP_CLIENT" {
lookup radius.client.ip;
mapping {
radius.client.secret = secret;
}
}
}
}
servers {
radius "RADIUS" {
listen {
protocol udp;
port 1812;
ip 0.0.0.0;
}
@pre-client {
backend {
name "RADIUS_CLIENTS";
query "LOOKUP_CLIENT";
}
}
}
}
When the ipmap lookup succeeds, radius.client.secret is set from the matched prefix entry, allowing Radiator to validate the RADIUS request authenticator against the dynamically resolved secret. If no prefix matches, the @pre-client hook rejects the request.
NAS classification example
Populate an ipmap from an SQLite backend and look up NAS-IP-Address at authentication time to classify devices:
backends {
sqlite "SQLITE" {
filename "/var/lib/radiator/devices.sqlite";
query "POPULATE_IPMAP" {
statement "SELECT network, name FROM devices";
mapping {
ipmap.network = network;
ipmap.name = name;
}
}
}
ipmap "IPMAP" {
query "CHECK_NAS" {
lookup radius.request.attr.NAS-IP-Address;
mapping {
vars.device_name = name;
}
}
@populate {
backend {
name "SQLITE";
query "POPULATE_IPMAP";
}
}
}
}
Configuration Options
interval
Set an automatic repopulation schedule. When configured, the ipmap periodically re-executes its @populate pipeline to refresh the prefix map contents.
Multiple interval entries can be defined in the same ipmap block. When multiple intervals are present, the server evaluates all of them and picks the closest upcoming scheduled time for the next repopulation. This is useful for defining multiple cron-based schedules (for example, different times of day or specific calendar dates).
The full form uses a block with either a cron or duration parameter and an optional random jitter:
# Cron-based interval with random jitter
interval {
cron "0 30 * * * * *";
random 10s;
}
# Duration-based interval with random jitter
interval {
duration 5m;
random 30s;
}
cron
A seven-field cron expression string that defines the repopulation schedule. The fields are: second, minute, hour, day-of-month, month, day-of-week, year.
duration
A duration value that specifies the interval between repopulations.
A block cannot contain both cron and duration.
random
Optional. A duration value specifying a maximum random jitter added to each scheduled repopulation. The actual delay added is uniformly distributed between zero and the specified value. Use this to prevent multiple Radiator instances from repopulating simultaneously when they share the same schedule.
Shorthand syntax
For simple schedules without jitter, a shorthand form is available:
# Duration shorthand
interval 5m;
# Cron shorthand
interval "0 0 * * * * *";
The shorthand is equivalent to a block with only cron or duration and no random.
If omitted, the ipmap populates once on first use and does not automatically refresh. Use the REST API or configure an interval to trigger subsequent repopulations.
prepopulate
Populate the map immediately when the backend is created:
prepopulate true;
When set to true, Radiator runs the @populate pipeline during backend creation instead of waiting for the first lookup. This avoids the lazy-population startup window for the backend.
If omitted or set to false, the ipmap keeps the existing behavior and populates lazily on first use.
query
Define a named lookup query. Each query specifies an expression to evaluate for the IP address and an optional mapping to apply when a match is found.
query "CHECK_NAS_IP_ADDRESS" {
lookup radius.request.attr.NAS-IP-Address;
mapping {
vars.device_name = name;
vars.client_secret = secret;
}
}
lookup
Required. An expression that resolves to the IP address used for the longest-prefix match. The expression result is interpreted as an IP address. Accepted value types include IP addresses, IP networks (the network address is used), and strings in standard IP address or CIDR notation.
mapping
Optional. Attribute mappings applied when a matching prefix is found. Within the mapping block, each named value assigned during population is available as a source identifier:
| Identifier | Description |
|---|---|
name | Value from ipmap.name on the matched prefix |
secret | Value from ipmap.secret on the matched prefix |
site | Value from ipmap.site on the matched prefix |
vlan | Value from ipmap.vlan on the matched prefix |
These identifiers can be mapped to any writable target attribute, such as vars.*, radius.reply.attr.*, or radius.client.*. If a field was not set on the matched entry, the identifier evaluates to none.
When no matching prefix is found, the query result is reject and aaa.reason is set to a descriptive message including the IP address that failed to match.
@populate
Define the pipeline executed to populate the IP prefix map. The populate block contains one or more backend calls whose result mappings write to ipmap.* target fields:
@populate {
backend {
name "SOURCE_BACKEND";
query "POPULATE_QUERY";
}
}
The source backend query mapping must assign ipmap.network. Named value fields are optional:
| Field | Required | Description |
|---|---|---|
ipmap.network | yes | IP network prefix in CIDR notation (e.g. 192.0.2.0/24 or 10.0.0.1/32) |
ipmap.<name> | no | Named value to associate with the prefix. Use any field name except network |
An entry is committed to the map once ipmap.network has been assigned. Assignment order does not matter for value fields, but ipmap.network starts each entry and must be set for every entry. A network-only entry can be used when the lookup result only needs to confirm that an IP address matches a configured prefix.
Example populate mapping from a SQL source:
query "POPULATE_IPMAP" {
statement "SELECT network, name, secret FROM devices";
mapping {
ipmap.network = network;
ipmap.name = name;
ipmap.secret = secret;
}
}
Multiple backend calls within a single @populate block are supported. Each call appends entries to the same prefix map:
@populate {
backend {
name "BACKEND_A";
query "POPULATE_SITE_A";
}
backend {
name "BACKEND_B";
query "POPULATE_SITE_B";
}
}
For static or small deployments, populate the map directly using modify actions instead of querying an external backend:
ipmap "IPMAP" {
query "CHECK_NAS" {
lookup radius.request.attr.NAS-IP-Address;
mapping {
vars.network_name = name;
}
}
@populate {
modify {
ipmap.network = "192.0.2.0/24";
ipmap.name = "internal-network";
ipmap.site = "datacenter-a";
}
modify {
ipmap.network = "198.51.100.0/24";
ipmap.name = "guest-network";
ipmap.site = "datacenter-b";
}
}
}
Each modify block assigns one entry. Use multiple modify blocks to add all required prefixes.
Population Behavior
By default, the ipmap populates lazily on first use. If a query executes before the map has been populated, population runs synchronously before the lookup proceeds.
If prepopulate true; is configured, the initial population happens during backend creation instead.
If multiple requests arrive concurrently before the initial lazy population completes, only the first request triggers the populate pipeline. Other concurrent requests proceed immediately with a lookup against the still-empty map, which results in no match. In practice this means that during the brief window of initial lazy population, concurrent requests may be rejected. Once population finishes, all subsequent lookups use the populated map. To avoid this startup window, set prepopulate true; or arrange for a warm-up request before the server begins accepting production traffic.
After initial population, the map remains static unless:
- An
intervalis configured for automatic periodic repopulation - A manual repopulation is triggered via the REST API
During repopulation, the entire map is replaced atomically. Concurrent lookups continue to use the previous snapshot until the new map is published.
Resilience to Source Backend Outages
Once populated, the ipmap serves all lookups entirely from its in-memory snapshot. Query execution does not contact the source backend at all - it is a pure local memory lookup. This means the ipmap continues to respond correctly even if the source backend (database, LDAP server, etc.) becomes unavailable after population.
If a scheduled or manual repopulation fails because the source backend is unreachable, the ipmap retains its last successfully populated snapshot and continues serving lookups from it. This makes the ipmap suitable for use cases where lookup availability must not depend on the health of an external data source, such as dynamic RADIUS client resolution in @pre-client hooks.
Performance Characteristics
The ipmap is designed for high-throughput, low-latency lookups:
- No network I/O - Lookups are pure in-memory operations. There are no network round-trips, database connections, or disk access involved in query execution.
- Concurrent reads without blocking - Multiple requests perform lookups simultaneously without waiting for each other, regardless of load.
- Fast lookup regardless of map size - Lookup time depends only on the IP address length, not on the number of entries. A map with millions of prefixes performs the same as one with a handful of entries.
- Non-blocking repopulation - Lookups continue uninterrupted while the map is being repopulated. The updated map becomes visible atomically once repopulation completes.
These properties make the ipmap well suited for the critical path of every request, including @pre-client hooks.
REST API
Trigger manual repopulation by sending a POST request to the management API:
POST /api/v1/backends/{backend_name}/ipmap/populate
This endpoint requires write authorization. On success it returns HTTP 200. The backend name in the URL must match the configured ipmap backend name.
IPv6 Support
The ipmap backend supports both IPv4 and IPv6 prefixes in the same map. Use standard IPv6 CIDR notation for ipmap.network values (e.g. 2001:db8::/32, 2001:db8:abcd::/48, fe80::1/128). Longest-prefix matching works identically for both address families - the most specific matching prefix wins regardless of whether the address is IPv4 or IPv6.
IPv6 prefix lengths range from /0 to /128. A /128 prefix represents a single host address, analogous to /32 in IPv4.
IPv6 example
Classify requests based on a Framed-IPv6-Address attribute using nested prefixes of varying specificity:
backends {
ipmap "IPMAP" {
query "LOOKUP_IPV6" {
lookup radius.request.attr.Framed-IPv6-Address;
mapping {
vars.ipv6_name = name;
}
}
@populate {
modify {
ipmap.network = "2001:db8::/32";
ipmap.name = "broad-32";
}
modify {
ipmap.network = "2001:db8:abcd::/48";
ipmap.name = "subnet-48";
}
modify {
ipmap.network = "2001:db8:abcd:1234::/64";
ipmap.name = "subnet-64";
}
modify {
ipmap.network = "2001:db8:abcd:1234::42/128";
ipmap.name = "host-128";
}
}
}
}
With this configuration:
- A lookup for
2001:db8:abcd:1234::42matches the/128host entry and returnshost-128 - A lookup for
2001:db8:abcd:1234::1matches the/64subnet and returnssubnet-64 - A lookup for
2001:db8:abcd:ffff::1matches the/48subnet and returnssubnet-48 - A lookup for
2001:db8:1::1matches the broad/32prefix and returnsbroad-32 - A lookup for
2001:db9::1matches no entry and the query returnsreject