diff --git a/interface-definitions/include/version/dhcpv6-server-version.xml.i b/interface-definitions/include/version/dhcpv6-server-version.xml.i
index cb026a54a..bfef27b77 100644
--- a/interface-definitions/include/version/dhcpv6-server-version.xml.i
+++ b/interface-definitions/include/version/dhcpv6-server-version.xml.i
@@ -1,3 +1,3 @@
 <!-- include start from include/version/dhcpv6-server-version.xml.i -->
-<syntaxVersion component='dhcpv6-server' version='3'></syntaxVersion>
+<syntaxVersion component='dhcpv6-server' version='4'></syntaxVersion>
 <!-- include end -->
diff --git a/interface-definitions/service_dhcp-server.xml.in b/interface-definitions/service_dhcp-server.xml.in
index 27485b6d4..5c9d4a360 100644
--- a/interface-definitions/service_dhcp-server.xml.in
+++ b/interface-definitions/service_dhcp-server.xml.in
@@ -1,211 +1,223 @@
 <?xml version="1.0"?>
 <!-- DHCP server configuration -->
 <interfaceDefinition>
   <node name="service">
     <children>
       <node name="dhcp-server" owner="${vyos_conf_scripts_dir}/service_dhcp-server.py">
         <properties>
           <help>Dynamic Host Configuration Protocol (DHCP) for DHCP server</help>
           <priority>911</priority>
         </properties>
         <children>
           #include <include/generic-disable-node.xml.i>
           <leafNode name="dynamic-dns-update">
             <properties>
               <help>Dynamically update Domain Name System (RFC4702)</help>
               <valueless/>
             </properties>
           </leafNode>
           <node name="failover">
             <properties>
               <help>DHCP failover configuration</help>
             </properties>
             <children>
               #include <include/source-address-ipv4.xml.i>
               <leafNode name="remote">
                 <properties>
                   <help>IPv4 remote address used for connectio</help>
                   <valueHelp>
                     <format>ipv4</format>
                     <description>IPv4 address of failover peer</description>
                   </valueHelp>
                   <constraint>
                     <validator name="ipv4-address"/>
                   </constraint>
                 </properties>
               </leafNode>
               <leafNode name="name">
                 <properties>
                   <help>Peer name used to identify connection</help>
                   <constraint>
                     #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
                   </constraint>
                   <constraintErrorMessage>Invalid failover peer name. May only contain letters, numbers and .-_</constraintErrorMessage>
                 </properties>
               </leafNode>
               <leafNode name="status">
                 <properties>
                   <help>Failover hierarchy</help>
                   <completionHelp>
                     <list>primary secondary</list>
                   </completionHelp>
                   <valueHelp>
                     <format>primary</format>
                     <description>Configure this server to be the primary node</description>
                   </valueHelp>
                   <valueHelp>
                     <format>secondary</format>
                     <description>Configure this server to be the secondary node</description>
                   </valueHelp>
                   <constraint>
                     <regex>(primary|secondary)</regex>
                   </constraint>
                   <constraintErrorMessage>Invalid DHCP failover peer status</constraintErrorMessage>
                 </properties>
               </leafNode>
               #include <include/pki/ca-certificate.xml.i>
               #include <include/pki/certificate.xml.i>
             </children>
           </node>
           <leafNode name="hostfile-update">
             <properties>
               <help>Updating /etc/hosts file (per client lease)</help>
               <valueless/>
             </properties>
           </leafNode>
           #include <include/listen-address-ipv4.xml.i>
           #include <include/listen-interface-multi-broadcast.xml.i>
           <tagNode name="shared-network-name">
             <properties>
               <help>Name of DHCP shared network</help>
               <constraint>
                 #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
               </constraint>
               <constraintErrorMessage>Invalid shared network name. May only contain letters, numbers and .-_</constraintErrorMessage>
             </properties>
             <children>
               <leafNode name="authoritative">
                 <properties>
                   <help>Option to make DHCP server authoritative for this physical network</help>
                   <valueless/>
                 </properties>
               </leafNode>
               #include <include/dhcp/option-v4.xml.i>
               #include <include/generic-description.xml.i>
               #include <include/generic-disable-node.xml.i>
               <tagNode name="subnet">
                 <properties>
                   <help>DHCP subnet for shared network</help>
                   <valueHelp>
                     <format>ipv4net</format>
                     <description>IPv4 address and prefix length</description>
                   </valueHelp>
                   <constraint>
                     <validator name="ipv4-prefix"/>
                   </constraint>
                   <constraintErrorMessage>Invalid IPv4 subnet definition</constraintErrorMessage>
                 </properties>
                 <children>
                   #include <include/dhcp/option-v4.xml.i>
                   #include <include/generic-description.xml.i>
                   #include <include/generic-disable-node.xml.i>
                   <leafNode name="exclude">
                     <properties>
                       <help>IP address to exclude from DHCP lease range</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>IPv4 address to exclude from lease range</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <leafNode name="lease">
                     <properties>
                       <help>Lease timeout in seconds</help>
                       <valueHelp>
                         <format>u32</format>
                         <description>DHCP lease time in seconds</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 0-4294967295"/>
                       </constraint>
                       <constraintErrorMessage>DHCP lease time must be between 0 and 4294967295 (49 days)</constraintErrorMessage>
                     </properties>
                     <defaultValue>86400</defaultValue>
                   </leafNode>
                   <tagNode name="range">
                     <properties>
                       <help>DHCP lease range</help>
                       <constraint>
                         #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
                       </constraint>
                       <constraintErrorMessage>Invalid range name, may only be alphanumeric, dot and hyphen</constraintErrorMessage>
                     </properties>
                     <children>
                       #include <include/dhcp/option-v4.xml.i>
                       <leafNode name="start">
                         <properties>
                           <help>First IP address for DHCP lease range</help>
                           <valueHelp>
                             <format>ipv4</format>
                             <description>IPv4 start address of pool</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv4-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="stop">
                         <properties>
                           <help>Last IP address for DHCP lease range</help>
                           <valueHelp>
                             <format>ipv4</format>
                             <description>IPv4 end address of pool</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv4-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
                     </children>
                   </tagNode>
                   <tagNode name="static-mapping">
                     <properties>
                       <help>Hostname for static mapping reservation</help>
                       <constraint>
                         <validator name="fqdn"/>
                       </constraint>
                       <constraintErrorMessage>Invalid static mapping hostname</constraintErrorMessage>
                     </properties>
                     <children>
                       #include <include/dhcp/option-v4.xml.i>
                       #include <include/generic-description.xml.i>
                       #include <include/generic-disable-node.xml.i>
                       <leafNode name="ip-address">
                         <properties>
                           <help>Fixed IP address of static mapping</help>
                           <valueHelp>
                             <format>ipv4</format>
                             <description>IPv4 address used in static mapping</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv4-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       #include <include/interface/mac.xml.i>
                       #include <include/interface/duid.xml.i>
                     </children>
                   </tagNode>
+                  <leafNode name="subnet-id">
+                    <properties>
+                      <help>Unique ID mapped to leases in the lease file</help>
+                      <valueHelp>
+                        <format>u32</format>
+                        <description>Unique subnet ID</description>
+                      </valueHelp>
+                      <constraint>
+                        <validator name="numeric" argument="--range 1-4294967295"/>
+                      </constraint>
+                    </properties>
+                  </leafNode>
                 </children>
               </tagNode>
             </children>
           </tagNode>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/interface-definitions/service_dhcpv6-server.xml.in b/interface-definitions/service_dhcpv6-server.xml.in
index 6f7f3c1da..6934ceeec 100644
--- a/interface-definitions/service_dhcpv6-server.xml.in
+++ b/interface-definitions/service_dhcpv6-server.xml.in
@@ -1,375 +1,387 @@
 <?xml version="1.0"?>
 <interfaceDefinition>
   <node name="service">
     <children>
       <node name="dhcpv6-server" owner="${vyos_conf_scripts_dir}/service_dhcpv6-server.py">
         <properties>
           <help>DHCP for IPv6 (DHCPv6) server</help>
           <priority>900</priority>
         </properties>
         <children>
           #include <include/generic-disable-node.xml.i>
           <node name="global-parameters">
             <properties>
               <help>Additional global parameters for DHCPv6 server</help>
             </properties>
             <children>
               #include <include/name-server-ipv6.xml.i>
             </children>
           </node>
           <leafNode name="preference">
             <properties>
               <help>Preference of this DHCPv6 server compared with others</help>
               <valueHelp>
                 <format>u32:0-255</format>
                 <description>DHCPv6 server preference (0-255)</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 0-255"/>
               </constraint>
               <constraintErrorMessage>Preference must be between 0 and 255</constraintErrorMessage>
             </properties>
           </leafNode>
           <tagNode name="shared-network-name">
             <properties>
               <help>DHCPv6 shared network name</help>
               <constraint>
                 #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
               </constraint>
               <constraintErrorMessage>Invalid DHCPv6 shared network name. May only contain letters, numbers and .-_</constraintErrorMessage>
             </properties>
             <children>
               #include <include/generic-disable-node.xml.i>
               #include <include/generic-description.xml.i>
               <leafNode name="interface">
                 <properties>
                   <help>Optional interface for this shared network to accept requests from</help>
                   <completionHelp>
                     <script>${vyos_completion_dir}/list_interfaces</script>
                   </completionHelp>
                   <valueHelp>
                     <format>txt</format>
                     <description>Interface name</description>
                   </valueHelp>
                   <constraint>
                     #include <include/constraint/interface-name.xml.i>
                   </constraint>
                 </properties>
               </leafNode>
               <node name="common-options">
                 <properties>
                   <help>Common options to distribute to all clients, including stateless clients</help>
                 </properties>
                 <children>
                   <leafNode name="info-refresh-time">
                     <properties>
                       <help>Time (in seconds) that stateless clients should wait between refreshing the information they were given</help>
                       <valueHelp>
                         <format>u32:1-4294967295</format>
                         <description>DHCPv6 information refresh time</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-4294967295"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   #include <include/dhcp/domain-search.xml.i>
                   #include <include/name-server-ipv6.xml.i>
                 </children>
               </node>
               <tagNode name="subnet">
                 <properties>
                   <help>IPv6 DHCP subnet for this shared network</help>
                   <valueHelp>
                     <format>ipv6net</format>
                     <description>IPv6 address and prefix length</description>
                   </valueHelp>
                   <constraint>
                     <validator name="ipv6-prefix"/>
                   </constraint>
                 </properties>
                 <children>
                   <node name="address-range">
                     <properties>
                       <help>Parameters setting ranges for assigning IPv6 addresses</help>
                     </properties>
                     <children>
                       <leafNode name="prefix">
                         <properties>
                           <help>IPv6 prefix defining range of addresses to assign</help>
                           <valueHelp>
                             <format>ipv6net</format>
                             <description>IPv6 address and prefix length</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv6-prefix"/>
                           </constraint>
                           <multi/>
                         </properties>
                       </leafNode>
                       <tagNode name="start">
                         <properties>
                           <help>First in range of consecutive IPv6 addresses to assign</help>
                           <valueHelp>
                             <format>ipv6</format>
                             <description>IPv6 address</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv6-address"/>
                           </constraint>
                         </properties>
                         <children>
                           <leafNode name="stop">
                             <properties>
                               <help>Last in range of consecutive IPv6 addresses</help>
                               <valueHelp>
                                 <format>ipv6</format>
                                 <description>IPv6 address</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="ipv6-address"/>
                               </constraint>
                             </properties>
                           </leafNode>
                         </children>
                       </tagNode>
                     </children>
                   </node>
                   #include <include/dhcp/captive-portal.xml.i>
                   #include <include/dhcp/domain-search.xml.i>
                   <node name="lease-time">
                     <properties>
                       <help>Parameters relating to the lease time</help>
                     </properties>
                     <children>
                       <leafNode name="default">
                         <properties>
                           <help>Default time (in seconds) that will be assigned to a lease</help>
                           <valueHelp>
                             <format>u32:1-4294967295</format>
                             <description>DHCPv6 valid lifetime</description>
                           </valueHelp>
                           <constraint>
                             <validator name="numeric" argument="--range 1-4294967295"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="maximum">
                         <properties>
                           <help>Maximum time (in seconds) that will be assigned to a lease</help>
                           <valueHelp>
                             <format>u32:1-4294967295</format>
                             <description>Maximum lease time in seconds</description>
                           </valueHelp>
                           <constraint>
                             <validator name="numeric" argument="--range 1-4294967295"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="minimum">
                         <properties>
                           <help>Minimum time (in seconds) that will be assigned to a lease</help>
                           <valueHelp>
                             <format>u32:1-4294967295</format>
                             <description>Minimum lease time in seconds</description>
                           </valueHelp>
                           <constraint>
                             <validator name="numeric" argument="--range 1-4294967295"/>
                           </constraint>
                         </properties>
                       </leafNode>
                     </children>
                   </node>
                   #include <include/name-server-ipv6.xml.i>
                   <leafNode name="nis-domain">
                     <properties>
                       <help>NIS domain name for client to use</help>
                       <constraint>
                         #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
                       </constraint>
                       <constraintErrorMessage>Invalid NIS domain name</constraintErrorMessage>
                     </properties>
                   </leafNode>
                   <leafNode name="nis-server">
                     <properties>
                       <help>IPv6 address of a NIS Server</help>
                       <valueHelp>
                         <format>ipv6</format>
                         <description>IPv6 address of NIS server</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv6-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <leafNode name="nisplus-domain">
                     <properties>
                       <help>NIS+ domain name for client to use</help>
                       <constraint>
                         #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
                       </constraint>
                       <constraintErrorMessage>Invalid NIS+ domain name. May only contain letters, numbers and .-_</constraintErrorMessage>
                     </properties>
                   </leafNode>
                   <leafNode name="nisplus-server">
                     <properties>
                       <help>IPv6 address of a NIS+ Server</help>
                       <valueHelp>
                         <format>ipv6</format>
                         <description>IPv6 address of NIS+ server</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv6-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <node name="prefix-delegation">
                     <properties>
                       <help>Parameters relating to IPv6 prefix delegation</help>
                     </properties>
                     <children>
                       <tagNode name="prefix">
                         <properties>
                           <help>IPv6 prefix to be used in prefix delegation</help>
                           <valueHelp>
                             <format>ipv6</format>
                             <description>IPv6 prefix used in prefix delegation</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv6-address"/>
                           </constraint>
                         </properties>
                         <children>
                           <leafNode name="prefix-length">
                             <properties>
                               <help>Length in bits of prefix</help>
                               <valueHelp>
                                 <format>u32:32-64</format>
                                 <description>Prefix length (32-64)</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="numeric" argument="--range 32-64"/>
                               </constraint>
                               <constraintErrorMessage>Prefix length must be between 32 and 64</constraintErrorMessage>
                             </properties>
                           </leafNode>
                           <leafNode name="delegated-length">
                             <properties>
                               <help>Length in bits of prefixes to be delegated</help>
                               <valueHelp>
                                 <format>u32:32-64</format>
                                 <description>Delegated prefix length (32-64)</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="numeric" argument="--range 32-96"/>
                               </constraint>
                               <constraintErrorMessage>Delegated prefix length must be between 32 and 96</constraintErrorMessage>
                             </properties>
                           </leafNode>
                         </children>
                       </tagNode>
                     </children>
                   </node>
                   <leafNode name="sip-server">
                     <properties>
                       <help>IPv6 address of SIP server</help>
                       <valueHelp>
                         <format>ipv6</format>
                         <description>IPv6 address of SIP server</description>
                       </valueHelp>
                       <valueHelp>
                         <format>hostname</format>
                         <description>FQDN of SIP server</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv6-address"/>
                         <validator name="fqdn"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <leafNode name="sntp-server">
                     <properties>
                       <help>IPv6 address of an SNTP server for client to use</help>
                       <constraint>
                         <validator name="ipv6-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <tagNode name="static-mapping">
                     <properties>
                       <help>Hostname for static mapping reservation</help>
                       <constraint>
                         <validator name="fqdn"/>
                       </constraint>
                       <constraintErrorMessage>Invalid static mapping hostname</constraintErrorMessage>
                     </properties>
                     <children>
                       #include <include/generic-disable-node.xml.i>
                       #include <include/interface/mac.xml.i>
                       #include <include/interface/duid.xml.i>
                       <leafNode name="ipv6-address">
                         <properties>
                           <help>Client IPv6 address for this static mapping</help>
                           <valueHelp>
                             <format>ipv6</format>
                             <description>IPv6 address for this static mapping</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv6-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="ipv6-prefix">
                         <properties>
                           <help>Client IPv6 prefix for this static mapping</help>
                           <valueHelp>
                             <format>ipv6net</format>
                             <description>IPv6 prefix for this static mapping</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv6-prefix"/>
                           </constraint>
                         </properties>
                       </leafNode>
                     </children>
                   </tagNode>
+                  <leafNode name="subnet-id">
+                    <properties>
+                      <help>Unique ID mapped to leases in the lease file</help>
+                      <valueHelp>
+                        <format>u32</format>
+                        <description>Unique subnet ID</description>
+                      </valueHelp>
+                      <constraint>
+                        <validator name="numeric" argument="--range 1-4294967295"/>
+                      </constraint>
+                    </properties>
+                  </leafNode>
                   <node name="vendor-option">
                     <properties>
                       <help>Vendor Specific Options</help>
                     </properties>
                     <children>
                       <node name="cisco">
                         <properties>
                           <help>Cisco specific parameters</help>
                         </properties>
                         <children>
                           <leafNode name="tftp-server">
                             <properties>
                               <help>TFTP server name</help>
                               <valueHelp>
                                 <format>ipv6</format>
                                 <description>TFTP server IPv6 address</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="ipv6-address"/>
                               </constraint>
                               <multi/>
                             </properties>
                           </leafNode>
                         </children>
                       </node>
                     </children>
                   </node>
                 </children>
               </tagNode>
             </children>
           </tagNode>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/python/vyos/kea.py b/python/vyos/kea.py
index 3d8cf3637..aa4fb7ae5 100644
--- a/python/vyos/kea.py
+++ b/python/vyos/kea.py
@@ -1,361 +1,361 @@
 # Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
 # License as published by the Free Software Foundation; either
 # version 2.1 of the License, or (at your option) any later version.
 #
 # This library is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public
 # License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
 import json
 import os
 import socket
 
 from datetime import datetime
 
 from vyos.template import is_ipv6
 from vyos.template import isc_static_route
 from vyos.template import netmask_from_cidr
 from vyos.utils.dict import dict_search_args
 from vyos.utils.file import file_permissions
 from vyos.utils.file import read_file
 from vyos.utils.process import run
 
 kea4_options = {
     'name_server': 'domain-name-servers',
     'domain_name': 'domain-name',
     'domain_search': 'domain-search',
     'ntp_server': 'ntp-servers',
     'pop_server': 'pop-server',
     'smtp_server': 'smtp-server',
     'time_server': 'time-servers',
     'wins_server': 'netbios-name-servers',
     'default_router': 'routers',
     'server_identifier': 'dhcp-server-identifier',
     'tftp_server_name': 'tftp-server-name',
     'bootfile_size': 'boot-size',
     'time_offset': 'time-offset',
     'wpad_url': 'wpad-url',
     'ipv6_only_preferred': 'v6-only-preferred',
     'captive_portal': 'v4-captive-portal'
 }
 
 kea6_options = {
     'info_refresh_time': 'information-refresh-time',
     'name_server': 'dns-servers',
     'domain_search': 'domain-search',
     'nis_domain': 'nis-domain-name',
     'nis_server': 'nis-servers',
     'nisplus_domain': 'nisp-domain-name',
     'nisplus_server': 'nisp-servers',
     'sntp_server': 'sntp-servers',
     'captive_portal': 'v6-captive-portal'
 }
 
 def kea_parse_options(config):
     options = []
 
     for node, option_name in kea4_options.items():
         if node not in config:
             continue
 
         value = ", ".join(config[node]) if isinstance(config[node], list) else config[node]
         options.append({'name': option_name, 'data': value})
 
     if 'client_prefix_length' in config:
         options.append({'name': 'subnet-mask', 'data': netmask_from_cidr('0.0.0.0/' + config['client_prefix_length'])})
 
     if 'ip_forwarding' in config:
         options.append({'name': 'ip-forwarding', 'data': "true"})
 
     if 'static_route' in config:
         default_route = ''
 
         if 'default_router' in config:
             default_route = isc_static_route('0.0.0.0/0', config['default_router'])
 
         routes = [isc_static_route(route, route_options['next_hop']) for route, route_options in config['static_route'].items()]
 
         options.append({'name': 'rfc3442-static-route', 'data': ", ".join(routes if not default_route else routes + [default_route])})
         options.append({'name': 'windows-static-route', 'data': ", ".join(routes)})
 
     if 'time_zone' in config:
         with open("/usr/share/zoneinfo/" + config['time_zone'], "rb") as f:
             tz_string = f.read().split(b"\n")[-2].decode("utf-8")
 
         options.append({'name': 'pcode', 'data': tz_string})
         options.append({'name': 'tcode', 'data': config['time_zone']})
 
     unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller')
     if unifi_controller:
         options.append({
             'name': 'unifi-controller',
             'data': unifi_controller,
             'space': 'ubnt'
         })
 
     return options
 
 def kea_parse_subnet(subnet, config):
-    out = {'subnet': subnet}
+    out = {'subnet': subnet, 'id': int(config['subnet_id'])}
     options = []
 
     if 'option' in config:
         out['option-data'] = kea_parse_options(config['option'])
 
         if 'bootfile_name' in config['option']:
             out['boot-file-name'] = config['option']['bootfile_name']
 
         if 'bootfile_server' in config['option']:
             out['next-server'] = config['option']['bootfile_server']
 
     if 'lease' in config:
         out['valid-lifetime'] = int(config['lease'])
         out['max-valid-lifetime'] = int(config['lease'])
 
     if 'range' in config:
         pools = []
         for num, range_config in config['range'].items():
             start, stop = range_config['start'], range_config['stop']
             pool = {
                 'pool': f'{start} - {stop}'
             }
 
             if 'option' in range_config:
                 pool['option-data'] = kea_parse_options(range_config['option'])
 
                 if 'bootfile_name' in range_config['option']:
                     pool['boot-file-name'] = range_config['option']['bootfile_name']
 
                 if 'bootfile_server' in range_config['option']:
                     pool['next-server'] = range_config['option']['bootfile_server']
 
             pools.append(pool)
         out['pools'] = pools
 
     if 'static_mapping' in config:
         reservations = []
         for host, host_config in config['static_mapping'].items():
             if 'disable' in host_config:
                 continue
 
             reservation = {
                 'hostname': host,
             }
 
             if 'mac' in host_config:
                 reservation['hw-address'] = host_config['mac']
 
             if 'duid' in host_config:
                 reservation['duid'] = host_config['duid']
 
             if 'ip_address' in host_config:
                 reservation['ip-address'] = host_config['ip_address']
 
             if 'option' in host_config:
                 reservation['option-data'] = kea_parse_options(host_config['option'])
 
                 if 'bootfile_name' in host_config['option']:
                     reservation['boot-file-name'] = host_config['option']['bootfile_name']
 
                 if 'bootfile_server' in host_config['option']:
                     reservation['next-server'] = host_config['option']['bootfile_server']
 
             reservations.append(reservation)
         out['reservations'] = reservations
 
     return out
 
 def kea6_parse_options(config):
     options = []
 
     if 'common_options' in config:
         common_opt = config['common_options']
 
         for node, option_name in kea6_options.items():
             if node not in common_opt:
                 continue
 
             value = ", ".join(common_opt[node]) if isinstance(common_opt[node], list) else common_opt[node]
             options.append({'name': option_name, 'data': value})
 
     for node, option_name in kea6_options.items():
         if node not in config:
             continue
 
         value = ", ".join(config[node]) if isinstance(config[node], list) else config[node]
         options.append({'name': option_name, 'data': value})
 
     if 'sip_server' in config:
         sip_servers = config['sip_server']
 
         addrs = []
         hosts = []
 
         for server in sip_servers:
             if is_ipv6(server):
                 addrs.append(server)
             else:
                 hosts.append(server)
 
         if addrs:
             options.append({'name': 'sip-server-addr', 'data': ", ".join(addrs)})
 
         if hosts:
             options.append({'name': 'sip-server-dns', 'data': ", ".join(hosts)})
 
     cisco_tftp = dict_search_args(config, 'vendor_option', 'cisco', 'tftp-server')
     if cisco_tftp:
         options.append({'name': 'tftp-servers', 'code': 2, 'space': 'cisco', 'data': cisco_tftp})
 
     return options
 
 def kea6_parse_subnet(subnet, config):
-    out = {'subnet': subnet}
+    out = {'subnet': subnet, 'id': int(config['subnet_id'])}
     options = kea6_parse_options(config)
 
     if 'address_range' in config:
         addr_range = config['address_range']
         pools = []
 
         if 'prefix' in addr_range:
             for prefix in addr_range['prefix']:
                 pools.append({'pool': prefix})
 
         if 'start' in addr_range:
             for start, range_conf in addr_range['start'].items():
                 stop = range_conf['stop']
                 pools.append({'pool': f'{start} - {stop}'})
 
         out['pools'] = pools
 
     if 'prefix_delegation' in config:
         pd_pools = []
 
         if 'prefix' in config['prefix_delegation']:
             for prefix, pd_conf in config['prefix_delegation']['prefix'].items():
                 pd_pools.append({
                     'prefix': prefix,
                     'prefix-len': int(pd_conf['prefix_length']),
                     'delegated-len': int(pd_conf['delegated_length'])
                 })
 
         out['pd-pools'] = pd_pools
 
     if 'lease_time' in config:
         if 'default' in config['lease_time']:
             out['valid-lifetime'] = int(config['lease_time']['default'])
         if 'maximum' in config['lease_time']:
             out['max-valid-lifetime'] = int(config['lease_time']['maximum'])
         if 'minimum' in config['lease_time']:
             out['min-valid-lifetime'] = int(config['lease_time']['minimum'])
 
     if 'static_mapping' in config:
         reservations = []
         for host, host_config in config['static_mapping'].items():
             if 'disable' in host_config:
                 continue
 
             reservation = {
                 'hostname': host
             }
 
             if 'mac' in host_config:
                 reservation['hw-address'] = host_config['mac']
 
             if 'duid' in host_config:
                 reservation['duid'] = host_config['duid']
 
             if 'ipv6_address' in host_config:
                 reservation['ip-addresses'] = [ host_config['ipv6_address'] ]
 
             if 'ipv6_prefix' in host_config:
                 reservation['prefixes'] = [ host_config['ipv6_prefix'] ]
 
             reservations.append(reservation)
 
         out['reservations'] = reservations
 
     if options:
         out['option-data'] = options
 
     return out
 
 def kea_parse_leases(lease_path):
     contents = read_file(lease_path)
     lines = contents.split("\n")
     output = []
 
     if len(lines) < 2:
         return output
 
     headers = lines[0].split(",")
 
     for line in lines[1:]:
         line_out = dict(zip(headers, line.split(",")))
 
         lifetime = int(line_out['valid_lifetime'])
         expiry = int(line_out['expire'])
 
         line_out['start_timestamp'] = datetime.utcfromtimestamp(expiry - lifetime)
         line_out['expire_timestamp'] = datetime.utcfromtimestamp(expiry) if expiry else None
 
         output.append(line_out)
 
     return output
 
 def _ctrl_socket_command(path, command, args=None):
     if not os.path.exists(path):
         return None
 
     if file_permissions(path) != '0775':
         run(f'sudo chmod 775 {path}')
 
     with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
         sock.connect(path)
 
         payload = {'command': command}
         if args:
             payload['arguments'] = args
 
         sock.send(bytes(json.dumps(payload), 'utf-8'))
         result = b''
         while True:
             data = sock.recv(4096)
             result += data
             if len(data) < 4096:
                 break
 
         return json.loads(result.decode('utf-8'))
 
 def kea_get_active_config(inet):
     ctrl_socket = f'/run/kea/dhcp{inet}-ctrl-socket'
 
     config = _ctrl_socket_command(ctrl_socket, 'config-get')
 
     if not config or 'result' not in config or config['result'] != 0:
         return None
 
     return config
 
 def kea_get_pool_from_subnet_id(config, inet, subnet_id):
     shared_networks = dict_search_args(config, 'arguments', f'Dhcp{inet}', 'shared-networks')
 
     if not shared_networks:
         return None
 
     for network in shared_networks:
         if f'subnet{inet}' not in network:
             continue
 
         for subnet in network[f'subnet{inet}']:
             if 'id' in subnet and int(subnet['id']) == int(subnet_id):
                 return network['name']
 
     return None
diff --git a/smoketest/config-tests/basic-vyos b/smoketest/config-tests/basic-vyos
index ef8bf374a..0bb68b75d 100644
--- a/smoketest/config-tests/basic-vyos
+++ b/smoketest/config-tests/basic-vyos
@@ -1,62 +1,63 @@
 set interfaces ethernet eth0 address '192.168.0.1/24'
 set interfaces ethernet eth0 duplex 'auto'
 set interfaces ethernet eth0 speed 'auto'
 set interfaces ethernet eth1 duplex 'auto'
 set interfaces ethernet eth1 speed 'auto'
 set interfaces ethernet eth2 duplex 'auto'
 set interfaces ethernet eth2 speed 'auto'
 set interfaces ethernet eth2 vif 100 address '100.100.0.1/24'
 set interfaces ethernet eth2 vif-s 200 address '100.64.200.254/24'
 set interfaces ethernet eth2 vif-s 200 vif-c 201 address '100.64.201.254/24'
 set interfaces ethernet eth2 vif-s 200 vif-c 202 address '100.64.202.254/24'
 set interfaces loopback lo
 set protocols static arp interface eth0 address 192.168.0.20 mac '00:50:00:00:00:20'
 set protocols static arp interface eth0 address 192.168.0.30 mac '00:50:00:00:00:30'
 set protocols static arp interface eth0 address 192.168.0.40 mac '00:50:00:00:00:40'
 set protocols static arp interface eth2.100 address 100.100.0.2 mac '00:50:00:00:02:02'
 set protocols static arp interface eth2.100 address 100.100.0.3 mac '00:50:00:00:02:03'
 set protocols static arp interface eth2.100 address 100.100.0.4 mac '00:50:00:00:02:04'
 set protocols static arp interface eth2.200 address 100.64.200.1 mac '00:50:00:00:00:01'
 set protocols static arp interface eth2.200 address 100.64.200.2 mac '00:50:00:00:00:02'
 set protocols static arp interface eth2.200.201 address 100.64.201.10 mac '00:50:00:00:00:10'
 set protocols static arp interface eth2.200.201 address 100.64.201.20 mac '00:50:00:00:00:20'
 set protocols static arp interface eth2.200.202 address 100.64.202.30 mac '00:50:00:00:00:30'
 set protocols static arp interface eth2.200.202 address 100.64.202.40 mac '00:50:00:00:00:40'
 set protocols static route 0.0.0.0/0 next-hop 100.64.0.1
 set service dhcp-server shared-network-name LAN authoritative
-set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 default-router '192.168.0.1'
-set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 domain-name 'vyos.net'
-set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 domain-search 'vyos.net'
-set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 name-server '192.168.0.1'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option default-router '192.168.0.1'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option domain-name 'vyos.net'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option domain-search 'vyos.net'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option name-server '192.168.0.1'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic start '192.168.0.20'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic stop '192.168.0.240'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 subnet-id '1'
 set service dns forwarding allow-from '192.168.0.0/16'
 set service dns forwarding cache-size '10000'
 set service dns forwarding dnssec 'off'
 set service dns forwarding listen-address '192.168.0.1'
 set service ssh ciphers 'aes128-ctr'
 set service ssh ciphers 'aes192-ctr'
 set service ssh ciphers 'aes256-ctr'
 set service ssh ciphers 'chacha20-poly1305@openssh.com'
 set service ssh ciphers 'rijndael-cbc@lysator.liu.se'
 set service ssh key-exchange 'curve25519-sha256@libssh.org'
 set service ssh key-exchange 'diffie-hellman-group1-sha1'
 set service ssh key-exchange 'diffie-hellman-group-exchange-sha1'
 set service ssh key-exchange 'diffie-hellman-group-exchange-sha256'
 set service ssh listen-address '192.168.0.1'
 set service ssh port '22'
 set system config-management commit-revisions '100'
 set system console device ttyS0 speed '115200'
 set system host-name 'vyos'
 set system name-server '192.168.0.1'
 set system syslog console facility all level 'emerg'
 set system syslog console facility mail level 'info'
 set system syslog global facility all level 'info'
 set system syslog global facility auth level 'info'
 set system syslog global facility local7 level 'debug'
 set system syslog global preserve-fqdn
 set system syslog host syslog.vyos.net facility auth level 'warning'
 set system syslog host syslog.vyos.net facility local7 level 'notice'
 set system syslog host syslog.vyos.net format octet-counted
 set system syslog host syslog.vyos.net port '8000'
 set system time-zone 'Europe/Berlin'
diff --git a/smoketest/config-tests/dialup-router-medium-vpn b/smoketest/config-tests/dialup-router-medium-vpn
index 8f3e4ade3..bdae6e807 100644
--- a/smoketest/config-tests/dialup-router-medium-vpn
+++ b/smoketest/config-tests/dialup-router-medium-vpn
@@ -1,316 +1,317 @@
 set firewall global-options all-ping 'enable'
 set firewall global-options broadcast-ping 'disable'
 set firewall global-options ip-src-route 'disable'
 set firewall global-options ipv6-receive-redirects 'disable'
 set firewall global-options ipv6-src-route 'disable'
 set firewall global-options log-martians 'enable'
 set firewall global-options receive-redirects 'disable'
 set firewall global-options send-redirects 'enable'
 set firewall global-options source-validation 'disable'
 set firewall global-options syn-cookies 'disable'
 set firewall global-options twa-hazards-protection 'enable'
 set firewall ipv4 name test_tcp_flags rule 1 action 'drop'
 set firewall ipv4 name test_tcp_flags rule 1 protocol 'tcp'
 set firewall ipv4 name test_tcp_flags rule 1 tcp flags ack
 set firewall ipv4 name test_tcp_flags rule 1 tcp flags not fin
 set firewall ipv4 name test_tcp_flags rule 1 tcp flags not rst
 set firewall ipv4 name test_tcp_flags rule 1 tcp flags syn
 set high-availability vrrp group LAN address 192.168.0.1/24
 set high-availability vrrp group LAN hello-source-address '192.168.0.250'
 set high-availability vrrp group LAN interface 'eth1'
 set high-availability vrrp group LAN peer-address '192.168.0.251'
 set high-availability vrrp group LAN priority '200'
 set high-availability vrrp group LAN vrid '1'
 set high-availability vrrp sync-group failover-group member 'LAN'
 set interfaces ethernet eth0 duplex 'auto'
 set interfaces ethernet eth0 mtu '9000'
 set interfaces ethernet eth0 offload gro
 set interfaces ethernet eth0 offload gso
 set interfaces ethernet eth0 offload sg
 set interfaces ethernet eth0 offload tso
 set interfaces ethernet eth0 speed 'auto'
 set interfaces ethernet eth1 address '192.168.0.250/24'
 set interfaces ethernet eth1 duplex 'auto'
 set interfaces ethernet eth1 ip source-validation 'strict'
 set interfaces ethernet eth1 mtu '9000'
 set interfaces ethernet eth1 offload gro
 set interfaces ethernet eth1 offload gso
 set interfaces ethernet eth1 offload sg
 set interfaces ethernet eth1 offload tso
 set interfaces ethernet eth1 speed 'auto'
 set interfaces loopback lo
 set interfaces openvpn vtun0 encryption cipher 'aes256'
 set interfaces openvpn vtun0 hash 'sha512'
 set interfaces openvpn vtun0 ip adjust-mss '1380'
 set interfaces openvpn vtun0 ip source-validation 'strict'
 set interfaces openvpn vtun0 keep-alive failure-count '3'
 set interfaces openvpn vtun0 keep-alive interval '30'
 set interfaces openvpn vtun0 mode 'client'
 set interfaces openvpn vtun0 openvpn-option 'comp-lzo adaptive'
 set interfaces openvpn vtun0 openvpn-option 'fast-io'
 set interfaces openvpn vtun0 openvpn-option 'persist-key'
 set interfaces openvpn vtun0 openvpn-option 'reneg-sec 86400'
 set interfaces openvpn vtun0 persistent-tunnel
 set interfaces openvpn vtun0 remote-host '192.0.2.10'
 set interfaces openvpn vtun0 tls auth-key 'openvpn_vtun0_auth'
 set interfaces openvpn vtun0 tls ca-certificate 'openvpn_vtun0_1'
 set interfaces openvpn vtun0 tls ca-certificate 'openvpn_vtun0_2'
 set interfaces openvpn vtun0 tls certificate 'openvpn_vtun0'
 set interfaces openvpn vtun1 authentication password 'vyos1'
 set interfaces openvpn vtun1 authentication username 'vyos1'
 set interfaces openvpn vtun1 encryption cipher 'aes256'
 set interfaces openvpn vtun1 hash 'sha1'
 set interfaces openvpn vtun1 ip adjust-mss '1380'
 set interfaces openvpn vtun1 keep-alive failure-count '3'
 set interfaces openvpn vtun1 keep-alive interval '30'
 set interfaces openvpn vtun1 mode 'client'
 set interfaces openvpn vtun1 openvpn-option 'comp-lzo adaptive'
 set interfaces openvpn vtun1 openvpn-option 'tun-mtu 1500'
 set interfaces openvpn vtun1 openvpn-option 'tun-mtu-extra 32'
 set interfaces openvpn vtun1 openvpn-option 'mssfix 1300'
 set interfaces openvpn vtun1 openvpn-option 'persist-key'
 set interfaces openvpn vtun1 openvpn-option 'mute 10'
 set interfaces openvpn vtun1 openvpn-option 'route-nopull'
 set interfaces openvpn vtun1 openvpn-option 'fast-io'
 set interfaces openvpn vtun1 openvpn-option 'reneg-sec 86400'
 set interfaces openvpn vtun1 persistent-tunnel
 set interfaces openvpn vtun1 protocol 'udp'
 set interfaces openvpn vtun1 remote-host '01.foo.com'
 set interfaces openvpn vtun1 remote-port '1194'
 set interfaces openvpn vtun1 tls auth-key 'openvpn_vtun1_auth'
 set interfaces openvpn vtun1 tls ca-certificate 'openvpn_vtun1_1'
 set interfaces openvpn vtun1 tls ca-certificate 'openvpn_vtun1_2'
 set interfaces openvpn vtun2 authentication password 'vyos2'
 set interfaces openvpn vtun2 authentication username 'vyos2'
 set interfaces openvpn vtun2 disable
 set interfaces openvpn vtun2 encryption cipher 'aes256'
 set interfaces openvpn vtun2 hash 'sha512'
 set interfaces openvpn vtun2 ip adjust-mss '1380'
 set interfaces openvpn vtun2 keep-alive failure-count '3'
 set interfaces openvpn vtun2 keep-alive interval '30'
 set interfaces openvpn vtun2 mode 'client'
 set interfaces openvpn vtun2 openvpn-option 'tun-mtu 1500'
 set interfaces openvpn vtun2 openvpn-option 'tun-mtu-extra 32'
 set interfaces openvpn vtun2 openvpn-option 'mssfix 1300'
 set interfaces openvpn vtun2 openvpn-option 'persist-key'
 set interfaces openvpn vtun2 openvpn-option 'mute 10'
 set interfaces openvpn vtun2 openvpn-option 'route-nopull'
 set interfaces openvpn vtun2 openvpn-option 'fast-io'
 set interfaces openvpn vtun2 openvpn-option 'remote-random'
 set interfaces openvpn vtun2 openvpn-option 'reneg-sec 86400'
 set interfaces openvpn vtun2 persistent-tunnel
 set interfaces openvpn vtun2 protocol 'udp'
 set interfaces openvpn vtun2 remote-host '01.myvpn.com'
 set interfaces openvpn vtun2 remote-host '02.myvpn.com'
 set interfaces openvpn vtun2 remote-host '03.myvpn.com'
 set interfaces openvpn vtun2 remote-port '1194'
 set interfaces openvpn vtun2 tls auth-key 'openvpn_vtun2_auth'
 set interfaces openvpn vtun2 tls ca-certificate 'openvpn_vtun2_1'
 set interfaces pppoe pppoe0 authentication password 'password'
 set interfaces pppoe pppoe0 authentication username 'vyos'
 set interfaces pppoe pppoe0 mtu '1500'
 set interfaces pppoe pppoe0 source-interface 'eth0'
 set interfaces wireguard wg0 address '192.168.10.1/24'
 set interfaces wireguard wg0 ip adjust-mss '1380'
 set interfaces wireguard wg0 peer blue allowed-ips '192.168.10.3/32'
 set interfaces wireguard wg0 peer blue persistent-keepalive '20'
 set interfaces wireguard wg0 peer blue preshared-key 'ztFDOY9UyaDvn8N3X97SFMDwIfv7EEfuUIPP2yab6UI='
 set interfaces wireguard wg0 peer blue public-key 'G4pZishpMRrLmd96Kr6V7LIuNGdcUb81gWaYZ+FWkG0='
 set interfaces wireguard wg0 peer green allowed-ips '192.168.10.21/32'
 set interfaces wireguard wg0 peer green persistent-keepalive '25'
 set interfaces wireguard wg0 peer green preshared-key 'LQ9qmlTh9G4nZu4UgElxRUwg7JB/qoV799aADJOijnY='
 set interfaces wireguard wg0 peer green public-key '5iQUD3VoCDBTPXAPHOwUJ0p7xzKGHEY/wQmgvBVmaFI='
 set interfaces wireguard wg0 peer pink allowed-ips '192.168.10.14/32'
 set interfaces wireguard wg0 peer pink allowed-ips '192.168.10.16/32'
 set interfaces wireguard wg0 peer pink persistent-keepalive '25'
 set interfaces wireguard wg0 peer pink preshared-key 'Qi9Odyx0/5itLPN5C5bEy3uMX+tmdl15QbakxpKlWqQ='
 set interfaces wireguard wg0 peer pink public-key 'i4qNPmxyy9EETL4tIoZOLKJF4p7IlVmpAE15gglnAk4='
 set interfaces wireguard wg0 peer red allowed-ips '192.168.10.4/32'
 set interfaces wireguard wg0 peer red persistent-keepalive '20'
 set interfaces wireguard wg0 peer red preshared-key 'CumyXX7osvUT9AwnS+m2TEfCaL0Ptc2LfuZ78Sujuk8='
 set interfaces wireguard wg0 peer red public-key 'ALGWvMJCKpHF2tVH3hEIHqUe9iFfAmZATUUok/WQzks='
 set interfaces wireguard wg0 port '7777'
 set interfaces wireguard wg1 address '10.89.90.2/30'
 set interfaces wireguard wg1 ip adjust-mss '1380'
 set interfaces wireguard wg1 peer sam address '192.0.2.45'
 set interfaces wireguard wg1 peer sam allowed-ips '10.1.1.0/24'
 set interfaces wireguard wg1 peer sam allowed-ips '10.89.90.1/32'
 set interfaces wireguard wg1 peer sam persistent-keepalive '20'
 set interfaces wireguard wg1 peer sam port '1200'
 set interfaces wireguard wg1 peer sam preshared-key 'XpFtzx2Z+nR8pBv9/sSf7I94OkZkVYTz0AeU5Q/QQUE='
 set interfaces wireguard wg1 peer sam public-key 'v5zfKGvH6W/lfDXJ0en96lvKo1gfFxMUWxe02+Fj5BU='
 set interfaces wireguard wg1 port '7778'
 set nat destination rule 50 destination port '49371'
 set nat destination rule 50 inbound-interface name 'pppoe0'
 set nat destination rule 50 protocol 'tcp_udp'
 set nat destination rule 50 translation address '192.168.0.5'
 set nat destination rule 51 destination port '58050-58051'
 set nat destination rule 51 inbound-interface name 'pppoe0'
 set nat destination rule 51 protocol 'tcp'
 set nat destination rule 51 translation address '192.168.0.5'
 set nat destination rule 52 destination port '22067-22070'
 set nat destination rule 52 inbound-interface name 'pppoe0'
 set nat destination rule 52 protocol 'tcp'
 set nat destination rule 52 translation address '192.168.0.5'
 set nat destination rule 53 destination port '34342'
 set nat destination rule 53 inbound-interface name 'pppoe0'
 set nat destination rule 53 protocol 'tcp_udp'
 set nat destination rule 53 translation address '192.168.0.121'
 set nat destination rule 54 destination port '45459'
 set nat destination rule 54 inbound-interface name 'pppoe0'
 set nat destination rule 54 protocol 'tcp_udp'
 set nat destination rule 54 translation address '192.168.0.120'
 set nat destination rule 55 destination port '22'
 set nat destination rule 55 inbound-interface name 'pppoe0'
 set nat destination rule 55 protocol 'tcp'
 set nat destination rule 55 translation address '192.168.0.5'
 set nat destination rule 56 destination port '8920'
 set nat destination rule 56 inbound-interface name 'pppoe0'
 set nat destination rule 56 protocol 'tcp'
 set nat destination rule 56 translation address '192.168.0.5'
 set nat destination rule 60 destination port '80,443'
 set nat destination rule 60 inbound-interface name 'pppoe0'
 set nat destination rule 60 protocol 'tcp'
 set nat destination rule 60 translation address '192.168.0.5'
 set nat destination rule 70 destination port '5001'
 set nat destination rule 70 inbound-interface name 'pppoe0'
 set nat destination rule 70 protocol 'tcp'
 set nat destination rule 70 translation address '192.168.0.5'
 set nat destination rule 80 destination port '25'
 set nat destination rule 80 inbound-interface name 'pppoe0'
 set nat destination rule 80 protocol 'tcp'
 set nat destination rule 80 translation address '192.168.0.5'
 set nat destination rule 90 destination port '8123'
 set nat destination rule 90 inbound-interface name 'pppoe0'
 set nat destination rule 90 protocol 'tcp'
 set nat destination rule 90 translation address '192.168.0.7'
 set nat destination rule 91 destination port '1880'
 set nat destination rule 91 inbound-interface name 'pppoe0'
 set nat destination rule 91 protocol 'tcp'
 set nat destination rule 91 translation address '192.168.0.7'
 set nat destination rule 500 destination address '!192.168.0.0/24'
 set nat destination rule 500 destination port '53'
 set nat destination rule 500 inbound-interface name 'eth1'
 set nat destination rule 500 protocol 'tcp_udp'
 set nat destination rule 500 source address '!192.168.0.1-192.168.0.5'
 set nat destination rule 500 translation address '192.168.0.1'
 set nat source rule 1000 outbound-interface name 'pppoe0'
 set nat source rule 1000 translation address 'masquerade'
 set nat source rule 2000 outbound-interface name 'vtun0'
 set nat source rule 2000 source address '192.168.0.0/16'
 set nat source rule 2000 translation address 'masquerade'
 set nat source rule 3000 outbound-interface name 'vtun1'
 set nat source rule 3000 translation address 'masquerade'
 set policy prefix-list user1-routes rule 1 action 'permit'
 set policy prefix-list user1-routes rule 1 prefix '192.168.0.0/24'
 set policy prefix-list user2-routes rule 1 action 'permit'
 set policy prefix-list user2-routes rule 1 prefix '10.1.1.0/24'
 set policy route LAN-POLICY-BASED-ROUTING interface 'eth1'
 set policy route LAN-POLICY-BASED-ROUTING rule 10 destination
 set policy route LAN-POLICY-BASED-ROUTING rule 10 disable
 set policy route LAN-POLICY-BASED-ROUTING rule 10 set table '10'
 set policy route LAN-POLICY-BASED-ROUTING rule 10 source address '192.168.0.119/32'
 set policy route LAN-POLICY-BASED-ROUTING rule 20 destination
 set policy route LAN-POLICY-BASED-ROUTING rule 20 set table '100'
 set policy route LAN-POLICY-BASED-ROUTING rule 20 source address '192.168.0.240'
 set policy route-map rm-static-to-bgp rule 10 action 'permit'
 set policy route-map rm-static-to-bgp rule 10 match ip address prefix-list 'user1-routes'
 set policy route-map rm-static-to-bgp rule 100 action 'deny'
 set policy route6 LAN6-POLICY-BASED-ROUTING interface 'eth1'
 set policy route6 LAN6-POLICY-BASED-ROUTING rule 10 destination
 set policy route6 LAN6-POLICY-BASED-ROUTING rule 10 disable
 set policy route6 LAN6-POLICY-BASED-ROUTING rule 10 set table '10'
 set policy route6 LAN6-POLICY-BASED-ROUTING rule 10 source address '2002::1'
 set policy route6 LAN6-POLICY-BASED-ROUTING rule 20 destination
 set policy route6 LAN6-POLICY-BASED-ROUTING rule 20 set table '100'
 set policy route6 LAN6-POLICY-BASED-ROUTING rule 20 source address '2008::f'
 set protocols bgp address-family ipv4-unicast redistribute connected route-map 'rm-static-to-bgp'
 set protocols bgp neighbor 10.89.90.1 address-family ipv4-unicast nexthop-self
 set protocols bgp neighbor 10.89.90.1 address-family ipv4-unicast prefix-list export 'user1-routes'
 set protocols bgp neighbor 10.89.90.1 address-family ipv4-unicast prefix-list import 'user2-routes'
 set protocols bgp neighbor 10.89.90.1 address-family ipv4-unicast soft-reconfiguration inbound
 set protocols bgp neighbor 10.89.90.1 password 'ericandre2020'
 set protocols bgp neighbor 10.89.90.1 remote-as '64589'
 set protocols bgp parameters log-neighbor-changes
 set protocols bgp parameters router-id '10.89.90.2'
 set protocols bgp system-as '64590'
 set protocols static route 100.64.160.23/32 interface pppoe0
 set protocols static route 100.64.165.25/32 interface pppoe0
 set protocols static route 100.64.165.26/32 interface pppoe0
 set protocols static route 100.64.198.0/24 interface vtun0
 set protocols static table 10 route 0.0.0.0/0 interface vtun1
 set protocols static table 100 route 0.0.0.0/0 next-hop 192.168.10.5
 set service conntrack-sync accept-protocol 'tcp'
 set service conntrack-sync accept-protocol 'udp'
 set service conntrack-sync accept-protocol 'icmp'
 set service conntrack-sync disable-external-cache
 set service conntrack-sync event-listen-queue-size '8'
 set service conntrack-sync expect-sync 'all'
 set service conntrack-sync failover-mechanism vrrp sync-group 'failover-group'
 set service conntrack-sync interface eth1 peer '192.168.0.251'
 set service conntrack-sync sync-queue-size '8'
 set service dhcp-server failover name 'DHCP02'
 set service dhcp-server failover remote '192.168.0.251'
 set service dhcp-server failover source-address '192.168.0.250'
 set service dhcp-server failover status 'primary'
 set service dhcp-server shared-network-name LAN authoritative
-set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 default-router '192.168.0.1'
-set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 domain-name 'vyos.net'
-set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 domain-search 'vyos.net'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 lease '86400'
-set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 name-server '192.168.0.1'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option default-router '192.168.0.1'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option domain-name 'vyos.net'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option domain-search 'vyos.net'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option name-server '192.168.0.1'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic start '192.168.0.200'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic stop '192.168.0.240'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Audio ip-address '192.168.0.107'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Audio mac '00:50:01:dc:91:14'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping IPTV ip-address '192.168.0.104'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping IPTV mac '00:50:01:31:b5:f6'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping McPrintus ip-address '192.168.0.60'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping McPrintus mac '00:50:01:58:ac:95'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Mobile01 ip-address '192.168.0.109'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Mobile01 mac '00:50:01:bc:ac:51'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera1 ip-address '192.168.0.11'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera1 mac '00:50:01:70:b9:4d'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera2 ip-address '192.168.0.12'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera2 mac '00:50:01:70:b7:4f'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping pearTV ip-address '192.168.0.101'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping pearTV mac '00:50:01:ba:62:79'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping sand ip-address '192.168.0.110'
 set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping sand mac '00:50:01:af:c5:d2'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 subnet-id '1'
 set service dns forwarding allow-from '192.168.0.0/16'
 set service dns forwarding cache-size '8192'
 set service dns forwarding dnssec 'off'
 set service dns forwarding listen-address '192.168.0.1'
 set service dns forwarding name-server 100.64.0.1
 set service dns forwarding name-server 100.64.0.2
 set service ntp allow-client address '192.168.0.0/16'
 set service ntp server nz.pool.ntp.org prefer
 set service snmp community AwesomeCommunity authorization 'ro'
 set service snmp community AwesomeCommunity client '127.0.0.1'
 set service snmp community AwesomeCommunity network '192.168.0.0/24'
 set service ssh access-control allow user 'vyos'
 set service ssh client-keepalive-interval '60'
 set service ssh listen-address '192.168.0.1'
 set service ssh listen-address '192.168.10.1'
 set service ssh listen-address '192.168.0.250'
 set system config-management commit-revisions '100'
 set system console device ttyS0 speed '115200'
 set system host-name 'vyos'
 set system ip arp table-size '1024'
 set system name-server '192.168.0.1'
 set system name-server 'pppoe0'
 set system option ctrl-alt-delete 'ignore'
 set system option reboot-on-panic
 set system option startup-beep
 set system static-host-mapping host-name host60.vyos.net inet '192.168.0.60'
 set system static-host-mapping host-name host104.vyos.net inet '192.168.0.104'
 set system static-host-mapping host-name host107.vyos.net inet '192.168.0.107'
 set system static-host-mapping host-name host109.vyos.net inet '192.168.0.109'
 set system sysctl parameter net.core.default_qdisc value 'fq'
 set system sysctl parameter net.ipv4.tcp_congestion_control value 'bbr'
 set system syslog global facility all level 'info'
 set system syslog host 192.168.0.252 facility all level 'debug'
 set system syslog host 192.168.0.252 protocol 'udp'
 set system task-scheduler task Update-Blacklists executable path '/config/scripts/vyos-foo-update.script'
 set system task-scheduler task Update-Blacklists interval '3h'
 set system time-zone 'Pacific/Auckland'
diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py
index 6f24d40ec..3e6adb149 100755
--- a/smoketest/scripts/cli/test_service_dhcp-server.py
+++ b/smoketest/scripts/cli/test_service_dhcp-server.py
@@ -1,705 +1,719 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2020-2021 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
 import unittest
 
 from json import loads
 
 from base_vyostest_shim import VyOSUnitTestSHIM
 
 from vyos.configsession import ConfigSessionError
 from vyos.utils.process import process_named_running
 from vyos.utils.file import read_file
 from vyos.template import inc_ip
 from vyos.template import dec_ip
 
 PROCESS_NAME = 'kea-dhcp4'
 CTRL_PROCESS_NAME = 'kea-ctrl-agent'
 KEA4_CONF = '/run/kea/kea-dhcp4.conf'
 KEA4_CTRL = '/run/kea/dhcp4-ctrl-socket'
 base_path = ['service', 'dhcp-server']
 interface = 'dum8765'
 subnet = '192.0.2.0/25'
 router = inc_ip(subnet, 1)
 dns_1 = inc_ip(subnet, 2)
 dns_2 = inc_ip(subnet, 3)
 domain_name = 'vyos.net'
 
 class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
     @classmethod
     def setUpClass(cls):
         super(TestServiceDHCPServer, cls).setUpClass()
        # Clear out current configuration to allow running this test on a live system
         cls.cli_delete(cls, base_path)
 
         cidr_mask = subnet.split('/')[-1]
         cls.cli_set(cls, ['interfaces', 'dummy', interface, 'address', f'{router}/{cidr_mask}'])
 
     @classmethod
     def tearDownClass(cls):
         cls.cli_delete(cls, ['interfaces', 'dummy', interface])
         super(TestServiceDHCPServer, cls).tearDownClass()
 
     def tearDown(self):
         self.cli_delete(base_path)
         self.cli_commit()
 
     def walk_path(self, obj, path):
         current = obj
 
         for i, key in enumerate(path):
             if isinstance(key, str):
                 self.assertTrue(isinstance(current, dict), msg=f'Failed path: {path}')
                 self.assertTrue(key in current, msg=f'Failed path: {path}')
             elif isinstance(key, int):
                 self.assertTrue(isinstance(current, list), msg=f'Failed path: {path}')
                 self.assertTrue(0 <= key < len(current), msg=f'Failed path: {path}')
             else:
                 assert False, "Invalid type"
 
             current = current[key]
 
         return current
 
     def verify_config_object(self, obj, path, value):
         base_obj = self.walk_path(obj, path)
         self.assertTrue(isinstance(base_obj, list))
         self.assertTrue(any(True for v in base_obj if v == value))
 
     def verify_config_value(self, obj, path, key, value):
         base_obj = self.walk_path(obj, path)
         if isinstance(base_obj, list):
             self.assertTrue(any(True for v in base_obj if key in v and v[key] == value))
         elif isinstance(base_obj, dict):
             self.assertTrue(key in base_obj)
             self.assertEqual(base_obj[key], value)
 
     def test_dhcp_single_pool_range(self):
         shared_net_name = 'SMOKE-1'
 
         range_0_start = inc_ip(subnet, 10)
         range_0_stop  = inc_ip(subnet, 20)
         range_1_start = inc_ip(subnet, 40)
         range_1_stop  = inc_ip(subnet, 50)
 
         self.cli_set(base_path + ['listen-interface', interface])
 
         pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+        self.cli_set(pool + ['subnet-id', '1'])
         # we use the first subnet IP address as default gateway
         self.cli_set(pool + ['option', 'default-router', router])
         self.cli_set(pool + ['option', 'name-server', dns_1])
         self.cli_set(pool + ['option', 'name-server', dns_2])
         self.cli_set(pool + ['option', 'domain-name', domain_name])
 
         # check validate() - No DHCP address range or active static-mapping set
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
         self.cli_set(pool + ['range', '0', 'start', range_0_start])
         self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
         self.cli_set(pool + ['range', '1', 'start', range_1_start])
         self.cli_set(pool + ['range', '1', 'stop', range_1_stop])
 
         # commit changes
         self.cli_commit()
 
         config = read_file(KEA4_CONF)
         obj = loads(config)
 
         self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [interface])
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+        self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
 
         # Verify options
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'domain-name', 'data': domain_name})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'routers', 'data': router})
 
         # Verify pools
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
                 {'pool': f'{range_0_start} - {range_0_stop}'})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
                 {'pool': f'{range_1_start} - {range_1_stop}'})
 
         # Check for running process
         self.assertTrue(process_named_running(PROCESS_NAME))
 
     def test_dhcp_single_pool_options(self):
         shared_net_name = 'SMOKE-0815'
 
         range_0_start       = inc_ip(subnet, 10)
         range_0_stop        = inc_ip(subnet, 20)
         smtp_server         = '1.2.3.4'
         time_server         = '4.3.2.1'
         tftp_server         = 'tftp.vyos.io'
         search_domains      = ['foo.vyos.net', 'bar.vyos.net']
         bootfile_name       = 'vyos'
         bootfile_server     = '192.0.2.1'
         wpad                = 'http://wpad.vyos.io/foo/bar'
         server_identifier   = bootfile_server
         ipv6_only_preferred = '300'
 
         pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+        self.cli_set(pool + ['subnet-id', '1'])
         # we use the first subnet IP address as default gateway
         self.cli_set(pool + ['option', 'default-router', router])
         self.cli_set(pool + ['option', 'name-server', dns_1])
         self.cli_set(pool + ['option', 'name-server', dns_2])
         self.cli_set(pool + ['option', 'domain-name', domain_name])
         self.cli_set(pool + ['option', 'ip-forwarding'])
         self.cli_set(pool + ['option', 'smtp-server', smtp_server])
         self.cli_set(pool + ['option', 'pop-server', smtp_server])
         self.cli_set(pool + ['option', 'time-server', time_server])
         self.cli_set(pool + ['option', 'tftp-server-name', tftp_server])
         for search in search_domains:
             self.cli_set(pool + ['option', 'domain-search', search])
         self.cli_set(pool + ['option', 'bootfile-name', bootfile_name])
         self.cli_set(pool + ['option', 'bootfile-server', bootfile_server])
         self.cli_set(pool + ['option', 'wpad-url', wpad])
         self.cli_set(pool + ['option', 'server-identifier', server_identifier])
 
         self.cli_set(pool + ['option', 'static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1'])
         self.cli_set(pool + ['option', 'ipv6-only-preferred', ipv6_only_preferred])
         self.cli_set(pool + ['option', 'time-zone', 'Europe/London'])
 
         self.cli_set(pool + ['range', '0', 'start', range_0_start])
         self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
 
         # commit changes
         self.cli_commit()
 
         config = read_file(KEA4_CONF)
         obj = loads(config)
 
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'boot-file-name', bootfile_name)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'next-server', bootfile_server)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
 
         # Verify options
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'domain-name', 'data': domain_name})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'domain-search', 'data': ', '.join(search_domains)})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'pop-server', 'data': smtp_server})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'smtp-server', 'data': smtp_server})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'time-servers', 'data': time_server})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'routers', 'data': router})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'dhcp-server-identifier', 'data': server_identifier})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'tftp-server-name', 'data': tftp_server})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'wpad-url', 'data': wpad})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'rfc3442-static-route', 'data': '24,10,0,0,192,0,2,1, 0,192,0,2,1'})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'windows-static-route', 'data': '24,10,0,0,192,0,2,1'})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'v6-only-preferred', 'data': ipv6_only_preferred})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'ip-forwarding', 'data': "true"})
 
         # Time zone
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'pcode', 'data': 'GMT0BST,M3.5.0/1,M10.5.0'})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'tcode', 'data': 'Europe/London'})
 
         # Verify pools
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
                 {'pool': f'{range_0_start} - {range_0_stop}'})
 
         # Check for running process
         self.assertTrue(process_named_running(PROCESS_NAME))
 
     def test_dhcp_single_pool_options_scoped(self):
         shared_net_name = 'SMOKE-2'
 
         range_0_start = inc_ip(subnet, 10)
         range_0_stop  = inc_ip(subnet, 20)
 
         range_router = inc_ip(subnet, 5)
         range_dns_1 = inc_ip(subnet, 6)
         range_dns_2 = inc_ip(subnet, 7)
 
         shared_network = base_path + ['shared-network-name', shared_net_name]
         pool = shared_network + ['subnet', subnet]
+
+        self.cli_set(pool + ['subnet-id', '1'])
+
         # we use the first subnet IP address as default gateway
         self.cli_set(shared_network + ['option', 'default-router', router])
         self.cli_set(shared_network + ['option', 'name-server', dns_1])
         self.cli_set(shared_network + ['option', 'name-server', dns_2])
         self.cli_set(shared_network + ['option', 'domain-name', domain_name])
 
         self.cli_set(pool + ['range', '0', 'start', range_0_start])
         self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
         self.cli_set(pool + ['range', '0', 'option', 'default-router', range_router])
         self.cli_set(pool + ['range', '0', 'option', 'name-server', range_dns_1])
         self.cli_set(pool + ['range', '0', 'option', 'name-server', range_dns_2])
 
         # commit changes
         self.cli_commit()
 
         config = read_file(KEA4_CONF)
         obj = loads(config)
 
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
 
         # Verify shared-network options
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'option-data'],
                 {'name': 'domain-name', 'data': domain_name})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'option-data'],
                 {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'option-data'],
                 {'name': 'routers', 'data': router})
 
         # Verify range options
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'],
                 {'name': 'domain-name-servers', 'data': f'{range_dns_1}, {range_dns_2}'})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'],
                 {'name': 'routers', 'data': range_router})
 
         # Verify pool
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], 'pool', f'{range_0_start} - {range_0_stop}')
 
         # Check for running process
         self.assertTrue(process_named_running(PROCESS_NAME))
 
     def test_dhcp_single_pool_static_mapping(self):
         shared_net_name = 'SMOKE-2'
         domain_name = 'private'
 
         pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+        self.cli_set(pool + ['subnet-id', '1'])
         # we use the first subnet IP address as default gateway
         self.cli_set(pool + ['option', 'default-router', router])
         self.cli_set(pool + ['option', 'name-server', dns_1])
         self.cli_set(pool + ['option', 'name-server', dns_2])
         self.cli_set(pool + ['option', 'domain-name', domain_name])
 
         # check validate() - No DHCP address range or active static-mapping set
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
 
         client_base = 10
         for client in ['client1', 'client2', 'client3']:
             mac = '00:50:00:00:00:{}'.format(client_base)
             self.cli_set(pool + ['static-mapping', client, 'mac', mac])
             self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)])
             client_base += 1
 
         # cannot have both mac-address and duid set
         with self.assertRaises(ConfigSessionError):
             self.cli_set(pool + ['static-mapping', 'client1', 'duid', '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:11'])
             self.cli_commit()
         self.cli_delete(pool + ['static-mapping', 'client1', 'duid'])
 
         # cannot have mappings with duplicate IP addresses
         with self.assertRaises(ConfigSessionError):
             self.cli_set(pool + ['static-mapping', 'dupe1', 'mac', '00:50:00:00:00:01'])
             self.cli_set(pool + ['static-mapping', 'dupe1', 'ip-address', inc_ip(subnet, 10)])
             self.cli_commit()
         self.cli_delete(pool + ['static-mapping', 'dupe1'])
 
         # commit changes
         self.cli_commit()
 
         config = read_file(KEA4_CONF)
         obj = loads(config)
 
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+        self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
 
         # Verify options
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'domain-name', 'data': domain_name})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'routers', 'data': router})
 
         client_base = 10
         for client in ['client1', 'client2', 'client3']:
             mac = '00:50:00:00:00:{}'.format(client_base)
             ip = inc_ip(subnet, client_base)
 
             self.verify_config_object(
                     obj,
                     ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'reservations'],
                     {'hostname': client, 'hw-address': mac, 'ip-address': ip})
 
             client_base += 1
 
         # Check for running process
         self.assertTrue(process_named_running(PROCESS_NAME))
 
     def test_dhcp_multiple_pools(self):
         lease_time = '14400'
 
         for network in ['0', '1', '2', '3']:
             shared_net_name = f'VyOS-SMOKETEST-{network}'
             subnet = f'192.0.{network}.0/24'
             router = inc_ip(subnet, 1)
             dns_1 = inc_ip(subnet, 2)
 
             range_0_start = inc_ip(subnet, 10)
             range_0_stop  = inc_ip(subnet, 20)
             range_1_start = inc_ip(subnet, 30)
             range_1_stop  = inc_ip(subnet, 40)
 
             pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+            self.cli_set(pool + ['subnet-id', str(int(network) + 1)])
             # we use the first subnet IP address as default gateway
             self.cli_set(pool + ['option', 'default-router', router])
             self.cli_set(pool + ['option', 'name-server', dns_1])
             self.cli_set(pool + ['option', 'domain-name', domain_name])
             self.cli_set(pool + ['lease', lease_time])
 
             self.cli_set(pool + ['range', '0', 'start', range_0_start])
             self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
             self.cli_set(pool + ['range', '1', 'start', range_1_start])
             self.cli_set(pool + ['range', '1', 'stop', range_1_stop])
 
             client_base = 60
             for client in ['client1', 'client2', 'client3', 'client4']:
                 mac = '02:50:00:00:00:{}'.format(client_base)
                 self.cli_set(pool + ['static-mapping', client, 'mac', mac])
                 self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)])
                 client_base += 1
 
         # commit changes
         self.cli_commit()
 
         config = read_file(KEA4_CONF)
         obj = loads(config)
 
         for network in ['0', '1', '2', '3']:
             shared_net_name = f'VyOS-SMOKETEST-{network}'
             subnet = f'192.0.{network}.0/24'
             router = inc_ip(subnet, 1)
             dns_1 = inc_ip(subnet, 2)
 
             range_0_start = inc_ip(subnet, 10)
             range_0_stop  = inc_ip(subnet, 20)
             range_1_start = inc_ip(subnet, 30)
             range_1_stop  = inc_ip(subnet, 40)
 
             self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
             self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'subnet', subnet)
+            self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'id', int(network) + 1)
             self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'valid-lifetime', int(lease_time))
             self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'max-valid-lifetime', int(lease_time))
 
             self.verify_config_object(
                     obj,
                     ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
                     {'name': 'domain-name', 'data': domain_name})
             self.verify_config_object(
                     obj,
                     ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
                     {'name': 'domain-name-servers', 'data': dns_1})
             self.verify_config_object(
                     obj,
                     ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
                     {'name': 'routers', 'data': router})
 
             self.verify_config_object(
                     obj,
                     ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'],
                     {'pool': f'{range_0_start} - {range_0_stop}'})
             self.verify_config_object(
                     obj,
                     ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'],
                     {'pool': f'{range_1_start} - {range_1_stop}'})
 
             client_base = 60
             for client in ['client1', 'client2', 'client3', 'client4']:
                 mac = '02:50:00:00:00:{}'.format(client_base)
                 ip = inc_ip(subnet, client_base)
 
                 self.verify_config_object(
                         obj,
                         ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'reservations'],
                         {'hostname': client, 'hw-address': mac, 'ip-address': ip})
 
                 client_base += 1
 
         # Check for running process
         self.assertTrue(process_named_running(PROCESS_NAME))
 
     def test_dhcp_exclude_not_in_range(self):
         # T3180: verify else path when slicing DHCP ranges and exclude address
         # is not part of the DHCP range
         range_0_start = inc_ip(subnet, 10)
         range_0_stop  = inc_ip(subnet, 20)
 
         pool = base_path + ['shared-network-name', 'EXCLUDE-TEST', 'subnet', subnet]
+        self.cli_set(pool + ['subnet-id', '1'])
         self.cli_set(pool + ['option', 'default-router', router])
         self.cli_set(pool + ['exclude', router])
         self.cli_set(pool + ['range', '0', 'start', range_0_start])
         self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
 
         # commit changes
         self.cli_commit()
 
         config = read_file(KEA4_CONF)
         obj = loads(config)
 
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST')
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
 
         # Verify options
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'routers', 'data': router})
 
         # Verify pools
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
                 {'pool': f'{range_0_start} - {range_0_stop}'})
 
         # Check for running process
         self.assertTrue(process_named_running(PROCESS_NAME))
 
     def test_dhcp_exclude_in_range(self):
         # T3180: verify else path when slicing DHCP ranges and exclude address
         # is not part of the DHCP range
         range_0_start = inc_ip(subnet, 10)
         range_0_stop  = inc_ip(subnet, 100)
 
         # the DHCP exclude addresse is blanked out of the range which is done
         # by slicing one range into two ranges
         exclude_addr  = inc_ip(range_0_start, 20)
         range_0_stop_excl = dec_ip(exclude_addr, 1)
         range_0_start_excl = inc_ip(exclude_addr, 1)
 
         pool = base_path + ['shared-network-name', 'EXCLUDE-TEST-2', 'subnet', subnet]
+        self.cli_set(pool + ['subnet-id', '1'])
         self.cli_set(pool + ['option', 'default-router', router])
         self.cli_set(pool + ['exclude', exclude_addr])
         self.cli_set(pool + ['range', '0', 'start', range_0_start])
         self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
 
         # commit changes
         self.cli_commit()
 
         config = read_file(KEA4_CONF)
         obj = loads(config)
 
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST-2')
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
 
         # Verify options
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'routers', 'data': router})
 
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
                 {'pool': f'{range_0_start} - {range_0_stop_excl}'})
 
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
                 {'pool': f'{range_0_start_excl} - {range_0_stop}'})
 
         # Check for running process
         self.assertTrue(process_named_running(PROCESS_NAME))
 
     def test_dhcp_relay_server(self):
         # Listen on specific address and return DHCP leases from a non
         # directly connected pool
         self.cli_set(base_path + ['listen-address', router])
 
         relay_subnet = '10.0.0.0/16'
         relay_router = inc_ip(relay_subnet, 1)
 
         range_0_start = '10.0.1.0'
         range_0_stop  = '10.0.250.255'
 
         pool = base_path + ['shared-network-name', 'RELAY', 'subnet', relay_subnet]
+        self.cli_set(pool + ['subnet-id', '1'])
         self.cli_set(pool + ['option', 'default-router', relay_router])
         self.cli_set(pool + ['range', '0', 'start', range_0_start])
         self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
 
         # commit changes
         self.cli_commit()
 
         config = read_file(KEA4_CONF)
         obj = loads(config)
 
         self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [f'{interface}/{router}'])
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'RELAY')
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet)
 
         # Verify options
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'routers', 'data': relay_router})
 
         # Verify pools
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
                 {'pool': f'{range_0_start} - {range_0_stop}'})
 
         # Check for running process
         self.assertTrue(process_named_running(PROCESS_NAME))
 
     def test_dhcp_failover(self):
         shared_net_name = 'FAILOVER'
         failover_name = 'VyOS-Failover'
 
         range_0_start = inc_ip(subnet, 10)
         range_0_stop  = inc_ip(subnet, 20)
 
         pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+        self.cli_set(pool + ['subnet-id', '1'])
         # we use the first subnet IP address as default gateway
         self.cli_set(pool + ['option', 'default-router', router])
 
         # check validate() - No DHCP address range or active static-mapping set
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
         self.cli_set(pool + ['range', '0', 'start', range_0_start])
         self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
 
         # failover
         failover_local = router
         failover_remote = inc_ip(router, 1)
 
         self.cli_set(base_path + ['failover', 'source-address', failover_local])
         self.cli_set(base_path + ['failover', 'name', failover_name])
         self.cli_set(base_path + ['failover', 'remote', failover_remote])
         self.cli_set(base_path + ['failover', 'status', 'primary'])
 
         # commit changes
         self.cli_commit()
 
         config = read_file(KEA4_CONF)
         obj = loads(config)
 
         # Verify failover
         self.verify_config_value(obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL)
 
         self.verify_config_object(
             obj,
             ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
             {'name': os.uname()[1], 'url': f'http://{failover_local}:647/', 'role': 'primary', 'auto-failover': True})
 
         self.verify_config_object(
             obj,
             ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
             {'name': failover_name, 'url': f'http://{failover_remote}:647/', 'role': 'standby', 'auto-failover': True})
 
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
         self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
 
         # Verify options
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
                 {'name': 'routers', 'data': router})
 
         # Verify pools
         self.verify_config_object(
                 obj,
                 ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
                 {'pool': f'{range_0_start} - {range_0_stop}'})
 
         # Check for running process
         self.assertTrue(process_named_running(PROCESS_NAME))
         self.assertTrue(process_named_running(CTRL_PROCESS_NAME))
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py
index f163cc69a..fcbfeb7be 100755
--- a/smoketest/scripts/cli/test_service_dhcpv6-server.py
+++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py
@@ -1,273 +1,275 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2020-2023 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import unittest
 
 from json import loads
 
 from base_vyostest_shim import VyOSUnitTestSHIM
 
 from vyos.configsession import ConfigSessionError
 from vyos.template import inc_ip
 from vyos.utils.process import process_named_running
 from vyos.utils.file import read_file
 
 PROCESS_NAME = 'kea-dhcp6'
 KEA6_CONF = '/run/kea/kea-dhcp6.conf'
 base_path = ['service', 'dhcpv6-server']
 
 subnet = '2001:db8:f00::/64'
 dns_1 = '2001:db8::1'
 dns_2 = '2001:db8::2'
 domain = 'vyos.net'
 nis_servers = ['2001:db8:ffff::1', '2001:db8:ffff::2']
 interface = 'eth0'
 interface_addr = inc_ip(subnet, 1) + '/64'
 
 class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase):
     @classmethod
     def setUpClass(cls):
         super(TestServiceDHCPv6Server, cls).setUpClass()
         # Clear out current configuration to allow running this test on a live system
         cls.cli_delete(cls, base_path)
 
         cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', interface_addr])
 
     @classmethod
     def tearDownClass(cls):
         cls.cli_delete(cls, ['interfaces', 'ethernet', interface, 'address', interface_addr])
         cls.cli_commit(cls)
 
         super(TestServiceDHCPv6Server, cls).tearDownClass()
 
     def tearDown(self):
         self.cli_delete(base_path)
         self.cli_commit()
 
     def walk_path(self, obj, path):
         current = obj
 
         for i, key in enumerate(path):
             if isinstance(key, str):
                 self.assertTrue(isinstance(current, dict), msg=f'Failed path: {path}')
                 self.assertTrue(key in current, msg=f'Failed path: {path}')
             elif isinstance(key, int):
                 self.assertTrue(isinstance(current, list), msg=f'Failed path: {path}')
                 self.assertTrue(0 <= key < len(current), msg=f'Failed path: {path}')
             else:
                 assert False, "Invalid type"
 
             current = current[key]
 
         return current
 
     def verify_config_object(self, obj, path, value):
         base_obj = self.walk_path(obj, path)
         self.assertTrue(isinstance(base_obj, list))
         self.assertTrue(any(True for v in base_obj if v == value))
 
     def verify_config_value(self, obj, path, key, value):
         base_obj = self.walk_path(obj, path)
         if isinstance(base_obj, list):
             self.assertTrue(any(True for v in base_obj if key in v and v[key] == value))
         elif isinstance(base_obj, dict):
             self.assertTrue(key in base_obj)
             self.assertEqual(base_obj[key], value)
 
     def test_single_pool(self):
         shared_net_name = 'SMOKE-1'
         search_domains  = ['foo.vyos.net', 'bar.vyos.net']
         lease_time = '1200'
         max_lease_time = '72000'
         min_lease_time = '600'
         preference = '10'
         sip_server = 'sip.vyos.net'
         sntp_server = inc_ip(subnet, 100)
         range_start = inc_ip(subnet, 256)  # ::100
         range_stop = inc_ip(subnet, 65535) # ::ffff
 
         pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
 
         self.cli_set(base_path + ['preference', preference])
-
+        self.cli_set(pool + ['subnet-id', '1'])
         # we use the first subnet IP address as default gateway
         self.cli_set(pool + ['name-server', dns_1])
         self.cli_set(pool + ['name-server', dns_2])
         self.cli_set(pool + ['name-server', dns_2])
         self.cli_set(pool + ['lease-time', 'default', lease_time])
         self.cli_set(pool + ['lease-time', 'maximum', max_lease_time])
         self.cli_set(pool + ['lease-time', 'minimum', min_lease_time])
         self.cli_set(pool + ['nis-domain', domain])
         self.cli_set(pool + ['nisplus-domain', domain])
         self.cli_set(pool + ['sip-server', sip_server])
         self.cli_set(pool + ['sntp-server', sntp_server])
         self.cli_set(pool + ['address-range', 'start', range_start, 'stop', range_stop])
 
         for server in nis_servers:
             self.cli_set(pool + ['nis-server', server])
             self.cli_set(pool + ['nisplus-server', server])
 
         for search in search_domains:
             self.cli_set(pool + ['domain-search', search])
 
         client_base = 1
         for client in ['client1', 'client2', 'client3']:
             duid = f'00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{client_base:02}'
             self.cli_set(pool + ['static-mapping', client, 'duid', duid])
             self.cli_set(pool + ['static-mapping', client, 'ipv6-address', inc_ip(subnet, client_base)])
             self.cli_set(pool + ['static-mapping', client, 'ipv6-prefix', inc_ip(subnet, client_base << 64) + '/64'])
             client_base += 1
 
         # cannot have both mac-address and duid set
         with self.assertRaises(ConfigSessionError):
             self.cli_set(pool + ['static-mapping', 'client1', 'mac', '00:50:00:00:00:11'])
             self.cli_commit()
         self.cli_delete(pool + ['static-mapping', 'client1', 'mac'])
 
         # commit changes
         self.cli_commit()
 
         config = read_file(KEA6_CONF)
         obj = loads(config)
 
         self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name)
         self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet)
+        self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'id', 1)
         self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'valid-lifetime', int(lease_time))
         self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'min-valid-lifetime', int(min_lease_time))
         self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'max-valid-lifetime', int(max_lease_time))
 
         # Verify options
         self.verify_config_object(
                 obj,
                 ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
                 {'name': 'dns-servers', 'data': f'{dns_1}, {dns_2}'})
         self.verify_config_object(
                 obj,
                 ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
                 {'name': 'domain-search', 'data': ", ".join(search_domains)})
         self.verify_config_object(
                 obj,
                 ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
                 {'name': 'nis-domain-name', 'data': domain})
         self.verify_config_object(
                 obj,
                 ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
                 {'name': 'nis-servers', 'data': ", ".join(nis_servers)})
         self.verify_config_object(
                 obj,
                 ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
                 {'name': 'nisp-domain-name', 'data': domain})
         self.verify_config_object(
                 obj,
                 ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
                 {'name': 'nisp-servers', 'data': ", ".join(nis_servers)})
         self.verify_config_object(
                 obj,
                 ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
                 {'name': 'sntp-servers', 'data': sntp_server})
         self.verify_config_object(
                 obj,
                 ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
                 {'name': 'sip-server-dns', 'data': sip_server})
 
         # Verify pools
         self.verify_config_object(
                 obj,
                 ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pools'],
                 {'pool': f'{range_start} - {range_stop}'})
 
         client_base = 1
         for client in ['client1', 'client2', 'client3']:
             duid = f'00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{client_base:02}'
             ip = inc_ip(subnet, client_base)
             prefix = inc_ip(subnet, client_base << 64) + '/64'
 
             self.verify_config_object(
                     obj,
                     ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'reservations'],
                     {'hostname': client, 'duid': duid, 'ip-addresses': [ip], 'prefixes': [prefix]})
 
             client_base += 1
 
         # Check for running process
         self.assertTrue(process_named_running(PROCESS_NAME))
 
 
     def test_prefix_delegation(self):
         shared_net_name = 'SMOKE-2'
         range_start = inc_ip(subnet, 256)  # ::100
         range_stop = inc_ip(subnet, 65535) # ::ffff
         delegate_start = '2001:db8:ee::'
         delegate_len = '64'
         prefix_len = '56'
 
         pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
-
+        self.cli_set(pool + ['subnet-id', '1'])
         self.cli_set(pool + ['address-range', 'start', range_start, 'stop', range_stop])
         self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'delegated-length', delegate_len])
         self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'prefix-length', prefix_len])
 
         # commit changes
         self.cli_commit()
 
         config = read_file(KEA6_CONF)
         obj = loads(config)
 
         self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name)
         self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet)
 
         # Verify pools
         self.verify_config_object(
                 obj,
                 ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pools'],
                 {'pool': f'{range_start} - {range_stop}'})
 
         self.verify_config_object(
                 obj,
                 ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pd-pools'],
                 {'prefix': delegate_start, 'prefix-len': int(prefix_len), 'delegated-len': int(delegate_len)})
 
         # Check for running process
         self.assertTrue(process_named_running(PROCESS_NAME))
 
     def test_global_nameserver(self):
         shared_net_name = 'SMOKE-3'
         ns_global_1 = '2001:db8::1111'
         ns_global_2 = '2001:db8::2222'
 
         self.cli_set(base_path + ['global-parameters', 'name-server', ns_global_1])
         self.cli_set(base_path + ['global-parameters', 'name-server', ns_global_2])
-        self.cli_set(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet])
+        self.cli_set(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet, 'subnet-id', '1'])
 
         # commit changes
         self.cli_commit()
 
         config = read_file(KEA6_CONF)
         obj = loads(config)
 
         self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name)
         self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet)
+        self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'id', 1)
 
         self.verify_config_object(
                 obj,
                 ['Dhcp6', 'option-data'],
                 {'name': 'dns-servers', "code": 23, "space": "dhcp6", "csv-format": True, 'data': f'{ns_global_1}, {ns_global_2}'})
 
         # Check for running process
         self.assertTrue(process_named_running(PROCESS_NAME))
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py
index ceaba019e..2418c8faa 100755
--- a/src/conf_mode/service_dhcp-server.py
+++ b/src/conf_mode/service_dhcp-server.py
@@ -1,398 +1,407 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2018-2023 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
 
 from ipaddress import ip_address
 from ipaddress import ip_network
 from netaddr import IPRange
 from sys import exit
 
 from vyos.config import Config
 from vyos.pki import wrap_certificate
 from vyos.pki import wrap_private_key
 from vyos.template import render
 from vyos.utils.dict import dict_search
 from vyos.utils.dict import dict_search_args
 from vyos.utils.file import chmod_775
 from vyos.utils.file import makedir
 from vyos.utils.file import write_file
 from vyos.utils.process import call
 from vyos.utils.network import interface_exists
 from vyos.utils.network import is_subnet_connected
 from vyos.utils.network import is_addr_assigned
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
 ctrl_config_file = '/run/kea/kea-ctrl-agent.conf'
 ctrl_socket = '/run/kea/dhcp4-ctrl-socket'
 config_file = '/run/kea/kea-dhcp4.conf'
 lease_file = '/config/dhcp/dhcp4-leases.csv'
 systemd_override = r'/run/systemd/system/kea-ctrl-agent.service.d/10-override.conf'
 user_group = '_kea'
 
 ca_cert_file = '/run/kea/kea-failover-ca.pem'
 cert_file = '/run/kea/kea-failover.pem'
 cert_key_file = '/run/kea/kea-failover-key.pem'
 
 def dhcp_slice_range(exclude_list, range_dict):
     """
     This function is intended to slice a DHCP range. What does it mean?
 
     Lets assume we have a DHCP range from '192.0.2.1' to '192.0.2.100'
     but want to exclude address '192.0.2.74' and '192.0.2.75'. We will
     pass an input 'range_dict' in the format:
       {'start' : '192.0.2.1', 'stop' : '192.0.2.100' }
     and we will receive an output list of:
       [{'start' : '192.0.2.1' , 'stop' : '192.0.2.73'  },
        {'start' : '192.0.2.76', 'stop' : '192.0.2.100' }]
     The resulting list can then be used in turn to build the proper dhcpd
     configuration file.
     """
     output = []
     # exclude list must be sorted for this to work
     exclude_list = sorted(exclude_list)
     range_start = range_dict['start']
     range_stop = range_dict['stop']
     range_last_exclude = ''
 
     for e in exclude_list:
         if (ip_address(e) >= ip_address(range_start)) and \
            (ip_address(e) <= ip_address(range_stop)):
             range_last_exclude = e
 
     for e in exclude_list:
         if (ip_address(e) >= ip_address(range_start)) and \
            (ip_address(e) <= ip_address(range_stop)):
 
             # Build new address range ending one address before exclude address
             r = {
                 'start' : range_start,
                 'stop' : str(ip_address(e) -1)
             }
             # On the next run our address range will start one address after
             # the exclude address
             range_start = str(ip_address(e) + 1)
 
             # on subsequent exclude addresses we can not
             # append them to our output
             if not (ip_address(r['start']) > ip_address(r['stop'])):
                 # Everything is fine, add range to result
                 output.append(r)
 
             # Take care of last IP address range spanning from the last exclude
             # address (+1) to the end of the initial configured range
             if ip_address(e) == ip_address(range_last_exclude):
                 r = {
                   'start': str(ip_address(e) + 1),
                   'stop': str(range_stop)
                 }
                 if not (ip_address(r['start']) > ip_address(r['stop'])):
                     output.append(r)
         else:
           # if the excluded address was not part of the range, we simply return
           # the entire ranga again
           if not range_last_exclude:
               if range_dict not in output:
                   output.append(range_dict)
 
     return output
 
 def get_config(config=None):
     if config:
         conf = config
     else:
         conf = Config()
     base = ['service', 'dhcp-server']
     if not conf.exists(base):
         return None
 
     dhcp = conf.get_config_dict(base, key_mangling=('-', '_'),
                                 no_tag_node_value_mangle=True,
                                 get_first_key=True,
                                 with_recursive_defaults=True)
 
     if 'shared_network_name' in dhcp:
         for network, network_config in dhcp['shared_network_name'].items():
             if 'subnet' in network_config:
                 for subnet, subnet_config in network_config['subnet'].items():
                     # If exclude IP addresses are defined we need to slice them out of
                     # the defined ranges
                     if {'exclude', 'range'} <= set(subnet_config):
                         new_range_id = 0
                         new_range_dict = {}
                         for r, r_config in subnet_config['range'].items():
                             for slice in dhcp_slice_range(subnet_config['exclude'], r_config):
                                 new_range_dict.update({new_range_id : slice})
                                 new_range_id +=1
 
                         dhcp['shared_network_name'][network]['subnet'][subnet].update(
                                 {'range' : new_range_dict})
 
     if dict_search('failover.certificate', dhcp):
         dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
 
     return dhcp
 
 def verify(dhcp):
     # bail out early - looks like removal from running config
     if not dhcp or 'disable' in dhcp:
         return None
 
     # If DHCP is enabled we need one share-network
     if 'shared_network_name' not in dhcp:
         raise ConfigError('No DHCP shared networks configured.\n' \
                           'At least one DHCP shared network must be configured.')
 
     # Inspect shared-network/subnet
     listen_ok = False
     subnets = []
     failover_ok = False
     shared_networks =  len(dhcp['shared_network_name'])
     disabled_shared_networks = 0
 
+    subnet_ids = []
 
     # A shared-network requires a subnet definition
     for network, network_config in dhcp['shared_network_name'].items():
         if 'disable' in network_config:
             disabled_shared_networks += 1
 
         if 'subnet' not in network_config:
             raise ConfigError(f'No subnets defined for {network}. At least one\n' \
                               'lease subnet must be configured.')
 
         for subnet, subnet_config in network_config['subnet'].items():
+            if 'subnet_id' not in subnet_config:
+                raise ConfigError(f'Unique subnet ID not specified for subnet "{subnet}"')
+
+            if subnet_config['subnet_id'] in subnet_ids:
+                raise ConfigError(f'Subnet ID for subnet "{subnet}" is not unique')
+
+            subnet_ids.append(subnet_config['subnet_id'])
+
             # All delivered static routes require a next-hop to be set
             if 'static_route' in subnet_config:
                 for route, route_option in subnet_config['static_route'].items():
                     if 'next_hop' not in route_option:
                         raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!')
 
             # Check if DHCP address range is inside configured subnet declaration
             if 'range' in subnet_config:
                 networks = []
                 for range, range_config in subnet_config['range'].items():
                     if not {'start', 'stop'} <= set(range_config):
                         raise ConfigError(f'DHCP range "{range}" start and stop address must be defined!')
 
                     # Start/Stop address must be inside network
                     for key in ['start', 'stop']:
                         if ip_address(range_config[key]) not in ip_network(subnet):
                             raise ConfigError(f'DHCP range "{range}" {key} address not within shared-network "{network}, {subnet}"!')
 
                     # Stop address must be greater or equal to start address
                     if ip_address(range_config['stop']) < ip_address(range_config['start']):
                         raise ConfigError(f'DHCP range "{range}" stop address must be greater or equal\n' \
                                           'to the ranges start address!')
 
                     for network in networks:
                         start = range_config['start']
                         stop = range_config['stop']
                         if start in network:
                             raise ConfigError(f'Range "{range}" start address "{start}" already part of another range!')
                         if stop in network:
                             raise ConfigError(f'Range "{range}" stop address "{stop}" already part of another range!')
 
                     tmp = IPRange(range_config['start'], range_config['stop'])
                     networks.append(tmp)
 
             # Exclude addresses must be in bound
             if 'exclude' in subnet_config:
                 for exclude in subnet_config['exclude']:
                     if ip_address(exclude) not in ip_network(subnet):
                         raise ConfigError(f'Excluded IP address "{exclude}" not within shared-network "{network}, {subnet}"!')
 
             # At least one DHCP address range or static-mapping required
             if 'range' not in subnet_config and 'static_mapping' not in subnet_config:
                 raise ConfigError(f'No DHCP address range or active static-mapping configured\n' \
                                   f'within shared-network "{network}, {subnet}"!')
 
             if 'static_mapping' in subnet_config:
                 # Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set)
                 used_ips = []
                 for mapping, mapping_config in subnet_config['static_mapping'].items():
                     if 'ip_address' in mapping_config:
                         if ip_address(mapping_config['ip_address']) not in ip_network(subnet):
                             raise ConfigError(f'Configured static lease address for mapping "{mapping}" is\n' \
                                               f'not within shared-network "{network}, {subnet}"!')
 
                         if ('mac' not in mapping_config and 'duid' not in mapping_config) or \
                             ('mac' in mapping_config and 'duid' in mapping_config):
                             raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for '
                                               f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!')
 
                         if mapping_config['ip_address'] in used_ips:
                             raise ConfigError(f'Configured IP address for static mapping "{mapping}" exists on another static mapping')
 
                         used_ips.append(mapping_config['ip_address'])
 
             # There must be one subnet connected to a listen interface.
             # This only counts if the network itself is not disabled!
             if 'disable' not in network_config:
                 if is_subnet_connected(subnet, primary=False):
                     listen_ok = True
 
             # Subnets must be non overlapping
             if subnet in subnets:
                 raise ConfigError(f'Configured subnets must be unique! Subnet "{subnet}"\n'
                                    'defined multiple times!')
             subnets.append(subnet)
 
             # Check for overlapping subnets
             net = ip_network(subnet)
             for n in subnets:
                 net2 = ip_network(n)
                 if (net != net2):
                     if net.overlaps(net2):
                         raise ConfigError(f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
 
     # Prevent 'disable' for shared-network if only one network is configured
     if (shared_networks - disabled_shared_networks) < 1:
         raise ConfigError(f'At least one shared network must be active!')
 
     if 'failover' in dhcp:
         for key in ['name', 'remote', 'source_address', 'status']:
             if key not in dhcp['failover']:
                 tmp = key.replace('_', '-')
                 raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!')
 
         if len({'certificate', 'ca_certificate'} & set(dhcp['failover'])) == 1:
             raise ConfigError(f'DHCP secured failover requires both certificate and CA certificate')
 
         if 'certificate' in dhcp['failover']:
             cert_name = dhcp['failover']['certificate']
 
             if cert_name not in dhcp['pki']['certificate']:
                 raise ConfigError(f'Invalid certificate specified for DHCP failover')
 
             if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'certificate'):
                 raise ConfigError(f'Invalid certificate specified for DHCP failover')
 
             if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'private', 'key'):
                 raise ConfigError(f'Missing private key on certificate specified for DHCP failover')
 
         if 'ca_certificate' in dhcp['failover']:
             ca_cert_name = dhcp['failover']['ca_certificate']
             if ca_cert_name not in dhcp['pki']['ca']:
                 raise ConfigError(f'Invalid CA certificate specified for DHCP failover')
 
             if not dict_search_args(dhcp['pki']['ca'], ca_cert_name, 'certificate'):
                 raise ConfigError(f'Invalid CA certificate specified for DHCP failover')
 
     for address in (dict_search('listen_address', dhcp) or []):
         if is_addr_assigned(address):
             listen_ok = True
             # no need to probe further networks, we have one that is valid
             continue
         else:
             raise ConfigError(f'listen-address "{address}" not configured on any interface')
 
     if not listen_ok:
         raise ConfigError('None of the configured subnets have an appropriate primary IP address on any\n'
                           'broadcast interface configured, nor was there an explicit listen-address\n'
                           'configured for serving DHCP relay packets!')
 
     if 'listen_address' in dhcp and 'listen_interface' in dhcp:
         raise ConfigError(f'Cannot define listen-address and listen-interface at the same time')
 
     for interface in (dict_search('listen_interface', dhcp) or []):
         if not interface_exists(interface):
             raise ConfigError(f'listen-interface "{interface}" does not exist')
 
     return None
 
 def generate(dhcp):
     # bail out early - looks like removal from running config
     if not dhcp or 'disable' in dhcp:
         return None
 
     dhcp['lease_file'] = lease_file
     dhcp['machine'] = os.uname().machine
 
     # Create directory for lease file if necessary
     lease_dir = os.path.dirname(lease_file)
     if not os.path.isdir(lease_dir):
         makedir(lease_dir, group='vyattacfg')
         chmod_775(lease_dir)
 
     # Create lease file if necessary and let kea own it - 'kea-lfc' expects it that way
     if not os.path.exists(lease_file):
         write_file(lease_file, '', user=user_group, group=user_group, mode=0o644)
 
     for f in [cert_file, cert_key_file, ca_cert_file]:
         if os.path.exists(f):
             os.unlink(f)
 
     if 'failover' in dhcp:
         if 'certificate' in dhcp['failover']:
             cert_name = dhcp['failover']['certificate']
             cert_data = dhcp['pki']['certificate'][cert_name]['certificate']
             key_data = dhcp['pki']['certificate'][cert_name]['private']['key']
             write_file(cert_file, wrap_certificate(cert_data), user=user_group, mode=0o600)
             write_file(cert_key_file, wrap_private_key(key_data), user=user_group, mode=0o600)
 
             dhcp['failover']['cert_file'] = cert_file
             dhcp['failover']['cert_key_file'] = cert_key_file
 
         if 'ca_certificate' in dhcp['failover']:
             ca_cert_name = dhcp['failover']['ca_certificate']
             ca_cert_data = dhcp['pki']['ca'][ca_cert_name]['certificate']
             write_file(ca_cert_file, wrap_certificate(ca_cert_data), user=user_group, mode=0o600)
 
             dhcp['failover']['ca_cert_file'] = ca_cert_file
 
         render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp)
 
     render(ctrl_config_file, 'dhcp-server/kea-ctrl-agent.conf.j2', dhcp, user=user_group, group=user_group)
     render(config_file, 'dhcp-server/kea-dhcp4.conf.j2', dhcp, user=user_group, group=user_group)
 
     return None
 
 def apply(dhcp):
     services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server']
 
     if not dhcp or 'disable' in dhcp:
         for service in services:
             call(f'systemctl stop {service}.service')
 
         if os.path.exists(config_file):
             os.unlink(config_file)
 
         return None
 
     for service in services:
         action = 'restart'
 
         if service == 'kea-dhcp-ddns-server' and 'dynamic_dns_update' not in dhcp:
             action = 'stop'
 
         if service == 'kea-ctrl-agent' and 'failover' not in dhcp:
             action = 'stop'
 
         call(f'systemctl {action} {service}.service')
 
     return None
 
 if __name__ == '__main__':
     try:
         c = get_config()
         verify(c)
         generate(c)
         apply(c)
     except ConfigError as e:
         print(e)
         exit(1)
diff --git a/src/conf_mode/service_dhcpv6-server.py b/src/conf_mode/service_dhcpv6-server.py
index 9cc57dbcf..7cd801cdd 100755
--- a/src/conf_mode/service_dhcpv6-server.py
+++ b/src/conf_mode/service_dhcpv6-server.py
@@ -1,222 +1,231 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2018-2023 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
 
 from ipaddress import ip_address
 from ipaddress import ip_network
 from sys import exit
 
 from vyos.config import Config
 from vyos.template import render
 from vyos.utils.process import call
 from vyos.utils.file import chmod_775
 from vyos.utils.file import makedir
 from vyos.utils.file import write_file
 from vyos.utils.dict import dict_search
 from vyos.utils.network import is_subnet_connected
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
 config_file = '/run/kea/kea-dhcp6.conf'
 ctrl_socket = '/run/kea/dhcp6-ctrl-socket'
 lease_file = '/config/dhcp/dhcp6-leases.csv'
 user_group = '_kea'
 
 def get_config(config=None):
     if config:
         conf = config
     else:
         conf = Config()
     base = ['service', 'dhcpv6-server']
     if not conf.exists(base):
         return None
 
     dhcpv6 = conf.get_config_dict(base, key_mangling=('-', '_'),
                                   get_first_key=True,
                                   no_tag_node_value_mangle=True)
     return dhcpv6
 
 def verify(dhcpv6):
     # bail out early - looks like removal from running config
     if not dhcpv6 or 'disable' in dhcpv6:
         return None
 
     # If DHCP is enabled we need one share-network
     if 'shared_network_name' not in dhcpv6:
         raise ConfigError('No DHCPv6 shared networks configured. At least '\
                           'one DHCPv6 shared network must be configured.')
 
     # Inspect shared-network/subnet
     subnets = []
+    subnet_ids = []
     listen_ok = False
     for network, network_config in dhcpv6['shared_network_name'].items():
         # A shared-network requires a subnet definition
         if 'subnet' not in network_config:
             raise ConfigError(f'No DHCPv6 lease subnets configured for "{network}". '\
                               'At least one lease subnet must be configured for '\
                               'each shared network!')
 
         for subnet, subnet_config in network_config['subnet'].items():
+            if 'subnet_id' not in subnet_config:
+                raise ConfigError(f'Unique subnet ID not specified for subnet "{subnet}"')
+
+            if subnet_config['subnet_id'] in subnet_ids:
+                raise ConfigError(f'Subnet ID for subnet "{subnet}" is not unique')
+
+            subnet_ids.append(subnet_config['subnet_id'])
+
             if 'address_range' in subnet_config:
                 if 'start' in subnet_config['address_range']:
                     range6_start = []
                     range6_stop = []
                     for start, start_config in subnet_config['address_range']['start'].items():
                         if 'stop' not in start_config:
                             raise ConfigError(f'address-range stop address for start "{start}" is not defined!')
                         stop = start_config['stop']
 
                         # Start address must be inside network
                         if not ip_address(start) in ip_network(subnet):
                             raise ConfigError(f'address-range start address "{start}" is not in subnet "{subnet}"!')
 
                         # Stop address must be inside network
                         if not ip_address(stop) in ip_network(subnet):
                              raise ConfigError(f'address-range stop address "{stop}" is not in subnet "{subnet}"!')
 
                         # Stop address must be greater or equal to start address
                         if not ip_address(stop) >= ip_address(start):
                             raise ConfigError(f'address-range stop address "{stop}" must be greater then or equal ' \
                                               f'to the range start address "{start}"!')
 
                         # DHCPv6 range start address must be unique - two ranges can't
                         # start with the same address - makes no sense
                         if start in range6_start:
                             raise ConfigError(f'Conflicting DHCPv6 lease range: '\
                                               f'Pool start address "{start}" defined multipe times!')
                         range6_start.append(start)
 
                         # DHCPv6 range stop address must be unique - two ranges can't
                         # end with the same address - makes no sense
                         if stop in range6_stop:
                             raise ConfigError(f'Conflicting DHCPv6 lease range: '\
                                               f'Pool stop address "{stop}" defined multipe times!')
                         range6_stop.append(stop)
 
                 if 'prefix' in subnet_config:
                     for prefix in subnet_config['prefix']:
                         if ip_network(prefix) not in ip_network(subnet):
                             raise ConfigError(f'address-range prefix "{prefix}" is not in subnet "{subnet}""')
 
             # Prefix delegation sanity checks
             if 'prefix_delegation' in subnet_config:
                 if 'prefix' not in subnet_config['prefix_delegation']:
                     raise ConfigError('prefix-delegation prefix not defined!')
 
                 for prefix, prefix_config in subnet_config['prefix_delegation']['prefix'].items():
                     if 'delegated_length' not in prefix_config:
                         raise ConfigError(f'Delegated IPv6 prefix length for "{prefix}" '\
                                           f'must be configured')
 
                     if 'prefix_length' not in prefix_config:
                         raise ConfigError('Length of delegated IPv6 prefix must be configured')
 
                     if prefix_config['prefix_length'] > prefix_config['delegated_length']:
                         raise ConfigError('Length of delegated IPv6 prefix must be within parent prefix')
 
             # Static mappings don't require anything (but check if IP is in subnet if it's set)
             if 'static_mapping' in subnet_config:
                 for mapping, mapping_config in subnet_config['static_mapping'].items():
                     if 'ipv6_address' in mapping_config:
                         # Static address must be in subnet
                         if ip_address(mapping_config['ipv6_address']) not in ip_network(subnet):
                             raise ConfigError(f'static-mapping address for mapping "{mapping}" is not in subnet "{subnet}"!')
 
                         if ('mac' not in mapping_config and 'duid' not in mapping_config) or \
                             ('mac' in mapping_config and 'duid' in mapping_config):
                             raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for '
                                               f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!')
 
             if 'vendor_option' in subnet_config:
                 if len(dict_search('vendor_option.cisco.tftp_server', subnet_config)) > 2:
                     raise ConfigError(f'No more then two Cisco tftp-servers should be defined for subnet "{subnet}"!')
 
             # Subnets must be unique
             if subnet in subnets:
                 raise ConfigError(f'DHCPv6 subnets must be unique! Subnet {subnet} defined multiple times!')
             subnets.append(subnet)
 
         # DHCPv6 requires at least one configured address range or one static mapping
         # (FIXME: is not actually checked right now?)
 
         # There must be one subnet connected to a listen interface if network is not disabled.
         if 'disable' not in network_config:
             if is_subnet_connected(subnet):
                 listen_ok = True
 
             # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping
             # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32"
             net = ip_network(subnet)
             for n in subnets:
                 net2 = ip_network(n)
                 if (net != net2):
                     if net.overlaps(net2):
                         raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2))
 
     if not listen_ok:
         raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on '\
                           'this machine. At least one subnet6 must be connected such that '\
                           'DHCPv6 listens on an interface!')
 
 
     return None
 
 def generate(dhcpv6):
     # bail out early - looks like removal from running config
     if not dhcpv6 or 'disable' in dhcpv6:
         return None
 
     dhcpv6['lease_file'] = lease_file
     dhcpv6['machine'] = os.uname().machine
 
     # Create directory for lease file if necessary
     lease_dir = os.path.dirname(lease_file)
     if not os.path.isdir(lease_dir):
         makedir(lease_dir, group='vyattacfg')
         chmod_775(lease_dir)
 
     # Create lease file if necessary and let kea own it - 'kea-lfc' expects it that way
     if not os.path.exists(lease_file):
         write_file(lease_file, '', user=user_group, group=user_group, mode=0o644)
 
     render(config_file, 'dhcp-server/kea-dhcp6.conf.j2', dhcpv6, user=user_group, group=user_group)
     return None
 
 def apply(dhcpv6):
     # bail out early - looks like removal from running config
     service_name = 'kea-dhcp6-server.service'
     if not dhcpv6 or 'disable' in dhcpv6:
         # DHCP server is removed in the commit
         call(f'systemctl stop {service_name}')
         if os.path.exists(config_file):
             os.unlink(config_file)
         return None
 
     call(f'systemctl restart {service_name}')
 
     return None
 
 if __name__ == '__main__':
     try:
         c = get_config()
         verify(c)
         generate(c)
         apply(c)
     except ConfigError as e:
         print(e)
         exit(1)
diff --git a/src/migration-scripts/dhcp-server/8-to-9 b/src/migration-scripts/dhcp-server/8-to-9
index 908420c18..810e403a6 100755
--- a/src/migration-scripts/dhcp-server/8-to-9
+++ b/src/migration-scripts/dhcp-server/8-to-9
@@ -1,69 +1,75 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2024 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 # T3316:
 # - Migrate dhcp options under new option node
+# - Add subnet IDs to existing subnets
 
 import sys
 import re
 from vyos.configtree import ConfigTree
 
 if len(sys.argv) < 2:
     print("Must specify file name!")
     sys.exit(1)
 
 file_name = sys.argv[1]
 
 with open(file_name, 'r') as f:
     config_file = f.read()
 
 base = ['service', 'dhcp-server', 'shared-network-name']
 config = ConfigTree(config_file)
 
 if not config.exists(base):
     # Nothing to do
     sys.exit(0)
 
 option_nodes = ['bootfile-name', 'bootfile-server', 'bootfile-size', 'captive-portal',
                 'client-prefix-length', 'default-router', 'domain-name', 'domain-search',
                 'name-server', 'ip-forwarding', 'ipv6-only-preferred', 'ntp-server',
                 'pop-server', 'server-identifier', 'smtp-server', 'static-route',
                 'tftp-server-name', 'time-offset', 'time-server', 'time-zone',
                 'vendor-option', 'wins-server', 'wpad-url']
 
+subnet_id = 1
+
 for network in config.list_nodes(base):
     for option in option_nodes:
         if config.exists(base + [network, option]):
             config.set(base + [network, 'option'])
             config.copy(base + [network, option], base + [network, 'option', option])
             config.delete(base + [network, option])
 
     if config.exists(base + [network, 'subnet']):
         for subnet in config.list_nodes(base + [network, 'subnet']):
             base_subnet = base + [network, 'subnet', subnet]
             
             for option in option_nodes:
-                if config.exists(base + [network, 'subnet', subnet, option]):
-                    config.set(base + [network, 'subnet', subnet, 'option'])
-                    config.copy(base + [network, 'subnet', subnet, option], base + [network, 'subnet', subnet, 'option', option])
-                    config.delete(base + [network, 'subnet', subnet, option])
+                if config.exists(base_subnet + [option]):
+                    config.set(base_subnet + ['option'])
+                    config.copy(base_subnet + [option], base_subnet + ['option', option])
+                    config.delete(base_subnet + [option])
+
+            config.set(base_subnet + ['subnet-id'], value=subnet_id)
+            subnet_id += 1
 
 try:
     with open(file_name, 'w') as f:
         f.write(config.to_string())
 except OSError as e:
     print("Failed to save the modified config: {}".format(e))
     exit(1)
diff --git a/src/migration-scripts/dhcp-server/8-to-9 b/src/migration-scripts/dhcpv6-server/3-to-4
similarity index 52%
copy from src/migration-scripts/dhcp-server/8-to-9
copy to src/migration-scripts/dhcpv6-server/3-to-4
index 908420c18..c065e3d43 100755
--- a/src/migration-scripts/dhcp-server/8-to-9
+++ b/src/migration-scripts/dhcpv6-server/3-to-4
@@ -1,69 +1,55 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2024 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 # T3316:
-# - Migrate dhcp options under new option node
+# - Add subnet IDs to existing subnets
 
 import sys
 import re
 from vyos.configtree import ConfigTree
 
 if len(sys.argv) < 2:
     print("Must specify file name!")
     sys.exit(1)
 
 file_name = sys.argv[1]
 
 with open(file_name, 'r') as f:
     config_file = f.read()
 
-base = ['service', 'dhcp-server', 'shared-network-name']
+base = ['service', 'dhcpv6-server', 'shared-network-name']
 config = ConfigTree(config_file)
 
 if not config.exists(base):
     # Nothing to do
     sys.exit(0)
 
-option_nodes = ['bootfile-name', 'bootfile-server', 'bootfile-size', 'captive-portal',
-                'client-prefix-length', 'default-router', 'domain-name', 'domain-search',
-                'name-server', 'ip-forwarding', 'ipv6-only-preferred', 'ntp-server',
-                'pop-server', 'server-identifier', 'smtp-server', 'static-route',
-                'tftp-server-name', 'time-offset', 'time-server', 'time-zone',
-                'vendor-option', 'wins-server', 'wpad-url']
+subnet_id = 1
 
 for network in config.list_nodes(base):
-    for option in option_nodes:
-        if config.exists(base + [network, option]):
-            config.set(base + [network, 'option'])
-            config.copy(base + [network, option], base + [network, 'option', option])
-            config.delete(base + [network, option])
-
     if config.exists(base + [network, 'subnet']):
         for subnet in config.list_nodes(base + [network, 'subnet']):
             base_subnet = base + [network, 'subnet', subnet]
-            
-            for option in option_nodes:
-                if config.exists(base + [network, 'subnet', subnet, option]):
-                    config.set(base + [network, 'subnet', subnet, 'option'])
-                    config.copy(base + [network, 'subnet', subnet, option], base + [network, 'subnet', subnet, 'option', option])
-                    config.delete(base + [network, 'subnet', subnet, option])
+
+            config.set(base_subnet + ['subnet-id'], value=subnet_id)
+            subnet_id += 1
 
 try:
     with open(file_name, 'w') as f:
         f.write(config.to_string())
 except OSError as e:
     print("Failed to save the modified config: {}".format(e))
     exit(1)