Lunar - Dictionaries
RADIUS is a binary protocol: on the wire every attribute is just a numeric code and a blob of bytes. A dictionary is what turns those numbers into the named, typed fields you work with in the API. It maps each code to a human name (User-Name), a data type (text, integer, ipv4addr, ...) and, for enums, the meaning of specific values (Acct-Status-Type 1 is Start). Lunar loads its dictionaries at start-up and uses them to decode every inbound packet and encode every reply.
Lunar ships a comprehensive set of standard (RFC) and vendor dictionaries under /opt/lunar/share/lunar/radius/, so most deployments never need to touch them. When you do (a new vendor, a private attribute) the format is designed to be trivial to read and extend.
Why YAML
Traditional RADIUS servers use a bespoke dictionary text format with its own keywords (ATTRIBUTE, VALUE, BEGIN-VENDOR, ...) that every tool has to parse specially. Lunar uses plain YAML instead, for a few reasons:
Machine-simple. YAML parses to an ordinary map/list structure with an off-the-shelf library. No custom parser, no bespoke grammar to maintain.
Human-simple. The structure is the data: an attribute is a name with a code and a type under it. There is nothing to learn beyond "name, code, type".
Easily extensible. Adding an attribute is adding a few indented lines. Adding a vendor is a new file listed in one includes: array. No merges into a monolith, no ordering rules to memorize.
Diff-friendly. Because every attribute is its own small block, changes show up as clean, reviewable diffs in version control.
How the dictionaries are organized
The master file is dictionary.yaml. It contains almost nothing itself, just an includes: list naming every other dictionary file to load, in order:
includes:
- ascend_illegal.yaml
- rfc2865.yaml
- rfc2866.yaml
- rfc2868.yaml
# ... the rest of the RFC dictionaries ...
- iana.yaml
- cisco.yaml
- juniper.yaml
# ... one file per vendor ...
Each included file defines a self-contained slice of the dictionary: one RFC, or one vendor. Lunar loads them in listed order into a single combined dictionary with two lookups: by name and by code.
Load order matters for conflicts. A handful of legacy vendor dictionaries illegally define attributes in the IETF-managed code space (1-255). Those are listed first (e.g. ascend_illegal.yaml) so the standard RFC dictionaries loaded after them win the by-code map and own the decode of any conflicting code. The legacy names stay resolvable by name for operators with older gear, but the RFC meaning is what decodes off the wire.
Attribute definitions
Each dictionary file has an attributes: map. Every entry is keyed by the attribute name and carries its code and type:
attributes:
User-Name:
code: 1
type: text
User-Password:
code: 2
type: text
encrypt: 1
NAS-IP-Address:
code: 4
type: ipv4addr
NAS-Port:
code: 5
type: integer
code: the attribute's numeric identity on the wire. For a standard attribute it is a plain number. For a vendor attribute it is a dotted path (see "Vendor dictionaries" below).
type: the data type, which tells lunar how to read and write the value.
encrypt: (optional) marks a value that is obfuscated on the wire and must be decrypted with the shared secret. 1 = User-Password hiding (RFC 2865 s5.2), 2 = Tunnel-Password (RFC 2868). Lunar decrypts these before handing the packet to your backend.
has_tag: (optional) true for tagged attributes (RFC 2868), where a small tag number groups related attributes (chiefly tunnel attributes). See Kafka and the RADIUS Integration guide for how tags appear in the JSON (name:tag).
Data types
The type is one of lunar's RADIUS data types. The common ones:
text: a UTF-8 string.
string: opaque binary (also seen as octets), emitted as 0x hex.
integer / short / byte: unsigned 32 / 16 / 8-bit numbers.
integer64: a 64-bit counter (e.g. where a 32-bit octet counter wraps).
signed: a signed 32-bit number.
ipv4addr / ipv6addr: IP addresses.
ipv4prefix / ipv6prefix: an address with a prefix length.
ifid: an IPv6 interface identifier.
ether: a MAC address.
time: a timestamp.
combo: an attribute that may be IPv4 or IPv6.
vsa: the container type of the Vendor-Specific attribute (code 26).
tlv / evs / extended: structured / extended attribute encodings (RFC 6929).
See Attribute value format in the Kafka reference for exactly how each type is written in the JSON the API consumes and emits.
Enum (named) values
Many integer attributes are enums: specific numbers have human names. Those are declared in a separate values: map in the same file, keyed by attribute name then value name:
values:
Acct-Status-Type:
Start: 1
Stop: 2
Interim-Update: 3
Accounting-On: 7
Accounting-Off: 8
Acct-Terminate-Cause:
User-Request: 1
Idle-Timeout: 4
Session-Timeout: 5
With this in place lunar accepts either the number or the name on input, and emits the name (in lower case) on output where one is known. See Enum attributes in the RADIUS Integration guide. If the dictionary has no name for a value, the bare number is used.
Vendor dictionaries
Vendor-Specific Attributes (VSAs, RFC 2865 s5.26) live in per-vendor files. A vendor file adds a title: and a vendor: block giving the vendor's IANA enterprise number, and its attribute codes are dotted, 26.<vendor-id>.<n> (26 is the Vendor-Specific code, then the vendor's enterprise number, then the vendor's own attribute number):
title: Cisco Systems
vendor:
id: 9
name: Cisco
attributes:
Cisco-AVPair:
code: 26.9.1
type: text
Cisco-NAS-Port:
code: 26.9.2
type: text
Enums for vendor attributes go in the same values: block shape as standard ones.
A real vendor dictionary: ERX and NetElastic
The Cisco snippet above is deliberately tiny. Here are two real vendor files lunar ships, so you can see what a production dictionary actually looks like. Both live in /opt/lunar/share/lunar/radius/ and are loaded via dictionary.yaml.
ERX: Juniper MX BNG
erx.yaml is the Juniper Networks / Unisphere / ERX dictionary (IANA vendor 4874). It is the VSA set the Juniper MX Series uses when it acts as a Broadband Network Gateway (BNG). In a subscriber deployment it is the vendor file you will reach for most often, covering address pools, virtual routers, ingress/egress QoS policies, ATM parameters and so on. The full file (~780 lines) defines the complete ERX VSA set. Below is its head plus a few representative attributes and enums:
title: Juniper Networks/Unisphere/ERX
vendor:
id: 4874
name: ERX
attributes:
ERX-Virtual-Router-Name:
code: 26.4874.1
type: text
ERX-Address-Pool-Name:
code: 26.4874.2
type: text
ERX-Primary-Dns:
code: 26.4874.4
type: ipv4addr
ERX-Secondary-Dns:
code: 26.4874.5
type: ipv4addr
ERX-Tunnel-Virtual-Router:
code: 26.4874.8
type: text
has_tag: true
ERX-Tunnel-Password:
code: 26.4874.9
type: text
has_tag: true
ERX-Ingress-Policy-Name:
code: 26.4874.10
type: text
ERX-Egress-Policy-Name:
code: 26.4874.11
type: text
ERX-Atm-Service-Category:
code: 26.4874.14
type: integer
# ... many more attributes (26.4874.15 onward) ...
values:
ERX-Ingress-Statistics:
disable: 0
enable: 1
ERX-Atm-Service-Category:
UBR: 1
UBRPCR: 2
nrtVBR: 3
CBR: 4
# ... more enums ...
Everything the format offers is visible here:
Every code is dotted 26.4874.<n>: the Vendor-Specific code 26, Juniper's enterprise number 4874, then the attribute's own number.
Types are the ordinary lunar types: text for names and policies, ipv4addr for the DNS servers, integer for the ATM parameters.
ERX-Tunnel-Virtual-Router and ERX-Tunnel-Password carry has_tag: true: they are tagged (RFC 2868), so several tunnels can be grouped, and in the API they appear as erx-tunnel-virtual-router:1 etc.
The values: block names the enums (ERX-Atm-Service-Category value 1 is UBR) and the disable / enable toggles map 0 / 1. Your backend may send or receive either the name or the number.
NetElastic
netelastic.yaml is the NetElastic dictionary (IANA vendor 54268), used by NetElastic's virtual BNG. It is smaller and shows the same shape: rate limits as integer, DNS as ipv4addr, an ipv6addr, and NAT port-range attributes:
title: NETELASTIC
vendor:
id: 54268
name: NetElastic
attributes:
NetElastic-Input-Average-Rate:
code: 26.54268.2
type: integer
NetElastic-Output-Average-Rate:
code: 26.54268.5
type: integer
NetElastic-Qos-Profile-Name:
code: 26.54268.31
type: text
NetElastic-Primary-DNS:
code: 26.54268.135
type: ipv4addr
NetElastic-Framed-IPv6-Address:
code: 26.54268.158
type: ipv6addr
NetElastic-NAT-Public-Address:
code: 26.54268.161
type: ipv4addr
NetElastic-NAT-Start-Port:
code: 26.54268.162
type: integer
NetElastic-NAT-End-Port:
code: 26.54268.163
type: integer
# ... more attributes ...
Both files are complete and ready to use: if your NAS is a Juniper MX or a NetElastic vBNG, you already have its attributes by name. For any other vendor, copy one of these as a template (a title, a vendor block, then the attributes and any values) and add it to includes: as described next.
Adding your own attributes
To add an attribute or a whole vendor:
Create a new <vendor>.yaml (or edit an existing file) using the format above (a vendor: block for VSAs, then the attributes: and any values: entries).
Add the filename to the includes: list in dictionary.yaml. Order only matters if you are intentionally overriding a code. Otherwise append it.
Restart lunar so it reloads the dictionaries.
An attribute lunar does not have in its dictionary is not lost. It is still carried through, keyed by its numeric code with an attr- prefix and typed as opaque bytes (see Unknown / vendor attributes in the Kafka reference). Adding it to a dictionary is simply what gives it a friendly name and a real type.