Appearance
RADIUS attributes
What Kiwire sends and expects on the RADIUS side.
Kiwire4 doesn't run its own RADIUS server — FreeRADIUS talks to it over rlm_rest, posting form fields (derived from the real RADIUS packet via FreeRADIUS xlat expressions) to POST /radius with an action parameter. That one endpoint handles authorize, post-auth/check-cert, and accounting. Everything below is drawn from the code behind that endpoint and from the login check that builds the actual attribute maps.
Authentication request
Fields FreeRADIUS forwards on action=authorize, mapped from the real RADIUS Access-Request attributes:
| POST field | RADIUS attribute | Notes |
|---|---|---|
nasid | NAS-Identifier | Falls back to parsing station_id when empty (handles Meraki/Aruba, which don't always send NAS-Identifier). |
station_id | Called-Station-Id | Format AA-BB-CC-DD-EE-FF:SSID or AA:BB:CC:DD:EE:FF:SSID — split into controller MAC + SSID. |
username | User-Name | For EAP inner-tunnel requests, overridden from a cache of the outer-tunnel context. |
password | User-Password | Only forwarded/used for PAP — EAP/CHAP are verified by FreeRADIUS itself against the Cleartext-Password Kiwire returns. |
macaddress | Calling-Station-Id | Normalised (dashes → colons, lowercase) before use. |
controller_ip | NAS-IP-Address | |
controller_zone | (derived from Called-Station-Id) | The SSID/zone half of station_id. |
chap_challenge, chap_password | CHAP-Challenge, CHAP-Password | Presence of both flips auth type to CHAP. |
eap | presence of an EAP attribute | Flips auth type to EAP; auth_method carries the inner method (peap/ttls/tls/nak). |
auth_method | — | pap (default) unless CHAP/EAP detected. |
Auth-type detection order is CHAP → EAP → PAP fallback. Password verification in the login check is skipped entirely for eap/chap/mschapv2 and for login_type=nak — those are verified by FreeRADIUS itself using the control:Cleartext-Password Kiwire returns in the Accept.
Access-Accept reply attributes
Built by the login check once the account passes, keyed with a control:/reply: prefix that maps to FreeRADIUS's rlm_rest JSON convention:
| Attribute | Source | Notes |
|---|---|---|
control:Cleartext-Password | Decrypted stored password (or the PMS-typed password) | Lets FreeRADIUS do the actual PAP/CHAP comparison. |
control:Auth-Type | PAP/CHAP, set explicitly | Left unset for EAP — FreeRADIUS's eap module already resolves it. |
control:Simultaneous-Use | Profile attribute JSON key control:Simultaneous-Use | Not a database column — simultaneous_use was dropped from kiwire_profiles; it now lives only inside the JSON attribute blob, defaulting to 1 when absent. |
control:Kiwire-Total-Quota | profile.quota_limit (MB) × 1,048,576 → bytes | Internal-only key — always replaced by a vendor-specific quota VSA (or dropped) during the vendor translation step below; a NAS never sees this attribute name. |
reply:Session-Timeout | Computed from remaining credit, dynamic | Remaining seconds for a time-type profile: Access-Period − elapsed (expiration profiles) or Max-All-Session − user.session_time (countdown profiles). Recomputed on every login, not a static profile field. |
reply:Idle-Timeout | profile.idle_timeout (seconds) | Set only when > 0. |
reply:Acct-Interim-Interval | Profile attribute JSON key reply:Acct-Interim-Interval | Defaults to 300 s if not set. |
reply:WISPr-Bandwidth-Max-Down / Max-Up / Min-Up / Min-Down | Profile attribute/attribute_custom JSON | Zero values are stripped before the reply is sent — a literal 0 means "0 bps / block all traffic" to most NAS vendors, not "unlimited", so it must never reach the wire. |
Attribute merge order (later wins): explicit fields above → profile.attribute JSON (skipped for any key already set explicitly) → profile.attribute_custom JSON, which always overrides everything, including the explicit fields — it's the hand-edited escape hatch.
Accounting
action=accounting fields, mapped from the real Accounting-Request attributes:
| POST field | RADIUS attribute |
|---|---|
type | Acct-Status-Type (Start / Interim-Update / Stop) |
nasid | NAS-Identifier |
username | User-Name |
macaddress | Calling-Station-Id |
unique_id | Acct-Unique-Session-Id |
session_id | Acct-Session-Id |
station_id | Called-Station-Id (used as the zone) |
ipaddress | Framed-IP-Address |
controller_ip | NAS-IP-Address |
session_time | Acct-Session-Time (seconds, cumulative) |
quota_in / quota_out | Acct-Input-Octets / Acct-Output-Octets (32-bit, bytes) |
quota_in_gw / quota_out_gw | Acct-Input-Gigawords / Acct-Output-Gigawords |
event-time (note the dash) | Event-Timestamp |
terminate | Acct-Terminate-Cause |
Accounting processing applies the Gigawords expansion (32-bit counter rollover) before dispatch, normalises the MAC, de-duplicates identical packets for 10 seconds (nasid+unique_id+type+tenant), and serialises Start/Interim-Update/Stop for the same session behind a per-session lock. For EAP-TLS, the username FreeRADIUS sends on accounting packets is the certificate CN, not the real Kiwire username — it's resolved back via a 6-hour cache keyed by MAC+NAS (falling back to a direct cert_name_slug lookup on a cache miss).
Vendor-specific attributes
Quota is translated to a vendor-specific attribute in a single step, just before the reply is sent. It removes the internal control:Kiwire-Total-Quota (bytes) and, if the value exceeds 2³² (4,294,967,296), splits it into a 32-bit value + gigawords, same as native RADIUS counters:
Vendor (kiwire_controller.vendor) | Reply attributes |
|---|---|
mikrotik, ubnt-console, ubnt, ubnt_controller | reply:Mikrotik-Total-Limit + reply:Mikrotik-Total-Limit-Gigawords |
chillispot, pfsense, wifidog | reply:ChilliSpot-Max-Total-Octets + reply:ChilliSpot-Max-Total-Gigawords |
cambium | reply:CAMB-Traffic-Quota-Limit-Total + reply:CAMB-Traffic-Quota-Limit-Total-Gigwords (note: "Gigwords", not "Gigawords" — matches the actual Cambium dictionary spelling) |
everything else (allied-telesis, aruba, aruba_os, cisco_wlc, cmcc, engenius, fortiap, fortigate, h3c, huawei, huawei-cloud-ugw, huawei-nce, huawei-wlc, meraki, mist, motorola, nomadix, nomadix_xml, other, peplink, ruijie-reyee, ruckus_ap, ruckus_scg, ruckus_vsz, sonicwall, sundray, tplink, virtual-nas, xirrus, zyxelap) | none — the quota key is dropped silently; the NAS is expected to rely on Session-Timeout or its own accounting enforcement. |
Every non-quota attribute (Session-Timeout, Idle-Timeout, WISPr bandwidth, etc.) passes through that step unchanged for every vendor.
Two additional VSA dictionaries are used specifically on the CoA/disconnect path (see below), not the login Access-Accept: WISPr under vendor id 14122, and Mikrotik under vendor id 14988.
CoA and disconnect
Kiwire sends RFC 5176 CoA-Request / Disconnect-Request packets directly to the NAS (not through FreeRADIUS's REST module) when switching a live session to a different profile or force-disconnecting a device.
CoA-Request sets:
| Attribute | Source |
|---|---|
Session-Timeout | control:Access-Period if set, else control:Max-All-Session, from the target profile's attributes. |
| WISPr-Bandwidth-Max-Up (VSA 14122/7), Min-Up (14122/5), Max-Down (14122/8), Min-Down (14122/6) | Target profile's reply:WISPr-Bandwidth-*, only when > 0. |
| Mikrotik-Rate-Limit (VSA 14988/8) | Target profile's reply:Mikrotik-Rate-Limit, when present. |
Idle-Timeout | Target profile's reply:Idle-Timeout, when > 0. |
Event-Timestamp | Current time. |
| Session identification | Ruckus SCG/VSZ: Acct-Session-Id only. Every other vendor: User-Name + Framed-IP-Address, plus a vendor quota VSA if the target profile has control:Kiwire-Total-Quota — Mikrotik-Total-Limit (VSA 14988/17) for mikrotik, ChilliSpot-Max-Total-Octets (VSA 14559/3) for chillispot. |
Disconnect-Request sends session-identification attributes only (same Ruckus-vs-everyone-else split as above: Acct-Session-Id alone, or User-Name + Framed-IP-Address) — no profile or bandwidth attributes apply to a disconnect.
Not every vendor goes through native RADIUS CoA/Disconnect: UniFi (ubnt-console) is reauthorised/kicked via the UniFi controller API, and Nomadix bandwidth changes go through its own HTTP API — Kiwire picks the right transport per controller vendor before falling back to RADIUS CoA/Disconnect.