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'