Skip to content

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.

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.

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:

Terminal window
setfacl -m u:nyxtrace:rx /var/lib/kea
setfacl -m u:nyxtrace:r /var/lib/kea/kea-leases4.csv
setfacl -d -m u:nyxtrace:r /var/lib/kea # the LFC rewrite creates a new file

With 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.

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-leases

into 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.

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.

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.

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.

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 120s

The 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.key

mode 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.

Terminal window
sudo -u nyxtrace env NDR_CONNECTORS_ENABLED=true ndr connectors validate
sudo -u nyxtrace env NDR_CONNECTORS_ENABLED=true ndr connectors test dhcp --live
sudo -u nyxtrace env NDR_CONNECTORS_ENABLED=true ndr connectors collect dhcp --once --live

test --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).

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.

Terminal window
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.

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.

  • The OPNsense copy recipe above is untested against a live OPNsense.
  • The ISC DHCPv6 file (ia-na blocks) 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 the dhcp object; only the detail does.