diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogReservation4.xml b/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogReservation4.xml index 6cc21be13c..a2ee92ee0d 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogReservation4.xml +++ b/src/opnsense/mvc/app/controllers/OPNsense/Kea/forms/dialogReservation4.xml @@ -17,6 +17,12 @@ text MAC/Ether address of the client in question + + reservation.client_id + + text + ID of the client in question + reservation.hostname diff --git a/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv4.php b/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv4.php index bf8408a8c6..2394f6f62f 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv4.php +++ b/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv4.php @@ -88,6 +88,11 @@ class KeaDhcpv4 extends BaseModel if (!Util::isIPInCIDR($reservation->ip_address->getValue(), $subnet)) { $messages->appendMessage(new Message(gettext("Address not in specified subnet"), $key . ".ip_address")); } + if (!$reservation->client_id->isEmpty() && !$reservation->hw_address->isEmpty()) { + $messages->appendMessage(new Message(gettext("Either a client ID or a MAC address should be specified, but not both"), $key . ".hw_address")); + } elseif ($reservation->client_id->isEmpty() && $reservation->hw_address->isEmpty()) { + $messages->appendMessage(new Message(gettext("Either a client ID or a MAC address should be specified."), $key . ".hw_address")); + } } return $messages; @@ -198,6 +203,8 @@ class KeaDhcpv4 extends BaseModel } if (!$reservation->hw_address->isEmpty()) { $res['hw-address'] = str_replace('-', ':', $reservation->hw_address->getValue()); + } elseif (!$reservation->client_id->isEmpty()) { + $res['client-id'] = $reservation->client_id->getValue(); } // Add DHCP option-data elements for reservations diff --git a/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv4.xml b/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv4.xml index bca38204b6..d76ab08eda 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv4.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv4.xml @@ -246,6 +246,9 @@ hw_address.check001 + + client_id.check001 + Y @@ -260,7 +263,6 @@ - Y Duplicate entry exists. @@ -272,6 +274,19 @@ + + /^(?:[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2})+)$/ + Value must be a colon-separated hexadecimal sequence (e.g., 01:02:f3). + + + Duplicate entry exists. + UniqueConstraint + + subnet + + + + Y diff --git a/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.php b/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.php index a74dcb5d2f..81b8703cfb 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.php +++ b/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.php @@ -64,9 +64,9 @@ class KeaDhcpv6 extends BaseModel $messages->appendMessage(new Message(gettext("Either an IP address or a Prefix should be specified."), $key . ".ip_address")); } if (!$reservation->duid->isEmpty() && !$reservation->hw_address->isEmpty()) { - $messages->appendMessage(new Message(gettext("Either a DUID or an Ether address should be specified, but not both"), $key . ".duid")); + $messages->appendMessage(new Message(gettext("Either a DUID or an MAC address should be specified, but not both"), $key . ".duid")); } elseif ($reservation->duid->isEmpty() && $reservation->hw_address->isEmpty()) { - $messages->appendMessage(new Message(gettext("Either a DUID or an Ether address should be specified."), $key . ".duid")); + $messages->appendMessage(new Message(gettext("Either a DUID or an MAC address should be specified."), $key . ".duid")); } } // validate changed subnets diff --git a/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.xml b/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.xml index b05a1cb345..260f75e731 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Kea/KeaDhcpv6.xml @@ -228,6 +228,9 @@ duid.check001 + + hw_address.check001 + Y diff --git a/src/opnsense/mvc/app/views/OPNsense/Kea/dhcpv4.volt b/src/opnsense/mvc/app/views/OPNsense/Kea/dhcpv4.volt index 1ebe596416..ede4fdaed3 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Kea/dhcpv4.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Kea/dhcpv4.volt @@ -184,6 +184,7 @@ if (params.has('hostname')) $('#reservation\\.hostname').val(params.get('hostname')); if (params.has('ip_address')) $('#reservation\\.ip_address').val(params.get('ip_address')); if (params.has('hw_address')) $('#reservation\\.hw_address').val(params.get('hw_address')); + if (params.has('client_id')) $('#reservation\\.client_id').val(params.get('client_id')); history.replaceState(null, null, window.location.pathname + '#reservations'); }); $(this).find('.command-add').trigger('click'); diff --git a/src/opnsense/mvc/app/views/OPNsense/Kea/leases4.volt b/src/opnsense/mvc/app/views/OPNsense/Kea/leases4.volt index 340ebddb61..dca777cd59 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Kea/leases4.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Kea/leases4.volt @@ -103,12 +103,22 @@ }, "commands": function (column, row) { const baseUrl = `/ui/kea/dhcp/v4#reservations`; - const searchUrl = `${baseUrl}&search=${encodeURIComponent(row.hwaddr || '')}`; + let searchValue = ''; + + if (row.is_reserved.includes('client_id')) { + searchValue = row.client_id || ''; + } else if (row.is_reserved.includes('hwaddr')) { + searchValue = row.hwaddr || ''; + } + const addUrlParams = { ip_address: row.address || '', hw_address: row.hwaddr || '', + client_id: row.client_id || '', hostname: row.hostname || '' }; + + const searchUrl = `${baseUrl}&search=${encodeURIComponent(searchValue)}`; const addUrl = `${baseUrl}?${new URLSearchParams(addUrlParams)}`; let reservationBtn; diff --git a/src/opnsense/scripts/kea/get_kea_leases.py b/src/opnsense/scripts/kea/get_kea_leases.py index d3aba7a141..37bdc14625 100755 --- a/src/opnsense/scripts/kea/get_kea_leases.py +++ b/src/opnsense/scripts/kea/get_kea_leases.py @@ -52,9 +52,11 @@ def get_reservation_keys(record, proto): subnet_id = record.get('subnet-id') if subnet_id is not None: + # Reservation returns hw-address and duid like this: "01:48:90"... hwaddr = record.get('hw-address', '').lower() duid = record.get('duid', '').lower() - client_id = record.get('client-id', '').lower() + # ...but client_id like this: "014890" + client_id = record.get('client-id', '').replace(':', '').lower() if hwaddr: keys.append((subnet_id, 'hwaddr', hwaddr)) @@ -88,7 +90,7 @@ def build_reserved_matches(config, leases, proto, dhcp_key, config_key): for reservation in subnet.get('reservations', []): hwaddr = reservation.get('hw-address', '').lower() duid = reservation.get('duid', '').lower() - client_id = reservation.get('client-id', '').lower() + client_id = reservation.get('client-id', '').replace(':', '').lower() if hwaddr and (subnet_id, 'hwaddr', hwaddr) in wanted: matches[(subnet_id, 'hwaddr', hwaddr)] = 'hwaddr'