diff --git a/src/etc/inc/plugins.inc.d/dhcpd.inc b/src/etc/inc/plugins.inc.d/dhcpd.inc index 7eaa475d0..b2a4daed9 100644 --- a/src/etc/inc/plugins.inc.d/dhcpd.inc +++ b/src/etc/inc/plugins.inc.d/dhcpd.inc @@ -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 = []; diff --git a/src/etc/inc/plugins.inc.d/dnsmasq.inc b/src/etc/inc/plugins.inc.d/dnsmasq.inc index d9935f096..db88427dd 100644 --- a/src/etc/inc/plugins.inc.d/dnsmasq.inc +++ b/src/etc/inc/plugins.inc.d/dnsmasq.inc @@ -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 */ diff --git a/src/etc/inc/plugins.inc.d/unbound.inc b/src/etc/inc/plugins.inc.d/unbound.inc index f6f932c15..3e1ff9445 100644 --- a/src/etc/inc/plugins.inc.d/unbound.inc +++ b/src/etc/inc/plugins.inc.d/unbound.inc @@ -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 */ diff --git a/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/Api/LeasesController.php b/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/Api/LeasesController.php new file mode 100644 index 000000000..582b00381 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/Api/LeasesController.php @@ -0,0 +1,248 @@ +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; + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/Api/ServiceController.php b/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/Api/ServiceController.php new file mode 100644 index 000000000..185d93d4d --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/Api/ServiceController.php @@ -0,0 +1,101 @@ +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; + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/LeasesController.php b/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/LeasesController.php new file mode 100644 index 000000000..872d5d7de --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/DHCPv6/LeasesController.php @@ -0,0 +1,39 @@ +view->pick('OPNsense/DHCPv6/leases'); + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml index 0276d0ee5..eb26415c5 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml @@ -507,7 +507,8 @@ Status: DHCPv6 leases - status_dhcpv6_leases.php* + ui/dhcpv6/leases + api/dhcpv6/leases/* diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml index 0840d62fb..f481c62aa 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml @@ -191,9 +191,7 @@ - -
- + diff --git a/src/opnsense/mvc/app/views/OPNsense/DHCPv6/leases.volt b/src/opnsense/mvc/app/views/OPNsense/DHCPv6/leases.volt new file mode 100644 index 000000000..1abdcbc09 --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/DHCPv6/leases.volt @@ -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. + #} + + + + +
+
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
{{ lang._('Interface') }}{{ lang._('IP Address') }}{{ lang._('IAID') }}{{ lang._('DUID') }}{{ lang._('MAC Address') }}{{ lang._('Description') }}{{ lang._('Last Transaction Time') }}{{ lang._('End') }}{{ lang._('Status') }}{{ lang._('State') }}{{ lang._('Lease Type') }}
+
+
+ + + + + + + + + + + + + + + + + +
{{ lang._('IPv6 Prefix') }}{{ lang._('IAID') }}{{ lang._('DUID') }}{{ lang._('Last Transaction Time') }}{{ lang._('End') }}{{ lang._('State') }}
+
+
diff --git a/src/opnsense/scripts/dhcp/cleanup_leases6.php b/src/opnsense/scripts/dhcp/cleanup_leases6.php new file mode 100755 index 000000000..44432f19e --- /dev/null +++ b/src/opnsense/scripts/dhcp/cleanup_leases6.php @@ -0,0 +1,108 @@ +#!/usr/local/bin/php + 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]); diff --git a/src/opnsense/scripts/dhcp/get_leases6.py b/src/opnsense/scripts/dhcp/get_leases6.py new file mode 100755 index 000000000..b092aa98a --- /dev/null +++ b/src/opnsense/scripts/dhcp/get_leases6.py @@ -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)) diff --git a/src/opnsense/service/conf/actions.d/actions_dhcpd.conf b/src/opnsense/service/conf/actions.d/actions_dhcpd.conf index 218c10b1a..091104ec5 100644 --- a/src/opnsense/service/conf/actions.d/actions_dhcpd.conf +++ b/src/opnsense/service/conf/actions.d/actions_dhcpd.conf @@ -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 diff --git a/src/www/status_dhcpv6_leases.php b/src/www/status_dhcpv6_leases.php deleted file mode 100644 index ae814f2da..000000000 --- a/src/www/status_dhcpv6_leases.php +++ /dev/null @@ -1,574 +0,0 @@ - - * Copyright (C) 2011 Seth Mos - * Copyright (C) 2003-2004 Manuel Kasper - * 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); -?> - - - - -
-
-
- - 0):?> -
-
-
- - - - - - - - - - - - - - - - - - -
-
-
-
- -
-
-
- - - - - - - - - - - - - - - - - -' . $mac_from_duid . ''; - } - } - $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 .= '
'.$vendor_from_duid.''; - } - if (!empty($mac_from_ndp) && $mac_from_duid !== $mac_from_ndp) { - $duid_content .= '
'.gettext('NDP MAC').': '.$mac_from_ndp; - if (!empty($vendor_from_ndp)) { - $duid_content .= '
'.$vendor_from_ndp.''; - } - } - ?> - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - -
-
-
-
- -
-
-
-

-
-
- - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
-
-
- -
-
- - - - - - - - -
- -

- -
-
-
-
-