Hi,
I'm contributing a bugfix to the third-party project opnsense-exporter (https://github.com/AthennaMind/opnsense-exporter) (a Prometheus integration for OPNsense), and I'd appreciate some guidance from the OPNsense team / community on the intended/stable shape of a few fields in the Kea DHCP leases API responses.
ContextThe exporter consumes
POST /api/kea/leases4/search (and the v6 counterpart) to expose lease metrics for Prometheus scrapers. Between OPNsense releases the response shape for several fields appears to have changed type (e.g.
lifetime and
expiration shifting from strings to integers, per the original PR author of the Kea collector on the exporter side). On 26.1.8_5 we've now confirmed three additional type mismatches that break JSON unmarshaling in any client using strict struct typing:
- state (per row) — observed as a number (Kea native enum: 0 = default, 1 = declined, 2 = expired-reclaimed, 3 = released); earlier assumed by the consumer as a string.
- is_reserved (per row) — observed as an array of strings, e.g. [] (empty) or ["hwaddr"]; earlier assumed as a string.
- Top-level interfaces — observed as an object {"opt1": "..."} when non-empty, but [] (empty array) when the map is empty; earlier assumed as always an object/map.
The third one in particular feels like a PHP-side serialization quirk (empty associative array encoded as [] instead of {}), but I want to confirm rather than guess.
Sample responsesPOST /api/kea/leases4/search (4 v4 leases, 2 of them reserved):
{
"total": 4, "rowCount": 4, "current": 1,
"rows": [
{"hostname": "host-a", "state": 0, "is_reserved": [], ...},
{"hostname": "host-b", "state": 0, "is_reserved": ["hwaddr"], ...}
],
"interfaces": {"opt1": "05HomeNET"}
}
POST /api/kea/leases6/search (0 v6 leases):
{"total": 0, "rowCount": 0, "current": 1, "rows": [], "interfaces": []}
Questions- 1. For state: is the integer encoding (Kea's native enum) the intended/stable representation going forward, or should consumers expect this to be normalised differently?
- 2. For is_reserved: is the [] / ["<reservation source>"] array form intended? Are the possible string values documented anywhere (only "hwaddr" observed so far in our environment)?
- 3. For the top-level interfaces field: is the {}-vs-[] switch on empty maps something consumers should defensively handle, or is normalisation to {} planned?
- 4. More generally, is there a documented stability commitment for /api/kea/leases4/search and /api/kea/leases6/search response schemas? Or should third-party tooling treat these as "best-effort with shape drift between minor versions"?
Why this matters / where the answer goesThe exporter-side fix has been discussed and merged at AthennaMind/opnsense-exporter#105 (https://github.com/AthennaMind/opnsense-exporter/pull/105). Any guidance from this thread will be linked back there so the integration can be made more robust against minor-version drift going forward.
Thanks in advance — happy to provide more sample responses, packet captures, or test against other 26.1.x patch levels if helpful.
— Golden Garlic (garlicKim21 on GitHub)
Hello,
I changed the API response and added new fields because the old API response did not fit the need of the front end anymore.
All major redesigns of this API should be done now, it was mostly bug fixes.
You can see all the work in the history of this file:
https://github.com/opnsense/core/blob/master/src/opnsense/scripts/kea/get_kea_leases.py
and
https://github.com/opnsense/core/blob/master/src/opnsense/mvc/app/controllers/OPNsense/Kea/Api/LeasesController.php
If nobody finds anything else, it should be good now. The is_reserved array can also be inspected here, it will return one or multiple of "hwaddr, duid, client_id", depending on if one or multiple of these matched a reservation in KEA.
Hi Cedrik,
Thanks a lot for the detailed answer and the pointers to the source. After going through both files, all four points are now clear:
1. state — sourced directly from Kea's native lease state (lease.get("state", 0) in get_kea_leases.py), so always an integer (Kea's enum: 0=default, 1=declined, 2=expired-reclaimed, 3=released). Stable as long as Kea's own data model is.
2. is_reserved — your explanation matches the code in get_reservation_keys: any of "hwaddr", "duid", "client_id", with the array carrying one entry per matched identifier (and the subnet-id scoping comment on build_reserved_matches is a nice touch — explains exactly why client roaming doesn't false-positive).
3. Top-level interfaces — looking at LeasesController.php confirms it's the json_encode behaviour on an empty PHP array: $interfaces starts empty and only becomes associative once a record's interface resolves via the if-map. With zero matching records, it stays empty and serializes as a JSON array rather than an object. So consumers need to handle both shapes defensively (or, in our case, simply ignore the field — the per-interface map can be rebuilt from each row's if / if_descr).
4. API stability — "major redesigns done, mostly bug fixes from here" is exactly the commitment we needed. The careful subnet-id reasoning in the script reinforces that the design is being maintained thoughtfully. Good enough basis to commit to the current types on the exporter side.
I'll link this thread back at AthennaMind/opnsense-exporter#105 (https://github.com/AthennaMind/opnsense-exporter/pull/105) for the record. Thanks again for taking the time.
— Golden Garlic (garlicKim21 on GitHub)