dhcp6: migrate leases page to MVC (#6653)

* dhcp6: add backend for listing dhcpv6 leases

* dhcp6: add leases view and controller

* dhcp6: lease deletion backend

* dhcp6: move to separate dhcpv6 directory to accomodate the service control UI
This commit is contained in:
Stephan de Wit 2023-07-12 09:39:55 +02:00 committed by GitHub
parent 8dc8d39d4b
commit f782be9cbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 901 additions and 605 deletions

View File

@ -1856,7 +1856,7 @@ function dhcpd_dhcrelay6_configure($verbose = false)
service_log("done.\n", $verbose);
}
function dhcpd_staticmap($proto = null, $domain_fallback = 'not.found', $ifconfig_details = null, $valid_addresses = true)
function dhcpd_staticmap($proto = null, $valid_addresses = true, $domain_fallback = 'not.found', $ifconfig_details = null)
{
$staticmap = [];
foreach (empty($proto) ? [4, 6] : [$proto] as $inet) {
@ -1926,29 +1926,6 @@ function dhcpd_staticmap($proto = null, $domain_fallback = 'not.found', $ifconfi
return $staticmap;
}
function dhcpd_leases($inet = 4)
{
$file = $inet == 6 ? '/var/dhcpd/var/db/dhcpd6.leases' : '/var/dhcpd/var/db/dhcpd.leases';
$token = $inet == 6 ? 'ia-' : 'lease';
$content = [];
$section = [];
$fin = @fopen($file, 'r');
if ($fin) {
while (($line = fgets($fin, 4096)) !== false) {
if (strpos($line, $token) === 0) {
$section = [];
} elseif (strpos($line, '}') === 0 && count($section) > 0) {
$content[] = implode('', $section);
}
$section[] = rtrim($line, "\n;");
}
fclose($fin);
}
return $content;
}
function dhcpd_parse_duid($duid_string)
{
$parsed_duid = [];

View File

@ -258,7 +258,7 @@ function _dnsmasq_add_host_entries()
$domain = $config['dnsmasq']['regdhcpdomain'];
}
foreach (plugins_run('static_mapping', [null, $domain, legacy_interfaces_details()]) as $map) {
foreach (plugins_run('static_mapping', [null, true, $domain, legacy_interfaces_details()]) as $map) {
foreach ($map as $host) {
if (empty($host['hostname'])) {
/* cannot register without a hostname */

View File

@ -544,7 +544,7 @@ function unbound_add_host_entries($ifconfig_details)
}
if (!empty($general['regdhcpstatic'])) {
foreach (plugins_run('static_mapping', [null, $config['system']['domain'], $ifconfig_details]) as $map) {
foreach (plugins_run('static_mapping', [null, true, $config['system']['domain'], $ifconfig_details]) as $map) {
foreach ($map as $host) {
if (empty($host['hostname'])) {
/* cannot register without a hostname */

View File

@ -0,0 +1,248 @@
<?php
/*
* Copyright (C) 2023 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\DHCPv6\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
use OPNsense\Firewall\Util;
class LeasesController extends ApiControllerBase
{
public function searchLeaseAction()
{
$this->sessionClose();
$inactive = $this->request->get('inactive');
$selected_interfaces = $this->request->get('selected_interfaces');
$backend = new Backend();
$config = Config::getInstance()->object();
$online = [];
$leases = [];
$ndp_data = json_decode($backend->configdRun('interface list ndp json'), true);
foreach ($ndp_data as $ndp_entry) {
array_push($online, $ndp_entry['mac'], $ndp_entry['ip']);
}
$raw_leases = json_decode($backend->configdpRun('dhcpd list leases6', [$inactive]), true);
foreach ($raw_leases as $raw_lease) {
if (!array_key_exists('addresses', $raw_lease)) {
continue;
}
/* set defaults */
$lease = [];
$lease['type'] = 'dynamic';
$lease['status'] = 'offline';
$lease['lease_type'] = $raw_lease['lease_type'];
$lease['iaid'] = $raw_lease['iaid'];
$lease['duid'] = $raw_lease['duid'];
$lease['iaid_duid'] = $raw_lease['iaid_duid'];
$lease['descr'] = '';
$lease['if'] = '';
if (array_key_exists('cltt', $raw_lease)) {
$lease['cltt'] = date('Y/m/d H:i:s', $raw_lease['cltt']);
}
/* XXX we pick the first address, this will be fine for a typical deployment
* according to RFC8415 section 6.6, but it should be noted that the protocol
* (and isc-dhcpv6) is capable of handing over multiple addresses/prefixes to
* a single client within a single lease. The backend accounts for this.
*/
$seg = $raw_lease['addresses'][0];
$lease['state'] = $seg['binding'] == 'free' ? 'expired' : $seg['binding'];
if (array_key_exists('ends', $seg)) {
$lease['ends'] = date('Y/m/d H:i:s', $seg['ends']);
}
$lease['address'] = $seg['iaaddr'];
$lease['online'] = in_array(strtolower($seg['iaaddr']), $online) ? 'online' : 'offline';
$leases[] = $lease;
}
$sleases = json_decode($backend->configdRun('dhcpd list static 6 0'), true);
$statics = [];
foreach ($sleases['dhcpd'] as $slease) {
$static = [
'address' => $slease['ipaddrv6'],
'type' => 'static',
'cltt' => '',
'ends' => '',
'descr' => $slease['descr'],
'iaid' => '',
'duid' => $slease['duid'],
'if_descr' => '',
'if' => $slease['interface'],
'state' => 'active',
'status' => in_array(strtolower($slease['ipaddrv6']), $online) ? 'online' : 'offline'
];
$statics[] = $static;
}
$leases = array_merge($leases, $statics);
$mac_man = json_decode($backend->configdRun('interface list macdb json'), true);
$interfaces = [];
/* fetch interfaces ranges so we can match leases to interfaces */
$if_ranges = [];
foreach ($config->dhcpdv6->children() as $dhcpif => $dhcpifconf) {
$if = $config->interfaces->$dhcpif;
if (!empty((string)$if->ipaddrv6) && !empty((string)$if->subnetv6)) {
$if_ranges[$dhcpif] = (string)$if->ipaddrv6.'/'.(string)$if->subnetv6;
}
}
foreach ($leases as $idx => $lease) {
$leases[$idx]['man'] = '';
$leases[$idx]['mac'] = '';
/* We infer the MAC from NDP data if available, otherwise we extract it out
* of the DUID. However, RFC8415 section 11 states that an attempt to parse
* a DUID to obtain a client's link-layer addresss is unreliable, as there is no
* guarantee that the client is still using the same link-layer address as when
* it generated its DUID. Therefore, if we can link it to a manufacturer, chances
* are fairly high that this is a valid MAC address, otherwise we omit the MAC
* address.
*/
if (!empty(['address'])) {
foreach ($ndp_data as $ndp) {
if ($ndp['ip'] == $lease['address']) {
$leases[$idx]['mac'] = $ndp['mac'];
$leases[$idx]['man'] = empty($ndp['manufacturer']) ? '' : $ndp['manufacturer'];
break;
}
}
}
if (!empty($lease['duid'])) {
$mac = '';
$duid_type = substr($lease['duid'], 0, 5);
if ($duid_type === "00:01" || $duid_type === "00:03") {
/* DUID generated based on LL addr with or without timestamp */
$hw_type = substr($lease['duid'], 6, 5);
if ($hw_type == "00:01") { /* HW type ethernet */
$mac = substr($lease['duid'], -17, 17);
}
}
if (!empty($mac)) {
$mac_hi = strtoupper(substr(str_replace(':', '', $mac), 0, 6));
if (array_key_exists($mac_hi, $mac_man)) {
$leases[$idx]['mac'] = $mac;
$leases[$idx]['man'] = $mac_man[$mac_hi];
}
}
}
/* include interface */
$intf = '';
$intf_descr = '';
if (!empty($lease['if'])) {
$if = $config->interfaces->{$lease['if']};
if (!empty((string)$if->ipaddrv6) && Util::isIpAddress((string)$if->ipaddrv6)) {
$intf = $lease['if'];
$intf_descr = (string)$if->descr;
}
} else {
foreach ($if_ranges as $if_name => $if_range) {
if (Util::isIPInCIDR($lease['address'], $if_range)) {
$intf = $if_name;
$intf_descr = $config->interfaces->$if_name->descr;
}
}
}
$leases[$idx]['if'] = $intf;
$leases[$idx]['if_descr'] = $intf_descr;
if (!empty($if_name) && !array_key_exists($if_name, $interfaces)) {
$interfaces[$intf] = $intf_descr;
}
}
$response = $this->searchRecordsetBase($leases, null, 'address', function ($key) use ($selected_interfaces) {
if (empty($selected_interfaces) || in_array($key['if'], $selected_interfaces)) {
return true;
}
return false;
}, SORT_REGULAR);
$response['interfaces'] = $interfaces;
return $response;
}
public function searchPrefixAction()
{
$this->sessionClose();
$backend = new Backend();
$prefixes = [];
$raw_leases = json_decode($backend->configdpRun('dhcpd list leases6 1'), true);
foreach ($raw_leases as $raw_lease) {
if ($raw_lease['lease_type'] === 'ia-pd' && array_key_exists('prefixes', $raw_lease)) {
$prefix = [];
$prefix['lease_type'] = $raw_lease['lease_type'];
$prefix['iaid'] = $raw_lease['iaid'];
$prefix['duid'] = $raw_lease['duid'];
if (array_key_exists('cltt', $raw_lease)) {
$prefix['cltt'] = date('Y/m/d H:i:s', $raw_lease['cltt']);
}
$prefix_raw = $raw_lease['prefixes'][0];
$prefix['prefix'] = $prefix_raw['iaprefix'];
if (array_key_exists('ends', $prefix_raw)) {
$prefix['ends'] = date('Y/m/d H:i:s', $prefix_raw['ends']);
}
$prefix['state'] = $prefix_raw['binding'] == 'free' ? 'expired' : $prefix_raw['binding'];
$prefixes[] = $prefix;
}
}
return $this->searchRecordsetBase($prefixes, null, 'prefix', null, SORT_REGULAR);
}
public function delLeaseAction($ip)
{
$result = ["result" => "failed"];
if ($this->request->isPost()) {
$this->sessionClose();
$response = json_decode((new Backend())->configdpRun("dhcpd remove lease6", [$ip]), true);
if ($response["removed_leases"] != "0") {
$result["result"] = "deleted";
}
}
return $result;
}
}

View File

@ -0,0 +1,101 @@
<?php
/*
* Copyright (C) 2023 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\DHCPv6\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\Backend;
class ServiceController extends ApiControllerBase
{
/**
* XXX most of this logic can be replaced when appropriate start/stop/restart/status
* hooks are provided to fit into an ApiMutableServiceControllerBase class. dhcpd being
* 'enabled' isn't as straight-forward however with current legacy config format.
*/
public function statusAction()
{
$response = trim((new Backend())->configdRun('service status dhcpd6'));
if (strpos($response, 'is running') > 0) {
$status = 'running';
} elseif (strpos($response, 'not running') > 0) {
$status = 'stopped';
} else {
$status = 'disabled';
}
return [
'status' => $status,
'widget' => [
'caption_stop' => gettext("stop service"),
'caption_start' => gettext("start service"),
'caption_restart' => gettext("restart service")
]
];
}
public function startAction()
{
$result = ['status' => 'failed'];
if ($this->request->isPost()) {
$this->sessionClose();
$response = trim((new Backend())->configdRun('service start dhcpd6'));
return ['status' => $response];
}
return $result;
}
public function stopAction()
{
$result = ['status' => 'failed'];
if ($this->request->isPost()) {
$this->sessionClose();
$response = trim((new Backend())->configdRun('service stop dhcpd6'));
return ['status' => $response];
}
return $result;
}
public function restartAction()
{
$result = ['status' => 'failed'];
if ($this->request->isPost()) {
$this->sessionClose();
$response = trim((new Backend())->configdRun('service restart dhcpd6'));
return ['status' => $response];
}
return $result;
}
}

View File

@ -0,0 +1,39 @@
<?php
/*
* Copyright (C) 2023 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\DHCPv6;
use OPNsense\Base\IndexController;
class LeasesController extends IndexController
{
public function indexAction()
{
$this->view->pick('OPNsense/DHCPv6/leases');
}
}

View File

@ -507,7 +507,8 @@
<page-status-dhcpv6leases>
<name>Status: DHCPv6 leases</name>
<patterns>
<pattern>status_dhcpv6_leases.php*</pattern>
<pattern>ui/dhcpv6/leases</pattern>
<pattern>api/dhcpv6/leases/*</pattern>
</patterns>
</page-status-dhcpv6leases>
<page-status-interfaces>

View File

@ -191,9 +191,7 @@
</DHCPv4>
<DHCPv6 cssClass="fa fa-bullseye fa-fw">
<Relay order="300" url="/services_dhcpv6_relay.php"/>
<Leases order="400" url="/status_dhcpv6_leases.php">
<Details url="/status_dhcpv6_leases.php?*" visibility="hidden"/>
</Leases>
<Leases order="400" url="/ui/dhcpv6/leases"/>
</DHCPv6>
<OpenDNS VisibleName="OpenDNS" url="/services_opendns.php" cssClass="fa fa-tags fa-fw"/>
<RouterAdv VisibleName="Router Advertisements" cssClass="fa fa-bullseye fa-fw" />

View File

@ -0,0 +1,207 @@
{#
# Copyright (c) 2023 Deciso B.V.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#}
<script>
$( document ).ready(function() {
let show_inactive = false;
let selected_interfaces = [];
if (window.localStorage) {
if (window.localStorage.getItem("api.dhcp.leases6.inactive") !== null) {
show_inactive = window.localStorage.getItem("api.dhcp.leases6.inactive") == 'true'
$("#show-inactive").prop('checked', show_inactive);
}
}
$("#show-inactive").change(function() {
show_inactive = this.checked;
if (window.localStorage) {
window.localStorage.setItem("api.dhcp.leases6.inactive", show_inactive);
}
$("#grid-leases").bootgrid('reload');
});
$("#interface-selection").on("changed.bs.select", function (e) {
console.log($(this).val());
selected_interfaces = $(this).val();
$("#grid-leases").bootgrid('reload');
})
$("#grid-leases").UIBootgrid({
search:'/api/dhcpv6/leases/searchLease/',
del:'/api/dhcpv6/leases/delLease/',
options: {
selection: false,
multiSelect: false,
useRequestHandlerOnGet: true,
requestHandler: function(request) {
request['inactive'] = show_inactive;
request['selected_interfaces'] = selected_interfaces;
return request;
},
responseHandler: function (response) {
if (response.hasOwnProperty('interfaces')) {
for ([intf, descr] of Object.entries(response['interfaces'])) {
let exists = false;
$('#interface-selection option').each(function() {
if (this.value == intf) {
exists = true;
}
});
if (!exists) {
$("#interface-selection").append($('<option>', {
value: intf,
text: descr
}));
}
}
$("#interface-selection").selectpicker('refresh');
}
return response;
},
formatters: {
"macformatter": function (column, row) {
if (row.man != '') {
return row.mac + '<br/>' + '<small><i>' + row.man + '</i></small>';
}
return row.mac;
},
"statusformatter": function (column, row) {
let connected = row.status == 'offline' ? 'text-danger' : 'text-success';
return '<i class="fa fa-plug ' + connected +'" title="' + row.status + '" data-toggle="tooltip"></i>'
},
"commands": function (column, row) {
/* we override the default commands data formatter in order to utilize
* two different types of data keys for two different actions. The mapping
* action needs a DUID, while the delete action requires an IPv6 address.
*/
if (row.type == 'static') {
return '';
}
let static_map = '';
if (row.if != '') {
static_map = '<a class="btn btn-default btn-xs" href="/services_dhcpv6_edit.php?if=' + row.if +
'&amp;duid=' + row.duid + '">' +
'<i class="fa fa-plus fa-fw act-map" data-value="' + row.duid + '" data-toggle="tooltip" ' +
'title="{{lang._("Add a static mapping for this MAC address")}}"></i>' +
'</a>';
}
/* The delete action can be hooked up to the default bootgrid behaviour */
let deleteip = '<button type="button" class="btn btn-xs btn-default bootgrid-tooltip command-delete"' +
'data-row-id="' + row.address + '" data-action="deleteSelected">' +
'<i class="fa fa-trash fa-fw"></i>' +
'</a>';
return static_map + ' ' + deleteip;
}
}
}
});
$("#grid-prefixes").UIBootgrid({
search:'/api/dhcpv6/leases/searchPrefix/'
});
$("#inactive-selection-wrapper").detach().prependTo('#grid-leases-header > .row > .actionBar');
$("#interface-selection-wrapper").detach().prependTo('#grid-leases-header > .row > .actionBar > .actions');
if (window.location.hash != "") {
$('a[href="' + window.location.hash + '"]').click();
} else {
$('a[href="#leases"]').click();
}
updateServiceControlUI('dhcpv6');
});
</script>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li><a data-toggle="tab" href="#leases" id="leases_tab">{{ lang._('Leases') }}</a></li>
<li><a data-toggle="tab" href="#prefixes" id="prefixes_tab">{{ lang._('Delegated Prefixes') }}</a></li>
</ul>
<div class="tab-content content-box col-xs-12 __mb">
<div id="leases" class="tab-pane fade in active">
<div id="inactive-selection-wrapper" style="float: left;">
<label>
<input id="show-inactive" type="checkbox"/>
{{ lang._('Show inactive') }}
</label>
</div>
<div class="btn-group" id="interface-selection-wrapper">
<select class="selectpicker" multiple="multiple" data-live-search="true" id="interface-selection" data-width="auto" title="All Interfaces">
</select>
</div>
<table id="grid-leases" class="table table-condensed table-hover table-striped table-responsive">
<tr>
<thead>
<tr>
<th data-column-id="if_descr" data-type="string">{{ lang._('Interface') }}</th>
<th data-column-id="address" data-identifier="true" data-type="string">{{ lang._('IP Address') }}</th>
<th data-column-id="iaid" data-type="number">{{ lang._('IAID') }}</th>
<th data-column-id="duid" data-type="string" data-width="15em">{{ lang._('DUID') }}</th>
<th data-column-id="mac" data-type="string" data-formatter="macformatter">{{ lang._('MAC Address') }}</th>
<th data-column-id="descr" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="cltt" data-type="string">{{ lang._('Last Transaction Time') }}</th>
<th data-column-id="ends" data-type="string">{{ lang._('End') }}</th>
<th data-column-id="status" data-type="string" data-formatter="statusformatter">{{ lang._('Status') }}</th>
<th data-column-id="state" data-type="string">{{ lang._('State') }}</th>
<th data-column-id="type" data-type="string">{{ lang._('Lease Type') }}</th>
<th data-column-id="commands" data-formatter="commands", data-sortable="false"></th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
</tfoot>
</tr>
</table>
</div>
<div id="prefixes" class="tab-pane fade in">
<table id="grid-prefixes" class="table table-condensed table-hover table-striped table-responsive">
<tr>
<thead>
<tr>
<th data-column-id="prefix" data-type="string">{{ lang._('IPv6 Prefix') }}</th>
<th data-column-id="iaid" data-type="number" data-width="5em">{{ lang._('IAID') }}</th>
<th data-column-id="duid" data-type="string">{{ lang._('DUID') }}</th>
<th data-column-id="cltt" data-type="string">{{ lang._('Last Transaction Time') }}</th>
<th data-column-id="ends" data-type="string">{{ lang._('End') }}</th>
<th data-column-id="state" data-type="string">{{ lang._('State') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
</tfoot>
</tr>
</table>
</div>
</div>

View File

@ -0,0 +1,108 @@
#!/usr/local/bin/php
<?php
/*
* Copyright (C) 2023 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
require_once("config.inc");
require_once("util.inc");
require_once("interfaces.inc");
require_once("plugins.inc.d/dhcpd.inc");
$dhcp_lease_file = "/var/dhcpd/var/db/dhcpd6.leases";
$opts = getopt('d::f::hs', []);
if (isset($opts['h']) || empty($opts)) {
echo "Usage: cleanup_leases6.php [-h]\n\n";
echo "\t-h show this help text and exit\n";
echo "\t-s restart service (required when service is active)\n";
echo "\t-d=xxx remove ipv6 address\n";
echo "\t-f=dhcpd6.leases file (default = /var/dhcpd/var/db/dhcpd6.leases)\n";
exit(0);
}
if (!empty($opts['f'])) {
$dhcp_lease_file = $opts['f'];
}
if (!empty($opts['d'])) {
$ip_to_remove = $opts['d'];
}
if (isset($opts['s'])) {
killbypid('/var/dhcpd/var/run/dhcpdv6.pid');
} elseif (isvalidpid('/var/dhcpd/var/run/dhcpdv6.pid')) {
echo "dhcpdv6 active, can't update lease file";
exit(1);
}
$removed_leases = 0;
$fin = @fopen($dhcp_lease_file, "r+");
$fout = @fopen($dhcp_lease_file.".new", "w");
if ($fin && flock($fin, LOCK_EX)) {
$iaaddr = "";
$content_to_flush = array();
while (($line = fgets($fin, 4096)) !== false) {
$fields = explode(' ', trim($line));
if ($fields[0] == 'iaaddr') {
// lease segment, record ip
$iaaddr = trim($fields[1]);
$content_to_flush[] = $line;
} elseif ($fields[0] == 'ia-na' || count($content_to_flush) > 0) {
$content_to_flush[] = $line;
} else {
// output data directly if we're not in a "ia-na" section
fputs($fout, $line);
}
if ($line == "}\n") {
if ($iaaddr != $ip_to_remove) {
// write ia-na section
foreach ($content_to_flush as $cached_line) {
fputs($fout, $cached_line);
}
} else {
$removed_leases++;
// skip empty line
fgets($fin, 4096);
}
// end of segment
$content_to_flush = array();
$iaaddr = "";
}
}
flock($fin, LOCK_UN);
fclose($fin);
fclose($fout);
@unlink($dhcp_lease_file);
@rename($dhcp_lease_file . ".new", $dhcp_lease_file);
}
if (isset($opts['s'])) {
dhcpd_dhcp6_configure();
}
echo json_encode(["removed_leases" => $removed_leases]);

View File

@ -0,0 +1,179 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2023 Deciso B.V.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------------
list dhcpv6 leases
"""
import ujson
import calendar
import datetime
import time
import argparse
def parse_date(ymd, hms):
dt = '%s %s' % (ymd, hms)
try:
return calendar.timegm(datetime.datetime.strptime(dt, "%Y/%m/%d %H:%M:%S;").timetuple())
except ValueError:
return None
def parse_iaaddr_iaprefix(input):
"""
parse either an iaaddr or iaprefix segment. return a tuple
containing the type parsed and the corresponding segment
"""
seg_type = input[0].split()[0]
segment = dict()
segment[seg_type] = input[0].split()[1]
for line in input[1:]:
parts = line.split()
field_name = parts[0] if len(parts) > 0 else ''
field_value = None
if field_name == 'binding':
segment[field_name] = parts[2].strip(';')
elif field_name in ('preferred-life', 'max-life'):
field_value = parts[1].strip(';')
elif field_name == 'ends':
field_value = parse_date(parts[2], parts[3])
if field_value is not None:
segment[field_name] = field_value
return (seg_type, segment)
def parse_iaid_duid(input):
"""
parse the combined IAID_DUID value. This is provided in the form
of ascii characters. Non-printable characters are provided as octal escapes.
We return the hex representation of the raw IAID_DUID value, the IAID integer,
as well as the separated DUID value in a dict. The IAID_DUID value is
used to uniquely identify a lease, so this value should be used to determine the last
relevant entry in the leases file.
"""
input = input[1:-1] # strip double quotes
parsed = []
i = 0
while i < len(input):
c = input[i]
if c == '\\':
next_c = input[i + 1]
if next_c == '\\' or next == '"':
parsed.append("%02x" % ord(next_c))
i += 1
elif next_c.isnumeric():
octal_to_decimal = int(input[i+1:i+4], 8)
parsed.append("%02x" % octal_to_decimal)
i += 3
else:
parsed.append("%02x" % ord(c))
i += 1
return {
'iaid': int(''.join(reversed(parsed[0:4]))),
'duid': ":".join([str(a) for a in parsed[4:]]),
'iaid_duid': ":".join([str(a) for a in parsed])
}
def parse_lease(lines):
"""
Parse a DHCPv6 lease. We return a two-tuple containing the combined iaid_duid
and the lease. a single lease may contain multiple addresses/prefixes.
"""
lease = dict()
cur_segment = []
addresses = []
prefixes = []
iaid_duid = parse_iaid_duid(lines[0].split()[1])
lease['lease_type'] = lines[0].split()[0]
lease.update(iaid_duid)
for line in lines:
parts = line.split()
if parts[0] == 'cltt' and len(parts) >= 3:
cltt = parse_date(parts[2], parts[3])
lease['cltt'] = cltt
if parts[0] == 'iaaddr' or parts[0] == 'iaprefix':
cur_segment.append(line)
elif len(line) > 3 and line[2] == '}' and len(cur_segment) > 0:
cur_segment.append(line)
(segment_type, segment) = parse_iaaddr_iaprefix(cur_segment)
if segment_type == 'iaaddr':
addresses.append(segment)
if segment_type == 'iaprefix':
prefixes.append(segment)
cur_segment = []
elif len(cur_segment) > 0:
cur_segment.append(line)
# ia_ta/ia_na (addresses) and ia_pd (prefixes) are mutually exclusive.
if addresses:
lease['addresses'] = addresses
if prefixes:
lease['prefixes'] = prefixes
return (iaid_duid['iaid_duid'], lease)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--inactive', help='include inactive leases', default='0', type=str)
args = parser.parse_args()
leasefile = '/var/dhcpd/var/db/dhcpd6.leases'
result = []
cur_lease = []
last_leases = dict()
try:
with open(leasefile, 'r') as leasef:
for line in leasef:
if len(line) > 5 and (line[0:5] == 'ia-ta' or line[0:5] == 'ia-na' or line[0:5] == 'ia-pd'):
cur_lease.append(line)
elif len(line) > 1 and line[0] == '}' and len(cur_lease) > 0:
cur_lease.append(line)
parsed_lease = parse_lease(cur_lease)
last_leases[parsed_lease[0]] = parsed_lease[1]
cur_lease = []
elif len(cur_lease) > 0:
cur_lease.append(line)
except IOError:
pass
for lease in last_leases.values():
if args.inactive == '1':
result.append(lease)
else:
for key in ('addresses', 'prefixes'):
if key in lease:
for i in range(len(lease[key])):
segment = lease[key][i]
if not ('ends' in segment and segment['ends'] is not None and segment['ends'] > time.time()):
del lease[key][i]
if key in lease and lease[key]:
result.append(lease)
print(ujson.dumps(result))

View File

@ -4,9 +4,15 @@ parameters:/inactive %s
type:script_output
message:list dhcp leases %s
[list.leases6]
command:/usr/local/opnsense/scripts/dhcp/get_leases6.py
parameters:--inactive %s
type:script_output
message:list dhcpv6 leases %s
[list.static]
command:/usr/local/sbin/pluginctl -r static_mapping
parameters:%s
parameters:%s %s
type:script_output
message: list dhcp static mappings %s
@ -28,3 +34,9 @@ command:/usr/local/opnsense/scripts/dhcp/cleanup_leases4.php
parameters:-d=%s -s
type:script_output
message:remove lease for %s
[remove.lease6]
command:/usr/local/opnsense/scripts/dhcp/cleanup_leases6.php
parameters:-d=%s -s
type:script_output
message:remove lease6 for %s

View File

@ -1,574 +0,0 @@
<?php
/*
* Copyright (C) 2014-2021 Deciso B.V.
* Copyright (C) 2004-2009 Scott Ullrich <sullrich@gmail.com>
* Copyright (C) 2011 Seth Mos <seth.mos@dds.nl>
* Copyright (C) 2003-2004 Manuel Kasper <mk@neon1.net>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
require_once("guiconfig.inc");
require_once("interfaces.inc");
require_once("plugins.inc.d/dhcpd.inc");
function adjust_utc($dt)
{
foreach (config_read_array('dhcpdv6') as $dhcpdv6) {
if (!empty($dhcpdv6['dhcpv6leaseinlocaltime'])) {
/* we want local time, so specify this is actually UTC */
return strftime('%Y/%m/%d %H:%M:%S', strtotime("{$dt} UTC"));
}
}
/* lease time is in UTC, here just pretend it's the correct time */
return strftime('%Y/%m/%d %H:%M:%S UTC', strtotime($dt));
}
function remove_duplicate($array, $field)
{
foreach ($array as $sub) {
$cmp[] = $sub[$field];
}
$unique = array_unique(array_reverse($cmp,true));
foreach ($unique as $k => $rien) {
$new[] = $array[$k];
}
return $new;
}
$interfaces = legacy_config_get_interfaces(array('virtual' => false));
$order = 'ip';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$leases_content = dhcpd_leases(6);
$leases_count = count($leases_content);
exec("/usr/sbin/ndp -an", $rawdata);
$ndpdata = array();
foreach ($rawdata as $line) {
$elements = preg_split('/\s+/ ',$line);
if ($elements[1] != "(incomplete)") {
$ndpent = array();
$ip = trim(str_replace(array('(',')'),'',$elements[0]));
$ndpent['mac'] = trim($elements[1]);
$ndpent['interface'] = trim($elements[2]);
$ndpdata[$ip] = $ndpent;
}
}
$pools = [];
$leases = [];
$prefixes = [];
$mappings = [];
$i = 0;
$l = 0;
$p = 0;
while($i < $leases_count) {
$is_prefix = false;
$duid_split = [];
$entry = [];
preg_match('/ia-.. "(.*)" { (.*)/ ', $leases_content[$i], $duid_split);
if (!empty($duid_split[1])) {
$iaid_duid = dhcpd_parse_duid($duid_split[1]);
$entry['iaid'] = hexdec(implode('', array_reverse($iaid_duid[0])));
$entry['duid'] = implode(':', $iaid_duid[1]);
$data = explode(' ', $duid_split[2]);
} else {
$data = explode(' ', $leases_content[$i]);
}
$f = 0;
$fcount = count($data);
/* with less than 12 fields there is nothing useful */
if ($fcount < 12) {
$i++;
continue;
}
while($f < $fcount) {
switch($data[$f]) {
case "failover":
$pools[$p]['name'] = $data[$f+2];
$pools[$p]['mystate'] = $data[$f+7];
$pools[$p]['peerstate'] = $data[$f+14];
$pools[$p]['mydate'] = $data[$f+10];
$pools[$p]['mydate'] .= " " . $data[$f+11];
$pools[$p]['peerdate'] = $data[$f+17];
$pools[$p]['peerdate'] .= " " . $data[$f+18];
$p++;
$i++;
continue 3;
case "ia-pd":
$is_prefix = true;
/* FALLTHROUGH */
case "ia-na":
if ($data[$f+1][0] == '"') {
$duid = "";
/* FIXME: This needs a safety belt to prevent an infinite loop */
while ($data[$f][strlen($data[$f])-1] != '"') {
$duid .= " " . $data[$f+1];
$f++;
}
$entry['duid'] = $duid;
} else {
$entry['duid'] = $data[$f+1];
}
$entry['type'] = "dynamic";
$f = $f+2;
break;
case "iaaddr":
$entry['ip'] = $data[$f+1];
$entry['type'] = "dynamic";
if (in_array($entry['ip'], array_keys($ndpdata))) {
$entry['online'] = 'online';
} else {
$entry['online'] = 'offline';
}
$f = $f+2;
break;
case "iaprefix":
$is_prefix = true;
$entry['prefix'] = $data[$f+1];
$entry['type'] = "dynamic";
$f = $f+2;
break;
case "starts":
$entry['start'] = $data[$f+2];
$entry['start'] .= " " . $data[$f+3];
$f = $f+3;
break;
case "ends":
$entry['end'] = $data[$f+2];
$entry['end'] .= " " . $data[$f+3];
$f = $f+3;
break;
case "tstp":
$f = $f+3;
break;
case "tsfp":
$f = $f+3;
break;
case "atsfp":
$f = $f+3;
break;
case "cltt":
$entry['start'] = $data[$f+2];
$entry['start'] .= " " . $data[$f+3];
$f = $f+3;
break;
case "binding":
switch($data[$f+2]) {
case "active":
$entry['act'] = "active";
break;
case "free":
$entry['act'] = "expired";
$entry['online'] = "offline";
break;
case "backup":
$entry['act'] = "reserved";
$entry['online'] = "offline";
break;
case "released":
$entry['act'] = "released";
$entry['online'] = "offline";
}
$f = $f+1;
break;
case "next":
/* skip the next binding statement */
$f = $f+3;
break;
case "hardware":
$f = $f+2;
break;
case "client-hostname":
if ($data[$f+1] != '') {
$entry['hostname'] = preg_replace('/"/','',$data[$f+1]);
} else {
$hostname = gethostbyaddr($entry['ip']);
if ($hostname != '') {
$entry['hostname'] = $hostname;
}
}
$f = $f+1;
break;
case "uid":
$f = $f+1;
break;
}
$f++;
}
if ($is_prefix) {
$prefixes[] = $entry;
} else {
$leases[] = $entry;
$mappings[$entry['iaid'] . $entry['duid']] = $entry['ip'];
}
$l++;
$i++;
}
if (count($leases) > 0) {
$leases = remove_duplicate($leases,"ip");
}
if (count($prefixes) > 0) {
$prefixes = remove_duplicate($prefixes,"prefix");
}
if (count($pools) > 0) {
$pools = remove_duplicate($pools,"name");
asort($pools);
}
$duids = [];
foreach ($leases as $i => $this_lease) {
if (!empty($this_lease['duid'])) {
if (!isset($duids[$this_lease['duid']])) {
$duids[$this_lease['duid']] = [];
}
$duids[$this_lease['duid']][] = $i;
}
}
foreach (dhcpd_staticmap(6, "not.found", legacy_interfaces_details(), false) as $static) {
$slease = [];
$slease['ip'] = $static['ipaddrv6'];
$slease['if'] = $static['interface'];
$slease['type'] = 'static';
$slease['duid'] = $static['duid'];
$slease['start'] = '';
$slease['end'] = '';
$slease['hostname'] = $static['hostname'];
$slease['descr'] = $static['descr'];
$slease['act'] = 'static';
$slease['online'] = in_array($slease['ip'], array_keys($ndpdata)) ? 'online' : 'offline';
if (isset($duids[$slease['duid']])) {
/* update lease with static data */
foreach ($slease as $key => $value) {
if (!empty($value)) {
foreach ($duids[$slease['duid']] as $idx) {
$leases[$idx][$key] = $value;
}
}
}
} else {
$leases[] = $slease;
}
}
if (isset($_GET['order']) && in_array($_GET['order'], ['int', 'ip', 'iaid', 'duid', 'hostname', 'descr', 'start', 'end', 'online', 'act'])) {
$order = $_GET['order'];
}
usort($leases,
function ($a, $b) use ($order) {
$cmp = ($order === 'ip') ? 0 : strnatcasecmp($a[$order] ?? '', $b[$order] ?? '');
if ($cmp === 0) {
$cmp = ipcmp($a['ip'], $b['ip']);
}
return $cmp;
}
);
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!empty($_POST['deleteip']) && is_ipaddr($_POST['deleteip'])) {
killbypid('/var/dhcpd/var/run/dhcpdv6.pid');
$leasesfile = '/var/dhcpd/var/db/dhcpd6.leases'; /* XXX needs wrapper */
$fin = @fopen($leasesfile, "r");
$fout = @fopen($leasesfile.".new", "w");
if ($fin) {
$ip_to_remove = $_POST['deleteip'];
$iaaddr = "";
$content_to_flush = array();
while (($line = fgets($fin, 4096)) !== false) {
$fields = explode(' ', trim($line));
if ($fields[0] == 'iaaddr') {
// lease segment, record ip
$iaaddr = trim($fields[1]);
$content_to_flush[] = $line;
} elseif ($fields[0] == 'ia-na' || count($content_to_flush) > 0) {
$content_to_flush[] = $line;
} else {
// output data directly if we're not in a "ia-na" section
fputs($fout, $line);
}
if ($line == "}\n") {
if ($iaaddr != $ip_to_remove) {
// write ia-na section
foreach ($content_to_flush as $cached_line) {
fputs($fout, $cached_line);
}
} else {
// skip empty line
fgets($fin, 4096);
}
// end of segment
$content_to_flush = array();
$iaaddr = "";
}
}
fclose($fin);
fclose($fout);
@unlink($leasesfile);
@rename($leasesfile.".new", $leasesfile);
dhcpd_dhcp6_configure();
}
}
exit;
}
$service_hook = 'dhcpd6';
include("head.inc");
$leases_count = 0;
foreach ($leases as $data) {
if (!($data['act'] == 'active' || $data['act'] == 'static' || $_GET['all'] == 1)) {
continue;
}
$leases_count++;
}
$gentitle_suffix = " ($leases_count)";
legacy_html_escape_form_data($leases);
?>
<body>
<script>
$( document ).ready(function() {
$(".act_delete").click(function(){
$.post(window.location, {deleteip: $(this).data('deleteip')}, function(data) {
location.reload();
});
});
// keep sorting in place.
$(".act_sort").click(function(){
var all = <?=!empty($_GET['all']) ? 1 : 0;?> ;
document.location = document.location.origin + window.location.pathname +"?all="+all+"&order="+$(this).data('field');
});
$(".act_sort :first-child").css('cursor', 'pointer');
});
</script>
<?php include("fbegin.inc"); ?>
<section class="page-content-main">
<div class="container-fluid">
<div class="row">
<?php
/* only print pool status when we have one */
if (count($pools) > 0):?>
<section class="col-xs-12">
<div class="content-box">
<div class="table-responsive">
<table class="table table-striped sortable __nomb">
<tr>
<td><?=gettext("Failover Group"); ?></a></td>
<td><?=gettext("My State"); ?></a></td>
<td><?=gettext("Since"); ?></a></td>
<td><?=gettext("Peer State"); ?></a></td>
<td><?=gettext("Since"); ?></a></td>
</tr>
<?php
foreach ($pools as $data):?>
<tr>
<td><?=$data['name'];?></td>
<td><?=$data['mystate'];?></td>
<td><?=adjust_utc($data['mydate']);?></td>
<td><?=$data['peerstate'];?></td>
<td><?=adjust_utc($data['peerdate']);?></td>
</tr>
<?php
endforeach;?>
</table>
</div>
</div>
</section>
<?php
endif;?>
<section class="col-xs-12">
<div class="content-box">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th class="act_sort" data-field="int"><span><?=gettext("Interface"); ?></span></th>
<th class="act_sort" data-field="ip"><span><?=gettext("IPv6 address"); ?></span></th>
<th class="act_sort" data-field="iaid"><span><?=gettext("IAID"); ?></span></th>
<th class="act_sort" data-field="duid"><span><?=gettext("DUID/MAC"); ?></span></th>
<th class="act_sort" data-field="hostname"><span><?=gettext("Hostname"); ?></span></th>
<th class="act_sort" data-field="descr"><span><?=gettext("Description"); ?></span></th>
<th class="act_sort" data-field="start"><span><?=gettext("Start"); ?></span></th>
<th class="act_sort" data-field="end"><span><?=gettext("End"); ?></span></th>
<th class="act_sort" data-field="online"><span><?=gettext("Online"); ?></span></th>
<th class="act_sort" data-field="act"><span><?=gettext("Lease Type"); ?></span></th>
<th class="text-nowrap"></th>
</tr>
</thead>
<tbody>
<?php
$mac_man = json_decode(configd_run("interface list macdb json"), true);
foreach ($leases as $data):
if (!($data['act'] == 'active' || $data['act'] == 'static' || $_GET['all'] == 1)) {
continue;
}
if (!isset($data['if'])) {
$data['if'] = convert_real_interface_to_friendly_interface_name(guess_interface_from_ip($data['ip']));
}
$data['int'] = htmlspecialchars($interfaces[$data['if']]['descr']);
$mac_from_ndp = !empty($ndpdata[$data['ip']]) ? $ndpdata[$data['ip']]['mac'] : "";
$vendor_from_ndp = empty($mac_from_ndp) ? "" : ($mac_man[strtoupper(implode("", explode(":", substr($mac_from_ndp, 0, 8))))] ?? "");
$mac_from_duid = "";
$duid_formatted = $data['duid'];
$duid_type = substr($data['duid'], 0, 5);
if ($duid_type === "00:01" || $duid_type === "00:03"){
$duid_subtype = substr($data['duid'], 6, 5);
if ($duid_subtype === "00:01") {
$mac_from_duid = substr($data['duid'], -17, 17);
$duid_formatted = substr($data['duid'], 0, strlen($data['duid']) - 17) . '<u>' . $mac_from_duid . '</u>';
}
}
$vendor_from_duid = empty($mac_from_duid) ? "" : ($mac_man[strtoupper(implode("", explode(":", substr($mac_from_duid, 0, 8))))] ?? "");
$duid_content = $duid_formatted;
if (!empty($vendor_from_duid)) {
$duid_content .= '<br/><small><i>'.$vendor_from_duid.'</i></small>';
}
if (!empty($mac_from_ndp) && $mac_from_duid !== $mac_from_ndp) {
$duid_content .= '</br>'.gettext('NDP MAC').': '.$mac_from_ndp;
if (!empty($vendor_from_ndp)) {
$duid_content .= '<br/><small><i>'.$vendor_from_ndp.'</i></small>';
}
}
?>
<tr>
<td><?=$data['int'];?></td>
<td><?=$data['ip'];?></td>
<td><?=$data['iaid'];?></td>
<td><?=$duid_content;?></td>
<td><?= !empty($data['hostname']) ? html_safe($data['hostname']) : '' ?></td>
<td><?= html_safe($data['descr'] ?? '');?></td>
<td><?= !empty($data['start']) ? adjust_utc($data['start']) : '' ?></td>
<td><?= !empty($data['end']) ? adjust_utc($data['end']) : '' ?></td>
<td>
<i class="fa fa-<?=$data['online']=='online' ? 'signal' : 'ban';?>" title="<?=$data['online'];?>" data-toggle="tooltip"></i>
</td>
<td><?=$data['act'];?></td>
<td class="text-nowrap">
<?php if (!empty($config['interfaces'][$data['if']])): ?>
<?php if (empty($config['interfaces'][$data['if']]['virtual']) && isset($config['interfaces'][$data['if']]['enable'])): ?>
<?php if (is_ipaddrv6($config['interfaces'][$data['if']]['ipaddrv6']) || !empty($config['interfaces'][$data['if']]['dhcpd6track6allowoverride'])): ?>
<?php if ($data['type'] == 'dynamic'): ?>
<a class="btn btn-default btn-xs" href="services_dhcpv6_edit.php?if=<?=$data['if'];?>&amp;duid=<?=$data['duid'];?>&amp;hostname=<?=$data['hostname'];?>">
<i class="fa fa-plus fa-fw"></i>
</a>
<?php if ($data['online'] != 'online'): ?>
<a class="act_delete btn btn-default btn-xs" href="#" data-deleteip="<?=$data['ip'];?>" title="<?= html_safe(gettext('Delete')) ?>" data-toggle="tooltip">
<i class="fa fa-trash fa-fw"></i>
</a>
<?php endif ?>
<?php endif ?>
<?php endif ?>
<?php endif ?>
<?php endif ?>
</td>
</tr>
<?php
endforeach;?>
</tbody>
</table>
</div>
</div>
</section>
<section class="col-xs-12">
<div class="content-box">
<header class="content-box-head container-fluid">
<h3><?=gettext("Delegated Prefixes");?></h3>
</header>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th><?=gettext("IPv6 Prefix"); ?></th>
<th><?=gettext("IAID"); ?></th>
<th><?=gettext("DUID"); ?></th>
<th><?=gettext("Start"); ?></th>
<th><?=gettext("End"); ?></th>
<th><?=gettext("State"); ?></th>
</tr>
</thead>
<tbody>
<?php
foreach ($prefixes as $data):?>
<tr>
<td>
<?=!empty($mappings[$data['iaid'] . $data['duid']]) ? $mappings[$data['iaid'] . $data['duid']] : "";?>
<?=$data['prefix'];?>
</td>
<td><?=$data['iaid'];?></td>
<td><?=$data['duid'];?></td>
<td><?= !empty($data['start']) ? adjust_utc($data['start']) : '' ?></td>
<td><?= !empty($data['end']) ? adjust_utc($data['end']) : '' ?></td>
<td><?=$data['act'];?></td>
</tr>
<?php
endforeach;?>
</tbody>
</table>
</div>
</div>
</section>
<section class="col-xs-12">
<form method="get">
<input type="hidden" name="order" value="<?= html_safe($order) ?>" />
<?php if ($_GET['all'] ?? 0): ?>
<input type="hidden" name="all" value="0" />
<input type="submit" class="btn btn-default" value="<?= html_safe(gettext('Show active and static leases only')) ?>" />
<?php else: ?>
<input type="hidden" name="all" value="1" />
<input type="submit" class="btn btn-default" value="<?= html_safe(gettext('Show all configured leases')) ?>" />
<?php endif; ?>
</form>
<?php if ($leases == 0): ?>
<p><?= gettext('No leases file found. Is the DHCP server active?') ?></p>
<?php endif; ?>
</section>
</div>
</div>
</section>
<?php
include("foot.inc");