Page MenuHomeVyOS Platform

DHCPv4 lease output omits the client identifier
In progress, NormalPublicFEATURE REQUEST

Description

show dhcp server leases shows the MAC address of every DHCPv4 lease but not the
client identifier (DHCP option 61), even though Kea returns both. The DHCPv6 path
has no such gap — it reports the DUID for every lease.

The value is present right up until VyOS discards it. kea_get_leases() returns
the control socket's lease4-get-all payload untouched, and every v4 lease in it
carries client-id:

$ sudo python3 -c "
from vyos.kea import kea_get_leases
for l in kea_get_leases('4', None)[:2]:
    print(sorted(l.keys()))
    print(l['hw-address'], '->', l['client-id'])
"
['client-id', 'cltt', 'fqdn-fwd', 'fqdn-rev', 'hostname', 'hw-address', 'ip-address', 'state', 'subnet-id', 'valid-lft']
00:e0:4c:68:4e:0d -> 01:00:e0:4c:68:4e:0d
['client-id', 'cltt', 'fqdn-fwd', 'fqdn-rev', 'hostname', 'hw-address', 'ip-address', 'state', 'subnet-id', 'valid-lft']
52:54:00:e3:be:41 -> ff:b5:5e:67:ff:00:02:00:00:ab:11:68:46:79:5a:ab:74:cd:25

kea_get_server_leases() in python/vyos/kea.py then builds the dict op-mode
returns, and copies the identifier for inet6 only:

data_lease['mac'] = lease.get('hw-address', '-')

if inet == '4':
    data_lease['start'] = lease['start_time'].timestamp()   # no client_id

if inet == '6':
    data_lease['last_communication'] = lease['start_time'].timestamp()
    data_lease['duid'] = _format_hex_string(lease['duid'])

Because this is the function op-mode calls, the identifier is unreachable from
show dhcp server leases, from its --raw JSON, and from the HTTP API built on
top of it. sort_valid_inet6 already offers duid as a sort key; sort_valid_inet
has no counterpart.

Why this matters

DHCPv4 static mappings can be matched on the client identifier instead of the MAC:

set service dhcp-server shared-network-name LAN subnet 192.168.10.0/24 \
    static-mapping example duid <client identifier>

Hosts running systemd-networkd — the default on current Ubuntu, Fedora and others
— send an RFC 4361 identifier, ff:<IAID>:<DUID>, rather than the traditional
01:<MAC>. Kea matches a duid reservation against the DUID it extracts from
that identifier, with no host-reservation-identifiers configured:

host-reservation-identifiers in /run/kea/kea-dhcp4.conf : unset (Kea defaults apply)

reservation   {"hostname": "ember-u26", "duid": "00:02:00:00:ab:11:68:46:79:5a:ab:74:cd:25", "ip-address": "192.168.10.92"}
host sends    client-id ff:b5:5e:67:ff:00:02:00:00:ab:11:68:46:79:5a:ab:74:cd:25
                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
resulting lease   192.168.10.92     the reserved address

So the identifier a v4 reservation has to carry is a substring of a value the
operator cannot see anywhere in the CLI.

There is currently no supported way to read that value. It is not in the lease
output, and show dhcp server static-mappings only reports what was configured,
not what the host sends. The only route is sudo plus the Kea lease file or a
control socket query.

The consequence is a silent failure. A static mapping written with the wrong
identifier is accepted by the CLI, passes commit, and is rendered into
/run/kea/kea-dhcp4.conf as a valid reservation:

{"hostname": "ember-u26", "duid": "00:04:51:46:98:c8:e7:d8:46:f6:90:a7:a0:54:3b:d6:8f:5d", "ip-address": "192.168.10.92"}

It then never matches, because that host sends
00:02:00:00:ab:11:68:46:79:5a:ab:74:cd:25 inside its client identifier. The host
carries on taking a dynamic address while show dhcp server static-mappings
reports it as reserved, and nothing in the CLI can show the operator why. Making
the identifier visible turns a silent mismatch into an obvious one.

A second, related case the column exposes: a host can hold leases under both
identifier styles at once — an initramfs or installer stack sending 01:<MAC> and
then networkd sending its RFC 4361 identifier — which is worth seeing before
choosing which one to pin on.

Proposed change

Copy the field in kea_get_server_leases() for inet 4, and surface it:

  • python/vyos/kea.py — data_lease['client_id'] = lease.get('client-id', '-'), inside the existing if inet == '4':. Kea returns client-id already colon separated, so unlike the DHCPv6 DUID it must not go through _format_hex_string() — that would insert a second colon after every existing one. Clients that send no option 61 get -, matching how hostname and mac are handled two lines above.
  • src/op_mode/dhcp.py — append a Client ID column to the inet table and add client_id to sort_valid_inet.

Appended rather than inserted beside the MAC: that matches where inet6 already
puts its DUID, and leaves every existing column in place.

Result:

IP Address      MAC address        State   ...  Hostname       Origin  Client ID
192.168.10.60   00:e0:4c:68:4e:0d  active  ...  arya           local   01:00:e0:4c:68:4e:0d
192.168.10.91   52:54:00:e3:be:31  active  ...  ember-u24      local   ff:b5:5e:67:ff:00:02:00:00:ab:11:f8:af:84:82:a0:58:c1:5d
192.168.10.92   52:54:00:e3:be:41  active  ...  ember-u26      local   ff:b5:5e:67:ff:00:02:00:00:ab:11:68:46:79:5a:ab:74:cd:25
192.168.10.93   52:54:00:e3:be:51  active  ...  ember-rocky10  local   01:52:54:00:e3:be:51

Compatibility

Additive. No configuration syntax changes, no new validation, and nothing in the
DHCP server's running behaviour is touched — the change is confined to how a
lease is reported. Every existing column keeps its position and meaning, and the
--raw JSON gains one key.

The only consumer that could notice is a script reading the last column of
show dhcp server leases positionally, which would now get the client identifier
instead of Origin. The column was appended specifically so that no existing
column moves.

Details

Version
2026.08.05-0033-rolling (present in 1.4 and 1.5 alike)
Is it a breaking change?
Perfectly compatible
Issue type
Feature (new functionality)