Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
use OPNsense\Kea\KeaDhcpv4;
use OPNsense\Kea\KeaDhcpv6;
use OPNsense\Base\UserException;

abstract class LeasesController extends ApiControllerBase
Expand All @@ -50,7 +48,6 @@ public function searchAction()
$interfaces = [];

$leases = json_decode($backend->configdpRun($this->configd_fetch_leases), true) ?? [];
$ifconfig = json_decode($backend->configdRun('interface list ifconfig'), true);
$mac_db = json_decode($backend->configdRun('interface list macdb'), true) ?? [];

$ifmap = [];
Expand All @@ -61,23 +58,6 @@ public function searchAction()
];
}

// Mark records as reserved based on hwaddr (IPv4) or duid/hwaddr (IPv6) match
$resv4 = [];
$resv6 = [];

foreach ((new KeaDhcpv4())->reservations->reservation->iterateItems() as $reservation) {
$resv4[strtolower($reservation->hw_address->getValue())] = 'hwaddr';
}

foreach ((new KeaDhcpv6())->reservations->reservation->iterateItems() as $reservation) {
// At least one of these is required in the model
if (!$reservation->duid->isEmpty()) {
$resv6[strtolower($reservation->duid->getValue())] = 'duid';
} elseif (!$reservation->hw_address->isEmpty()) {
$resv6[strtolower($reservation->hw_address->getValue())] = 'hwaddr';
}
}

if (!empty($leases) && isset($leases['records'])) {
$records = $leases['records'];
foreach ($records as &$record) {
Expand All @@ -92,23 +72,6 @@ public function searchAction()
// Vendor
$mac = strtoupper(substr(str_replace(':', '', $record['hwaddr']), 0, 6));
$record['mac_info'] = isset($mac_db[$mac]) ? $mac_db[$mac] : '';
// Reservation
$record['is_reserved'] = '';
$addr = $record['address'] ?? '';
if (strpos($addr, ':') !== false) {
$duid = strtolower($record['duid'] ?? '');
$mac = strtolower($record['hwaddr'] ?? '');
if (isset($resv6[$duid])) {
$record['is_reserved'] = $resv6[$duid];
} elseif (isset($resv6[$mac])) {
$record['is_reserved'] = $resv6[$mac];
}
} else {
$mac = strtolower($record['hwaddr'] ?? '');
if (isset($resv4[$mac])) {
$record['is_reserved'] = $resv4[$mac];
}
}
}
} else {
$records = [];
Expand Down
5 changes: 3 additions & 2 deletions src/opnsense/mvc/app/views/OPNsense/Kea/leases4.volt
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
return moment.unix(row[column.id]).local().format('YYYY-MM-DD HH:mm:ss');
},
"reservation": function (column, row) {
return row.is_reserved !== ''
return row.is_reserved.length > 0
? "{{ lang._('static') }}"
: "{{ lang._('dynamic') }}";
},
Expand All @@ -103,7 +103,7 @@

let reservationBtn;

if (row.is_reserved !== '') {
if (row.is_reserved.length > 0) {
reservationBtn = $(`
<button type="button" class="btn btn-xs" data-toggle="tooltip"
title="{{ lang._('Find Reservation') }}">
Expand Down Expand Up @@ -163,6 +163,7 @@
<th data-column-id="if_descr" data-type="string">{{ lang._('Interface') }}</th>
<th data-column-id="address" data-identifier="true" data-type="string" data-formatter="overflowformatter">{{ lang._('IP Address') }}</th>
<th data-column-id="hwaddr" data-type="string" data-formatter="macformatter" data-width="9em">{{ lang._('MAC Address') }}</th>
<th data-column-id="client_id" data-type="string" data-width="9em">{{ lang._('Client ID') }}</th>
<th data-column-id="valid_lifetime" data-type="integer">{{ lang._('Lifetime') }}</th>
<th data-column-id="expire" data-type="string" data-formatter="timestamp">{{ lang._('Expire') }}</th>
<th data-column-id="hostname" data-type="string" data-formatter="overflowformatter">{{ lang._('Hostname') }}</th>
Expand Down
8 changes: 4 additions & 4 deletions src/opnsense/mvc/app/views/OPNsense/Kea/leases6.volt
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,17 @@
return moment.unix(row[column.id]).local().format('YYYY-MM-DD HH:mm:ss');
},
"reservation": function (column, row) {
return row.is_reserved !== ''
return row.is_reserved.length > 0
? "{{ lang._('static') }}"
: "{{ lang._('dynamic') }}";
},
"commands": function (column, row) {
const baseUrl = `/ui/kea/dhcp/v6#reservations`;
let searchValue = '';

if (row.is_reserved === 'duid') {
if (row.is_reserved.includes('duid')) {
searchValue = row.duid || '';
} else if (row.is_reserved === 'hwaddr') {
} else if (row.is_reserved.includes('hwaddr')) {
searchValue = row.hwaddr || '';
}

Expand All @@ -117,7 +117,7 @@

let reservationBtn;

if (row.is_reserved !== '') {
if (row.is_reserved.length > 0) {
reservationBtn = $(`
<button type="button" class="btn btn-xs" data-toggle="tooltip"
title="{{ lang._('Find Reservation') }}">
Expand Down
64 changes: 62 additions & 2 deletions src/opnsense/scripts/kea/get_kea_leases.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,58 @@ def build_ranges(proto):

return ranges

def get_reservation_keys(record, proto):
keys = []
subnet_id = record.get('subnet-id')

if subnet_id is not None:
hwaddr = record.get('hw-address', '').lower()
duid = record.get('duid', '').lower()
client_id = record.get('client-id', '').lower()

if hwaddr:
keys.append((subnet_id, 'hwaddr', hwaddr))
if proto == 'inet6' and duid:
keys.append((subnet_id, 'duid', duid))
if proto == 'inet' and client_id:
keys.append((subnet_id, 'client_id', client_id))

return keys

def build_reserved_matches(config, leases, proto, dhcp_key, config_key):
"""
Kea does not expose whether a lease came from a reservation, so we infer it
by comparing client identity within the same subnet-id. The subnet-id check
matters because the same MAC, DUID or client-id may have reservations on
one subnet, but due to client roaming currently exist on a different subnet as lease.
They should not be marked as reserved when they are not in the expected subnet-id scope.
"""
wanted = set()
matches = {}

for lease in leases:
wanted.update(get_reservation_keys(lease, proto))

if wanted:
for subnet in config.get('arguments', {}).get(dhcp_key, {}).get(config_key, []):
subnet_id = subnet.get('id')
if subnet_id is None:
continue

for reservation in subnet.get('reservations', []):
hwaddr = reservation.get('hw-address', '').lower()
duid = reservation.get('duid', '').lower()
client_id = reservation.get('client-id', '').lower()

if hwaddr and (subnet_id, 'hwaddr', hwaddr) in wanted:
matches[(subnet_id, 'hwaddr', hwaddr)] = 'hwaddr'
if proto == 'inet6' and duid and (subnet_id, 'duid', duid) in wanted:
matches[(subnet_id, 'duid', duid)] = 'duid'
if proto == 'inet' and client_id and (subnet_id, 'client_id', client_id) in wanted:
matches[(subnet_id, 'client_id', client_id)] = 'client_id'

return matches

if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--proto', help='protocol to fetch (inet, inet6)', default='inet', choices=['inet', 'inet6'])
Expand All @@ -69,31 +121,39 @@ def build_ranges(proto):
]

leases = KeaCtrl.send_command(lease_cmd, {"subnets": subnets}, service)
leases = leases.get("arguments", {}).get("leases", [])
reserved_matches = build_reserved_matches(config, leases, inputargs.proto, dhcp_key, config_key)

records = []
for lease in leases.get("arguments", {}).get("leases", []):
for lease in leases:
address = lease.get("ip-address")
lease_if = None
is_reserved = []

if address:
for net, ifname in ranges.items():
if net.overlaps(ipaddress.ip_network(address)):
lease_if = ifname
break

for key in get_reservation_keys(lease, inputargs.proto):
if key in reserved_matches:
is_reserved.append(reserved_matches[key])

records.append({
"address": address,
"prefix_len": lease.get("prefix-len", 128),
"type": lease.get("type", ""),
"hwaddr": lease.get("hw-address", ""),
"duid": lease.get("duid", ""),
"client_id": lease.get("client-id", ""),
"iaid": lease.get("iaid", ""),
"valid_lifetime": lease.get("valid-lft", 0),
"expire": lease.get("cltt", 0) + lease.get("valid-lft", 0),
"hostname": lease.get("hostname", ""),
"if": lease_if,
"if_descr": "",
"is_reserved": ""
"is_reserved": is_reserved
})

if records:
Expand Down