DHCP leases
The DHCP server is the one system that knows, for every address it hands out,
which MAC asked for it, what the device calls itself and until when the binding
holds — on any network, whatever the firewall. The dhcp-leases connector
reads the lease store itself: the lease hostname becomes the device name
(name_source: dhcp, via DHCP) until the lease ends, the MAC↔IP binding and
the client identifier reach the identity model as anchors whose window is the
lease, and the device detail shows the live lease.
It is a Free connector. Before it, the only DHCP names were the ones the IDS saw cross the mirror; a firewall’s syslog feeds carry IDS and NetFlow records, not leases.
Three inputs, one connector
Section titled “Three inputs, one connector”The mode is chosen by the required field mode:
mode |
Reads | Needs |
|---|---|---|
kea-memfile |
Kea’s CSV lease file kea-leases4.csv (and path6: kea-leases6.csv) |
The file readable by the service user, locally or through a read-only mount |
isc-leases |
ISC dhcpd’s dhcpd.leases (DHCPv4 form) |
The same |
kea-api |
The Kea Control Agent (lease4-get-page, lease4-get-all, dhcp6 on request) |
The lease_cmds hook library loaded in Kea; optional basic auth, TLS pin, client certificate |
The file modes open no network connection: test --live and
collect --once --live simply read the configured file as the service user,
which is exactly the check an operator needs. A poll costs one stat unless
the file changed. The Control Agent mode asks the agent every two minutes
unless poll_interval says otherwise, and recomputes its counters in between.
What a lease becomes. Only a lease that is binding right now — active, and
its end still ahead — becomes an item. It carries mac (or mac_random) plus
mac_oui, dhcp_hostname from the host label of the recorded name (so Kea’s
laptop-01.home.arpa and an IDS-observed laptop-01 are one name), duid
from the client identifier (a DHCPv4 option 61 that merely repeats the MAC is
dropped; a DHCPv6 DUID is kept from seven bytes up), and dhcp_fingerprint
from the vendor class where the server kept it (ISC). Each anchor’s window is
the lease: first seen at the lease start — bounded, see below — and last seen
at the poll. The inventory record carries the name, the mode, the MAC, the
client identifier, the vendor class (option 60), the subnet and the lease
start and end.
An expired, released or declined lease contributes nothing. Its name stays listed as a former name of the device, and what it proved ages out of the identity job’s window on its own.
How far back a lease start may reach. An anchor’s first-seen is the
recorded lease start, but not at any distance. Since 0.27 it is bounded to the
lease’s own lifetime (end minus start) or the identity model’s 24-hour gap,
whichever is longer, and never further back than seven days. A lease
server’s longest lifetime is measured in days; a start further back is a
back-dated record — an ISC starts or a Kea cltt edited by hand, or a server
whose clock was wrong when it bound the lease — and taking it at its word would
open an anchor window that meets every other window since, through which the
identity job would merge unrelated devices. A lease without a recorded start,
or one that starts in the future, is first seen at the poll. The bound applies
to the anchor only: lease_start in the inventory record and in the API’s
dhcp object stays the raw value.
Kea specifics. Columns are read by header name, so lease-file schema 2.0
through 4.0 read; the file is an append-only journal and the last record per
address is the current one; state 1/2/3 (declined, expired-reclaimed,
released) and a zero lifetime (how Kea before 2.7 deletes) are read as such,
4294967295 is a lease without an end, and a delegated prefix (lease_type
2) is skipped. Kea’s lease file cleanup rewrites the file by rename: a missing
file is re-checked once after a quarter second, a smaller file is re-parsed,
and a last line without its ending is a write in progress — skipped and read on
the next poll rather than counted as a failure. A file over 64 MiB is
too_large.
ISC specifics. lease blocks with starts/ends (weekday form, epoch,
never), binding state (active; expired, released; abandoned counts
as declined), hardware ethernet, uid in both the octal-string and the
colon-hex spelling, client-hostname and set vendor-class-identifier. Later
blocks supersede earlier ones. Only the DHCPv4 file is read.
Set it up
Section titled “Set it up”1a. Kea memfile, local or mounted
Section titled “1a. Kea memfile, local or mounted”On a Debian or Ubuntu Kea the file is /var/lib/kea/kea-leases4.csv, owned by
_kea and not group-readable. Do not widen the Kea directory; give the service
user a read-only view instead. With Kea and the appliance on one host:
setfacl -m u:nyxtrace:rx /var/lib/keasetfacl -m u:nyxtrace:r /var/lib/kea/kea-leases4.csvsetfacl -d -m u:nyxtrace:r /var/lib/kea # the LFC rewrite creates a new fileWith Kea elsewhere, export /var/lib/kea read-only over NFS to the appliance
(ro,root_squash, mounted ro,noexec,nosuid at /srv/dhcp/kea), or copy the
file every minute with rsync over an SSH forced command; both hand the
connector an ordinary path. The connector never writes to the file and never
needs the .1, .2 or .completed siblings the cleanup leaves behind.
1b. OPNsense 25.x with Kea
Section titled “1b. OPNsense 25.x with Kea”OPNsense runs Kea with the memfile backend and keeps the lease files under
/var/db/kea/ — kea-leases4.csv and, with DHCPv6 enabled, kea-leases6.csv.
Those are the paths the OPNsense Kea plugin writes into
/usr/local/etc/kea/kea-dhcp4.conf; check lease-database.name there if a
release moved them.
There is no supported way to hand a foreign process a read-only view of that
directory from the OPNsense interface, so the documented path is a copy: an
rsync (or scp) of /var/db/kea/kea-leases4.csv every minute from the
appliance, with a key restricted to that one command in the firewall’s
authorized_keys:
command="/usr/local/bin/rsync --server --sender . /var/db/kea/kea-leases4.csv" ssh-ed25519 AAAA… nyxtrace-leasesinto a directory the service user can read, with path pointing at the copy.
Use rsync --temp-dir so the copy is renamed into place atomically and the
connector sees either the old or the new file.
1c. ISC dhcpd
Section titled “1c. ISC dhcpd”path: /var/lib/dhcp/dhcpd.leases (Debian) or /var/lib/dhcpd/dhcpd.leases
(RHEL), made readable the same way as the Kea file. dhcpd rewrites the file
periodically by rename; the connector handles it as it does Kea’s cleanup.
1d. Kea Control Agent
Section titled “1d. Kea Control Agent”Load lease_cmds in kea-dhcp4.conf (and kea-dhcp6.conf when ipv6: true), enable kea-ctrl-agent with an http-host/http-port the appliance
can reach, and prefer its authentication block (type: basic) plus TLS
(trust-anchor, cert-file, key-file; cert-required: true for a client
certificate). The connector sends version-get and lease4-get-page (falling
back to lease4-get-all on a server without paging) and nothing else, and
refuses to walk a server that ignores from. Put the agent’s certificate
fingerprint into tls_fingerprint_sha256 the way the
UniFi recipe
reads it, or let the host trust store accept it; plain http:// is accepted
for an agent on a loopback or an SSH tunnel.
2. Credentials (kea-api only)
Section titled “2. Credentials (kea-api only)”Basic auth goes through username/password references, the optional client
certificate through client_cert/client_key (PEM), each a credential file in
one of the two accepted shapes of
UniFi, step 3. The file
modes take no secrets at all.
3. Declare the instance
Section titled “3. Declare the instance”connectors: - id: dhcp-leases instance: dhcp tenant: example enabled: true config: mode: kea-memfile path: /srv/dhcp/kea/kea-leases4.csv # path6: /srv/dhcp/kea/kea-leases6.csv poll_interval: 60s # floor 30s; the Control Agent defaults to 120sThe other two modes:
- id: dhcp-leases instance: dhcp-isc tenant: example enabled: true config: mode: isc-leases path: /var/lib/dhcp/dhcpd.leases
- id: dhcp-leases instance: dhcp-api tenant: example enabled: true config: mode: kea-api url: https://dhcp.example.invalid:8000 tls_fingerprint_sha256: "<sha256 of the leaf>" ipv6: false # also read the dhcp6 service page_size: 100 # leases per lease4-get-page call, 10..1000 secrets: username: file:/etc/ndr/secrets/kea-username password: file:/etc/ndr/secrets/kea-password # client_cert: file:/etc/ndr/secrets/kea-client.crt # client_key: file:/etc/ndr/secrets/kea-client.keymode is the only required field; path, url and the secrets are required
or refused per mode at validation, which touches neither the file nor the
agent — a lease file that does not exist yet is a runtime unreachable, not a
start error. NDR_CONNECTORS_ENABLED=true in /etc/ndr/ndr.env is required
as for every connector declared in the file — see
connectors.yaml.
4. Prove it before the service polls it
Section titled “4. Prove it before the service polls it”sudo -u nyxtrace env NDR_CONNECTORS_ENABLED=true ndr connectors validatesudo -u nyxtrace env NDR_CONNECTORS_ENABLED=true ndr connectors test dhcp --livesudo -u nyxtrace env NDR_CONNECTORS_ENABLED=true ndr connectors collect dhcp --once --livetest --live reports the file shape (kea memfile, 12 columns, isc dhcpd leases) or the Kea version, the number of leases, and per file the active,
expired, released, named and unusable counts. collect --once --live
prints how many leases became items and how many anchors of which class
(mac, mac_oui, dhcp_hostname, duid, dhcp_fingerprint) arrived, and
stores nothing.
A failure names its class: unreachable (file missing — the mount is gone or
the path is wrong — or agent down), forbidden (file not readable by the
service user, with the chgrp/chmod in the message; or agent refusal),
auth, tls, unexpected_shape (not a lease file of that mode; an answer
that is no command result), upstream_error naming lease_cmds when the hook
is not loaded, too_large (a file over 64 MiB).
5. Switch on and verify
Section titled “5. Switch on and verify”NDR_CONNECTORS_ENABLED=true in /etc/ndr/ndr.env and systemctl restart ndr-api. The system view then shows connector:dhcp
with its state and the counters leases_active, leases_expired,
leases_released, leases_declined and hosts_named, which are recomputed
against the clock on every poll — a lease that ran out since the file was last
written is counted as expired without a write. Stale after three missed polls.
journalctl -u ndr-api -g 'instance=dhcp'An unchanged lease set produces an empty poll, which is still a successful sync; the set is re-emitted in full once a day so the anchors of a long or infinite lease stay inside the identity job’s window.
Where it shows up
Section titled “Where it shows up”Device name. The hourly naming job ranks the lease
hostname as dhcp — rank 4, after a manual, NetBox or controller name and
above mDNS — labelled via DHCP, with the lease end as the name’s validity. A
lease without an end is valid for 48 hours from its last observation, which
the daily re-emission keeps renewing while the lease holds. A name whose lease
ended is shown as a former name.
Device detail. A DHCP block in the identity panel with the hostname, the MAC, the vendor class, the client identifier where there is one, Lease until as a relative and an absolute time (no end for an infinite lease) and the instance it was read through. Only a lease that still binds is shown. See devices.
API. GET /api/v1/devices/{id} returns a dhcp object — hostname,
mac, client_id, vendor_class, subnet_id, lease_start, valid_until,
via, source: "dhcp-leases" — or null. See
endpoints.
Identity. The anchors appear in the device’s anchor list with
source = connector:dhcp-leases, first seen at the lease start — no further
back than the lease’s lifetime, and never more than seven days, as described
under three inputs, one connector.
Known limits
Section titled “Known limits”- The OPNsense copy recipe above is untested against a live OPNsense.
- The ISC DHCPv6 file (
ia-nablocks) is not read. - Class fingerprints from option 55 are not available in any lease store, and Kea’s allocation log is not read as a fingerprint source.
- The device list (
GET /api/v1/devices) does not carry thedhcpobject; only the detail does.