+
+
+
+
+
+
+
+
+
+
+
+ | {{ 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