diff --git a/data/templates/dhcp-server/10-override.conf.j2 b/data/templates/dhcp-server/10-override.conf.j2
deleted file mode 100644
index 1504b6808..000000000
--- a/data/templates/dhcp-server/10-override.conf.j2
+++ /dev/null
@@ -1,30 +0,0 @@
-### Autogenerated by dhcp_server.py ###
-{% set lease_file = '/config/dhcpd.leases' %}
-[Unit]
-Description=ISC DHCP IPv4 server
-Documentation=man:dhcpd(8)
-RequiresMountsFor=/run
-ConditionPathExists=
-ConditionPathExists=/run/dhcp-server/dhcpd.conf
-After=
-After=vyos-router.service
-
-[Service]
-Type=forking
-WorkingDirectory=
-WorkingDirectory=/run/dhcp-server
-RuntimeDirectory=dhcp-server
-RuntimeDirectoryPreserve=yes
-Environment=PID_FILE=/run/dhcp-server/dhcpd.pid CONFIG_FILE=/run/dhcp-server/dhcpd.conf LEASE_FILE={{ lease_file }}
-PIDFile=/run/dhcp-server/dhcpd.pid
-ExecStartPre=/bin/sh -ec '\
-touch ${LEASE_FILE}; \
-chown dhcpd:vyattacfg ${LEASE_FILE}* ; \
-chmod 664 ${LEASE_FILE}* ; \
-/usr/sbin/dhcpd -4 -t -T -q -user dhcpd -group vyattacfg -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} '
-ExecStart=
-ExecStart=/usr/sbin/dhcpd -4 -q -user dhcpd -group vyattacfg -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE}
-Restart=always
-
-[Install]
-WantedBy=multi-user.target
diff --git a/data/templates/dhcp-server/dhcpd.conf.j2 b/data/templates/dhcp-server/dhcpd.conf.j2
deleted file mode 100644
index 639526532..000000000
--- a/data/templates/dhcp-server/dhcpd.conf.j2
+++ /dev/null
@@ -1,250 +0,0 @@
-### Autogenerated by dhcp_server.py ###
-
-# For options please consult the following website:
-# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html
-#
-# log-facility local7;
-{% if hostfile_update is vyos_defined %}
-on release {
-    set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name);
-    set ClientIp = binary-to-ascii(10, 8, ".",leased-address);
-    execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", "", ClientIp, "", "");
-}
-on expiry {
-    set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name);
-    set ClientIp = binary-to-ascii(10, 8, ".",leased-address);
-    execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", "", ClientIp, "", "");
-}
-{% endif %}
-
-{{ 'use-host-decl-names on;' if host_decl_name is vyos_defined }}
-ddns-update-style {{ 'interim' if dynamic_dns_update is vyos_defined else 'none' }};
-option rfc3442-static-route code 121 = array of integer 8;
-option windows-static-route code 249 = array of integer 8;
-option wpad-url code 252 = text;
-option rfc8925-ipv6-only-preferred code 108 = unsigned integer 32;
-
-# Vendor specific options - Ubiquiti Networks
-option space ubnt;
-option ubnt.unifi-controller code 1 = ip-address;
-class "ubnt" {
-    match if substring (option vendor-class-identifier , 0, 4) = "ubnt";
-    option vendor-class-identifier "ubnt";
-    vendor-option-space ubnt;
-}
-
-{% if global_parameters is vyos_defined %}
-# The following {{ global_parameters | length }} line(s) have been added as
-# global-parameters in the CLI and have not been validated !!!
-{%     for parameter in global_parameters %}
-{{ parameter }}
-{%     endfor %}
-
-{% endif %}
-{% if failover is vyos_defined %}
-# DHCP failover configuration
-failover peer "{{ failover.name }}" {
-{%     if failover.status == 'primary' %}
-    primary;
-    mclt 1800;
-    split 128;
-{%     elif failover.status == 'secondary' %}
-    secondary;
-{%     endif %}
-    address {{ failover.source_address }};
-    port 647;
-    peer address {{ failover.remote }};
-    peer port 647;
-    max-response-delay 30;
-    max-unacked-updates 10;
-    load balance max seconds 3;
-}
-{% endif %}
-{% if listen_address is vyos_defined %}
-
-# DHCP server serving relay subnet, we need a connector to the real world
-{%     for address in listen_address %}
-# Connected subnet statement for listen-address {{ address }}
-subnet {{ address | network_from_ipv4 }} netmask {{ address | netmask_from_ipv4 }} { }
-{%     endfor %}
-{% endif %}
-
-# Shared network configration(s)
-{% if shared_network_name is vyos_defined %}
-{%     for network, network_config in shared_network_name.items() if network_config.disable is not vyos_defined %}
-shared-network {{ network }} {
-{%         if network_config.authoritative is vyos_defined %}
-    authoritative;
-{%         endif %}
-{%         if network_config.name_server is vyos_defined %}
-    option domain-name-servers {{ network_config.name_server | join(', ') }};
-{%         endif %}
-{%         if network_config.domain_name is vyos_defined %}
-    option domain-name "{{ network_config.domain_name }}";
-{%         endif %}
-{%         if network_config.domain_search is vyos_defined %}
-    option domain-search "{{ network_config.domain_search | join('", "') }}";
-{%         endif %}
-{%         if network_config.ntp_server is vyos_defined %}
-    option ntp-servers {{ network_config.ntp_server | join(', ') }};
-{%         endif %}
-{%         if network_config.ping_check is vyos_defined %}
-    ping-check true;
-{%         endif %}
-{%         if network_config.shared_network_parameters is vyos_defined %}
-    # The following {{ network_config.shared_network_parameters | length }} line(s)
-    # were added as shared-network-parameters in the CLI and have not been validated
-{%             for parameter in network_config.shared_network_parameters %}
-    {{ parameter }}
-{%             endfor %}
-{%         endif %}
-{%         if network_config.subnet is vyos_defined %}
-{%             for subnet, subnet_config in network_config.subnet.items() %}
-{%                 if subnet_config.description is vyos_defined %}
-    # {{ subnet_config.description }}
-{%                 endif %}
-    subnet {{ subnet | address_from_cidr }} netmask {{ subnet | netmask_from_cidr }} {
-{%                 if subnet_config.name_server is vyos_defined %}
-        option domain-name-servers {{ subnet_config.name_server | join(', ') }};
-{%                 endif %}
-{%                 if subnet_config.domain_name is vyos_defined %}
-        option domain-name "{{ subnet_config.domain_name }}";
-{%                 endif %}
-{%                 if subnet_config.domain_search is vyos_defined %}
-        option domain-search "{{ subnet_config.domain_search | join('", "') }}";
-{%                 endif %}
-{%                 if subnet_config.ntp_server is vyos_defined %}
-        option ntp-servers {{ subnet_config.ntp_server | join(', ') }};
-{%                 endif %}
-{%                 if subnet_config.pop_server is vyos_defined %}
-        option pop-server {{ subnet_config.pop_server | join(', ') }};
-{%                 endif %}
-{%                 if subnet_config.smtp_server is vyos_defined %}
-        option smtp-server {{ subnet_config.smtp_server | join(', ') }};
-{%                 endif %}
-{%                 if subnet_config.time_server is vyos_defined %}
-        option time-servers {{ subnet_config.time_server | join(', ') }};
-{%                 endif %}
-{%                 if subnet_config.wins_server is vyos_defined %}
-        option netbios-name-servers {{ subnet_config.wins_server | join(', ') }};
-{%                 endif %}
-{%                 if subnet_config.ipv6_only_preferred is vyos_defined %}
-        option rfc8925-ipv6-only-preferred {{ subnet_config.ipv6_only_preferred }};
-{%                 endif %}
-{%                 if subnet_config.static_route is vyos_defined %}
-{%                     set static_default_route = '' %}
-{%                     if subnet_config.default_router is vyos_defined %}
-{%                         set static_default_route = ', ' ~ '0.0.0.0/0' | isc_static_route(subnet_config.default_router) %}
-{%                     endif %}
-{%                     if subnet_config.static_route is vyos_defined %}
-{%                         set rfc3442_routes = [] %}
-{%                         for route, route_options in subnet_config.static_route.items() %}
-{%                             set rfc3442_routes = rfc3442_routes.append(route | isc_static_route(route_options.next_hop)) %}
-{%                         endfor %}
-        option rfc3442-static-route {{ rfc3442_routes | join(', ') }}{{ static_default_route }};
-        option windows-static-route {{ rfc3442_routes | join(', ') }};
-{%                     endif %}
-{%                 endif %}
-{%                 if subnet_config.ip_forwarding is vyos_defined %}
-        option ip-forwarding true;
-{%                 endif %}
-{%                 if subnet_config.default_router is vyos_defined %}
-        option routers {{ subnet_config.default_router }};
-{%                 endif %}
-{%                 if subnet_config.server_identifier is vyos_defined %}
-        option dhcp-server-identifier {{ subnet_config.server_identifier }};
-{%                 endif %}
-{%                 if subnet_config.subnet_parameters is vyos_defined %}
-        # The following {{ subnet_config.subnet_parameters | length }} line(s) were added as
-        # subnet-parameters in the CLI and have not been validated!!!
-{%                     for parameter in subnet_config.subnet_parameters %}
-        {{ parameter }}
-{%                     endfor %}
-{%                 endif %}
-{%                 if subnet_config.tftp_server_name is vyos_defined %}
-        option tftp-server-name "{{ subnet_config.tftp_server_name }}";
-{%                 endif %}
-{%                 if subnet_config.bootfile_name is vyos_defined %}
-        option bootfile-name "{{ subnet_config.bootfile_name }}";
-        filename "{{ subnet_config.bootfile_name }}";
-{%                 endif %}
-{%                 if subnet_config.bootfile_server is vyos_defined %}
-        next-server {{ subnet_config.bootfile_server }};
-{%                 endif %}
-{%                 if subnet_config.bootfile_size is vyos_defined %}
-        option boot-size {{ subnet_config.bootfile_size }};
-{%                 endif %}
-{%                 if subnet_config.time_offset is vyos_defined %}
-        option time-offset {{ subnet_config.time_offset }};
-{%                 endif %}
-{%                 if subnet_config.wpad_url is vyos_defined %}
-        option wpad-url "{{ subnet_config.wpad_url }}";
-{%                 endif %}
-{%                 if subnet_config.client_prefix_length is vyos_defined %}
-        option subnet-mask {{ ('0.0.0.0/' ~ subnet_config.client_prefix_length) | netmask_from_cidr }};
-{%                 endif %}
-{%                 if subnet_config.lease is vyos_defined %}
-        default-lease-time {{ subnet_config.lease }};
-        max-lease-time {{ subnet_config.lease }};
-{%                 endif %}
-{%                 if network_config.ping_check is not vyos_defined and subnet_config.ping_check is vyos_defined %}
-        ping-check true;
-{%                 endif %}
-{%                 if subnet_config.static_mapping is vyos_defined %}
-{%                     for host, host_config in subnet_config.static_mapping.items() if host_config.disable is not vyos_defined %}
-        host {{ host | replace('_','-') if host_decl_name is vyos_defined else network | replace('_','-') ~ '_' ~ host | replace('_','-') }} {
-{%                         if host_config.ip_address is vyos_defined %}
-            fixed-address {{ host_config.ip_address }};
-{%                         endif %}
-            hardware ethernet {{ host_config.mac_address }};
-{%                         if host_config.static_mapping_parameters is vyos_defined %}
-            # The following {{ host_config.static_mapping_parameters | length }} line(s) were added
-            # as static-mapping-parameters in the CLI and have not been validated
-{%                             for parameter in host_config.static_mapping_parameters %}
-            {{ parameter }}
-{%                             endfor %}
-{%                         endif %}
-        }
-{%                     endfor %}
-{%                 endif %}
-{%                 if subnet_config.vendor_option.ubiquiti.unifi_controller is vyos_defined %}
-        option ubnt.unifi-controller {{ subnet_config.vendor_option.ubiquiti.unifi_controller }};
-{%                 endif %}
-{%                 if subnet_config.range is vyos_defined %}
-{#           pool configuration can only be used if there follows a range option #}
-        pool {
-{%                 endif %}
-{%                 if subnet_config.enable_failover is vyos_defined %}
-            failover peer "{{ failover.name }}";
-            deny dynamic bootp clients;
-{%                 endif %}
-{%                 if subnet_config.range is vyos_defined %}
-{%                     for range, range_options in subnet_config.range.items() %}
-            range {{ range_options.start }} {{ range_options.stop }};
-{%                     endfor %}
-{%                 endif %}
-{%                 if subnet_config.range is vyos_defined %}
-{#           pool configuration can only be used if there follows a range option #}
-        }
-{%                 endif %}
-    }
-{%             endfor %}
-{%         endif %}
-    on commit {
-        set shared-networkname = "{{ network }}";
-{%         if hostfile_update is vyos_defined %}
-        set ClientIp = binary-to-ascii(10, 8, ".", leased-address);
-        set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6));
-        set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name, "empty_hostname");
-        if not (ClientName = "empty_hostname") {
-            set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!");
-            execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain);
-        } else {
-            log(concat("Hostname is not defined for client with IP: ", ClientIP, " MAC: ", ClientMac));
-        }
-{%         endif %}
-    }
-}
-
-{%     endfor %}
-{% endif %}
diff --git a/data/templates/dhcp-server/dhcpdv6.conf.j2 b/data/templates/dhcp-server/dhcpdv6.conf.j2
deleted file mode 100644
index 5c3471316..000000000
--- a/data/templates/dhcp-server/dhcpdv6.conf.j2
+++ /dev/null
@@ -1,132 +0,0 @@
-### Autogenerated by dhcpv6_server.py ###
-
-# For options please consult the following website:
-# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html
-
-log-facility local7;
-{% if preference is vyos_defined %}
-option dhcp6.preference {{ preference }};
-{% endif %}
-
-{% if global_parameters.name_server is vyos_defined %}
-option dhcp6.name-servers {{ global_parameters.name_server | join(', ') }};
-{% endif %}
-
-# Vendor specific options - Cisco
-option space cisco code width 2 length width 2;
-option cisco.tftp-servers code 1 = array of ip6-address;
-option vsio.cisco code 9 = encapsulate cisco;
-
-# Shared network configration(s)
-{% if shared_network_name is vyos_defined %}
-{%     for network, network_config in shared_network_name.items() if network_config.disable is not vyos_defined %}
-shared-network {{ network }} {
-{%         if network_config.common_options is vyos_defined %}
-{%             if network_config.common_options.info_refresh_time is vyos_defined %}
-    option dhcp6.info-refresh-time {{ network_config.common_options.info_refresh_time }};
-{%             endif %}
-{%             if network_config.common_options.domain_search is vyos_defined %}
-    option dhcp6.domain-search "{{ network_config.common_options.domain_search | join('", "') }}";
-{%             endif %}
-{%             if network_config.common_options.name_server is vyos_defined %}
-    option dhcp6.name-servers {{ network_config.common_options.name_server | join(', ') }};
-{%             endif %}
-{%         endif %}
-{%         if network_config.subnet is vyos_defined %}
-{%             for subnet, subnet_config in network_config.subnet.items() %}
-    subnet6 {{ subnet }} {
-{%                 if subnet_config.address_range is vyos_defined %}
-{%                     if subnet_config.address_range.prefix is vyos_defined %}
-{%                         for prefix, prefix_config in subnet_config.address_range.prefix.items() %}
-        range6 {{ prefix }} {{ "temporary" if prefix_config.temporary is vyos_defined }};
-{%                         endfor %}
-{%                     endif %}
-{%                     if subnet_config.address_range.start is vyos_defined %}
-{%                         for address, address_config in subnet_config.address_range.start.items() %}
-        range6 {{ address }} {{ address_config.stop }};
-{%                         endfor %}
-{%                     endif %}
-{%                 endif %}
-{%                 if subnet_config.domain_search is vyos_defined %}
-        option dhcp6.domain-search "{{ subnet_config.domain_search | join('", "') }}";
-{%                 endif %}
-{%                 if subnet_config.lease_time is vyos_defined %}
-{%                     if subnet_config.lease_time.default is vyos_defined %}
-        default-lease-time {{ subnet_config.lease_time.default }};
-{%                     endif %}
-{%                     if subnet_config.lease_time.maximum is vyos_defined %}
-        max-lease-time {{ subnet_config.lease_time.maximum }};
-{%                     endif %}
-{%                     if subnet_config.lease_time.minimum is vyos_defined %}
-        min-lease-time {{ subnet_config.lease_time.minimum }};
-{%                     endif %}
-{%                 endif %}
-{%                 if subnet_config.name_server is vyos_defined %}
-        option dhcp6.name-servers {{ subnet_config.name_server | join(', ') }};
-{%                 endif %}
-{%                 if subnet_config.nis_domain is vyos_defined %}
-        option dhcp6.nis-domain-name "{{ subnet_config.nis_domain }}";
-{%                 endif %}
-{%                 if subnet_config.nis_server is vyos_defined %}
-        option dhcp6.nis-servers {{ subnet_config.nis_server | join(', ') }};
-{%                 endif %}
-{%                 if subnet_config.nisplus_domain is vyos_defined %}
-        option dhcp6.nisp-domain-name "{{ subnet_config.nisplus_domain }}";
-{%                 endif %}
-{%                 if subnet_config.nisplus_server is vyos_defined %}
-        option dhcp6.nisp-servers {{ subnet_config.nisplus_server | join(', ') }};
-{%                 endif %}
-{%                 if subnet_config.sip_server is vyos_defined %}
-{%                     set server_ip = [] %}
-{%                     set server_fqdn = [] %}
-{%                     for address in subnet_config.sip_server %}
-{%                         if address | is_ipv6 %}
-{%                             set server_ip = server_ip.append(address) %}
-{%                         else %}
-{%                             set server_fqdn = server_fqdn.append(address) %}
-{%                         endif %}
-{%                     endfor %}
-{%                     if server_ip is vyos_defined and server_ip | length > 0 %}
-        option dhcp6.sip-servers-addresses {{ server_ip | join(', ') }};
-{%                     endif %}
-{%                     if server_fqdn is vyos_defined and server_fqdn | length > 0 %}
-        option dhcp6.sip-servers-names "{{ server_fqdn | join('", "') }}";
-{%                     endif %}
-{%                 endif %}
-{%                 if subnet_config.sntp_server is vyos_defined %}
-        option dhcp6.sntp-servers {{ subnet_config.sntp_server | join(', ') }};
-{%                 endif %}
-{%                 if subnet_config.prefix_delegation.start is vyos_defined %}
-{%                     for prefix, prefix_config in subnet_config.prefix_delegation.start.items() %}
-        prefix6 {{ prefix }} {{ prefix_config.stop }} /{{ prefix_config.prefix_length }};
-{%                     endfor %}
-{%                 endif %}
-{%                 if subnet_config.static_mapping is vyos_defined %}
-
-        # begin configuration of static client mappings
-{%                     for host, host_config in subnet_config.static_mapping.items() if host_config.disable is not vyos_defined %}
-        host {{ network | replace('_','-') }}_{{ host | replace('_','-') }} {
-{%                         if host_config.identifier is vyos_defined %}
-            host-identifier option dhcp6.client-id {{ host_config.identifier }};
-{%                         endif %}
-{%                         if host_config.ipv6_address is vyos_defined %}
-            fixed-address6 {{ host_config.ipv6_address }};
-{%                         endif %}
-{%                         if host_config.ipv6_prefix is vyos_defined %}
-            fixed-prefix6 {{ host_config.ipv6_prefix }};
-{%                         endif %}
-        }
-{%                     endfor %}
-{%                 endif %}
-{%                 if subnet_config.vendor_option.cisco.tftp_server is vyos_defined %}
-        option cisco.tftp-servers {{ subnet_config.vendor_option.cisco.tftp_server | join(', ') }};
-{%                 endif %}
-    }
-{%             endfor %}
-{%         endif %}
-    on commit {
-        set shared-networkname = "{{ network }}";
-    }
-}
-{%     endfor %}
-{% endif %}
diff --git a/data/templates/dhcp-server/kea-ctrl-agent.conf.j2 b/data/templates/dhcp-server/kea-ctrl-agent.conf.j2
new file mode 100644
index 000000000..74c63a7a0
--- /dev/null
+++ b/data/templates/dhcp-server/kea-ctrl-agent.conf.j2
@@ -0,0 +1,14 @@
+{
+    "Control-agent": {
+{% if failover is vyos_defined %}
+        "http-host": "{{ failover.source_address }}",
+        "http-port": 647,
+        "control-sockets": {
+            "dhcp4": {
+                "socket-type": "unix",
+                "socket-name": "/run/kea/dhcp4-ctrl-socket"
+            }
+        }
+{% endif %}
+    }
+}
diff --git a/data/templates/dhcp-server/kea-dhcp4.conf.j2 b/data/templates/dhcp-server/kea-dhcp4.conf.j2
new file mode 100644
index 000000000..6ab13ab27
--- /dev/null
+++ b/data/templates/dhcp-server/kea-dhcp4.conf.j2
@@ -0,0 +1,72 @@
+{
+    "Dhcp4": {
+        "interfaces-config": {
+            "interfaces": [ "*" ],
+            "dhcp-socket-type": "raw",
+            "service-sockets-max-retries": 5,
+            "service-sockets-retry-wait-time": 5000
+        },
+        "control-socket": {
+            "socket-type": "unix",
+            "socket-name": "/run/kea/dhcp4-ctrl-socket"
+        },
+        "lease-database": {
+            "type": "memfile",
+            "persist": true,
+            "name": "{{ lease_file }}"
+        },
+        "option-def": [
+            {
+                "name": "rfc3442-static-route",
+                "code": 121,
+                "type": "record",
+                "array": true,
+                "record-types": "uint8,uint8,uint8,uint8,uint8,uint8,uint8,uint8"
+            },
+            {
+                "name": "windows-static-route",
+                "code": 249,
+                "type": "record",
+                "array": true,
+                "record-types": "uint8,uint8,uint8,uint8,uint8,uint8,uint8,uint8"
+            },
+            {
+                "name": "wpad-url",
+                "code": 252,
+                "type": "string"
+            },
+            {
+                "name": "unifi-controller",
+                "code": 1,
+                "type": "ipv4-address",
+                "space": "ubnt"
+            }
+        ],
+        "hooks-libraries": [
+{% if failover is vyos_defined %}
+            {
+                "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_ha.so",
+                "parameters": {
+                    "high-availability": [{{ failover | kea_failover_json }}]
+                }
+            },
+{% endif %}
+{% if hostfile_update is vyos_defined %}
+            {
+                "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_run_script.so",
+                "parameters": {
+                    "name": "/usr/libexec/vyos/system/on-dhcp-event.sh",
+                    "sync": false
+                }
+            },
+{% endif %}
+            {
+                "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_lease_cmds.so",
+                "parameters": {}
+            }
+        ],
+{% if shared_network_name is vyos_defined %}
+        "shared-networks": {{ shared_network_name | kea_shared_network_json }}
+{% endif %}
+    }
+}
diff --git a/data/templates/dhcp-server/kea-dhcp6.conf.j2 b/data/templates/dhcp-server/kea-dhcp6.conf.j2
new file mode 100644
index 000000000..3ce4e6370
--- /dev/null
+++ b/data/templates/dhcp-server/kea-dhcp6.conf.j2
@@ -0,0 +1,48 @@
+{
+    "Dhcp6": {
+        "interfaces-config": {
+            "interfaces": [ "*" ],
+            "service-sockets-max-retries": 5,
+            "service-sockets-retry-wait-time": 5000
+        },
+        "control-socket": {
+            "socket-type": "unix",
+            "socket-name": "/run/kea/dhcp6-ctrl-socket"
+        },
+        "lease-database": {
+            "type": "memfile",
+            "persist": true,
+            "name": "{{ lease_file }}"
+        },
+        "hooks-libraries": [
+            {
+                "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_lease_cmds.so",
+                "parameters": {}
+            }
+        ],
+        "option-data": [
+{% if global_parameters.name_server is vyos_defined %}
+            {
+                "name": "dns-servers",
+                "code": 23,
+                "space": "dhcp6",
+                "csv-format": true,
+                "data": "{{ global_parameters.name_server | join(", ") }}"
+            }{{ ',' if preference is vyos_defined else '' }}
+{% endif %}
+{% if preference is vyos_defined %}
+            {
+                "name": "preference",
+                "code": 7,
+                "space": "dhcp6",
+                "csv-format": true,
+                "data": "{{ preference }}"
+            }
+{% endif %}
+        ],
+{% if shared_network_name is vyos_defined %}
+        "shared-networks": {{ shared_network_name | kea6_shared_network_json }}
+{% endif %}
+
+    }
+}
diff --git a/debian/control b/debian/control
index f20268444..816d41944 100644
--- a/debian/control
+++ b/debian/control
@@ -1,329 +1,328 @@
 Source: vyos-1x
 Section: contrib/net
 Priority: extra
 Maintainer: VyOS Package Maintainers <maintainers@vyos.net>
 Build-Depends:
   debhelper (>= 9),
   dh-python,
   fakeroot,
   gcc,
   iproute2,
   libvyosconfig0 (>= 0.0.7),
   libzmq3-dev,
   python3 (>= 3.10),
 # For generating command definitions
   python3-lxml,
   python3-xmltodict,
 # For running tests
   python3-coverage,
   python3-netifaces,
   python3-nose,
   python3-jinja2,
   python3-psutil,
   python3-setuptools,
   python3-sphinx,
   quilt,
   whois
 Standards-Version: 3.9.6
 
 Package: vyos-1x
 Architecture: amd64 arm64
 Pre-Depends:
   libnss-tacplus [amd64],
   libpam-tacplus [amd64],
   libpam-radius-auth [amd64]
 Depends:
 ## Fundamentals
   ${python3:Depends} (>= 3.10),
   libvyosconfig0,
   vyatta-bash,
   vyatta-cfg,
   vyos-http-api-tools,
   vyos-utils,
 ## End of Fundamentals
 ## Python libraries used in multiple modules and scripts
   python3,
   python3-certbot-nginx,
   python3-cryptography,
   python3-hurry.filesize,
   python3-inotify,
   python3-jinja2,
   python3-jmespath,
   python3-netaddr,
   python3-netifaces,
   python3-paramiko,
   python3-passlib,
   python3-psutil,
   python3-pyhumps,
   python3-pystache,
   python3-pyudev,
   python3-six,
   python3-tabulate,
   python3-voluptuous,
   python3-xmltodict,
   python3-zmq,
 ## End of Python libraries
 ## Basic System services and utilities
   sudo,
   systemd,
   bsdmainutils,
   openssl,
   curl,
   dbus,
   file,
   iproute2 (>= 6.0.0),
   linux-cpupower,
 # ipaddrcheck is widely used in IP value validators
   ipaddrcheck,
   ethtool,
   fdisk,
   lm-sensors,
   procps,
   netplug,
   sed,
   ssl-cert,
   tuned,
   beep,
   wide-dhcpv6-client,
 # Generic colorizer
   grc,
 ## End of System services and utilities
 ## For the installer
 # Image signature verification tool
   minisign,
 # Live filesystem tools
   squashfs-tools,
   fuse-overlayfs,
 ## End installer
   auditd,
   iputils-arping,
   isc-dhcp-client,
 # For "vpn pptp", "vpn l2tp", "vpn sstp", "service ipoe-server"
   accel-ppp,
 # End "vpn pptp", "vpn l2tp", "vpn sstp", "service ipoe-server"
   avahi-daemon,
   conntrack,
   conntrackd,
 ## Conf mode features
 # For "interfaces wireless"
   hostapd,
   hsflowd,
   iw,
   wireless-regdb,
   wpasupplicant (>= 0.6.7),
 # End "interfaces wireless"
 # For "interfaces wwan"
   modemmanager,
   usb-modeswitch,
   libqmi-utils,
 # End "interfaces wwan"
 # For "interfaces openvpn"
   openvpn,
   openvpn-auth-ldap,
   openvpn-auth-radius,
   openvpn-otp,
   libpam-google-authenticator,
 # End "interfaces openvpn"
 # For "interfaces wireguard"
   wireguard-tools,
   qrencode,
 # End "interfaces wireguard"
 # For "interfaces pppoe"
   pppoe,
 # End "interfaces pppoe"
 # For "interfaces sstpc"
   sstp-client,
 # End "interfaces sstpc"
 # For "protocols *"
   frr (>= 7.5),
   frr-pythontools,
   frr-rpki-rtrlib,
   frr-snmp,
 # End "protocols *"
 # For "protocols nhrp" (part of DMVPN)
   opennhrp,
 # End "protocols nhrp"
 # For "protocols igmp-proxy"
   igmpproxy,
 # End "protocols igmp-proxy"
 # For "service console-server"
   conserver-client,
   conserver-server,
   console-data,
   dropbear,
 # End "service console-server"
 # For "set service aws glb"
   aws-gwlbtun,
 # For "service dns dynamic"
   ddclient (>= 3.11.1),
 # End "service dns dynamic"
 # # For "service ids"
   fastnetmon [amd64],
 # End "service ids"
 # For "service router-advert"
   radvd,
 # End "service route-advert"
 # For "high-availability reverse-proxy"
   haproxy,
 # End "high-availability reverse-proxy"
 # For "service dhcp-relay"
   isc-dhcp-relay,
 # For "service dhcp-server"
-  isc-dhcp-server,
-  python3-isc-dhcp-leases,
+  kea,
 # End "service dhcp-server"
 # For "service lldp"
   lldpd,
 # End "service lldp"
 # For "service https"
   nginx-light,
 # End "service https"
 # For "service ssh"
   openssh-server,
   sshguard,
 # End "service ssh"
 # For "service salt-minion"
   salt-minion,
 # End "service salt-minion"
 # For "service snmp"
   snmp,
   snmpd,
 # End "service snmp"
 # For "service upnp"
   miniupnpd-nftables,
 # End "service upnp"
 # For "service webproxy"
   squid,
   squidclient,
   squidguard,
 # End "service webproxy"
 # For "service monitoring telegraf"
   telegraf (>= 1.20),
 # End "service monitoring telegraf"
 # For "service monitoring zabbix-agent"
   zabbix-agent2,
 # End "service monitoring zabbix-agent"
 # For "service tftp-server"
   tftpd-hpa,
 # End "service tftp-server"
 # For "service dns forwarding"
   pdns-recursor,
 # End "service dns forwarding"
 # For "service sla owamp"
   owamp-client,
   owamp-server,
 # End "service sla owamp"
 # For "service sla twamp"
   twamp-client,
   twamp-server,
 # End "service sla twamp"
 # For "service broadcast-relay"
   udp-broadcast-relay,
 # End "service broadcast-relay"
 # For "high-availability vrrp"
   keepalived (>=2.0.5),
 # End "high-availability-vrrp"
 # For "system task-scheduler"
   cron,
 # End "system task-scheduler"
 # For "system lcd"
   lcdproc,
   lcdproc-extra-drivers,
 # End "system lcd"
 # For "system config-management commit-archive"
   git,
 # End "system config-management commit-archive"
 # For firewall
   libndp-tools,
   libnetfilter-conntrack3,
   libnfnetlink0,
   nfct,
   nftables (>= 0.9.3),
 # For "vpn ipsec"
   strongswan (>= 5.9),
   strongswan-swanctl (>= 5.9),
   charon-systemd,
   libcharon-extra-plugins (>=5.9),
   libcharon-extauth-plugins (>=5.9),
   libstrongswan-extra-plugins (>=5.9),
   libstrongswan-standard-plugins (>=5.9),
   python3-vici (>= 5.7.2),
 # End "vpn ipsec"
 # For "nat64"
   jool,
 # End "nat64"
 # For nat66
   ndppd,
 # End nat66
 # For "system ntp"
   chrony,
 # End "system ntp"
 # For "vpn openconnect"
   ocserv,
 # End "vpn openconnect"
 # For "set system flow-accounting"
   pmacct (>= 1.6.0),
 # End "set system flow-accounting"
 # For container
   podman,
   netavark,
   aardvark-dns,
 # iptables is only used for containers now, not the the firewall CLI
   iptables,
 # End container
 ## End Configuration mode
 ## Operational mode
 # Used for hypervisor model in "run show version"
   hvinfo,
 # For "run traceroute"
   traceroute,
 # For "run monitor traffic"
   tcpdump,
 # End "run monitor traffic"
 # For "run show hardware storage smart"
   smartmontools,
 # For "run show hardware scsi"
   lsscsi,
 # For "run show hardware pci"
   pciutils,
 # For "show hardware usb"
   usbutils,
 # For "run show hardware storage nvme"
   nvme-cli,
 # For "run monitor bandwidth-test"
   iperf,
   iperf3,
 # End "run monitor bandwidth-test"
 # For "run wake-on-lan"
   etherwake,
 # For "run force ipv6-nd"
   ndisc6,
 # For "run monitor bandwidth"
   bmon,
 # End Operational mode
 ## Optional utilities
   easy-rsa,
   tcptraceroute,
   mtr-tiny,
   telnet,
   stunnel4,
   uidmap
 ## End optional utilities
 Description: VyOS configuration scripts and data
  VyOS configuration scripts, interface definitions, and everything
 
 Package: vyos-1x-vmware
 Architecture: amd64
 Depends:
  vyos-1x,
  open-vm-tools
 Description: VyOS configuration scripts and data for VMware
  Adds configuration files required for VyOS running on VMware hosts.
 
 Package: vyos-1x-smoketest
 Architecture: all
 Depends:
  skopeo,
  snmp,
  vyos-1x
 Description: VyOS build sanity checking toolkit
diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in
index 583de7ba9..081f7ed42 100644
--- a/interface-definitions/dhcp-server.xml.in
+++ b/interface-definitions/dhcp-server.xml.in
@@ -1,490 +1,466 @@
 <?xml version="1.0"?>
 <!-- DHCP server configuration -->
 <interfaceDefinition>
   <node name="service">
     <children>
       <node name="dhcp-server" owner="${vyos_conf_scripts_dir}/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>
                     <regex>[-_a-zA-Z0-9.]+</regex>
                   </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="global-parameters">
-            <properties>
-              <help>Additional global parameters for DHCP server. You must use the syntax of dhcpd.conf in this text-field. Using this without proper knowledge may result in a crashed DHCP server. Check system log to look for errors.</help>
-              <multi/>
-            </properties>
-          </leafNode>
           <leafNode name="hostfile-update">
             <properties>
               <help>Updating /etc/hosts file (per client lease)</help>
               <valueless/>
             </properties>
           </leafNode>
-          <leafNode name="host-decl-name">
-            <properties>
-              <help>Use host declaration name for forward DNS name</help>
-              <valueless/>
-            </properties>
-          </leafNode>
           #include <include/listen-address-ipv4.xml.i>
           <tagNode name="shared-network-name">
             <properties>
               <help>Name of DHCP shared network</help>
               <constraint>
                 <regex>[-_a-zA-Z0-9.]+</regex>
               </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/domain-name.xml.i>
               #include <include/dhcp/domain-search.xml.i>
               #include <include/dhcp/ntp-server.xml.i>
-              #include <include/dhcp/ping-check.xml.i>
               #include <include/generic-description.xml.i>
               #include <include/generic-disable-node.xml.i>
               #include <include/name-server-ipv4.xml.i>
-              <leafNode name="shared-network-parameters">
-                <properties>
-                  <help>Additional shared-network parameters for DHCP server. You must use the syntax of dhcpd.conf in this text-field. Using this without proper knowledge may result in a crashed DHCP server. Check system log to look for errors.</help>
-                  <multi/>
-                </properties>
-              </leafNode>
               <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>
                   <leafNode name="bootfile-name">
                     <properties>
                       <help>Bootstrap file name</help>
                       <constraint>
                         <regex>[[:ascii:]]{1,253}</regex>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="bootfile-server">
                     <properties>
                       <help>Server from which the initial boot file is to be loaded</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>Bootfile server IPv4 address</description>
                       </valueHelp>
                       <valueHelp>
                         <format>hostname</format>
                         <description>Bootfile server FQDN</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                         <validator name="fqdn"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="bootfile-size">
                     <properties>
                       <help>Bootstrap file size</help>
                       <valueHelp>
                         <format>u32:1-16</format>
                         <description>Bootstrap file size in 512 byte blocks</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-16"/>
                       </constraint>
                     </properties>
                   </leafNode>
+                  #include <include/dhcp/captive-portal.xml.i>
                   <leafNode name="client-prefix-length">
                     <properties>
                       <help>Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used.</help>
                       <valueHelp>
                         <format>u32:0-32</format>
                         <description>DHCP client prefix length must be 0 to 32</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 0-32"/>
                       </constraint>
                       <constraintErrorMessage>DHCP client prefix length must be 0 to 32</constraintErrorMessage>
                     </properties>
                   </leafNode>
                   <leafNode name="default-router">
                     <properties>
                       <help>IP address of default router</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>Default router IPv4 address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   #include <include/dhcp/domain-name.xml.i>
                   #include <include/dhcp/domain-search.xml.i>
                   #include <include/generic-description.xml.i>
                   #include <include/name-server-ipv4.xml.i>
-                  <leafNode name="enable-failover">
-                    <properties>
-                      <help>Enable DHCP failover support for this subnet</help>
-                      <valueless/>
-                    </properties>
-                  </leafNode>
                   <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="ip-forwarding">
                     <properties>
                       <help>Enable IP forwarding on client</help>
                       <valueless/>
                     </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>
                   #include <include/dhcp/ntp-server.xml.i>
-                  #include <include/dhcp/ping-check.xml.i>
                   <leafNode name="pop-server">
                     <properties>
                       <help>IP address of POP3 server</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>POP3 server IPv4 address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <leafNode name="server-identifier">
                     <properties>
                       <help>Address for DHCP server identifier</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>DHCP server identifier IPv4 address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="smtp-server">
                     <properties>
                       <help>IP address of SMTP server</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>SMTP server IPv4 address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <tagNode name="range">
                     <properties>
                       <help>DHCP lease range</help>
                       <constraint>
                         <regex>[-_a-zA-Z0-9.]+</regex>
                       </constraint>
                       <constraintErrorMessage>Invalid range name, may only be alphanumeric, dot and hyphen</constraintErrorMessage>
                     </properties>
                     <children>
                       <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>Name of static mapping</help>
                       <constraint>
                         <regex>[-_a-zA-Z0-9.]+</regex>
                       </constraint>
                       <constraintErrorMessage>Invalid static mapping name, may only be alphanumeric, dot and hyphen</constraintErrorMessage>
                     </properties>
                     <children>
                       #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>
                       <leafNode name="mac-address">
                         <properties>
                           <help>Media Access Control (MAC) address</help>
                           <valueHelp>
                             <format>macaddr</format>
                             <description>Hardware (MAC) address</description>
                           </valueHelp>
                           <constraint>
                             <validator name="mac-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
-                      <leafNode name="static-mapping-parameters">
-                        <properties>
-                          <help>Additional static-mapping parameters for DHCP server. Will be placed inside the "host" block of the mapping. You must use the syntax of dhcpd.conf in this text-field. Using this without proper knowledge may result in a crashed DHCP server. Check system log to look for errors.</help>
-                          <multi/>
-                        </properties>
-                      </leafNode>
                     </children>
                   </tagNode>
                   <tagNode name="static-route">
                     <properties>
                       <help>Classless static route destination subnet</help>
                       <valueHelp>
                         <format>ipv4net</format>
                         <description>IPv4 address and prefix length</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-prefix"/>
                       </constraint>
                     </properties>
                     <children>
                       <leafNode name="next-hop">
                         <properties>
                           <help>IP address of router to be used to reach the destination subnet</help>
                           <valueHelp>
                             <format>ipv4</format>
                             <description>IPv4 address of router</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ip-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
                     </children>
                   </tagNode >
                   <leafNode name="ipv6-only-preferred">
                     <properties>
                       <help>Disable IPv4 on IPv6 only hosts (RFC 8925)</help>
                       <valueHelp>
                         <format>u32</format>
                         <description>Seconds</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 0-4294967295"/>
                       </constraint>
                       <constraintErrorMessage>Seconds must be between 0 and 4294967295 (49 days)</constraintErrorMessage>
                     </properties>
                   </leafNode>
-                  <leafNode name="subnet-parameters">
-                    <properties>
-                      <help>Additional subnet parameters for DHCP server. You must use the syntax of dhcpd.conf in this text-field. Using this without proper knowledge may result in a crashed DHCP server. Check system log to look for errors.</help>
-                      <multi/>
-                    </properties>
-                  </leafNode>
                   <leafNode name="tftp-server-name">
                     <properties>
                       <help>TFTP server name</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>TFTP server IPv4 address</description>
                       </valueHelp>
                       <valueHelp>
                         <format>hostname</format>
                         <description>TFTP server FQDN</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                         <validator name="fqdn"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="time-offset">
                     <properties>
                       <help>Client subnet offset in seconds from Coordinated Universal Time (UTC)</help>
                       <valueHelp>
                         <format>[-]N</format>
                         <description>Time offset (number, may be negative)</description>
                       </valueHelp>
                       <constraint>
                         <regex>-?[0-9]+</regex>
                       </constraint>
                       <constraintErrorMessage>Invalid time offset value</constraintErrorMessage>
                     </properties>
                   </leafNode>
                   <leafNode name="time-server">
                     <properties>
                       <help>IP address of time server</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>Time server IPv4 address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
+                  <leafNode name="time-zone">
+                    <properties>
+                      <help>Time zone to send to clients. Uses RFC4833 options 100 and 101</help>
+                      <completionHelp>
+                        <script>timedatectl list-timezones</script>
+                      </completionHelp>
+                      <constraint>
+                        <validator name="timezone" argument="--validate"/>
+                      </constraint>
+                    </properties>
+                  </leafNode>
                   <node name="vendor-option">
                     <properties>
                       <help>Vendor Specific Options</help>
                     </properties>
                     <children>
                       <node name="ubiquiti">
                         <properties>
                           <help>Ubiquiti specific parameters</help>
                         </properties>
                         <children>
                           <leafNode name="unifi-controller">
                             <properties>
                               <help>Address of UniFi controller</help>
                               <valueHelp>
                                 <format>ipv4</format>
                                 <description>IP address of UniFi controller</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="ipv4-address"/>
                               </constraint>
                             </properties>
                           </leafNode>
                         </children>
                       </node>
                     </children>
                   </node>
                   <leafNode name="wins-server">
                     <properties>
                       <help>IP address for Windows Internet Name Service (WINS) server</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>WINS server IPv4 address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <leafNode name="wpad-url">
                     <properties>
                       <help>Web Proxy Autodiscovery (WPAD) URL</help>
                     </properties>
                   </leafNode>
                 </children>
               </tagNode>
             </children>
           </tagNode>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/interface-definitions/dhcpv6-server.xml.in b/interface-definitions/dhcpv6-server.xml.in
index 9dff68a24..b37f79434 100644
--- a/interface-definitions/dhcpv6-server.xml.in
+++ b/interface-definitions/dhcpv6-server.xml.in
@@ -1,376 +1,386 @@
 <?xml version="1.0"?>
 <interfaceDefinition>
   <node name="service">
     <children>
       <node name="dhcpv6-server" owner="${vyos_conf_scripts_dir}/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>
                 <regex>[-_a-zA-Z0-9.]+</regex>
               </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>
-                      <tagNode name="prefix">
+                      <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>
-                        <children>
-                          <leafNode name="temporary">
-                            <properties>
-                              <help>Address range will be used for temporary addresses</help>
-                              <valueless/>
-                            </properties>
-                          </leafNode>
-                        </children>
-                      </tagNode>
+                      </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>
                         <regex>[-_a-zA-Z0-9.]+</regex>
                       </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>
                         <regex>[-_a-zA-Z0-9.]+</regex>
                       </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="start">
+                      <tagNode name="prefix">
                         <properties>
-                          <help>First in range of IPv6 addresses to be used in prefix delegation</help>
+                          <help>IPv6 prefix to be used in prefix delegation</help>
                           <valueHelp>
                             <format>ipv6</format>
-                            <description>IPv6 address used in prefix delegation</description>
+                            <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 prefixes to be delegated</help>
+                              <help>Length in bits of prefix</help>
                               <valueHelp>
                                 <format>u32:32-64</format>
-                                <description>Delagated prefix length (32-64)</description>
+                                <description>Prefix length (32-64)</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="numeric" argument="--range 32-64"/>
                               </constraint>
-                              <constraintErrorMessage>Delegated prefix length must be between 32 and 64</constraintErrorMessage>
+                              <constraintErrorMessage>Prefix length must be between 32 and 64</constraintErrorMessage>
                             </properties>
                           </leafNode>
-                          <leafNode name="stop">
+                          <leafNode name="delegated-length">
                             <properties>
-                              <help>Last in range of IPv6 addresses to be used in prefix delegation</help>
+                              <help>Length in bits of prefixes to be delegated</help>
                               <valueHelp>
-                                <format>ipv6</format>
-                                <description>IPv6 address used in prefix delegation</description>
+                                <format>u32:32-64</format>
+                                <description>Delegated prefix length (32-64)</description>
                               </valueHelp>
                               <constraint>
-                                <validator name="ipv6-address"/>
+                                <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>Name of static mapping</help>
                       <constraint>
                         <regex>[-_a-zA-Z0-9.]+</regex>
                       </constraint>
                       <constraintErrorMessage>Invalid static mapping name. May only contain letters, numbers and .-_</constraintErrorMessage>
                     </properties>
                     <children>
                       #include <include/generic-disable-node.xml.i>
                       <leafNode name="identifier">
                         <properties>
                           <help>Client identifier (DUID) for this static mapping</help>
                           <valueHelp>
                             <format>h[[:h]...]</format>
                             <description>DUID: colon-separated hex list (as used by isc-dhcp option dhcpv6.client-id)</description>
                           </valueHelp>
                           <constraint>
                             <regex>([0-9A-Fa-f]{1,2}[:])*([0-9A-Fa-f]{1,2})</regex>
                           </constraint>
                           <constraintErrorMessage>Invalid DUID, must be in the format h[[:h]...]</constraintErrorMessage>
                         </properties>
                       </leafNode>
                       <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>
                   <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/interface-definitions/include/dhcp/captive-portal.xml.i b/interface-definitions/include/dhcp/captive-portal.xml.i
new file mode 100644
index 000000000..643f055a8
--- /dev/null
+++ b/interface-definitions/include/dhcp/captive-portal.xml.i
@@ -0,0 +1,11 @@
+<!-- include start from dhcp/captive-portal.xml.i -->
+<leafNode name="captive-portal">
+  <properties>
+    <help>Captive portal API endpoint</help>
+    <valueHelp>
+      <format>txt</format>
+      <description>Captive portal API endpoint</description>
+    </valueHelp>
+  </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/dhcp/ping-check.xml.i b/interface-definitions/include/dhcp/ping-check.xml.i
deleted file mode 100644
index a506f68e4..000000000
--- a/interface-definitions/include/dhcp/ping-check.xml.i
+++ /dev/null
@@ -1,8 +0,0 @@
-<!-- include start from dhcp/ping-check.xml.i -->
-<leafNode name="ping-check">
-  <properties>
-    <help>Sends ICMP Echo request to the address being assigned</help>
-    <valueless/>
-  </properties>
-</leafNode>
-<!-- include end -->
diff --git a/interface-definitions/include/version/dhcp-server-version.xml.i b/interface-definitions/include/version/dhcp-server-version.xml.i
index 330cb7d1b..7c4b5633e 100644
--- a/interface-definitions/include/version/dhcp-server-version.xml.i
+++ b/interface-definitions/include/version/dhcp-server-version.xml.i
@@ -1,3 +1,3 @@
 <!-- include start from include/version/dhcp-server-version.xml.i -->
-<syntaxVersion component='dhcp-server' version='6'></syntaxVersion>
+<syntaxVersion component='dhcp-server' version='7'></syntaxVersion>
 <!-- include end -->
diff --git a/interface-definitions/include/version/dhcpv6-server-version.xml.i b/interface-definitions/include/version/dhcpv6-server-version.xml.i
index 4b2cf40aa..ae4178c90 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='1'></syntaxVersion>
+<syntaxVersion component='dhcpv6-server' version='2'></syntaxVersion>
 <!-- include end -->
diff --git a/op-mode-definitions/dhcp.xml.in b/op-mode-definitions/dhcp.xml.in
index 9c2e2be76..ceb321f3e 100644
--- a/op-mode-definitions/dhcp.xml.in
+++ b/op-mode-definitions/dhcp.xml.in
@@ -1,236 +1,236 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interfaceDefinition>
   <node name="show">
     <children>
       <node name="dhcp">
         <properties>
           <help>Show DHCP (Dynamic Host Configuration Protocol) information</help>
         </properties>
         <children>
           <node name="client">
             <properties>
               <help>Show DHCP client information</help>
             </properties>
             <children>
               <node name="leases">
                 <properties>
                   <help>Show DHCP client leases</help>
                 </properties>
                 <children>
                   <tagNode name="interface">
                     <properties>
                       <help> Show DHCP client information for a given interface</help>
                       <completionHelp>
                         <script>${vyos_completion_dir}/list_interfaces --broadcast</script>
                       </completionHelp>
                     </properties>
                     <command>${vyos_op_scripts_dir}/dhcp.py show_client_leases --family inet --interface $6</command>
                   </tagNode>
                 </children>
                 <command>${vyos_op_scripts_dir}/dhcp.py show_client_leases --family inet</command>
               </node>
              </children>
           </node>
           <node name="server">
             <properties>
               <help>Show DHCP server information</help>
             </properties>
             <children>
               <node name="leases">
                 <properties>
                   <help>Show DHCP server leases</help>
                 </properties>
                 <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet</command>
                 <children>
                   <tagNode name="origin">
                     <properties>
                       <help>Show DHCP server leases granted by local or remote DHCP server</help>
                       <completionHelp>
                         <list>local remote</list>
                       </completionHelp>
                     </properties>
                     <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet --origin $6</command>
                   </tagNode>
                   <tagNode name="pool">
                     <properties>
                       <help>Show DHCP server leases for a specific pool</help>
                       <completionHelp>
                         <path>service dhcp-server shared-network-name</path>
                       </completionHelp>
                     </properties>
                     <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet --pool $6</command>
                   </tagNode>
                   <tagNode name="sort">
                     <properties>
                       <help>Show DHCP server leases sorted by the specified key</help>
                       <completionHelp>
                         <list>end hostname ip mac pool remaining start state</list>
                       </completionHelp>
                     </properties>
                     <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet --sort $6</command>
                   </tagNode>
                   <tagNode name="state">
                     <properties>
                       <help>Show DHCP server leases with a specific state (can be multiple, comma-separated)</help>
                       <completionHelp>
                         <list>abandoned active all backup expired free released reset</list>
                       </completionHelp>
                     </properties>
                     <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet --state $6</command>
                   </tagNode>
                 </children>
               </node>
               <node name="statistics">
                 <properties>
                   <help>Show DHCP server statistics</help>
                 </properties>
                 <command>${vyos_op_scripts_dir}/dhcp.py show_pool_statistics --family inet</command>
                 <children>
                   <tagNode name="pool">
                     <properties>
                       <help>Show DHCP server statistics for a specific pool</help>
                       <completionHelp>
                         <path>service dhcp-server shared-network-name</path>
                       </completionHelp>
                     </properties>
                     <command>${vyos_op_scripts_dir}/dhcp.py show_pool_statistics --family inet --pool $6</command>
                   </tagNode>
                 </children>
               </node>
             </children>
           </node>
         </children>
       </node>
       <node name="dhcpv6">
         <properties>
           <help>Show DHCPv6 (IPv6 Dynamic Host Configuration Protocol) information</help>
         </properties>
         <children>
           <node name="server">
             <properties>
               <help>Show DHCPv6 server information</help>
             </properties>
             <children>
               <node name="leases">
                 <properties>
                   <help>Show DHCPv6 server leases</help>
                 </properties>
                 <command>sudo ${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet6</command>
                 <children>
                   <tagNode name="pool">
                     <properties>
                       <help>Show DHCPv6 server leases for a specific pool</help>
                       <completionHelp>
                         <path>service dhcpv6-server shared-network-name</path>
                       </completionHelp>
                     </properties>
                     <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet6 --pool $6</command>
                   </tagNode>
                   <tagNode name="sort">
                     <properties>
                       <help>Show DHCPv6 server leases sorted by the specified key</help>
                       <completionHelp>
                         <list>end iaid_duid ip last_communication pool remaining state type</list>
                       </completionHelp>
                     </properties>
                     <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet6 --sort $6</command>
                   </tagNode>
                   <tagNode name="state">
                     <properties>
                       <help>Show DHCPv6 server leases with a specific state (can be multiple, comma-separated)</help>
                       <completionHelp>
                         <list>abandoned active all backup expired free released reset</list>
                       </completionHelp>
                     </properties>
                     <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet6 --state $6</command>
                   </tagNode>
                 </children>
               </node>
             </children>
           </node>
         </children>
       </node>
     </children>
   </node>
   <node name="restart">
     <children>
       <node name="dhcp">
         <properties>
           <help>Restart DHCP processes</help>
         </properties>
         <children>
           <node name="server">
             <properties>
               <help>Restart DHCP server</help>
             </properties>
-            <command>if cli-shell-api existsActive service dhcp-server; then sudo systemctl restart isc-dhcp-server.service; else echo "DHCP server not configured"; fi</command>
+            <command>if cli-shell-api existsActive service dhcp-server; then sudo systemctl restart kea-dhcp4-server.service; else echo "DHCP server not configured"; fi</command>
           </node>
           <node name="relay-agent">
             <properties>
               <help>Restart DHCP relay-agent</help>
             </properties>
             <command>sudo ${vyos_op_scripts_dir}/restart_dhcp_relay.py --ipv4</command>
           </node>
         </children>
       </node>
       <node name="dhcpv6">
         <properties>
           <help>Restart DHCPv6 processes</help>
         </properties>
         <children>
           <node name="server">
             <properties>
               <help>Restart DHCPv6 server</help>
             </properties>
-            <command>if cli-shell-api existsActive service dhcpv6-server; then sudo systemctl restart isc-dhcp-server6.service; else echo "DHCPv6 server not configured"; fi</command>
+            <command>if cli-shell-api existsActive service dhcpv6-server; then sudo systemctl restart kea-dhcp6-server.service; else echo "DHCPv6 server not configured"; fi</command>
           </node>
           <node name="relay-agent">
             <properties>
               <help>Restart DHCPv6 relay-agent</help>
             </properties>
             <command>sudo ${vyos_op_scripts_dir}/restart_dhcp_relay.py --ipv6</command>
           </node>
         </children>
       </node>
     </children>
   </node>
   <node name="renew">
     <properties>
       <help>Renew specified variable</help>
     </properties>
     <children>
       <node name="dhcp">
         <properties>
           <help>Renew DHCP client lease</help>
         </properties>
         <children>
           <tagNode name="interface">
             <properties>
               <help>Renew DHCP client lease for specified interface</help>
               <completionHelp>
                 <script>${vyos_completion_dir}/list_interfaces</script>
               </completionHelp>
             </properties>
             <command>sudo systemctl restart "dhclient@$4.service"</command>
           </tagNode>
         </children>
       </node>
       <node name="dhcpv6">
         <properties>
           <help>Renew DHCPv6 client lease</help>
         </properties>
         <children>
           <tagNode name="interface">
             <properties>
               <help>Renew DHCPv6 client lease for specified interface</help>
               <completionHelp>
                 <script>${vyos_completion_dir}/list_interfaces</script>
               </completionHelp>
             </properties>
             <command>sudo systemctl restart "dhcp6c@$4.service"</command>
           </tagNode>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in
index 44628a112..3a8118dcb 100644
--- a/op-mode-definitions/monitor-log.xml.in
+++ b/op-mode-definitions/monitor-log.xml.in
@@ -1,354 +1,354 @@
 <?xml version="1.0"?>
 <interfaceDefinition>
   <node name="monitor">
     <properties>
       <help>Monitor system information</help>
     </properties>
     <children>
       <node name="log">
         <properties>
           <help>Monitor last lines of messages file</help>
         </properties>
         <command>SYSTEMD_LOG_COLOR=false journalctl --no-hostname --follow --boot</command>
         <children>
           <node name="color">
             <properties>
               <help>Output log in a colored fashion</help>
             </properties>
             <command>SYSTEMD_LOG_COLOR=false grc journalctl --no-hostname --follow --boot</command>
           </node>
           <node name="ids">
             <properties>
               <help>Monitor Intrusion Detection System log</help>
             </properties>
             <children>
               <leafNode name="ddos-protection">
                 <properties>
                   <help>Monitor last lines of DDOS protection</help>
                 </properties>
                 <command>journalctl --no-hostname --follow --boot --unit fastnetmon.service</command>
               </leafNode>
             </children>
           </node>
           <leafNode name="conntrack-sync">
             <properties>
               <help>Monitor last lines of conntrack-sync log</help>
             </properties>
             <command>journalctl --no-hostname --follow --boot --unit conntrackd.service</command>
           </leafNode>
           <leafNode name="console-server">
             <properties>
               <help>Monitor last lines of console server log</help>
             </properties>
             <command>journalctl --no-hostname --follow --boot --unit conserver-server.service</command>
           </leafNode>
           <node name="dhcp">
             <properties>
               <help>Monitor last lines of Dynamic Host Control Protocol log</help>
             </properties>
             <children>
               <node name="server">
                 <properties>
                   <help>Monitor last lines of DHCP server log</help>
                 </properties>
-                <command>journalctl --no-hostname --follow --boot --unit isc-dhcp-server.service</command>
+                <command>journalctl --no-hostname --follow --boot --unit kea-dhcp4-server.service</command>
               </node>
               <node name="client">
                 <properties>
                   <help>Monitor last lines of DHCP client log</help>
                 </properties>
                 <command>journalctl --no-hostname --follow --boot --unit "dhclient@*.service"</command>
                 <children>
                   <tagNode name="interface">
                     <properties>
                       <help>Show DHCP client log on specific interface</help>
                       <completionHelp>
                         <script>${vyos_completion_dir}/list_interfaces --broadcast</script>
                       </completionHelp>
                     </properties>
                     <command>journalctl --no-hostname --follow --boot --unit "dhclient@$6.service"</command>
                   </tagNode>
                 </children>
               </node>
             </children>
           </node>
           <node name="dhcpv6">
             <properties>
               <help>Monitor last lines of Dynamic Host Control Protocol IPv6 log</help>
             </properties>
             <children>
               <node name="server">
                 <properties>
                   <help>Monitor last lines of DHCPv6 server log</help>
                 </properties>
-                <command>journalctl --no-hostname --follow --boot --unit isc-dhcp-server6.service</command>
+                <command>journalctl --no-hostname --follow --boot --unit kea-dhcp6-server.service</command>
               </node>
               <node name="client">
                 <properties>
                   <help>Monitor last lines of DHCPv6 client log</help>
                 </properties>
                 <command>journalctl --no-hostname --follow --boot --unit "dhcp6c@*.service"</command>
                 <children>
                   <tagNode name="interface">
                     <properties>
                       <help>Show DHCPv6 client log on specific interface</help>
                       <completionHelp>
                         <script>${vyos_completion_dir}/list_interfaces</script>
                       </completionHelp>
                     </properties>
                     <command>journalctl --no-hostname --follow --boot --unit "dhcp6c@$6.service"</command>
                   </tagNode>
                 </children>
               </node>
             </children>
           </node>
           <leafNode name="flow-accounting">
             <properties>
               <help>Monitor last lines of flow-accounting log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit uacctd.service</command>
           </leafNode>
           <leafNode name="ipoe-server">
             <properties>
               <help>Monitor last lines of IP over Ethernet server log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit accel-ppp@ipoe.service</command>
           </leafNode>
           <leafNode name="kernel">
             <properties>
               <help>Monitor last lines of Linux Kernel log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --dmesg</command>
           </leafNode>
           <leafNode name="nhrp">
             <properties>
               <help>Monitor last lines of Next Hop Resolution Protocol log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit opennhrp.service</command>
           </leafNode>
           <leafNode name="ntp">
             <properties>
               <help>Monitor last lines of Network Time Protocol log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit chrony.service</command>
           </leafNode>
           <node name="openvpn">
             <properties>
               <help>Monitor last lines of OpenVPN log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit openvpn@*.service</command>
             <children>
               <tagNode name="interface">
                 <properties>
                   <help>Monitor last lines of specific OpenVPN interface log</help>
                   <completionHelp>
                     <path>interfaces openvpn</path>
                   </completionHelp>
                 </properties>
                 <command>journalctl --no-hostname --boot --unit openvpn@$5.service</command>
               </tagNode>
             </children>
           </node>
           <node name="pppoe">
             <properties>
               <help>Monitor last lines of PPPoE interface log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit "ppp@pppoe*.service"</command>
             <children>
               <tagNode name="interface">
                 <properties>
                   <help>Monitor last lines of PPPoE log for specific interface</help>
                   <completionHelp>
                     <path>interfaces pppoe</path>
                   </completionHelp>
                 </properties>
                 <command>journalctl --no-hostname --boot --follow --unit "ppp@$5.service"</command>
               </tagNode>
             </children>
           </node>
           <leafNode name="pppoe-server">
             <properties>
               <help>Monitor last lines of PPPoE server log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit accel-ppp@pppoe.service</command>
           </leafNode>
           <node name="protocol">
             <properties>
               <help>Monitor routing protocol logs</help>
             </properties>
             <children>
               <leafNode name="ospf">
                 <properties>
                   <help>Monitor log for OSPF</help>
                 </properties>
                 <command>journalctl --follow --no-hostname --boot /usr/lib/frr/ospfd</command>
               </leafNode>
               <leafNode name="ospfv3">
                 <properties>
                   <help>Monitor log for OSPF for IPv6</help>
                 </properties>
                 <command>journalctl --follow --no-hostname --boot /usr/lib/frr/ospf6d</command>
               </leafNode>
               <leafNode name="bgp">
                 <properties>
                   <help>Monitor log for BGP</help>
                 </properties>
                 <command>journalctl --follow --no-hostname --boot /usr/lib/frr/bgpd</command>
               </leafNode>
               <leafNode name="rip">
                 <properties>
                   <help>Monitor log for RIP</help>
                 </properties>
                 <command>journalctl --follow --no-hostname --boot /usr/lib/frr/ripd</command>
               </leafNode>
               <leafNode name="ripng">
                 <properties>
                   <help>Monitor log for RIPng</help>
                 </properties>
                 <command>journalctl --follow --no-hostname --boot /usr/lib/frr/ripngd</command>
               </leafNode>
               <leafNode name="static">
                 <properties>
                   <help>Monitor log for static route</help>
                 </properties>
                 <command>journalctl --follow --no-hostname --boot /usr/lib/frr/staticd</command>
               </leafNode>
               <leafNode name="multicast">
                 <properties>
                   <help>Monitor log for Multicast protocol</help>
                 </properties>
                 <command>journalctl --follow --no-hostname --boot /usr/lib/frr/pimd</command>
               </leafNode>
               <leafNode name="isis">
                 <properties>
                   <help>Monitor log for ISIS</help>
                 </properties>
                 <command>journalctl --follow --no-hostname --boot /usr/lib/frr/isisd</command>
               </leafNode>
               <leafNode name="nhrp">
                 <properties>
                   <help>Monitor log for NHRP</help>
                 </properties>
                 <command>journalctl --follow --no-hostname --boot /usr/lib/frr/nhrpd</command>
               </leafNode>
               <leafNode name="bfd">
                 <properties>
                   <help>Monitor log for BFD</help>
                 </properties>
                 <command>journalctl --follow --no-hostname --boot /usr/lib/frr/bfdd</command>
               </leafNode>
               <leafNode name="mpls">
                 <properties>
                   <help>Monitor log for MPLS</help>
                 </properties>
                 <command>journalctl --follow --no-hostname --boot /usr/lib/frr/ldpd</command>
               </leafNode>
             </children>
           </node>
           <node name="macsec">
             <properties>
               <help>Monitor last lines of MACsec</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit "wpa_supplicant-macsec@*.service"</command>
             <children>
               <tagNode name="interface">
                 <properties>
                   <help>Monitor last lines of specific MACsec interface</help>
                   <completionHelp>
                     <path>interfaces macsec</path>
                   </completionHelp>
                 </properties>
                 <command>SRC=$(cli-shell-api returnValue interfaces macsec "$5" source-interface); journalctl --no-hostname --boot --follow --unit "wpa_supplicant-macsec@$SRC.service"</command>
               </tagNode>
             </children>
           </node>
           <leafNode name="router-advert">
             <properties>
               <help>Monitor last lines of Router Advertisement Daemon log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit radvd.service</command>
           </leafNode>
           <leafNode name="snmp">
             <properties>
               <help>Monitor last lines of Simple Network Monitoring Protocol log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit snmpd.service</command>
           </leafNode>
           <node name="ssh">
             <properties>
               <help>Monitor last lines of Secure Shell log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit ssh.service</command>
             <children>
               <node name="dynamic-protection">
                 <properties>
                   <help>Monitor last lines of SSH guard log</help>
                 </properties>
                 <command>journalctl --no-hostname --boot --follow --unit sshguard.service</command>
               </node>
             </children>
           </node>
           <leafNode name="vpn">
             <properties>
               <help>Monitor last lines of ALL Virtual Private Network services</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit strongswan.service --unit accel-ppp@*.service --unit ocserv.service</command>
           </leafNode>
           <leafNode name="ipsec">
             <properties>
               <help>Monitor last lines of IPsec log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit strongswan.service</command>
           </leafNode>
           <leafNode name="l2tp">
             <properties>
               <help>Monitor last lines of L2TP log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit accel-ppp@l2tp.service</command>
           </leafNode>
           <leafNode name="openconnect">
             <properties>
               <help>Monitor last lines of OpenConnect log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit ocserv.service</command>
           </leafNode>
           <leafNode name="pptp">
             <properties>
               <help>Monitor last lines of PPTP log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit accel-ppp@pptp.service</command>
           </leafNode>
           <leafNode name="sstp">
             <properties>
               <help>Monitor last lines of Secure Socket Tunneling Protocol server</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit accel-ppp@sstp.service</command>
           </leafNode>
           <node name="sstpc">
             <properties>
               <help>Monitor last lines of Secure Socket Tunneling Protocol client</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit "ppp@sstpc*.service"</command>
             <children>
               <tagNode name="interface">
                 <properties>
                   <help>Monitor last lines of SSTP client log for specific interface</help>
                   <completionHelp>
                     <path>interfaces sstpc</path>
                   </completionHelp>
                 </properties>
                 <command>journalctl --no-hostname --boot --follow --unit "ppp@$5.service"</command>
               </tagNode>
             </children>
           </node>
           <leafNode name="vrrp">
             <properties>
               <help>Monitor last lines of Virtual Router Redundancy Protocol log</help>
             </properties>
             <command>journalctl --no-hostname --boot --follow --unit keepalived.service</command>
           </leafNode>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in
index 3a622cfb5..399c6acf8 100644
--- a/op-mode-definitions/show-log.xml.in
+++ b/op-mode-definitions/show-log.xml.in
@@ -1,719 +1,719 @@
 <?xml version="1.0"?>
 <interfaceDefinition>
   <node name="show">
     <properties>
       <help>Show system information</help>
     </properties>
     <children>
       <tagNode name="log">
         <properties>
           <help>Show last number of messages in master logging buffer</help>
           <completionHelp>
             <list>&lt;1-9999&gt;</list>
           </completionHelp>
         </properties>
         <command>if ${vyos_validators_dir}/numeric --range 1-9999 "$3"; then journalctl --no-hostname --boot --lines "$3"; fi</command>
       </tagNode>
       <node name="log">
         <properties>
           <help>Show contents of current master logging buffer</help>
         </properties>
         <command>journalctl --no-hostname --boot</command>
         <children>
           <leafNode name="audit">
             <properties>
               <help>Show audit logs</help>
             </properties>
             <command>cat /var/log/audit/audit.log</command>
           </leafNode>
           <leafNode name="all">
             <properties>
               <help>Show contents of all master log files</help>
             </properties>
             <command>sudo bash -c 'eval $(lesspipe); less $_vyatta_less_options --prompt=".logm, file %i of %m., page %dt of %D" -- `printf "%s\n" /var/log/messages* | sort -nr`'</command>
           </leafNode>
           <leafNode name="authorization">
             <properties>
               <help>Show listing of authorization attempts</help>
             </properties>
             <command>journalctl --no-hostname --boot --quiet SYSLOG_FACILITY=10 SYSLOG_FACILITY=4</command>
           </leafNode>
           <leafNode name="cluster">
             <properties>
               <help>Show log for Cluster</help>
             </properties>
             <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e heartbeat -e cl_status -e mach_down -e ha_log</command>
           </leafNode>
           <leafNode name="conntrack-sync">
             <properties>
               <help>Show log for Conntrack-sync</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit conntrackd.service</command>
           </leafNode>
           <leafNode name="console-server">
             <properties>
               <help>Show log for console server</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit conserver-server.service</command>
           </leafNode>
           <node name="ids">
             <properties>
               <help>Show log for for Intrusion Detection System</help>
             </properties>
             <children>
               <leafNode name="ddos-protection">
                 <properties>
                   <help>Show log for DDOS protection</help>
                 </properties>
                 <command>journalctl --no-hostname --boot --unit fastnetmon.service</command>
               </leafNode>
             </children>
           </node>
           <node name="dhcp">
             <properties>
               <help>Show log for Dynamic Host Control Protocol (DHCP)</help>
             </properties>
             <children>
               <node name="server">
                 <properties>
                   <help>Show log for DHCP server</help>
                 </properties>
-                <command>journalctl --no-hostname --boot --unit isc-dhcp-server.service</command>
+                <command>journalctl --no-hostname --boot --unit kea-dhcp4-server.service</command>
               </node>
               <node name="client">
                 <properties>
                   <help>Show DHCP client logs</help>
                 </properties>
                 <command>journalctl --no-hostname --boot --unit "dhclient@*.service"</command>
                 <children>
                   <tagNode name="interface">
                     <properties>
                       <help>Show DHCP client log on specific interface</help>
                       <completionHelp>
                         <script>${vyos_completion_dir}/list_interfaces --broadcast</script>
                       </completionHelp>
                     </properties>
                     <command>journalctl --no-hostname --boot --unit "dhclient@$6.service"</command>
                   </tagNode>
                 </children>
               </node>
             </children>
           </node>
           <node name="dhcpv6">
             <properties>
               <help>Show log for Dynamic Host Control Protocol IPv6 (DHCPv6)</help>
             </properties>
             <children>
               <node name="server">
                 <properties>
                   <help>Show log for DHCPv6 server</help>
                 </properties>
-                <command>journalctl --no-hostname --boot --unit isc-dhcp-server6.service</command>
+                <command>journalctl --no-hostname --boot --unit kea-dhcp6-server.service</command>
               </node>
               <node name="client">
                 <properties>
                   <help>Show DHCPv6 client logs</help>
                 </properties>
                 <command>journalctl --no-hostname --boot --unit "dhcp6c@*.service"</command>
                 <children>
                   <tagNode name="interface">
                     <properties>
                       <help>Show DHCPv6 client log on specific interface</help>
                       <completionHelp>
                         <script>${vyos_completion_dir}/list_interfaces</script>
                       </completionHelp>
                     </properties>
                     <command>journalctl --no-hostname --boot --unit "dhcp6c@$6.service"</command>
                   </tagNode>
                 </children>
               </node>
             </children>
           </node>
           <node name="firewall">
             <properties>
               <help>Show log for Firewall</help>
             </properties>
             <command>journalctl --no-hostname --boot -k | egrep "(ipv[46]|bri)-(FWD|INP|OUT|NAM)"</command>
             <children>
               <node name="bridge">
                 <properties>
                   <help>Show firewall bridge log</help>
                 </properties>
                 <command>journalctl --no-hostname --boot -k | egrep "bri-(FWD|INP|OUT|NAM)"</command>
                 <children>
                   <node name="forward">
                     <properties>
                       <help>Show Bridge forward firewall log</help>
                     </properties>
                     <command>journalctl --no-hostname --boot -k | grep bri-FWD</command>
                     <children>
                       <node name="filter">
                         <properties>
                           <help>Show Bridge firewall forward filter</help>
                         </properties>
                         <command>journalctl --no-hostname --boot -k | grep bri-FWD-filter</command>
                         <children>
                           <tagNode name="rule">
                             <properties>
                               <help>Show log for a rule in the specified firewall</help>
                               <completionHelp>
                                 <path>firewall bridge forward filter rule</path>
                               </completionHelp>
                             </properties>
                             <command>journalctl --no-hostname --boot -k | egrep "\[bri-FWD-filter-$8-[ADRJC]\]"</command>
                           </tagNode>
                         </children>
                       </node>
                     </children>
                   </node>
                   <tagNode name="name">
                     <properties>
                       <help>Show custom Bridge firewall log</help>
                       <completionHelp>
                         <path>firewall bridge name</path>
                       </completionHelp>
                     </properties>
                     <command>journalctl --no-hostname --boot -k | grep bri-NAM-$6</command>
                     <children>
                       <tagNode name="rule">
                         <properties>
                           <help>Show log for a rule in the specified firewall</help>
                           <completionHelp>
                             <path>firewall bridge name ${COMP_WORDS[5]} rule</path>
                           </completionHelp>
                         </properties>
                         <command>journalctl --no-hostname --boot -k | egrep "\[bri-NAM-$6-$8-[ADRJC]\]"</command>
                       </tagNode>
                     </children>
                   </tagNode>
                 </children>
               </node>
               <node name="ipv4">
                 <properties>
                   <help>Show firewall IPv4 log</help>
                 </properties>
                 <command>journalctl --no-hostname --boot -k | egrep "ipv4-(FWD|INP|OUT|NAM)"</command>
                 <children>
                   <node name="forward">
                     <properties>
                       <help>Show firewall IPv4 forward log</help>
                     </properties>
                     <command>journalctl --no-hostname --boot -k | grep ipv4-FWD</command>
                     <children>
                       <node name="filter">
                         <properties>
                           <help>Show firewall IPv4 forward filter log</help>
                         </properties>
                         <command>journalctl --no-hostname --boot -k | grep ipv4-FWD-filter</command>
                         <children>
                           <tagNode name="rule">
                             <properties>
                               <help>Show log for a rule in the specified firewall</help>
                               <completionHelp>
                                 <path>firewall ipv4 forward filter rule</path>
                               </completionHelp>
                             </properties>
                             <command>journalctl --no-hostname --boot -k | egrep "\[ipv4-FWD-filter-$8-[ADRJC]\]"</command>
                           </tagNode>
                         </children>
                       </node>
                     </children>
                   </node>
                   <node name="input">
                     <properties>
                       <help>Show firewall IPv4 input log</help>
                     </properties>
                     <command>journalctl --no-hostname --boot -k | grep ipv4-INP</command>
                     <children>
                       <node name="filter">
                         <properties>
                           <help>Show firewall IPv4 input filter log</help>
                         </properties>
                         <command>journalctl --no-hostname --boot -k | grep ipv4-INP-filter</command>
                         <children>
                           <tagNode name="rule">
                             <properties>
                               <help>Show log for a rule in the specified firewall</help>
                               <completionHelp>
                                 <path>firewall ipv4 input filter rule</path>
                               </completionHelp>
                             </properties>
                             <command>journalctl --no-hostname --boot -k | egrep "\[ipv4-INP-filter-$8-[ADRJC]\]"</command>
                           </tagNode>
                         </children>
                       </node>
                     </children>
                   </node>
                   <tagNode name="name">
                     <properties>
                       <help>Show custom IPv4 firewall log</help>
                       <completionHelp>
                         <path>firewall ipv4 name</path>
                       </completionHelp>
                     </properties>
                     <command>journalctl --no-hostname --boot -k | grep ipv4-NAM-$6</command>
                     <children>
                       <tagNode name="rule">
                         <properties>
                           <help>Show log for a rule in the specified firewall</help>
                           <completionHelp>
                             <path>firewall ipv4 name ${COMP_WORDS[5]} rule</path>
                           </completionHelp>
                         </properties>
                         <command>journalctl --no-hostname --boot -k | egrep "\[ipv4-NAM-$6-$8-[ADRJC]\]"</command>
                       </tagNode>
                     </children>
                   </tagNode>
                   <node name="output">
                     <properties>
                       <help>Show firewall IPv4 output log</help>
                     </properties>
                     <command>journalctl --no-hostname --boot -k | grep ipv4-OUT</command>
                     <children>
                       <node name="filter">
                         <properties>
                           <help>Show firewall IPv4 output filter log</help>
                         </properties>
                         <command>journalctl --no-hostname --boot -k | grep ipv4-OUT-filter</command>
                         <children>
                           <tagNode name="rule">
                             <properties>
                               <help>Show log for a rule in the specified firewall</help>
                               <completionHelp>
                                 <path>firewall ipv4 output filter rule</path>
                               </completionHelp>
                             </properties>
                             <command>journalctl --no-hostname --boot -k | egrep "\[ipv4-OUT-filter-$8-[ADRJC]\]"</command>
                           </tagNode>
                         </children>
                       </node>
                     </children>
                   </node>
                 </children>
               </node>
               <node name="ipv6">
                 <properties>
                   <help>Show firewall IPv6 log</help>
                 </properties>
                 <command>journalctl --no-hostname --boot -k | egrep "ipv6-(FWD|INP|OUT|NAM)"</command>
                 <children>
                   <node name="forward">
                     <properties>
                       <help>Show firewall IPv6 forward log</help>
                     </properties>
                     <command>journalctl --no-hostname --boot -k | grep ipv6-FWD</command>
                     <children>
                       <node name="filter">
                         <properties>
                           <help>Show firewall IPv6 forward filter log</help>
                         </properties>
                         <command>journalctl --no-hostname --boot -k | grep ipv6-FWD-filter</command>
                         <children>
                           <tagNode name="rule">
                             <properties>
                               <help>Show log for a rule in the specified firewall</help>
                               <completionHelp>
                                 <path>firewall ipv6 forward filter rule</path>
                               </completionHelp>
                             </properties>
                             <command>journalctl --no-hostname --boot -k | egrep "\[ipv6-FWD-filter-$8-[ADRJC]\]"</command>
                           </tagNode>
                         </children>
                       </node>
                     </children>
                   </node>
                   <node name="input">
                     <properties>
                       <help>Show firewall IPv6 input log</help>
                     </properties>
                     <command>journalctl --no-hostname --boot -k | grep ipv6-INP</command>
                     <children>
                       <node name="filter">
                         <properties>
                           <help>Show firewall IPv6 input filter log</help>
                         </properties>
                         <command>journalctl --no-hostname --boot -k | grep ipv6-INP-filter</command>
                         <children>
                           <tagNode name="rule">
                             <properties>
                               <help>Show log for a rule in the specified firewall</help>
                               <completionHelp>
                                 <path>firewall ipv6 input filter rule</path>
                               </completionHelp>
                             </properties>
                             <command>journalctl --no-hostname --boot -k | egrep "\[ipv6-INP-filter-$8-[ADRJC]\]"</command>
                           </tagNode>
                         </children>
                       </node>
                     </children>
                   </node>
                   <tagNode name="name">
                     <properties>
                       <help>Show custom IPv6 firewall log</help>
                       <completionHelp>
                         <path>firewall ipv6 name</path>
                       </completionHelp>
                     </properties>
                     <command>journalctl --no-hostname --boot -k | grep ipv6-NAM-$6</command>
                     <children>
                       <tagNode name="rule">
                         <properties>
                           <help>Show log for a rule in the specified firewall</help>
                           <completionHelp>
                             <path>firewall ipv6 name ${COMP_WORDS[5]} rule</path>
                           </completionHelp>
                         </properties>
                         <command>journalctl --no-hostname --boot -k | egrep "\[ipv6-NAM-$6-$8-[ADRJC]\]"</command>
                       </tagNode>
                     </children>
                   </tagNode>
                   <node name="output">
                     <properties>
                       <help>Show firewall IPv6 output log</help>
                     </properties>
                     <command>journalctl --no-hostname --boot -k | grep ipv6-OUT</command>
                     <children>
                       <node name="filter">
                         <properties>
                           <help>Show firewall IPv6 output filter log</help>
                         </properties>
                         <command>journalctl --no-hostname --boot -k | grep ipv6-OUT-filter</command>
                         <children>
                           <tagNode name="rule">
                             <properties>
                               <help>Show log for a rule in the specified firewall</help>
                               <completionHelp>
                                 <path>firewall ipv6 output filter rule</path>
                               </completionHelp>
                             </properties>
                             <command>journalctl --no-hostname --boot -k | egrep "\[ipv6-OUT-filter-$8-[ADRJC]\]"</command>
                           </tagNode>
                         </children>
                       </node>
                     </children>
                   </node>
                 </children>
               </node>
             </children>
           </node>
           <leafNode name="flow-accounting">
             <properties>
               <help>Show log for flow-accounting</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit uacctd.service</command>
           </leafNode>
           <leafNode name="https">
             <properties>
               <help>Show log for HTTPs</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit nginx.service</command>
           </leafNode>
           <tagNode name="image">
             <properties>
               <help>Show contents of master log file for image</help>
               <completionHelp>
                 <script>compgen -f /lib/live/mount/persistence/boot/ | grep -v grub | sed -e s@/lib/live/mount/persistence/boot/@@</script>
               </completionHelp>
             </properties>
             <command>less $_vyatta_less_options --prompt=".log, page %dt of %D" -- /lib/live/mount/persistence/boot/$4/rw/var/log/messages</command>
             <children>
               <leafNode name="all">
                 <properties>
                   <help>Show contents of all master log files for image</help>
                 </properties>
                 <command>eval $(lesspipe); less $_vyatta_less_options --prompt=".log?m, file %i of %m., page %dt of %D" -- `printf "%s\n" /lib/live/mount/persistence/boot/$4/rw/var/log/messages* | sort -nr`</command>
               </leafNode>
               <leafNode name="authorization">
                 <properties>
                   <help>Show listing of authorization attempts for image</help>
                 </properties>
                 <command>less $_vyatta_less_options --prompt=".log, page %dt of %D" -- /lib/live/mount/persistence/boot/$4/rw/var/log/auth.log</command>
               </leafNode>
               <tagNode name="tail">
                 <properties>
                   <help>Show last changes to messages</help>
                   <completionHelp>
                     <list>&lt;NUMBER&gt;</list>
                   </completionHelp>
                 </properties>
                 <command>tail -n "$6" /lib/live/mount/persistence/boot/$4/rw/var/log/messages | ${VYATTA_PAGER:-cat}</command>
               </tagNode>
             </children>
           </tagNode>
           <leafNode name="ipoe-server">
             <properties>
               <help>Show log for IPoE server</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit accel-ppp@ipoe.service</command>
           </leafNode>
           <leafNode name="kernel">
             <properties>
               <help>Show log for Linux Kernel</help>
             </properties>
             <command>journalctl --no-hostname --boot --dmesg</command>
           </leafNode>
           <leafNode name="lldp">
             <properties>
               <help>Show log for Link Layer Discovery Protocol (LLDP)</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit lldpd.service</command>
           </leafNode>
           <leafNode name="nat">
             <properties>
               <help>Show log for Network Address Translation (NAT)</help>
             </properties>
             <command>egrep -i "kernel:.*\[NAT-[A-Z]{3,}-[0-9]+(-MASQ)?\]" $(find /var/log -maxdepth 1 -type f -name messages\* | sort -t. -k2nr)</command>
           </leafNode>
           <leafNode name="nhrp">
             <properties>
               <help>Show log for Next Hop Resolution Protocol (NHRP)</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit opennhrp.service</command>
           </leafNode>
           <leafNode name="ntp">
             <properties>
               <help>Show log for Network Time Protocol (NTP)</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit chrony.service</command>
           </leafNode>
           <node name="macsec">
             <properties>
               <help>Show log for MACsec</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit "wpa_supplicant-macsec@*.service"</command>
             <children>
               <tagNode name="interface">
                 <properties>
                   <help>Show MACsec log on specific interface</help>
                   <completionHelp>
                     <path>interfaces macsec</path>
                   </completionHelp>
                 </properties>
                 <command>SRC=$(cli-shell-api returnValue interfaces macsec "$5" source-interface); journalctl --no-hostname --boot --unit "wpa_supplicant-macsec@$SRC.service"</command>
               </tagNode>
             </children>
           </node>
           <node name="openvpn">
             <properties>
               <help>Show log for OpenVPN</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit openvpn@*.service</command>
             <children>
               <tagNode name="interface">
                 <properties>
                   <help>Show OpenVPN log on specific interface</help>
                   <completionHelp>
                     <path>interfaces openvpn</path>
                   </completionHelp>
                 </properties>
                 <command>journalctl --no-hostname --boot --unit openvpn@$5.service</command>
               </tagNode>
             </children>
           </node>
           <node name="pppoe">
             <properties>
               <help>Show log for PPPoE interface</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit "ppp@pppoe*.service"</command>
             <children>
               <tagNode name="interface">
                 <properties>
                   <help>Show PPPoE log on specific interface</help>
                   <completionHelp>
                     <path>interfaces pppoe</path>
                   </completionHelp>
                 </properties>
                 <command>journalctl --no-hostname --boot --unit "ppp@$5.service"</command>
               </tagNode>
             </children>
           </node>
           <leafNode name="pppoe-server">
             <properties>
               <help>Show log for PPPoE server</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit accel-ppp@pppoe.service</command>
           </leafNode>
           <node name="protocol">
             <properties>
               <help>Show log for Routing Protocol</help>
             </properties>
             <children>
               <leafNode name="ospf">
                 <properties>
                   <help>Show log for OSPF</help>
                 </properties>
                 <command>journalctl --boot /usr/lib/frr/ospfd</command>
               </leafNode>
               <leafNode name="ospfv3">
                 <properties>
                   <help>Show log for OSPF for IPv6</help>
                 </properties>
                 <command>journalctl --boot /usr/lib/frr/ospf6d</command>
               </leafNode>
               <leafNode name="bgp">
                 <properties>
                   <help>Show log for BGP</help>
                 </properties>
                 <command>journalctl --boot /usr/lib/frr/bgpd</command>
               </leafNode>
               <leafNode name="rip">
                 <properties>
                   <help>Show log for RIP</help>
                 </properties>
                 <command>journalctl --boot /usr/lib/frr/ripd</command>
               </leafNode>
               <leafNode name="ripng">
                 <properties>
                   <help>Show log for RIPng</help>
                 </properties>
                 <command>journalctl --boot /usr/lib/frr/ripngd</command>
               </leafNode>
               <leafNode name="static">
                 <properties>
                   <help>Show log for static route</help>
                 </properties>
                 <command>journalctl --boot /usr/lib/frr/staticd</command>
               </leafNode>
               <leafNode name="multicast">
                 <properties>
                   <help>Show log for Multicast protocol</help>
                 </properties>
                 <command>journalctl --boot /usr/lib/frr/pimd</command>
               </leafNode>
               <leafNode name="isis">
                 <properties>
                   <help>Show log for ISIS</help>
                 </properties>
                 <command>journalctl --boot /usr/lib/frr/isisd</command>
               </leafNode>
               <leafNode name="nhrp">
                 <properties>
                   <help>Show log for NHRP</help>
                 </properties>
                 <command>journalctl --boot /usr/lib/frr/nhrpd</command>
               </leafNode>
               <leafNode name="bfd">
                 <properties>
                   <help>Show log for BFD</help>
                 </properties>
                 <command>journalctl --boot /usr/lib/frr/bfdd</command>
               </leafNode>
               <leafNode name="mpls">
                 <properties>
                   <help>Show log for MPLS</help>
                 </properties>
                 <command>journalctl --boot /usr/lib/frr/ldpd</command>
               </leafNode>
             </children>
           </node>
           <leafNode name="router-advert">
             <properties>
               <help>Show log for Router Advertisement Daemon (radvd)</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit radvd.service</command>
           </leafNode>
           <leafNode name="snmp">
             <properties>
               <help>Show log for Simple Network Monitoring Protocol (SNMP)</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit snmpd.service</command>
           </leafNode>
           <node name="ssh">
             <properties>
               <help>Show log for Secure Shell (SSH)</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit ssh.service</command>
             <children>
               <node name="dynamic-protection">
                 <properties>
                   <help>Show SSH guard log</help>
                 </properties>
                 <command>journalctl --no-hostname --boot --unit sshguard.service</command>
               </node>
             </children>
           </node>
           <tagNode name="tail">
             <properties>
               <help>Show last n changes to messages</help>
               <completionHelp>
                 <list>&lt;NUMBER&gt;</list>
               </completionHelp>
             </properties>
             <command>tail -n "$4" /var/log/messages | ${VYATTA_PAGER:-cat}</command>
           </tagNode>
           <node name="tail">
             <properties>
               <help>Show last 10 lines of /var/log/messages file</help>
             </properties>
             <command>tail -n 10 /var/log/messages</command>
           </node>
           <leafNode name="vpn">
             <properties>
               <help>Monitor last lines of ALL Virtual Private Network services</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit strongswan.service --unit accel-ppp@*.service --unit ocserv.service</command>
           </leafNode>
           <leafNode name="ipsec">
             <properties>
               <help>Show log for IPsec</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit strongswan.service</command>
           </leafNode>
           <leafNode name="l2tp">
             <properties>
               <help>Show log for L2TP</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit accel-ppp@l2tp.service</command>
           </leafNode>
           <leafNode name="openconnect">
             <properties>
               <help>Show log for OpenConnect</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit ocserv.service</command>
           </leafNode>
           <leafNode name="pptp">
             <properties>
               <help>Show log for PPTP</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit accel-ppp@pptp.service</command>
           </leafNode>
           <leafNode name="sstp">
             <properties>
               <help>Show log for Secure Socket Tunneling Protocol (SSTP) server</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit accel-ppp@sstp.service</command>
           </leafNode>
           <node name="sstpc">
             <properties>
               <help>Show log for Secure Socket Tunneling Protocol (SSTP) client</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit "ppp@sstpc*.service"</command>
             <children>
               <tagNode name="interface">
                 <properties>
                   <help>Show SSTP client log on specific interface</help>
                   <completionHelp>
                     <path>interfaces sstpc</path>
                   </completionHelp>
                 </properties>
                 <command>journalctl --no-hostname --boot --unit "ppp@$5.service"</command>
               </tagNode>
             </children>
           </node>
           <leafNode name="vrrp">
             <properties>
               <help>Show log for Virtual Router Redundancy Protocol (VRRP)</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit keepalived.service</command>
           </leafNode>
           <leafNode name="webproxy">
             <properties>
               <help>Show log for Webproxy</help>
             </properties>
             <command>journalctl --no-hostname --boot --unit squid.service</command>
           </leafNode>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/python/vyos/kea.py b/python/vyos/kea.py
new file mode 100644
index 000000000..cb341e0f2
--- /dev/null
+++ b/python/vyos/kea.py
@@ -0,0 +1,319 @@
+# 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 read_file
+
+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']})
+
+    return options
+
+def kea_parse_subnet(subnet, config):
+    out = {'subnet': subnet}
+    options = kea_parse_options(config)
+
+    if 'bootfile_name' in config:
+        out['boot-file-name'] = config['bootfile_name']
+
+    if 'bootfile_server' in config:
+        out['next-server'] = config['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']
+            pools.append({'pool': f'{start} - {stop}'})
+        out['pools'] = pools
+
+    if 'static_mapping' in config:
+        reservations = []
+        for host, host_config in config['static_mapping'].items():
+            if 'disable' in host_config:
+                continue
+
+            reservations.append({
+                'hw-address': host_config['mac_address'],
+                'ip-address': host_config['ip_address']
+            })
+        out['reservations'] = reservations
+
+    unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller')
+    if unifi_controller:
+        options.append({
+            'name': 'unifi-controller',
+            'data': unifi_controller,
+            'space': 'ubnt'
+        })
+
+    if options:
+        out['option-data'] = options
+
+    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}
+    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 = {}
+
+            if 'identifier' in host_config:
+                reservation['duid'] = host_config['identifier']
+
+            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
+
+    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/python/vyos/template.py b/python/vyos/template.py
index 2d4beeec2..f0a50e728 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -1,844 +1,944 @@
 # Copyright 2019-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 functools
 import os
 
 from jinja2 import Environment
 from jinja2 import FileSystemLoader
 from jinja2 import ChainableUndefined
 from vyos.defaults import directories
 from vyos.utils.dict import dict_search_args
 from vyos.utils.file import makedir
 from vyos.utils.permission import chmod
 from vyos.utils.permission import chown
 
 # Holds template filters registered via register_filter()
 _FILTERS = {}
 _TESTS = {}
 
 # reuse Environments with identical settings to improve performance
 @functools.lru_cache(maxsize=2)
 def _get_environment(location=None):
     if location is None:
         loc_loader=FileSystemLoader(directories["templates"])
     else:
         loc_loader=FileSystemLoader(location)
     env = Environment(
         # Don't check if template files were modified upon re-rendering
         auto_reload=False,
         # Cache up to this number of templates for quick re-rendering
         cache_size=100,
         loader=loc_loader,
         trim_blocks=True,
         undefined=ChainableUndefined,
         extensions=['jinja2.ext.loopcontrols']
     )
     env.filters.update(_FILTERS)
     env.tests.update(_TESTS)
     return env
 
 
 def register_filter(name, func=None):
     """Register a function to be available as filter in templates under given name.
 
     It can also be used as a decorator, see below in this module for examples.
 
     :raise RuntimeError:
         when trying to register a filter after a template has been rendered already
     :raise ValueError: when trying to register a name which was taken already
     """
     if func is None:
         return functools.partial(register_filter, name)
     if _get_environment.cache_info().currsize:
         raise RuntimeError(
             "Filters can only be registered before rendering the first template"
         )
     if name in _FILTERS:
         raise ValueError(f"A filter with name {name!r} was registered already")
     _FILTERS[name] = func
     return func
 
 def register_test(name, func=None):
     """Register a function to be available as test in templates under given name.
 
     It can also be used as a decorator, see below in this module for examples.
 
     :raise RuntimeError:
         when trying to register a test after a template has been rendered already
     :raise ValueError: when trying to register a name which was taken already
     """
     if func is None:
         return functools.partial(register_test, name)
     if _get_environment.cache_info().currsize:
         raise RuntimeError(
             "Tests can only be registered before rendering the first template"
             )
     if name in _TESTS:
         raise ValueError(f"A test with name {name!r} was registered already")
     _TESTS[name] = func
     return func
 
 
 def render_to_string(template, content, formater=None, location=None):
     """Render a template from the template directory, raise on any errors.
 
     :param template: the path to the template relative to the template folder
     :param content: the dictionary of variables to put into rendering context
     :param formater:
         if given, it has to be a callable the rendered string is passed through
 
     The parsed template files are cached, so rendering the same file multiple times
     does not cause as too much overhead.
     If used everywhere, it could be changed to load the template from Python
     environment variables from an importable Python module generated when the Debian
     package is build (recovering the load time and overhead caused by having the
     file out of the code).
     """
     template = _get_environment(location).get_template(template)
     rendered = template.render(content)
     if formater is not None:
         rendered = formater(rendered)
     return rendered
 
 
 def render(
     destination,
     template,
     content,
     formater=None,
     permission=None,
     user=None,
     group=None,
     location=None,
 ):
     """Render a template from the template directory to a file, raise on any errors.
 
     :param destination: path to the file to save the rendered template in
     :param permission: permission bitmask to set for the output file
     :param user: user to own the output file
     :param group: group to own the output file
 
     All other parameters are as for :func:`render_to_string`.
     """
     # Create the directory if it does not exist
     folder = os.path.dirname(destination)
     makedir(folder, user, group)
 
     # As we are opening the file with 'w', we are performing the rendering before
     # calling open() to not accidentally erase the file if rendering fails
     rendered = render_to_string(template, content, formater, location)
 
     # Write to file
     with open(destination, "w") as file:
         chmod(file.fileno(), permission)
         chown(file.fileno(), user, group)
         file.write(rendered)
 
 
 ##################################
 # Custom template filters follow #
 ##################################
 @register_filter('force_to_list')
 def force_to_list(value):
     """ Convert scalars to single-item lists and leave lists untouched """
     if isinstance(value, list):
         return value
     else:
         return [value]
 
 @register_filter('seconds_to_human')
 def seconds_to_human(seconds, separator=""):
     """ Convert seconds to human-readable values like 1d6h15m23s """
     from vyos.utils.convert import seconds_to_human
     return seconds_to_human(seconds, separator=separator)
 
 @register_filter('bytes_to_human')
 def bytes_to_human(bytes, initial_exponent=0, precision=2):
     """ Convert bytes to human-readable values like 1.44M """
     from vyos.utils.convert import bytes_to_human
     return bytes_to_human(bytes, initial_exponent=initial_exponent, precision=precision)
 
 @register_filter('human_to_bytes')
 def human_to_bytes(value):
     """ Convert a data amount with a unit suffix to bytes, like 2K to 2048 """
     from vyos.utils.convert import human_to_bytes
     return human_to_bytes(value)
 
 @register_filter('ip_from_cidr')
 def ip_from_cidr(prefix):
     """ Take an IPv4/IPv6 CIDR host and strip cidr mask.
     Example:
     192.0.2.1/24 -> 192.0.2.1, 2001:db8::1/64 -> 2001:db8::1
     """
     from ipaddress import ip_interface
     return str(ip_interface(prefix).ip)
 
 @register_filter('address_from_cidr')
 def address_from_cidr(prefix):
     """ Take an IPv4/IPv6 CIDR prefix and convert the network to an "address".
     Example:
     192.0.2.0/24 -> 192.0.2.0, 2001:db8::/48 -> 2001:db8::
     """
     from ipaddress import ip_network
     return str(ip_network(prefix).network_address)
 
 @register_filter('bracketize_ipv6')
 def bracketize_ipv6(address):
     """ Place a passed IPv6 address into [] brackets, do nothing for IPv4 """
     if is_ipv6(address):
         return f'[{address}]'
     return address
 
 @register_filter('dot_colon_to_dash')
 def dot_colon_to_dash(text):
     """ Replace dot and colon to dash for string
     Example:
     192.0.2.1 => 192-0-2-1, 2001:db8::1 => 2001-db8--1
     """
     text = text.replace(":", "-")
     text = text.replace(".", "-")
     return text
 
 @register_filter('generate_uuid4')
 def generate_uuid4(text):
     """ Generate random unique ID
     Example:
       % uuid4()
       UUID('958ddf6a-ef14-4e81-8cfb-afb12456d1c5')
     """
     from uuid import uuid4
     return uuid4()
 
 @register_filter('netmask_from_cidr')
 def netmask_from_cidr(prefix):
     """ Take CIDR prefix and convert the prefix length to a "subnet mask".
     Example:
       - 192.0.2.0/24 -> 255.255.255.0
       - 2001:db8::/48 -> ffff:ffff:ffff::
     """
     from ipaddress import ip_network
     return str(ip_network(prefix).netmask)
 
 @register_filter('netmask_from_ipv4')
 def netmask_from_ipv4(address):
     """ Take IP address and search all attached interface IP addresses for the
     given one. After address has been found, return the associated netmask.
 
     Example:
       - 172.18.201.10 -> 255.255.255.128
     """
     from netifaces import interfaces
     from netifaces import ifaddresses
     from netifaces import AF_INET
     for interface in interfaces():
         tmp = ifaddresses(interface)
         if AF_INET in tmp:
             for af_addr in tmp[AF_INET]:
                 if 'addr' in af_addr:
                     if af_addr['addr'] == address:
                         return af_addr['netmask']
 
     raise ValueError
 
 @register_filter('is_ip_network')
 def is_ip_network(addr):
     """ Take IP(v4/v6) address and validate if the passed argument is a network
     or a host address.
 
     Example:
       - 192.0.2.0          -> False
       - 192.0.2.10/24      -> False
       - 192.0.2.0/24       -> True
       - 2001:db8::         -> False
       - 2001:db8::100      -> False
       - 2001:db8::/48      -> True
       - 2001:db8:1000::/64 -> True
     """
     try:
         from ipaddress import ip_network
         # input variables must contain a / to indicate its CIDR notation
         if len(addr.split('/')) != 2:
             raise ValueError()
         ip_network(addr)
         return True
     except:
         return False
 
 @register_filter('network_from_ipv4')
 def network_from_ipv4(address):
     """ Take IP address and search all attached interface IP addresses for the
     given one. After address has been found, return the associated network
     address.
 
     Example:
       - 172.18.201.10 has mask 255.255.255.128 -> network is 172.18.201.0
     """
     netmask = netmask_from_ipv4(address)
     from ipaddress import ip_interface
     cidr_prefix = ip_interface(f'{address}/{netmask}').network
     return address_from_cidr(cidr_prefix)
 
 @register_filter('is_interface')
 def is_interface(interface):
     """ Check if parameter is a valid local interface name """
     return os.path.exists(f'/sys/class/net/{interface}')
 
 @register_filter('is_ip')
 def is_ip(addr):
     """ Check addr if it is an IPv4 or IPv6 address """
     return is_ipv4(addr) or is_ipv6(addr)
 
 @register_filter('is_ipv4')
 def is_ipv4(text):
     """ Filter IP address, return True on IPv4 address, False otherwise """
     from ipaddress import ip_interface
     try: return ip_interface(text).version == 4
     except: return False
 
 @register_filter('is_ipv6')
 def is_ipv6(text):
     """ Filter IP address, return True on IPv6 address, False otherwise """
     from ipaddress import ip_interface
     try: return ip_interface(text).version == 6
     except: return False
 
 @register_filter('first_host_address')
 def first_host_address(text):
     """ Return first usable (host) IP address from given prefix.
     Example:
       - 10.0.0.0/24 -> 10.0.0.1
       - 2001:db8::/64 -> 2001:db8::
     """
     from ipaddress import ip_interface
     from ipaddress import IPv4Network
     from ipaddress import IPv6Network
 
     addr = ip_interface(text)
     if addr.version == 4:
         return str(addr.ip +1)
     return str(addr.ip)
 
 @register_filter('last_host_address')
 def last_host_address(text):
     """ Return first usable IP address from given prefix.
     Example:
       - 10.0.0.0/24 -> 10.0.0.254
       - 2001:db8::/64 -> 2001:db8::ffff:ffff:ffff:ffff
     """
     from ipaddress import ip_interface
     from ipaddress import IPv4Network
     from ipaddress import IPv6Network
 
     addr = ip_interface(text)
     if addr.version == 4:
         return str(IPv4Network(addr).broadcast_address - 1)
 
     return str(IPv6Network(addr).broadcast_address)
 
 @register_filter('inc_ip')
 def inc_ip(address, increment):
     """ Increment given IP address by 'increment'
 
     Example (inc by 2):
       - 10.0.0.0/24 -> 10.0.0.2
       - 2001:db8::/64 -> 2001:db8::2
     """
     from ipaddress import ip_interface
     return str(ip_interface(address).ip + int(increment))
 
 @register_filter('dec_ip')
 def dec_ip(address, decrement):
     """ Decrement given IP address by 'decrement'
 
     Example (inc by 2):
       - 10.0.0.0/24 -> 10.0.0.2
       - 2001:db8::/64 -> 2001:db8::2
     """
     from ipaddress import ip_interface
     return str(ip_interface(address).ip - int(decrement))
 
 @register_filter('compare_netmask')
 def compare_netmask(netmask1, netmask2):
     """
     Compare two IP netmask if they have the exact same size.
 
     compare_netmask('10.0.0.0/8', '20.0.0.0/8') -> True
     compare_netmask('10.0.0.0/8', '20.0.0.0/16') -> False
     """
     from ipaddress import ip_network
     try:
         return ip_network(netmask1).netmask == ip_network(netmask2).netmask
     except:
         return False
 
 @register_filter('isc_static_route')
 def isc_static_route(subnet, router):
     # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server
     # Option format is:
     # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3>
     # where bytes with the value 0 are omitted.
     from ipaddress import ip_network
     net = ip_network(subnet)
     # add netmask
     string = str(net.prefixlen) + ','
     # add network bytes
     if net.prefixlen:
         width = net.prefixlen // 8
         if net.prefixlen % 8:
             width += 1
         string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ','
 
     # add router bytes
     string += ','.join(router.split('.'))
 
     return string
 
 @register_filter('is_file')
 def is_file(filename):
     if os.path.exists(filename):
         return os.path.isfile(filename)
     return False
 
 @register_filter('get_dhcp_router')
 def get_dhcp_router(interface):
     """ Static routes can point to a router received by a DHCP reply. This
     helper is used to get the current default router from the DHCP reply.
 
     Returns False of no router is found, returns the IP address as string if
     a router is found.
     """
     lease_file = directories['isc_dhclient_dir'] + f'/dhclient_{interface}.leases'
     if not os.path.exists(lease_file):
         return None
 
     from vyos.utils.file import read_file
     for line in read_file(lease_file).splitlines():
         if 'option routers' in line:
             (_, _, address) = line.split()
             return address.rstrip(';')
 
 @register_filter('natural_sort')
 def natural_sort(iterable):
     import re
     from jinja2.runtime import Undefined
 
     if isinstance(iterable, Undefined) or iterable is None:
         return list()
 
     def convert(text):
         return int(text) if text.isdigit() else text.lower()
     def alphanum_key(key):
         return [convert(c) for c in re.split('([0-9]+)', str(key))]
 
     return sorted(iterable, key=alphanum_key)
 
 @register_filter('get_ipv4')
 def get_ipv4(interface):
     """ Get interface IPv4 addresses"""
     from vyos.ifconfig import Interface
     return Interface(interface).get_addr_v4()
 
 @register_filter('get_ipv6')
 def get_ipv6(interface):
     """ Get interface IPv6 addresses"""
     from vyos.ifconfig import Interface
     return Interface(interface).get_addr_v6()
 
 @register_filter('get_ip')
 def get_ip(interface):
     """ Get interface IP addresses"""
     from vyos.ifconfig import Interface
     return Interface(interface).get_addr()
 
 def get_first_ike_dh_group(ike_group):
     if ike_group and 'proposal' in ike_group:
         for priority, proposal in ike_group['proposal'].items():
             if 'dh_group' in proposal:
                 return 'dh-group' + proposal['dh_group']
     return 'dh-group2' # Fallback on dh-group2
 
 @register_filter('get_esp_ike_cipher')
 def get_esp_ike_cipher(group_config, ike_group=None):
     pfs_lut = {
         'dh-group1'  : 'modp768',
         'dh-group2'  : 'modp1024',
         'dh-group5'  : 'modp1536',
         'dh-group14' : 'modp2048',
         'dh-group15' : 'modp3072',
         'dh-group16' : 'modp4096',
         'dh-group17' : 'modp6144',
         'dh-group18' : 'modp8192',
         'dh-group19' : 'ecp256',
         'dh-group20' : 'ecp384',
         'dh-group21' : 'ecp521',
         'dh-group22' : 'modp1024s160',
         'dh-group23' : 'modp2048s224',
         'dh-group24' : 'modp2048s256',
         'dh-group25' : 'ecp192',
         'dh-group26' : 'ecp224',
         'dh-group27' : 'ecp224bp',
         'dh-group28' : 'ecp256bp',
         'dh-group29' : 'ecp384bp',
         'dh-group30' : 'ecp512bp',
         'dh-group31' : 'curve25519',
         'dh-group32' : 'curve448'
     }
 
     ciphers = []
     if 'proposal' in group_config:
         for priority, proposal in group_config['proposal'].items():
             # both encryption and hash need to be specified for a proposal
             if not {'encryption', 'hash'} <= set(proposal):
                 continue
 
             tmp = '{encryption}-{hash}'.format(**proposal)
             if 'prf' in proposal:
                 tmp += '-' + proposal['prf']
             if 'dh_group' in proposal:
                 tmp += '-' + pfs_lut[ 'dh-group' +  proposal['dh_group'] ]
             elif 'pfs' in group_config and group_config['pfs'] != 'disable':
                 group = group_config['pfs']
                 if group_config['pfs'] == 'enable':
                     group = get_first_ike_dh_group(ike_group)
                 tmp += '-' + pfs_lut[group]
 
             ciphers.append(tmp)
     return ciphers
 
 @register_filter('get_uuid')
 def get_uuid(interface):
     """ Get interface IP addresses"""
     from uuid import uuid1
     return uuid1()
 
 openvpn_translate = {
     'des': 'des-cbc',
     '3des': 'des-ede3-cbc',
     'bf128': 'bf-cbc',
     'bf256': 'bf-cbc',
     'aes128gcm': 'aes-128-gcm',
     'aes128': 'aes-128-cbc',
     'aes192gcm': 'aes-192-gcm',
     'aes192': 'aes-192-cbc',
     'aes256gcm': 'aes-256-gcm',
     'aes256': 'aes-256-cbc'
 }
 
 @register_filter('openvpn_cipher')
 def get_openvpn_cipher(cipher):
     if cipher in openvpn_translate:
         return openvpn_translate[cipher].upper()
     return cipher.upper()
 
 @register_filter('openvpn_ncp_ciphers')
 def get_openvpn_ncp_ciphers(ciphers):
     out = []
     for cipher in ciphers:
         if cipher in openvpn_translate:
             out.append(openvpn_translate[cipher])
         else:
             out.append(cipher)
     return ':'.join(out).upper()
 
 @register_filter('snmp_auth_oid')
 def snmp_auth_oid(type):
     if type not in ['md5', 'sha', 'aes', 'des', 'none']:
         raise ValueError()
 
     OIDs = {
         'md5' : '.1.3.6.1.6.3.10.1.1.2',
         'sha' : '.1.3.6.1.6.3.10.1.1.3',
         'aes' : '.1.3.6.1.6.3.10.1.2.4',
         'des' : '.1.3.6.1.6.3.10.1.2.2',
         'none': '.1.3.6.1.6.3.10.1.2.1'
     }
     return OIDs[type]
 
 @register_filter('nft_action')
 def nft_action(vyos_action):
     if vyos_action == 'accept':
         return 'return'
     return vyos_action
 
 @register_filter('nft_rule')
 def nft_rule(rule_conf, fw_hook, fw_name, rule_id, ip_name='ip'):
     from vyos.firewall import parse_rule
     return parse_rule(rule_conf, fw_hook, fw_name, rule_id, ip_name)
 
 @register_filter('nft_default_rule')
 def nft_default_rule(fw_conf, fw_name, family):
     output = ['counter']
     default_action = fw_conf['default_action']
     #family = 'ipv6' if ipv6 else 'ipv4'
 
     if 'enable_default_log' in fw_conf:
         action_suffix = default_action[:1].upper()
         output.append(f'log prefix "[{family}-{fw_name[:19]}-default-{action_suffix}]"')
 
     #output.append(nft_action(default_action))
     output.append(f'{default_action}')
     if 'default_jump_target' in fw_conf:
         target = fw_conf['default_jump_target']
         def_suffix = '6' if family == 'ipv6' else ''
         output.append(f'NAME{def_suffix}_{target}')
 
     output.append(f'comment "{fw_name} default-action {default_action}"')
     return " ".join(output)
 
 @register_filter('nft_state_policy')
 def nft_state_policy(conf, state):
     out = [f'ct state {state}']
 
     if 'log' in conf:
         log_state = state[:3].upper()
         log_action = (conf['action'] if 'action' in conf else 'accept')[:1].upper()
         out.append(f'log prefix "[STATE-POLICY-{log_state}-{log_action}]"')
 
         if 'log_level' in conf:
             log_level = conf['log_level']
             out.append(f'level {log_level}')
 
     out.append('counter')
 
     if 'action' in conf:
         out.append(conf['action'])
 
     return " ".join(out)
 
 @register_filter('nft_intra_zone_action')
 def nft_intra_zone_action(zone_conf, ipv6=False):
     if 'intra_zone_filtering' in zone_conf:
         intra_zone = zone_conf['intra_zone_filtering']
         fw_name = 'ipv6_name' if ipv6 else 'name'
         name_prefix = 'NAME6_' if ipv6 else 'NAME_'
 
         if 'action' in intra_zone:
             if intra_zone['action'] == 'accept':
                 return 'return'
             return intra_zone['action']
         elif dict_search_args(intra_zone, 'firewall', fw_name):
             name = dict_search_args(intra_zone, 'firewall', fw_name)
             return f'jump {name_prefix}{name}'
     return 'return'
 
 @register_filter('nft_nested_group')
 def nft_nested_group(out_list, includes, groups, key):
     if not vyos_defined(out_list):
         out_list = []
 
     def add_includes(name):
         if key in groups[name]:
             for item in groups[name][key]:
                 if item in out_list:
                     continue
                 out_list.append(item)
 
         if 'include' in groups[name]:
             for name_inc in groups[name]['include']:
                 add_includes(name_inc)
 
     for name in includes:
         add_includes(name)
     return out_list
 
 @register_filter('nat_rule')
 def nat_rule(rule_conf, rule_id, nat_type, ipv6=False):
     from vyos.nat import parse_nat_rule
     return parse_nat_rule(rule_conf, rule_id, nat_type, ipv6)
 
 @register_filter('nat_static_rule')
 def nat_static_rule(rule_conf, rule_id, nat_type):
     from vyos.nat import parse_nat_static_rule
     return parse_nat_static_rule(rule_conf, rule_id, nat_type)
 
 @register_filter('conntrack_rule')
 def conntrack_rule(rule_conf, rule_id, action, ipv6=False):
     ip_prefix = 'ip6' if ipv6 else 'ip'
     def_suffix = '6' if ipv6 else ''
     output = []
 
     if 'inbound_interface' in rule_conf:
         ifname = rule_conf['inbound_interface']
         if ifname != 'any':
             output.append(f'iifname {ifname}')
 
     if 'protocol' in rule_conf:
         if action != 'timeout':
             proto = rule_conf['protocol']
         else:
             for protocol, protocol_config in rule_conf['protocol'].items():
                 proto = protocol
         output.append(f'meta l4proto {proto}')
 
     tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
     if tcp_flags and action != 'timeout':
         from vyos.firewall import parse_tcp_flags
         output.append(parse_tcp_flags(tcp_flags))
 
     for side in ['source', 'destination']:
         if side in rule_conf:
             side_conf = rule_conf[side]
             prefix = side[0]
 
             if 'address' in side_conf:
                 address = side_conf['address']
                 operator = ''
                 if address[0] == '!':
                     operator = '!='
                     address = address[1:]
                 output.append(f'{ip_prefix} {prefix}addr {operator} {address}')
 
             if 'port' in side_conf:
                 port = side_conf['port']
                 operator = ''
                 if port[0] == '!':
                     operator = '!='
                     port = port[1:]
                 output.append(f'th {prefix}port {operator} {port}')
 
             if 'group' in side_conf:
                 group = side_conf['group']
 
                 if 'address_group' in group:
                     group_name = group['address_group']
                     operator = ''
                     if group_name[0] == '!':
                         operator = '!='
                         group_name = group_name[1:]
                     output.append(f'{ip_prefix} {prefix}addr {operator} @A{def_suffix}_{group_name}')
                 # Generate firewall group domain-group
                 elif 'domain_group' in group:
                     group_name = group['domain_group']
                     operator = ''
                     if group_name[0] == '!':
                         operator = '!='
                         group_name = group_name[1:]
                     output.append(f'{ip_prefix} {prefix}addr {operator} @D_{group_name}')
                 elif 'network_group' in group:
                     group_name = group['network_group']
                     operator = ''
                     if group_name[0] == '!':
                         operator = '!='
                         group_name = group_name[1:]
                     output.append(f'{ip_prefix} {prefix}addr {operator} @N{def_suffix}_{group_name}')
                 if 'port_group' in group:
                     group_name = group['port_group']
 
                     if proto == 'tcp_udp':
                         proto = 'th'
 
                     operator = ''
                     if group_name[0] == '!':
                         operator = '!='
                         group_name = group_name[1:]
 
                     output.append(f'{proto} {prefix}port {operator} @P_{group_name}')
 
     if action == 'ignore':
         output.append('counter notrack')
         output.append(f'comment "ignore-{rule_id}"')
     else:
         output.append(f'counter ct timeout set ct-timeout-{rule_id}')
         output.append(f'comment "timeout-{rule_id}"')
 
     return " ".join(output)
 
 @register_filter('conntrack_ct_policy')
 def conntrack_ct_policy(protocol_conf):
     output = []
     for item in protocol_conf:
         item_value = protocol_conf[item]
         output.append(f'{item}: {item_value}')
 
     return ", ".join(output)
 
 @register_filter('range_to_regex')
 def range_to_regex(num_range):
     """Convert range of numbers or list of ranges
        to regex
 
        % range_to_regex('11-12')
        '(1[1-2])'
        % range_to_regex(['11-12', '14-15'])
        '(1[1-2]|1[4-5])'
     """
     from vyos.range_regex import range_to_regex
     if isinstance(num_range, list):
         data = []
         for entry in num_range:
             if '-' not in entry:
                 data.append(entry)
             else:
                 data.append(range_to_regex(entry))
         return f'({"|".join(data)})'
 
     if '-' not in num_range:
         return num_range
 
     regex = range_to_regex(num_range)
     return f'({regex})'
 
+@register_filter('kea_failover_json')
+def kea_failover_json(config):
+    from json import dumps
+
+    source_addr = config['source_address']
+    remote_addr = config['remote']
+
+    data = {
+        'this-server-name': os.uname()[1],
+        'mode': 'hot-standby',
+        'heartbeat-delay': 10000,
+        'max-response-delay': 10000,
+        'max-ack-delay': 5000,
+        'max-unacked-clients': 0,
+        'peers': [
+        {
+            'name': os.uname()[1],
+            'url': f'http://{source_addr}:647/',
+            'role': 'standby' if config['status'] == 'secondary' else 'primary',
+            'auto-failover': True
+        },
+        {
+            'name': config['name'],
+            'url': f'http://{remote_addr}:647/',
+            'role': 'primary' if config['status'] == 'secondary' else 'standby',
+            'auto-failover': True
+        }]
+    }
+
+    if 'ca_cert_file' in config:
+        data['trust-anchor'] = config['ca_cert_file']
+
+    if 'cert_file' in config:
+        data['cert-file'] = config['cert_file']
+
+    if 'cert_key_file' in config:
+        data['key-file'] = config['cert_key_file']
+
+    return dumps(data)
+
+@register_filter('kea_shared_network_json')
+def kea_shared_network_json(shared_networks):
+    from vyos.kea import kea_parse_options
+    from vyos.kea import kea_parse_subnet
+    from json import dumps
+    out = []
+
+    for name, config in shared_networks.items():
+        if 'disable' in config:
+            continue
+
+        network = {
+            'name': name,
+            'authoritative': ('authoritative' in config),
+            'subnet4': []
+        }
+        options = kea_parse_options(config)
+
+        if 'subnet' in config:
+            for subnet, subnet_config in config['subnet'].items():
+                network['subnet4'].append(kea_parse_subnet(subnet, subnet_config))
+
+        if options:
+            network['option-data'] = options
+
+        out.append(network)
+
+    return dumps(out, indent=4)
+
+@register_filter('kea6_shared_network_json')
+def kea6_shared_network_json(shared_networks):
+    from vyos.kea import kea6_parse_options
+    from vyos.kea import kea6_parse_subnet
+    from json import dumps
+    out = []
+
+    for name, config in shared_networks.items():
+        if 'disable' in config:
+            continue
+
+        network = {
+            'name': name,
+            'subnet6': []
+        }
+        options = kea6_parse_options(config)
+
+        if 'interface' in config:
+            network['interface'] = config['interface']
+
+        if 'subnet' in config:
+            for subnet, subnet_config in config['subnet'].items():
+                network['subnet6'].append(kea6_parse_subnet(subnet, subnet_config))
+
+        if options:
+            network['option-data'] = options
+
+        out.append(network)
+
+    return dumps(out, indent=4)
+
 @register_test('vyos_defined')
 def vyos_defined(value, test_value=None, var_type=None):
     """
     Jinja2 plugin to test if a variable is defined and not none - vyos_defined
     will test value if defined and is not none and return true or false.
 
     If test_value is supplied, the value must also pass == test_value to return true.
     If var_type is supplied, the value must also be of the specified class/type
 
     Examples:
     1. Test if var is defined and not none:
     {% if foo is vyos_defined %}
     ...
     {% endif %}
 
     2. Test if variable is defined, not none and has value "something"
     {% if bar is vyos_defined("something") %}
     ...
     {% endif %}
 
     Parameters
     ----------
     value : any
         Value to test from ansible
     test_value : any, optional
         Value to test in addition of defined and not none, by default None
     var_type : ['float', 'int', 'str', 'list', 'dict', 'tuple', 'bool'], optional
         Type or Class to test for
 
     Returns
     -------
     boolean
         True if variable matches criteria, False in other cases.
 
     Implementation inspired and re-used from https://github.com/aristanetworks/ansible-avd/
     """
 
     from jinja2 import Undefined
 
     if isinstance(value, Undefined) or value is None:
         # Invalid value - return false
         return False
     elif test_value is not None and value != test_value:
         # Valid value but not matching the optional argument
         return False
     elif str(var_type).lower() in ['float', 'int', 'str', 'list', 'dict', 'tuple', 'bool'] and str(var_type).lower() != type(value).__name__:
         # Invalid class - return false
         return False
     else:
         # Valid value and is matching optional argument if provided - return true
         return True
diff --git a/python/vyos/utils/file.py b/python/vyos/utils/file.py
index 9f27a7fb9..2af87a0ca 100644
--- a/python/vyos/utils/file.py
+++ b/python/vyos/utils/file.py
@@ -1,189 +1,197 @@
 # 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 os
 from vyos.utils.permission import chown
 
 def makedir(path, user=None, group=None):
     if os.path.exists(path):
         return
     os.makedirs(path, mode=0o755)
     chown(path, user, group)
 
 def file_is_persistent(path):
     import re
     location = r'^(/config|/opt/vyatta/etc/config)'
     absolute = os.path.abspath(os.path.dirname(path))
     return re.match(location,absolute)
 
 def read_file(fname, defaultonfailure=None):
     """
     read the content of a file, stripping any end characters (space, newlines)
     should defaultonfailure be not None, it is returned on failure to read
     """
     try:
         """ Read a file to string """
         with open(fname, 'r') as f:
             data = f.read().strip()
         return data
     except Exception as e:
         if defaultonfailure is not None:
             return defaultonfailure
         raise e
 
 def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=None, append=False):
     """
     Write content of data to given fname, should defaultonfailure be not None,
     it is returned on failure to read.
 
     If directory of file is not present, it is auto-created.
     """
     dirname = os.path.dirname(fname)
     if not os.path.isdir(dirname):
         os.makedirs(dirname, mode=0o755, exist_ok=False)
         chown(dirname, user, group)
 
     try:
         """ Write a file to string """
         bytes = 0
         with open(fname, 'w' if not append else 'a') as f:
             bytes = f.write(data)
         chown(fname, user, group)
         chmod(fname, mode)
         return bytes
     except Exception as e:
         if defaultonfailure is not None:
             return defaultonfailure
         raise e
 
 def read_json(fname, defaultonfailure=None):
     """
     read and json decode the content of a file
     should defaultonfailure be not None, it is returned on failure to read
     """
     import json
     try:
         with open(fname, 'r') as f:
             data = json.load(f)
         return data
     except Exception as e:
         if defaultonfailure is not None:
             return defaultonfailure
         raise e
 
 def chown(path, user, group):
     """ change file/directory owner """
     from pwd import getpwnam
     from grp import getgrnam
 
     if user is None or group is None:
         return False
 
     # path may also be an open file descriptor
     if not isinstance(path, int) and not os.path.exists(path):
         return False
 
     uid = getpwnam(user).pw_uid
     gid = getgrnam(group).gr_gid
     os.chown(path, uid, gid)
     return True
 
 
 def chmod(path, bitmask):
     # path may also be an open file descriptor
     if not isinstance(path, int) and not os.path.exists(path):
         return
     if bitmask is None:
         return
     os.chmod(path, bitmask)
 
 
 def chmod_600(path):
     """ Make file only read/writable by owner """
     from stat import S_IRUSR, S_IWUSR
 
     bitmask = S_IRUSR | S_IWUSR
     chmod(path, bitmask)
 
 
 def chmod_750(path):
     """ Make file/directory only executable to user and group """
     from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP
 
     bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP
     chmod(path, bitmask)
 
 
 def chmod_755(path):
     """ Make file executable by all """
     from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH
 
     bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | \
               S_IROTH | S_IXOTH
     chmod(path, bitmask)
 
 def chmod_2775(path):
     """ user/group permissions with set-group-id bit set """
     from stat import S_ISGID, S_IRWXU, S_IRWXG, S_IROTH, S_IXOTH
 
     bitmask = S_ISGID | S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH
     chmod(path, bitmask)
 
+def chmod_775(path):
+    """ Make file executable by all """
+    from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IXOTH
+
+    bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | \
+              S_IROTH | S_IXOTH
+    chmod(path, bitmask)
+
 def makedir(path, user=None, group=None):
     if os.path.exists(path):
         return
     os.makedirs(path, mode=0o755)
     chown(path, user, group)
 
 def wait_for_inotify(file_path, pre_hook=None, event_type=None, timeout=None, sleep_interval=0.1):
     """ Waits for an inotify event to occur """
     if not os.path.dirname(file_path):
         raise ValueError(
           "File path {} does not have a directory part (required for inotify watching)".format(file_path))
     if not os.path.basename(file_path):
         raise ValueError(
           "File path {} does not have a file part, do not know what to watch for".format(file_path))
 
     from inotify.adapters import Inotify
     from time import time
     from time import sleep
 
     time_start = time()
 
     i = Inotify()
     i.add_watch(os.path.dirname(file_path))
 
     if pre_hook:
         pre_hook()
 
     for event in i.event_gen(yield_nones=True):
         if (timeout is not None) and ((time() - time_start) > timeout):
             # If the function didn't return until this point,
             # the file failed to have been written to and closed within the timeout
             raise OSError("Waiting for file {} to be written has failed".format(file_path))
 
         # Most such events don't take much time, so it's better to check right away
         # and sleep later.
         if event is not None:
             (_, type_names, path, filename) = event
             if filename == os.path.basename(file_path):
                 if event_type in type_names:
                     return
         sleep(sleep_interval)
 
 def wait_for_file_write_complete(file_path, pre_hook=None, timeout=None, sleep_interval=0.1):
     """ Waits for a process to close a file after opening it in write mode. """
     wait_for_inotify(file_path,
       event_type='IN_CLOSE_WRITE', pre_hook=pre_hook, timeout=timeout, sleep_interval=sleep_interval)
diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py
index f1b3bac73..997ee6309 100644
--- a/python/vyos/utils/network.py
+++ b/python/vyos/utils/network.py
@@ -1,522 +1,556 @@
 # 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/>.
 
 def _are_same_ip(one, two):
     from socket import AF_INET
     from socket import AF_INET6
     from socket import inet_pton
     from vyos.template import is_ipv4
     # compare the binary representation of the IP
     f_one = AF_INET if is_ipv4(one) else AF_INET6
     s_two = AF_INET if is_ipv4(two) else AF_INET6
     return inet_pton(f_one, one) == inet_pton(f_one, two)
 
 def get_protocol_by_name(protocol_name):
     """Get protocol number by protocol name
 
        % get_protocol_by_name('tcp')
        % 6
     """
     import socket
     try:
         protocol_number = socket.getprotobyname(protocol_name)
         return protocol_number
     except socket.error:
         return protocol_name
 
 def interface_exists(interface) -> bool:
     import os
     return os.path.exists(f'/sys/class/net/{interface}')
 
 def is_netns_interface(interface, netns):
     from vyos.utils.process import rc_cmd
     rc, out = rc_cmd(f'sudo ip netns exec {netns} ip link show dev {interface}')
     if rc == 0:
         return True
     return False
 
 def get_netns_all() -> list:
     from json import loads
     from vyos.utils.process import cmd
     tmp = loads(cmd('ip --json netns ls'))
     return [ netns['name'] for netns in tmp ]
 
 def get_vrf_members(vrf: str) -> list:
     """
     Get list of interface VRF members
     :param vrf: str
     :return: list
     """
     import json
     from vyos.utils.process import cmd
     interfaces = []
     try:
         if not interface_exists(vrf):
             raise ValueError(f'VRF "{vrf}" does not exist!')
         output = cmd(f'ip --json --brief link show vrf {vrf}')
         answer = json.loads(output)
         for data in answer:
             if 'ifname' in data:
                 interfaces.append(data.get('ifname'))
     except:
         pass
     return interfaces
 
 def get_interface_vrf(interface):
     """ Returns VRF of given interface """
     from vyos.utils.dict import dict_search
     from vyos.utils.network import get_interface_config
     tmp = get_interface_config(interface)
     if dict_search('linkinfo.info_slave_kind', tmp) == 'vrf':
         return tmp['master']
     return 'default'
 
 def get_interface_config(interface):
     """ Returns the used encapsulation protocol for given interface.
         If interface does not exist, None is returned.
     """
     if not interface_exists(interface):
         return None
     from json import loads
     from vyos.utils.process import cmd
     tmp = loads(cmd(f'ip --detail --json link show dev {interface}'))[0]
     return tmp
 
 def get_interface_address(interface):
     """ Returns the used encapsulation protocol for given interface.
         If interface does not exist, None is returned.
     """
     if not interface_exists(interface):
         return None
     from json import loads
     from vyos.utils.process import cmd
     tmp = loads(cmd(f'ip --detail --json addr show dev {interface}'))[0]
     return tmp
 
 def get_interface_namespace(interface: str):
     """
        Returns wich netns the interface belongs to
     """
     from json import loads
     from vyos.utils.process import cmd
 
     # Bail out early if netns does not exist
     tmp = cmd(f'ip --json netns ls')
     if not tmp: return None
 
     for ns in loads(tmp):
         netns = f'{ns["name"]}'
         # Search interface in each netns
         data = loads(cmd(f'ip netns exec {netns} ip --json link show'))
         for tmp in data:
             if interface == tmp["ifname"]:
                 return netns
 
 def is_ipv6_tentative(iface: str, ipv6_address: str) -> bool:
     """Check if IPv6 address is in tentative state.
 
     This function checks if an IPv6 address on a specific network interface is
     in the tentative state. IPv6 tentative addresses are not fully configured
     and are undergoing Duplicate Address Detection (DAD) to ensure they are
     unique on the network.
 
     Args:
         iface (str): The name of the network interface.
         ipv6_address (str): The IPv6 address to check.
 
     Returns:
         bool: True if the IPv6 address is tentative, False otherwise.
     """
     import json
     from vyos.utils.process import rc_cmd
 
     rc, out = rc_cmd(f'ip -6 --json address show dev {iface}')
     if rc:
         return False
 
     data = json.loads(out)
     for addr_info in data[0]['addr_info']:
         if (
             addr_info.get('local') == ipv6_address and
             addr_info.get('tentative', False)
         ):
             return True
     return False
 
 def is_wwan_connected(interface):
     """ Determine if a given WWAN interface, e.g. wwan0 is connected to the
     carrier network or not """
     import json
     from vyos.utils.process import cmd
 
     if not interface.startswith('wwan'):
         raise ValueError(f'Specified interface "{interface}" is not a WWAN interface')
 
     # ModemManager is required for connection(s) - if service is not running,
     # there won't be any connection at all!
     if not is_systemd_service_active('ModemManager.service'):
         return False
 
     modem = interface.lstrip('wwan')
 
     tmp = cmd(f'mmcli --modem {modem} --output-json')
     tmp = json.loads(tmp)
 
     # return True/False if interface is in connected state
     return dict_search('modem.generic.state', tmp) == 'connected'
 
 def get_bridge_fdb(interface):
     """ Returns the forwarding database entries for a given interface """
     if not interface_exists(interface):
         return None
     from json import loads
     from vyos.utils.process import cmd
     tmp = loads(cmd(f'bridge -j fdb show dev {interface}'))
     return tmp
 
 def get_all_vrfs():
     """ Return a dictionary of all system wide known VRF instances """
     from json import loads
     from vyos.utils.process import cmd
     tmp = loads(cmd('ip --json vrf list'))
     # Result is of type [{"name":"red","table":1000},{"name":"blue","table":2000}]
     # so we will re-arrange it to a more nicer representation:
     # {'red': {'table': 1000}, 'blue': {'table': 2000}}
     data = {}
     for entry in tmp:
         name = entry.pop('name')
         data[name] = entry
     return data
 
 def interface_list() -> list:
     from vyos.ifconfig import Section
     """
     Get list of interfaces in system
     :rtype: list
     """
     return Section.interfaces()
 
 
 def vrf_list() -> list:
     """
     Get list of VRFs in system
     :rtype: list
     """
     return list(get_all_vrfs().keys())
 
 def mac2eui64(mac, prefix=None):
     """
     Convert a MAC address to a EUI64 address or, with prefix provided, a full
     IPv6 address.
     Thankfully copied from https://gist.github.com/wido/f5e32576bb57b5cc6f934e177a37a0d3
     """
     import re
     from ipaddress import ip_network
     # http://tools.ietf.org/html/rfc4291#section-2.5.1
     eui64 = re.sub(r'[.:-]', '', mac).lower()
     eui64 = eui64[0:6] + 'fffe' + eui64[6:]
     eui64 = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:]
 
     if prefix is None:
         return ':'.join(re.findall(r'.{4}', eui64))
     else:
         try:
             net = ip_network(prefix, strict=False)
             euil = int('0x{0}'.format(eui64), 16)
             return str(net[euil])
         except:  # pylint: disable=bare-except
             return
 
 def check_port_availability(ipaddress, port, protocol):
     """
     Check if port is available and not used by any service
     Return False if a port is busy or IP address does not exists
     Should be used carefully for services that can start listening
     dynamically, because IP address may be dynamic too
     """
     from socketserver import TCPServer, UDPServer
     from ipaddress import ip_address
 
     # verify arguments
     try:
         ipaddress = ip_address(ipaddress).compressed
     except:
         raise ValueError(f'The {ipaddress} is not a valid IPv4 or IPv6 address')
     if port not in range(1, 65536):
         raise ValueError(f'The port number {port} is not in the 1-65535 range')
     if protocol not in ['tcp', 'udp']:
         raise ValueError(f'The protocol {protocol} is not supported. Only tcp and udp are allowed')
 
     # check port availability
     try:
         if protocol == 'tcp':
             server = TCPServer((ipaddress, port), None, bind_and_activate=True)
         if protocol == 'udp':
             server = UDPServer((ipaddress, port), None, bind_and_activate=True)
         server.server_close()
     except Exception as e:
         # errno.h:
         #define EADDRINUSE  98  /* Address already in use */
         if e.errno == 98:
             return False
 
     return True
 
 def is_listen_port_bind_service(port: int, service: str) -> bool:
     """Check if listen port bound to expected program name
     :param port: Bind port
     :param service: Program name
     :return: bool
 
     Example:
         % is_listen_port_bind_service(443, 'nginx')
         True
         % is_listen_port_bind_service(443, 'ocserv-main')
         False
     """
     from psutil import net_connections as connections
     from psutil import Process as process
     for connection in connections():
         addr = connection.laddr
         pid = connection.pid
         pid_name = process(pid).name()
         pid_port = addr.port
         if service == pid_name and port == pid_port:
             return True
     return False
 
 def is_ipv6_link_local(addr):
     """ Check if addrsss is an IPv6 link-local address. Returns True/False """
     from ipaddress import ip_interface
     from vyos.template import is_ipv6
     addr = addr.split('%')[0]
     if is_ipv6(addr):
         if ip_interface(addr).is_link_local:
             return True
 
     return False
 
 def is_addr_assigned(ip_address, vrf=None) -> bool:
     """ Verify if the given IPv4/IPv6 address is assigned to any interface """
     from netifaces import interfaces
     from vyos.utils.network import get_interface_config
     from vyos.utils.dict import dict_search
 
     for interface in interfaces():
         # Check if interface belongs to the requested VRF, if this is not the
         # case there is no need to proceed with this data set - continue loop
         # with next element
         tmp = get_interface_config(interface)
         if dict_search('master', tmp) != vrf:
             continue
 
         if is_intf_addr_assigned(interface, ip_address):
             return True
 
     return False
 
 def is_intf_addr_assigned(ifname: str, addr: str, netns: str=None) -> bool:
     """
     Verify if the given IPv4/IPv6 address is assigned to specific interface.
     It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR
     address 192.0.2.1/24.
     """
     import json
     import jmespath
 
     from vyos.utils.process import rc_cmd
     from ipaddress import ip_interface
 
     netns_cmd = f'ip netns exec {netns}' if netns else ''
     rc, out = rc_cmd(f'{netns_cmd} ip --json address show dev {ifname}')
     if rc == 0:
         json_out = json.loads(out)
         addresses = jmespath.search("[].addr_info[].{family: family, address: local, prefixlen: prefixlen}", json_out)
         for address_info in addresses:
             family = address_info['family']
             address = address_info['address']
             prefixlen = address_info['prefixlen']
             # Remove the interface name if present in the given address
             if '%' in addr:
                 addr = addr.split('%')[0]
             interface = ip_interface(f"{address}/{prefixlen}")
             if ip_interface(addr) == interface or address == addr:
                 return True
 
     return False
 
 def is_loopback_addr(addr):
     """ Check if supplied IPv4/IPv6 address is a loopback address """
     from ipaddress import ip_address
     return ip_address(addr).is_loopback
 
 def is_wireguard_key_pair(private_key: str, public_key:str) -> bool:
     """
      Checks if public/private keys are keypair
     :param private_key: Wireguard private key
     :type private_key: str
     :param public_key: Wireguard public key
     :type public_key: str
     :return: If public/private keys are keypair returns True else False
     :rtype: bool
     """
     from vyos.utils.process import cmd
     gen_public_key = cmd('wg pubkey', input=private_key)
     if gen_public_key == public_key:
         return True
     else:
         return False
 
 def is_subnet_connected(subnet, primary=False):
     """
     Verify is the given IPv4/IPv6 subnet is connected to any interface on this
     system.
 
     primary check if the subnet is reachable via the primary IP address of this
     interface, or in other words has a broadcast address configured. ISC DHCP
     for instance will complain if it should listen on non broadcast interfaces.
 
     Return True/False
     """
     from ipaddress import ip_address
     from ipaddress import ip_network
 
     from netifaces import ifaddresses
     from netifaces import interfaces
     from netifaces import AF_INET
     from netifaces import AF_INET6
 
     from vyos.template import is_ipv6
 
     # determine IP version (AF_INET or AF_INET6) depending on passed address
     addr_type = AF_INET
     if is_ipv6(subnet):
         addr_type = AF_INET6
 
     for interface in interfaces():
         # check if the requested address type is configured at all
         if addr_type not in ifaddresses(interface).keys():
             continue
 
         # An interface can have multiple addresses, but some software components
         # only support the primary address :(
         if primary:
             ip = ifaddresses(interface)[addr_type][0]['addr']
             if ip_address(ip) in ip_network(subnet):
                 return True
         else:
             # Check every assigned IP address if it is connected to the subnet
             # in question
             for ip in ifaddresses(interface)[addr_type]:
                 # remove interface extension (e.g. %eth0) that gets thrown on the end of _some_ addrs
                 addr = ip['addr'].split('%')[0]
                 if ip_address(addr) in ip_network(subnet):
                     return True
 
     return False
 
 def is_afi_configured(interface: str, afi):
     """ Check if given address family is configured, or in other words - an IP
     address is assigned to the interface. """
     from netifaces import ifaddresses
     from netifaces import AF_INET
     from netifaces import AF_INET6
 
     if afi not in [AF_INET, AF_INET6]:
         raise ValueError('Address family must be in [AF_INET, AF_INET6]')
 
     try:
         addresses = ifaddresses(interface)
     except ValueError as e:
         print(e)
         return False
 
     return afi in addresses
 
 def get_vxlan_vlan_tunnels(interface: str) -> list:
     """ Return a list of strings with VLAN IDs configured in the Kernel """
     from json import loads
     from vyos.utils.process import cmd
 
     if not interface.startswith('vxlan'):
         raise ValueError('Only applicable for VXLAN interfaces!')
 
     # Determine current OS Kernel configured VLANs
     #
     # $ bridge -j -p vlan tunnelshow dev vxlan0
     # [ {
     #         "ifname": "vxlan0",
     #         "tunnels": [ {
     #                 "vlan": 10,
     #                 "vlanEnd": 11,
     #                 "tunid": 10010,
     #                 "tunidEnd": 10011
     #             },{
     #                 "vlan": 20,
     #                 "tunid": 10020
     #             } ]
     #     } ]
     #
     os_configured_vlan_ids = []
     tmp = loads(cmd(f'bridge --json vlan tunnelshow dev {interface}'))
     if tmp:
         for tunnel in tmp[0].get('tunnels', {}):
             vlanStart = tunnel['vlan']
             if 'vlanEnd' in tunnel:
                 vlanEnd = tunnel['vlanEnd']
                 # Build a real list for user VLAN IDs
                 vlan_list = list(range(vlanStart, vlanEnd +1))
                 # Convert list of integers to list or strings
                 os_configured_vlan_ids.extend(map(str, vlan_list))
                 # Proceed with next tunnel - this one is complete
                 continue
 
             # Add single tunel id - not part of a range
             os_configured_vlan_ids.append(str(vlanStart))
 
     return os_configured_vlan_ids
 
 def get_vxlan_vni_filter(interface: str) -> list:
     """ Return a list of strings with VNIs configured in the Kernel"""
     from json import loads
     from vyos.utils.process import cmd
 
     if not interface.startswith('vxlan'):
         raise ValueError('Only applicable for VXLAN interfaces!')
 
     # Determine current OS Kernel configured VNI filters in VXLAN interface
     #
     # $ bridge -j vni show dev vxlan1
     # [{"ifname":"vxlan1","vnis":[{"vni":100},{"vni":200},{"vni":300,"vniEnd":399}]}]
     #
     # Example output: ['10010', '10020', '10021', '10022']
     os_configured_vnis = []
     tmp = loads(cmd(f'bridge --json vni show dev {interface}'))
     if tmp:
         for tunnel in tmp[0].get('vnis', {}):
             vniStart = tunnel['vni']
             if 'vniEnd' in tunnel:
                 vniEnd = tunnel['vniEnd']
                 # Build a real list for user VNIs
                 vni_list = list(range(vniStart, vniEnd +1))
                 # Convert list of integers to list or strings
                 os_configured_vnis.extend(map(str, vni_list))
                 # Proceed with next tunnel - this one is complete
                 continue
 
             # Add single tunel id - not part of a range
             os_configured_vnis.append(str(vniStart))
 
     return os_configured_vnis
+
+# Calculate prefix length of an IPv6 range, where possible
+# Python-ified from source: https://gitlab.isc.org/isc-projects/dhcp/-/blob/master/keama/confparse.c#L4591
+def ipv6_prefix_length(low, high):
+    import socket
+
+    bytemasks = [0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe, 0xff]
+
+    try:
+        lo = bytearray(socket.inet_pton(socket.AF_INET6, low))
+        hi = bytearray(socket.inet_pton(socket.AF_INET6, high))
+    except:
+        return None
+
+    xor = bytearray(a ^ b for a, b in zip(lo, hi))
+        
+    plen = 0
+    while plen < 128 and xor[plen // 8] == 0:
+        plen += 8
+        
+    if plen == 128:
+        return plen
+    
+    for i in range((plen // 8) + 1, 16):
+        if xor[i] != 0:
+            return None
+    
+    for i in range(8):
+        msk = ~xor[plen // 8] & 0xff
+        
+        if msk == bytemasks[i]:
+            return plen + i + 1
+
+    return None
diff --git a/smoketest/config-tests/dialup-router-medium-vpn b/smoketest/config-tests/dialup-router-medium-vpn
index e10adbbc6..039a50594 100644
--- a/smoketest/config-tests/dialup-router-medium-vpn
+++ b/smoketest/config-tests/dialup-router-medium-vpn
@@ -1,321 +1,316 @@
 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 enable-failover
 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 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-address '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-address '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-address '00:50:01:58:ac:95'
-set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping McPrintus static-mapping-parameters 'option domain-name-servers 192.168.0.6,192.168.0.17;'
 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-address '00:50:01:bc:ac:51'
-set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Mobile01 static-mapping-parameters 'option domain-name-servers 192.168.0.6,192.168.0.17;'
 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-address '00:50:01:70:b9:4d'
-set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera1 static-mapping-parameters 'option domain-name-servers 192.168.0.6,192.168.0.17;'
 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-address '00:50:01:70:b7:4f'
-set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera2 static-mapping-parameters 'option domain-name-servers 192.168.0.6,192.168.0.17;'
 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-address '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-address '00:50:01:af:c5:d2'
 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/configs/basic-vyos b/smoketest/configs/basic-vyos
index 78dba3ee2..fca4964bf 100644
--- a/smoketest/configs/basic-vyos
+++ b/smoketest/configs/basic-vyos
@@ -1,182 +1,200 @@
 interfaces {
     ethernet eth0 {
         address 192.168.0.1/24
+        address fe88::1/56
         duplex auto
         smp-affinity auto
         speed auto
     }
     ethernet eth1 {
         duplex auto
         smp-affinity auto
         speed auto
     }
     ethernet eth2 {
         duplex auto
         smp-affinity auto
         speed auto
         vif 100 {
             address 100.100.0.1/24
         }
         vif-s 200 {
             address 100.64.200.254/24
             vif-c 201 {
                 address 100.64.201.254/24
             }
             vif-c 202 {
                 address 100.64.202.254/24
             }
         }
     }
     loopback lo {
     }
 }
 protocols {
     static {
         arp 192.168.0.20 {
             hwaddr 00:50:00:00:00:20
         }
         arp 192.168.0.30 {
             hwaddr 00:50:00:00:00:30
         }
         arp 192.168.0.40 {
             hwaddr 00:50:00:00:00:40
         }
         arp 100.100.0.2 {
             hwaddr 00:50:00:00:02:02
         }
         arp 100.100.0.3 {
             hwaddr 00:50:00:00:02:03
         }
         arp 100.100.0.4 {
             hwaddr 00:50:00:00:02:04
         }
         arp 100.64.200.1 {
             hwaddr 00:50:00:00:00:01
         }
         arp 100.64.200.2 {
             hwaddr 00:50:00:00:00:02
         }
         arp 100.64.201.10 {
             hwaddr 00:50:00:00:00:10
         }
         arp 100.64.201.20 {
             hwaddr 00:50:00:00:00:20
         }
         arp 100.64.202.30 {
             hwaddr 00:50:00:00:00:30
         }
         arp 100.64.202.40 {
             hwaddr 00:50:00:00:00:40
         }
         route 0.0.0.0/0 {
             next-hop 100.64.0.1 {
             }
         }
     }
 }
 service {
     dhcp-server {
         shared-network-name LAN {
             authoritative
             subnet 192.168.0.0/24 {
                 default-router 192.168.0.1
                 dns-server 192.168.0.1
                 domain-name vyos.net
                 domain-search vyos.net
                 range LANDynamic {
                     start 192.168.0.20
                     stop 192.168.0.240
                 }
             }
         }
     }
+    dhcpv6-server {
+        shared-network-name LAN6 {
+            subnet fe88::/56 {
+                address-range {
+                    prefix fe88::/56 {
+                        temporary
+                    }
+                }
+                prefix-delegation {
+                    start fe88:0000:0000:0001:: {
+                        prefix-length 64
+                        stop fe88:0000:0000:0010::
+                    }
+                }
+            }
+        }
+    }
     dns {
         forwarding {
             allow-from 192.168.0.0/16
             cache-size 10000
             dnssec off
             listen-address 192.168.0.1
         }
     }
     ssh {
         ciphers aes128-ctr,aes192-ctr,aes256-ctr
         ciphers chacha20-poly1305@openssh.com,rijndael-cbc@lysator.liu.se
         listen-address 192.168.0.1
         key-exchange curve25519-sha256@libssh.org
         key-exchange diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group-exchange-sha256
         port 22
     }
 }
 system {
     config-management {
         commit-revisions 100
     }
     console {
         device ttyS0 {
             speed 115200
         }
     }
     conntrack {
         ignore {
             rule 1 {
                 destination {
                     address 192.0.2.2
                 }
                 source {
                     address 192.0.2.1
                 }
             }
         }
     }
     host-name vyos
     login {
         user vyos {
             authentication {
                 encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0
                 plaintext-password ""
             }
         }
     }
     name-server 192.168.0.1
     syslog {
         console {
             facility all {
                 level emerg
             }
             facility mail {
                 level info
             }
         }
         global {
             facility all {
                 level info
             }
             facility protocols {
                 level debug
             }
             facility security {
                 level info
             }
             preserve-fqdn
         }
         host syslog.vyos.net {
             facility local7 {
                 level notice
             }
             facility protocols {
                 level alert
             }
             facility security {
                 level warning
             }
             format {
                 octet-counted
             }
             port 8000
         }
     }
     time-zone Europe/Berlin
 }
 /* Warning: Do not remove the following line. */
 /* === vyatta-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@2:dhcp-server@5:dns-forwarding@1:firewall@5:ipsec@5:l2tp@1:mdns@1:nat@4:ntp@1:pptp@1:qos@1:quagga@6:snmp@1:ssh@1:system@9:vrrp@2:wanloadbalance@3:webgui@1:webproxy@1:webproxy@2:zone-policy@1" === */
 /* Release version: 1.2.6 */
diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py
index 093e43494..9f6e05ff3 100755
--- a/smoketest/scripts/cli/test_service_dhcp-server.py
+++ b/smoketest/scripts/cli/test_service_dhcp-server.py
@@ -1,489 +1,626 @@
 #!/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.dict import dict_search_recursive
 from vyos.utils.process import process_named_running
 from vyos.utils.file import read_file
 from vyos.template import address_from_cidr
 from vyos.template import inc_ip
 from vyos.template import dec_ip
 from vyos.template import netmask_from_cidr
 
-PROCESS_NAME = 'dhcpd'
-DHCPD_CONF = '/run/dhcp-server/dhcpd.conf'
+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']
 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()
 
         cidr_mask = subnet.split('/')[-1]
         cls.cli_set(cls, ['interfaces', 'dummy', 'dum8765', 'address', f'{router}/{cidr_mask}'])
 
     @classmethod
     def tearDownClass(cls):
         cls.cli_delete(cls, ['interfaces', 'dummy', 'dum8765'])
         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 + ['dynamic-dns-update'])
-
         pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
         # we use the first subnet IP address as default gateway
         self.cli_set(pool + ['default-router', router])
         self.cli_set(pool + ['name-server', dns_1])
         self.cli_set(pool + ['name-server', dns_2])
         self.cli_set(pool + ['domain-name', domain_name])
-        self.cli_set(pool + ['ping-check'])
 
         # 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(DHCPD_CONF)
-        network = address_from_cidr(subnet)
-        netmask = netmask_from_cidr(subnet)
-        self.assertIn(f'ddns-update-style interim;', config)
-        self.assertIn(f'subnet {network} netmask {netmask}' + r' {', config)
-        self.assertIn(f'option domain-name-servers {dns_1}, {dns_2};', config)
-        self.assertIn(f'option routers {router};', config)
-        self.assertIn(f'option domain-name "{domain_name}";', config)
-        self.assertIn(f'default-lease-time 86400;', config)
-        self.assertIn(f'max-lease-time 86400;', config)
-        self.assertIn(f'ping-check true;', config)
-        self.assertIn(f'range {range_0_start} {range_0_stop};', config)
-        self.assertIn(f'range {range_1_start} {range_1_stop};', config)
-        self.assertIn(f'set shared-networkname = "{shared_net_name}";', config)
+        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 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]
         # we use the first subnet IP address as default gateway
         self.cli_set(pool + ['default-router', router])
         self.cli_set(pool + ['name-server', dns_1])
         self.cli_set(pool + ['name-server', dns_2])
         self.cli_set(pool + ['domain-name', domain_name])
         self.cli_set(pool + ['ip-forwarding'])
         self.cli_set(pool + ['smtp-server', smtp_server])
         self.cli_set(pool + ['pop-server', smtp_server])
         self.cli_set(pool + ['time-server', time_server])
         self.cli_set(pool + ['tftp-server-name', tftp_server])
         for search in search_domains:
             self.cli_set(pool + ['domain-search', search])
         self.cli_set(pool + ['bootfile-name', bootfile_name])
         self.cli_set(pool + ['bootfile-server', bootfile_server])
         self.cli_set(pool + ['wpad-url', wpad])
         self.cli_set(pool + ['server-identifier', server_identifier])
 
         self.cli_set(pool + ['static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1'])
         self.cli_set(pool + ['ipv6-only-preferred', ipv6_only_preferred])
+        self.cli_set(pool + ['time-zone', 'Europe/London'])
 
         # 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])
 
         # commit changes
         self.cli_commit()
 
-        config = read_file(DHCPD_CONF)
-
-        network = address_from_cidr(subnet)
-        netmask = netmask_from_cidr(subnet)
-        self.assertIn(f'ddns-update-style none;', config)
-        self.assertIn(f'subnet {network} netmask {netmask}' + r' {', config)
-        self.assertIn(f'option domain-name-servers {dns_1}, {dns_2};', config)
-        self.assertIn(f'option routers {router};', config)
-        self.assertIn(f'option domain-name "{domain_name}";', config)
-
-        search = '"' + ('", "').join(search_domains) + '"'
-        self.assertIn(f'option domain-search {search};', config)
-
-        self.assertIn(f'option ip-forwarding true;', config)
-        self.assertIn(f'option smtp-server {smtp_server};', config)
-        self.assertIn(f'option pop-server {smtp_server};', config)
-        self.assertIn(f'option time-servers {time_server};', config)
-        self.assertIn(f'option wpad-url "{wpad}";', config)
-        self.assertIn(f'option dhcp-server-identifier {server_identifier};', config)
-        self.assertIn(f'option tftp-server-name "{tftp_server}";', config)
-        self.assertIn(f'option bootfile-name "{bootfile_name}";', config)
-        self.assertIn(f'filename "{bootfile_name}";', config)
-        self.assertIn(f'next-server {bootfile_server};', config)
-        self.assertIn(f'default-lease-time 86400;', config)
-        self.assertIn(f'max-lease-time 86400;', config)
-        self.assertIn(f'range {range_0_start} {range_0_stop};', config)
-        self.assertIn(f'set shared-networkname = "{shared_net_name}";', config)
-        self.assertIn(f'option rfc8925-ipv6-only-preferred {ipv6_only_preferred};', config)
-
-        # weird syntax for those static routes
-        self.assertIn(f'option rfc3442-static-route 24,10,0,0,192,0,2,1, 0,192,0,2,1;', config)
-        self.assertIn(f'option windows-static-route 24,10,0,0,192,0,2,1;', config)
+        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_static_mapping(self):
         shared_net_name = 'SMOKE-2'
         domain_name = 'private'
 
         pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
         # we use the first subnet IP address as default gateway
         self.cli_set(pool + ['default-router', router])
         self.cli_set(pool + ['name-server', dns_1])
         self.cli_set(pool + ['name-server', dns_2])
         self.cli_set(pool + ['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-address', 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(DHCPD_CONF)
-        network = address_from_cidr(subnet)
-        netmask = netmask_from_cidr(subnet)
-        self.assertIn(f'ddns-update-style none;', config)
-        self.assertIn(f'subnet {network} netmask {netmask}' + r' {', config)
-        self.assertIn(f'option domain-name-servers {dns_1}, {dns_2};', config)
-        self.assertIn(f'option routers {router};', config)
-        self.assertIn(f'option domain-name "{domain_name}";', config)
-        self.assertIn(f'default-lease-time 86400;', config)
-        self.assertIn(f'max-lease-time 86400;', config)
+        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 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.assertIn(f'host {shared_net_name}_{client}' + ' {', config)
-            self.assertIn(f'fixed-address {ip};', config)
-            self.assertIn(f'hardware ethernet {mac};', config)
-            client_base += 1
 
-        self.assertIn(f'set shared-networkname = "{shared_net_name}";', config)
+            self.verify_config_object(
+                    obj,
+                    ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'reservations'],
+                    {'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]
             # we use the first subnet IP address as default gateway
             self.cli_set(pool + ['default-router', router])
             self.cli_set(pool + ['name-server', dns_1])
             self.cli_set(pool + ['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-address', 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(DHCPD_CONF)
+        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)
 
-            network = address_from_cidr(subnet)
-            netmask = netmask_from_cidr(subnet)
-
-            self.assertIn(f'ddns-update-style none;', config)
-            self.assertIn(f'subnet {network} netmask {netmask}' + r' {', config)
-            self.assertIn(f'option domain-name-servers {dns_1};', config)
-            self.assertIn(f'option routers {router};', config)
-            self.assertIn(f'option domain-name "{domain_name}";', config)
-            self.assertIn(f'default-lease-time {lease_time};', config)
-            self.assertIn(f'max-lease-time {lease_time};', config)
-            self.assertIn(f'range {range_0_start} {range_0_stop};', config)
-            self.assertIn(f'range {range_1_start} {range_1_stop};', config)
-            self.assertIn(f'set shared-networkname = "{shared_net_name}";', config)
+            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'], '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.assertIn(f'host {shared_net_name}_{client}' + ' {', config)
-                self.assertIn(f'fixed-address {ip};', config)
-                self.assertIn(f'hardware ethernet {mac};', config)
+
+                self.verify_config_object(
+                        obj,
+                        ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'reservations'],
+                        {'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 + ['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()
 
-        # VErify
-        config = read_file(DHCPD_CONF)
-        network = address_from_cidr(subnet)
-        netmask = netmask_from_cidr(subnet)
+        config = read_file(KEA4_CONF)
+        obj = loads(config)
 
-        self.assertIn(f'subnet {network} netmask {netmask}' + r' {', config)
-        self.assertIn(f'option routers {router};', config)
-        self.assertIn(f'range {range_0_start} {range_0_stop};', 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 + ['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()
 
-        # Verify
-        config = read_file(DHCPD_CONF)
-        network = address_from_cidr(subnet)
-        netmask = netmask_from_cidr(subnet)
+        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)
 
-        self.assertIn(f'subnet {network} netmask {netmask}' + r' {', config)
-        self.assertIn(f'option routers {router};', config)
-        self.assertIn(f'range {range_0_start} {range_0_stop_excl};', config)
-        self.assertIn(f'range {range_0_start_excl} {range_0_stop};', config)
+        # 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 + ['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(DHCPD_CONF)
-        network = address_from_cidr(subnet)
-        netmask = netmask_from_cidr(subnet)
-        # Check the relay network
-        self.assertIn(f'subnet {network} netmask {netmask}' + r' { }', config)
-
-        relay_network = address_from_cidr(relay_subnet)
-        relay_netmask = netmask_from_cidr(relay_subnet)
-        self.assertIn(f'subnet {relay_network} netmask {relay_netmask}' + r' {', config)
-        self.assertIn(f'option routers {relay_router};', config)
-        self.assertIn(f'range {range_0_start} {range_0_stop};', config)
-
-        # Check for running process
-        self.assertTrue(process_named_running(PROCESS_NAME))
-
-    def test_dhcp_invalid_raw_options(self):
-        shared_net_name = 'SMOKE-5'
+        config = read_file(KEA4_CONF)
+        obj = loads(config)
 
-        range_0_start = inc_ip(subnet, 10)
-        range_0_stop  = inc_ip(subnet, 20)
+        self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'RELAY')
+        self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet)
 
-        pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
-        # we use the first subnet IP address as default gateway
-        self.cli_set(pool + ['default-router', router])
-        self.cli_set(pool + ['range', '0', 'start', range_0_start])
-        self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
+        # Verify options
+        self.verify_config_object(
+                obj,
+                ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+                {'name': 'routers', 'data': relay_router})
 
-        self.cli_set(base_path + ['global-parameters', 'this-is-crap'])
-        # check generate() - dhcpd should not acceot this garbage config
-        with self.assertRaises(ConfigSessionError):
-            self.cli_commit()
-        self.cli_delete(base_path + ['global-parameters'])
-
-        # commit changes
-        self.cli_commit()
+        # 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]
         # we use the first subnet IP address as default gateway
         self.cli_set(pool + ['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'])
 
-        # check validate() - failover needs to be enabled for at least one subnet
-        with self.assertRaises(ConfigSessionError):
-            self.cli_commit()
-        self.cli_set(pool + ['enable-failover'])
-
         # commit changes
         self.cli_commit()
 
-        config = read_file(DHCPD_CONF)
-
-        self.assertIn(f'failover peer "{failover_name}"' + r' {', config)
-        self.assertIn(f'primary;', config)
-        self.assertIn(f'mclt 1800;', config)
-        self.assertIn(f'mclt 1800;', config)
-        self.assertIn(f'split 128;', config)
-        self.assertIn(f'port 647;', config)
-        self.assertIn(f'peer port 647;', config)
-        self.assertIn(f'max-response-delay 30;', config)
-        self.assertIn(f'max-unacked-updates 10;', config)
-        self.assertIn(f'load balance max seconds 3;', config)
-        self.assertIn(f'address {failover_local};', config)
-        self.assertIn(f'peer address {failover_remote};', config)
-
-        network = address_from_cidr(subnet)
-        netmask = netmask_from_cidr(subnet)
-        self.assertIn(f'ddns-update-style none;', config)
-        self.assertIn(f'subnet {network} netmask {netmask}' + r' {', config)
-        self.assertIn(f'option routers {router};', config)
-        self.assertIn(f'range {range_0_start} {range_0_stop};', config)
-        self.assertIn(f'set shared-networkname = "{shared_net_name}";', config)
-        self.assertIn(f'failover peer "{failover_name}";', config)
-        self.assertIn(f'deny dynamic bootp clients;', config)
+        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 4d9dabc3f..175a67537 100755
--- a/smoketest/scripts/cli/test_service_dhcpv6-server.py
+++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py
@@ -1,182 +1,264 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2020-2022 VyOS maintainers and contributors
+# 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 = 'dhcpd'
-DHCPD_CONF = '/run/dhcp-server/dhcpdv6.conf'
+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()
         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])
 
         # 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']:
             cid = '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{}'.format(client_base)
             self.cli_set(pool + ['static-mapping', client, 'identifier', cid])
             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
 
         # commit changes
         self.cli_commit()
 
-        config = read_file(DHCPD_CONF)
-        self.assertIn(f'option dhcp6.preference {preference};', config)
-
-        self.assertIn(f'subnet6 {subnet}' + r' {', config)
-        search = '"' + '", "'.join(search_domains) + '"'
-        nissrv = ', '.join(nis_servers)
-        self.assertIn(f'range6 {range_start} {range_stop};', config)
-        self.assertIn(f'default-lease-time {lease_time};', config)
-        self.assertIn(f'default-lease-time {lease_time};', config)
-        self.assertIn(f'max-lease-time {max_lease_time};', config)
-        self.assertIn(f'min-lease-time {min_lease_time};', config)
-        self.assertIn(f'option dhcp6.domain-search {search};', config)
-        self.assertIn(f'option dhcp6.name-servers {dns_1}, {dns_2};', config)
-        self.assertIn(f'option dhcp6.nis-domain-name "{domain}";', config)
-        self.assertIn(f'option dhcp6.nis-servers {nissrv};', config)
-        self.assertIn(f'option dhcp6.nisp-domain-name "{domain}";', config)
-        self.assertIn(f'option dhcp6.nisp-servers {nissrv};', config)
-        self.assertIn(f'set shared-networkname = "{shared_net_name}";', config)
+        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'], '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']:
             cid = '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{}'.format(client_base)
             ip = inc_ip(subnet, client_base)
             prefix = inc_ip(subnet, client_base << 64) + '/64'
-            self.assertIn(f'host {shared_net_name}_{client}' + ' {', config)
-            self.assertIn(f'fixed-address6 {ip};', config)
-            self.assertIn(f'fixed-prefix6 {prefix};', config)
-            self.assertIn(f'host-identifier option dhcp6.client-id {cid};', config)
+
+            self.verify_config_object(
+                    obj,
+                    ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'reservations'],
+                    {'duid': cid, '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_stop = '2001:db8:ee:ff00::'
-        delegate_len = '56'
+        delegate_len = '64'
+        prefix_len = '56'
 
         pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
 
         self.cli_set(pool + ['address-range', 'start', range_start, 'stop', range_stop])
-        self.cli_set(pool + ['prefix-delegation', 'start', delegate_start, 'stop', delegate_stop])
-        self.cli_set(pool + ['prefix-delegation', 'start', delegate_start, 'prefix-length', delegate_len])
+        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(DHCPD_CONF)
-        self.assertIn(f'subnet6 {subnet}' + r' {', config)
-        self.assertIn(f'range6 {range_start} {range_stop};', config)
-        self.assertIn(f'prefix6 {delegate_start} {delegate_stop} /{delegate_len};', config)
+        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])
 
         # commit changes
         self.cli_commit()
 
-        config = read_file(DHCPD_CONF)
-        self.assertIn(f'option dhcp6.name-servers {ns_global_1}, {ns_global_2};', config)
-        self.assertIn(f'subnet6 {subnet}' + r' {', config)
-        self.assertIn(f'set shared-networkname = "{shared_net_name}";', config)
+        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_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/dhcp_server.py b/src/conf_mode/dhcp_server.py
index ac7d95632..66f7c8057 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -1,328 +1,384 @@
 #!/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 IPAddress
 from netaddr import IPRange
 from sys import exit
+from time import sleep
 
 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 write_file
 from vyos.utils.process import call
 from vyos.utils.process import run
 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()
 
-config_file = '/run/dhcp-server/dhcpd.conf'
-systemd_override = r'/run/systemd/system/isc-dhcp-server.service.d/10-override.conf'
+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/dhcp4.leases'
+
+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
 
 
     # 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():
             # 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!')
 
-            # DHCP failover needs at least one subnet that uses it
-            if 'enable_failover' in subnet_config:
-                if 'failover' not in dhcp:
-                    raise ConfigError(f'Can not enable failover for "{subnet}" in "{network}".\n' \
-                                      'Failover is not configured globally!')
-                failover_ok = True
-
             # 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)
                 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_address' not in mapping_config:
                             raise ConfigError(f'MAC address required for static mapping "{mapping}"\n' \
                                               f'within shared-network "{network}, {subnet}"!')
 
             # 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:
-        if not failover_ok:
-            raise ConfigError('DHCP failover must be enabled for at least one subnet!')
-
         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!')
 
     return None
 
 def generate(dhcp):
     # bail out early - looks like removal from running config
     if not dhcp or 'disable' in dhcp:
         return None
 
-    # Please see: https://vyos.dev/T1129 for quoting of the raw
-    # parameters we can pass to ISC DHCPd
-    tmp_file = '/tmp/dhcpd.conf'
-    render(tmp_file, 'dhcp-server/dhcpd.conf.j2', dhcp,
-           formater=lambda _: _.replace("&quot;", '"'))
-    # XXX: as we have the ability for a user to pass in "raw" options via VyOS
-    # CLI (see T3544) we now ask ISC dhcpd to test the newly rendered
-    # configuration
-    tmp = run(f'/usr/sbin/dhcpd -4 -q -t -cf {tmp_file}')
-    if tmp > 0:
-        if os.path.exists(tmp_file):
-            os.unlink(tmp_file)
-        raise ConfigError('Configuration file errors encountered - check your options!')
-
-    # Now that we know that the newly rendered configuration is "good" we can
-    # render the "real" configuration
-    render(config_file, 'dhcp-server/dhcpd.conf.j2', dhcp,
-           formater=lambda _: _.replace("&quot;", '"'))
-    render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp)
-
-    # Clean up configuration test file
-    if os.path.exists(tmp_file):
-        os.unlink(tmp_file)
+    dhcp['lease_file'] = lease_file
+    dhcp['machine'] = os.uname().machine
+
+    if not os.path.exists(lease_file):
+        write_file(lease_file, '', user='_kea', group='vyattacfg', mode=0o755)
+
+    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='_kea', mode=0o600)
+            write_file(cert_key_file, wrap_private_key(key_data), user='_kea', 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='_kea', mode=0o600)
+
+            dhcp['failover']['ca_cert_file'] = ca_cert_file
+
+    render(ctrl_config_file, 'dhcp-server/kea-ctrl-agent.conf.j2', dhcp)
+    render(config_file, 'dhcp-server/kea-dhcp4.conf.j2', dhcp)
 
     return None
 
 def apply(dhcp):
-    call('systemctl daemon-reload')
-    # bail out early - looks like removal from running config
+    services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server']
+
     if not dhcp or 'disable' in dhcp:
-        call('systemctl stop isc-dhcp-server.service')
+        for service in services:
+            call(f'systemctl stop {service}.service')
+
         if os.path.exists(config_file):
             os.unlink(config_file)
 
         return None
 
-    call('systemctl restart isc-dhcp-server.service')
+    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')
+
+    # op-mode needs ctrl socket permission change
+    i = 0
+    while not os.path.exists(ctrl_socket):
+        if i > 15:
+            break
+        i += 1
+        sleep(1)
+    chmod_775(ctrl_socket)
+
     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/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index 427001609..73a708ff5 100755
--- a/src/conf_mode/dhcpv6_server.py
+++ b/src/conf_mode/dhcpv6_server.py
@@ -1,195 +1,219 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2018-2022 VyOS maintainers and contributors
+# 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 time import sleep
 
 from vyos.config import Config
 from vyos.template import render
 from vyos.template import is_ipv6
 from vyos.utils.process import call
+from vyos.utils.file import chmod_775
+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/dhcp-server/dhcpdv6.conf'
+config_file = '/run/kea/kea-dhcp6.conf'
+ctrl_socket = '/run/kea/dhcp6-ctrl-socket'
+lease_file = '/config/dhcp6.leases'
 
 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 = []
     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 '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 'start' not in subnet_config['prefix_delegation']:
-                    raise ConfigError('prefix-delegation start address not defined!')
+                if 'prefix' not in subnet_config['prefix_delegation']:
+                    raise ConfigError('prefix-delegation prefix not defined!')
 
-                for prefix, prefix_config in subnet_config['prefix_delegation']['start'].items():
-                    if 'stop' not in prefix_config:
-                        raise ConfigError(f'Stop address of delegated IPv6 prefix range "{prefix}" '\
+                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 '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
 
-    render(config_file, 'dhcp-server/dhcpdv6.conf.j2', dhcpv6)
+    dhcpv6['lease_file'] = lease_file
+    dhcpv6['machine'] = os.uname().machine
+
+    if not os.path.exists(lease_file):
+        write_file(lease_file, '', user='_kea', group='vyattacfg', mode=0o755)
+
+    render(config_file, 'dhcp-server/kea-dhcp6.conf.j2', dhcpv6)
     return None
 
 def apply(dhcpv6):
     # bail out early - looks like removal from running config
-    service_name = 'isc-dhcp-server6.service'
+    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}')
+
+    # op-mode needs ctrl socket permission change
+    i = 0
+    while not os.path.exists(ctrl_socket):
+        if i > 15:
+            break
+        i += 1
+        sleep(1)
+    chmod_775(ctrl_socket)
+
     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/system-login.py b/src/conf_mode/system-login.py
index cd85a5066..aeac82462 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -1,419 +1,419 @@
 #!/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 os
 
 from passlib.hosts import linux_context
 from psutil import users
 from pwd import getpwall
 from pwd import getpwnam
 from sys import exit
 from time import sleep
 
 from vyos.config import Config
 from vyos.configverify import verify_vrf
 from vyos.defaults import directories
 from vyos.template import render
 from vyos.template import is_ipv4
 from vyos.utils.dict import dict_search
 from vyos.utils.process import cmd
 from vyos.utils.process import call
 from vyos.utils.process import rc_cmd
 from vyos.utils.process import run
 from vyos.utils.process import DEVNULL
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
 autologout_file = "/etc/profile.d/autologout.sh"
 limits_file = "/etc/security/limits.d/10-vyos.conf"
 radius_config_file = "/etc/pam_radius_auth.conf"
 tacacs_pam_config_file = "/etc/tacplus_servers"
 tacacs_nss_config_file = "/etc/tacplus_nss.conf"
 nss_config_file = "/etc/nsswitch.conf"
 
 # Minimum UID used when adding system users
 MIN_USER_UID: int = 1000
 # Maximim UID used when adding system users
 MAX_USER_UID: int = 59999
 # LOGIN_TIMEOUT from /etc/loign.defs minus 10 sec
 MAX_RADIUS_TIMEOUT: int = 50
 # MAX_RADIUS_TIMEOUT divided by 2 sec (minimum recomended timeout)
 MAX_RADIUS_COUNT: int = 8
 # Maximum number of supported TACACS servers
 MAX_TACACS_COUNT: int = 8
 
 # List of local user accounts that must be preserved
 SYSTEM_USER_SKIP_LIST: list = ['radius_user', 'radius_priv_user', 'tacacs0', 'tacacs1',
                               'tacacs2', 'tacacs3', 'tacacs4', 'tacacs5', 'tacacs6',
                               'tacacs7', 'tacacs8', 'tacacs9', 'tacacs10',' tacacs11',
                               'tacacs12', 'tacacs13', 'tacacs14', 'tacacs15']
 
 def get_local_users():
     """Return list of dynamically allocated users (see Debian Policy Manual)"""
     local_users = []
     for s_user in getpwall():
         if getpwnam(s_user.pw_name).pw_uid < MIN_USER_UID:
             continue
         if getpwnam(s_user.pw_name).pw_uid > MAX_USER_UID:
             continue
         if s_user.pw_name in SYSTEM_USER_SKIP_LIST:
             continue
         local_users.append(s_user.pw_name)
 
     return local_users
 
 def get_shadow_password(username):
     with open('/etc/shadow') as f:
         for user in f.readlines():
             items = user.split(":")
             if username == items[0]:
                 return items[1]
     return None
 
 def get_config(config=None):
     if config:
         conf = config
     else:
         conf = Config()
     base = ['system', 'login']
     login = conf.get_config_dict(base, key_mangling=('-', '_'),
                                  no_tag_node_value_mangle=True,
                                  get_first_key=True,
                                  with_recursive_defaults=True)
 
     # users no longer existing in the running configuration need to be deleted
     local_users = get_local_users()
     cli_users = []
     if 'user' in login:
         cli_users = list(login['user'])
 
     # prune TACACS global defaults if not set by user
     if login.from_defaults(['tacacs']):
         del login['tacacs']
     # same for RADIUS
     if login.from_defaults(['radius']):
         del login['radius']
 
     # create a list of all users, cli and users
     all_users = list(set(local_users + cli_users))
     # We will remove any normal users that dos not exist in the current
     # configuration. This can happen if user is added but configuration was not
     # saved and system is rebooted.
     rm_users = [tmp for tmp in all_users if tmp not in cli_users]
     if rm_users: login.update({'rm_users' : rm_users})
 
     return login
 
 def verify(login):
     if 'rm_users' in login:
         # This check is required as the script is also executed from vyos-router
         # init script and there is no SUDO_USER environment variable available
         # during system boot.
         if 'SUDO_USER' in os.environ:
             cur_user = os.environ['SUDO_USER']
             if cur_user in login['rm_users']:
                 raise ConfigError(f'Attempting to delete current user: {cur_user}')
 
     if 'user' in login:
         system_users = getpwall()
         for user, user_config in login['user'].items():
             # Linux system users range up until UID 1000, we can not create a
             # VyOS CLI user which already exists as system user
             for s_user in system_users:
                 if s_user.pw_name == user and s_user.pw_uid < MIN_USER_UID:
                     raise ConfigError(f'User "{user}" can not be created, conflict with local system account!')
 
             for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items():
                 if 'type' not in pubkey_options:
                     raise ConfigError(f'Missing type for public-key "{pubkey}"!')
                 if 'key' not in pubkey_options:
                     raise ConfigError(f'Missing key for public-key "{pubkey}"!')
 
     if {'radius', 'tacacs'} <= set(login):
         raise ConfigError('Using both RADIUS and TACACS at the same time is not supported!')
 
     # At lease one RADIUS server must not be disabled
     if 'radius' in login:
         if 'server' not in login['radius']:
             raise ConfigError('No RADIUS server defined!')
         sum_timeout: int = 0
         radius_servers_count: int = 0
         fail = True
         for server, server_config in dict_search('radius.server', login).items():
             if 'key' not in server_config:
                 raise ConfigError(f'RADIUS server "{server}" requires key!')
             if 'disable' not in server_config:
                 sum_timeout += int(server_config['timeout'])
                 radius_servers_count += 1
                 fail = False
 
         if fail:
             raise ConfigError('All RADIUS servers are disabled')
 
         if radius_servers_count > MAX_RADIUS_COUNT:
             raise ConfigError(f'Number of RADIUS servers exceeded maximum of {MAX_RADIUS_COUNT}!')
 
         if sum_timeout > MAX_RADIUS_TIMEOUT:
             raise ConfigError('Sum of RADIUS servers timeouts '
                               'has to be less or eq 50 sec')
 
         verify_vrf(login['radius'])
 
         if 'source_address' in login['radius']:
             ipv4_count = 0
             ipv6_count = 0
             for address in login['radius']['source_address']:
                 if is_ipv4(address): ipv4_count += 1
                 else:                ipv6_count += 1
 
             if ipv4_count > 1:
                 raise ConfigError('Only one IPv4 source-address can be set!')
             if ipv6_count > 1:
                 raise ConfigError('Only one IPv6 source-address can be set!')
 
     if 'tacacs' in login:
         tacacs_servers_count: int = 0
         fail = True
         for server, server_config in dict_search('tacacs.server', login).items():
             if 'key' not in server_config:
                 raise ConfigError(f'TACACS server "{server}" requires key!')
             if 'disable' not in server_config:
                 tacacs_servers_count += 1
                 fail = False
 
         if fail:
             raise ConfigError('All RADIUS servers are disabled')
 
         if tacacs_servers_count > MAX_TACACS_COUNT:
             raise ConfigError(f'Number of TACACS servers exceeded maximum of {MAX_TACACS_COUNT}!')
 
         verify_vrf(login['tacacs'])
 
     if 'max_login_session' in login and 'timeout' not in login:
         raise ConfigError('"login timeout" must be configured!')
 
     return None
 
 
 def generate(login):
     # calculate users encrypted password
     if 'user' in login:
         for user, user_config in login['user'].items():
             tmp = dict_search('authentication.plaintext_password', user_config)
             if tmp:
                 encrypted_password = linux_context.hash(tmp)
                 login['user'][user]['authentication']['encrypted_password'] = encrypted_password
                 del login['user'][user]['authentication']['plaintext_password']
 
                 # remove old plaintext password and set new encrypted password
                 env = os.environ.copy()
                 env['vyos_libexec_dir'] = directories['base']
 
                 # Set default commands for re-adding user with encrypted password
                 del_user_plain = f"system login user {user} authentication plaintext-password"
                 add_user_encrypt = f"system login user {user} authentication encrypted-password '{encrypted_password}'"
 
                 lvl = env['VYATTA_EDIT_LEVEL']
                 # We're in config edit level, for example "edit system login"
                 # Change default commands for re-adding user with encrypted password
                 if lvl != '/':
                     # Replace '/system/login' to 'system login'
                     lvl = lvl.strip('/').split('/')
                     # Convert command str to list
                     del_user_plain = del_user_plain.split()
                     # New command exclude level, for example "edit system login"
                     del_user_plain = del_user_plain[len(lvl):]
                     # Convert string to list
                     del_user_plain = " ".join(del_user_plain)
 
                     add_user_encrypt = add_user_encrypt.split()
                     add_user_encrypt = add_user_encrypt[len(lvl):]
                     add_user_encrypt = " ".join(add_user_encrypt)
 
                 ret, out = rc_cmd(f"/opt/vyatta/sbin/my_delete {del_user_plain}", env=env)
                 if ret: raise ConfigError(out)
                 ret, out = rc_cmd(f"/opt/vyatta/sbin/my_set {add_user_encrypt}", env=env)
                 if ret: raise ConfigError(out)
             else:
                 try:
                     if get_shadow_password(user) == dict_search('authentication.encrypted_password', user_config):
                         # If the current encrypted bassword matches the encrypted password
                         # from the config - do not update it. This will remove the encrypted
                         # value from the system logs.
                         #
                         # The encrypted password will be set only once during the first boot
                         # after an image upgrade.
                         del login['user'][user]['authentication']['encrypted_password']
                 except:
                     pass
 
     ### RADIUS based user authentication
     if 'radius' in login:
         render(radius_config_file, 'login/pam_radius_auth.conf.j2', login,
                    permission=0o600, user='root', group='root')
     else:
         if os.path.isfile(radius_config_file):
             os.unlink(radius_config_file)
 
     ### TACACS+ based user authentication
     if 'tacacs' in login:
         render(tacacs_pam_config_file, 'login/tacplus_servers.j2', login,
                    permission=0o644, user='root', group='root')
         render(tacacs_nss_config_file, 'login/tacplus_nss.conf.j2', login,
                    permission=0o644, user='root', group='root')
     else:
         if os.path.isfile(tacacs_pam_config_file):
             os.unlink(tacacs_pam_config_file)
         if os.path.isfile(tacacs_nss_config_file):
             os.unlink(tacacs_nss_config_file)
 
 
 
     # NSS must always be present on the system
     render(nss_config_file, 'login/nsswitch.conf.j2', login,
                permission=0o644, user='root', group='root')
 
     # /etc/security/limits.d/10-vyos.conf
     if 'max_login_session' in login:
         render(limits_file, 'login/limits.j2', login,
                    permission=0o644, user='root', group='root')
     else:
         if os.path.isfile(limits_file):
             os.unlink(limits_file)
 
     if 'timeout' in login:
         render(autologout_file, 'login/autologout.j2', login,
                    permission=0o755, user='root', group='root')
     else:
         if os.path.isfile(autologout_file):
             os.unlink(autologout_file)
 
     return None
 
 
 def apply(login):
     enable_otp = False
     if 'user' in login:
         for user, user_config in login['user'].items():
             # make new user using vyatta shell and make home directory (-m),
             # default group of 100 (users)
             command = 'useradd --create-home --no-user-group '
             # check if user already exists:
             if user in get_local_users():
                 # update existing account
                 command = 'usermod'
 
             # all accounts use /bin/vbash
             command += ' --shell /bin/vbash'
             # we need to use '' quotes when passing formatted data to the shell
             # else it will not work as some data parts are lost in translation
             tmp = dict_search('authentication.encrypted_password', user_config)
             if tmp: command += f" --password '{tmp}'"
 
             tmp = dict_search('full_name', user_config)
             if tmp: command += f" --comment '{tmp}'"
 
             tmp = dict_search('home_directory', user_config)
             if tmp: command += f" --home '{tmp}'"
             else: command += f" --home '/home/{user}'"
 
-            command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk {user}'
+            command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk,_kea {user}'
             try:
                 cmd(command)
 
                 # we should not rely on the value stored in
                 # user_config['home_directory'], as a crazy user will choose
                 # username root or any other system user which will fail.
                 #
                 # XXX: Should we deny using root at all?
                 home_dir = getpwnam(user).pw_dir
                 render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.j2',
                        user_config, permission=0o600,
                        formater=lambda _: _.replace("&quot;", '"'),
                        user=user, group='users')
 
             except Exception as e:
                 raise ConfigError(f'Adding user "{user}" raised exception: "{e}"')
 
             # Generate 2FA/MFA One-Time-Pad configuration
             if dict_search('authentication.otp.key', user_config):
                 enable_otp = True
                 render(f'{home_dir}/.google_authenticator', 'login/pam_otp_ga.conf.j2',
                        user_config, permission=0o400, user=user, group='users')
             else:
                 # delete configuration as it's not enabled for the user
                 if os.path.exists(f'{home_dir}/.google_authenticator'):
                     os.remove(f'{home_dir}/.google_authenticator')
 
     if 'rm_users' in login:
         for user in login['rm_users']:
             try:
                 # Disable user to prevent re-login
                 call(f'usermod -s /sbin/nologin {user}')
 
                 # Logout user if he is still logged in
                 if user in list(set([tmp[0] for tmp in users()])):
                     print(f'{user} is logged in, forcing logout!')
                     # re-run command until user is logged out
                     while run(f'pkill -HUP -u {user}'):
                         sleep(0.250)
 
                 # Remove user account but leave home directory in place. Re-run
                 # command until user is removed - userdel might return 8 as
                 # SSH sessions are not all yet properly cleaned away, thus we
                 # simply re-run the command until the account wen't away
                 while run(f'userdel {user}', stderr=DEVNULL):
                     sleep(0.250)
 
             except Exception as e:
                 raise ConfigError(f'Deleting user "{user}" raised exception: {e}')
 
     # Enable/disable RADIUS in PAM configuration
     cmd('pam-auth-update --disable radius-mandatory radius-optional')
     if 'radius' in login:
         if login['radius'].get('security_mode', '') == 'mandatory':
             pam_profile = 'radius-mandatory'
         else:
             pam_profile = 'radius-optional'
         cmd(f'pam-auth-update --enable {pam_profile}')
 
     # Enable/disable TACACS+ in PAM configuration
     cmd('pam-auth-update --disable tacplus-mandatory tacplus-optional')
     if 'tacacs' in login:
         if login['tacacs'].get('security_mode', '') == 'mandatory':
             pam_profile = 'tacplus-mandatory'
         else:
             pam_profile = 'tacplus-optional'
         cmd(f'pam-auth-update --enable {pam_profile}')
 
     # Enable/disable Google authenticator
     cmd('pam-auth-update --disable mfa-google-authenticator')
     if enable_otp:
         cmd(f'pam-auth-update --enable mfa-google-authenticator')
 
     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/etc/systemd/system/kea-ctrl-agent.service.d/override.conf b/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf
new file mode 100644
index 000000000..0f5bf801e
--- /dev/null
+++ b/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf
@@ -0,0 +1,9 @@
+[Unit]
+After=
+After=vyos-router.service
+
+[Service]
+ExecStart=
+ExecStart=/usr/sbin/kea-ctrl-agent -c /run/kea/kea-ctrl-agent.conf
+AmbientCapabilities=CAP_NET_BIND_SERVICE
+CapabilityBoundingSet=CAP_NET_BIND_SERVICE
diff --git a/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf b/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf
new file mode 100644
index 000000000..682e5bbce
--- /dev/null
+++ b/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf
@@ -0,0 +1,7 @@
+[Unit]
+After=
+After=vyos-router.service
+
+[Service]
+ExecStart=
+ExecStart=/usr/sbin/kea-dhcp4 -c /run/kea/kea-dhcp4.conf
diff --git a/src/etc/systemd/system/kea-dhcp6-server.service.d/override.conf b/src/etc/systemd/system/kea-dhcp6-server.service.d/override.conf
new file mode 100644
index 000000000..cb33fc057
--- /dev/null
+++ b/src/etc/systemd/system/kea-dhcp6-server.service.d/override.conf
@@ -0,0 +1,7 @@
+[Unit]
+After=
+After=vyos-router.service
+
+[Service]
+ExecStart=
+ExecStart=/usr/sbin/kea-dhcp6 -c /run/kea/kea-dhcp6.conf
diff --git a/src/migration-scripts/dhcp-server/6-to-7 b/src/migration-scripts/dhcp-server/6-to-7
new file mode 100755
index 000000000..ccf385a30
--- /dev/null
+++ b/src/migration-scripts/dhcp-server/6-to-7
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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/>.
+
+# T3316: Migrate to Kea
+#        - global-parameters will not function
+#        - shared-network-parameters will not function
+#        - subnet-parameters will not function
+#        - static-mapping-parameters will not function
+#        - host-decl-name is on by default, option removed
+#        - ping-check no longer supported
+#        - failover is default enabled on all subnets that exist on failover servers
+
+import sys
+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']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+    # Nothing to do
+    sys.exit(0)
+
+if config.exists(base + ['host-decl-name']):
+    config.delete(base + ['host-decl-name'])
+
+if config.exists(base + ['global-parameters']):
+    config.delete(base + ['global-parameters'])
+
+if config.exists(base + ['shared-network-name']):
+    for network in config.list_nodes(base + ['shared-network-name']):
+        base_network = base + ['shared-network-name', network]
+
+        if config.exists(base_network + ['ping-check']):
+            config.delete(base_network + ['ping-check'])
+
+        if config.exists(base_network + ['shared-network-parameters']):
+            config.delete(base_network +['shared-network-parameters'])
+
+        if not config.exists(base_network + ['subnet']):
+            continue
+
+        # Run this for every specified 'subnet'
+        for subnet in config.list_nodes(base_network + ['subnet']):
+            base_subnet = base_network + ['subnet', subnet]
+
+            if config.exists(base_subnet + ['enable-failover']):
+                config.delete(base_subnet + ['enable-failover'])
+
+            if config.exists(base_subnet + ['ping-check']):
+                config.delete(base_subnet + ['ping-check'])
+
+            if config.exists(base_subnet + ['subnet-parameters']):
+                config.delete(base_subnet + ['subnet-parameters'])
+
+            if config.exists(base_subnet + ['static-mapping']):
+                for mapping in config.list_nodes(base_subnet + ['static-mapping']):
+                    if config.exists(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters']):
+                        config.delete(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters'])
+
+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/dhcpv6-server/1-to-2 b/src/migration-scripts/dhcpv6-server/1-to-2
new file mode 100755
index 000000000..cc5a8900a
--- /dev/null
+++ b/src/migration-scripts/dhcpv6-server/1-to-2
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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/>.
+
+# T3316: Migrate to Kea
+# - Kea was meant to have support for key "prefix-highest" under PD which would allow an address range
+#   However this seems to have never been implemented. A conversion to prefix length is needed (where possible).
+#   Ref: https://lists.isc.org/pipermail/kea-users/2022-November/003686.html
+# - Remove prefix temporary value, convert to multi leafNode (https://kea.readthedocs.io/en/kea-2.2.0/arm/dhcp6-srv.html#dhcpv6-server-limitations)
+
+import sys
+from vyos.configtree import ConfigTree
+from vyos.utils.network import ipv6_prefix_length
+
+if (len(sys.argv) < 1):
+    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', 'dhcpv6-server', 'shared-network-name']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+    # Nothing to do
+    exit(0)
+
+for network in config.list_nodes(base):
+    if not config.exists(base + [network, 'subnet']):
+        continue
+
+    for subnet in config.list_nodes(base + [network, 'subnet']):
+        # Delete temporary value under address-range prefix, convert tagNode to leafNode multi
+        if config.exists(base + [network, 'subnet', subnet, 'address-range', 'prefix']):
+            prefix_base = base + [network, 'subnet', subnet, 'address-range', 'prefix']
+            prefixes = config.list_nodes(prefix_base)
+            
+            config.delete(prefix_base)
+
+            for prefix in prefixes:
+                config.set(prefix_base, value=prefix, replace=False)
+
+        if config.exists(base + [network, 'subnet', subnet, 'prefix-delegation', 'prefix']):
+            prefix_base = base + [network, 'subnet', subnet, 'prefix-delegation', 'prefix']
+
+            config.set(prefix_base)
+            config.set_tag(prefix_base)
+
+            for start in config.list_nodes(base + [network, 'subnet', subnet, 'prefix-delegation', 'start']):
+                path = base + [network, 'subnet', subnet, 'prefix-delegation', 'start', start]
+
+                delegated_length = config.return_value(path + ['prefix-length'])
+                stop = config.return_value(path + ['stop'])
+
+                prefix_length = ipv6_prefix_length(start, stop)
+
+                # This range could not be converted into a simple prefix length and must be skipped
+                if not prefix_length:
+                    continue
+
+                config.set(prefix_base + [start, 'delegated-length'], value=delegated_length)
+                config.set(prefix_base + [start, 'prefix-length'], value=prefix_length)
+
+            config.delete(base + [network, 'subnet', subnet, 'prefix-delegation', 'start'])
+
+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/op_mode/clear_dhcp_lease.py b/src/op_mode/clear_dhcp_lease.py
index f372d3af0..2c95a2b08 100755
--- a/src/op_mode/clear_dhcp_lease.py
+++ b/src/op_mode/clear_dhcp_lease.py
@@ -1,78 +1,89 @@
 #!/usr/bin/env python3
+#
+# 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 argparse
 import re
 
-from isc_dhcp_leases import Lease
-from isc_dhcp_leases import IscDhcpLeases
-
 from vyos.configquery import ConfigTreeQuery
+from vyos.kea import kea_parse_leases
 from vyos.utils.io import ask_yes_no
 from vyos.utils.process import call
 from vyos.utils.commit import commit_in_progress
 
+# TODO: Update to use Kea control socket command "lease4-del"
 
 config = ConfigTreeQuery()
 base = ['service', 'dhcp-server']
-lease_file = '/config/dhcpd.leases'
+lease_file = '/config/dhcp4.leases'
 
 
 def del_lease_ip(address):
     """
     Read lease_file and write data to this file
     without specific section "lease ip"
     Delete section "lease x.x.x.x { x;x;x; }"
     """
     with open(lease_file, encoding='utf-8') as f:
         data = f.read().rstrip()
-        lease_config_ip = '{(?P<config>[\s\S]+?)\n}'
-        pattern = rf"lease {address} {lease_config_ip}"
+        pattern = rf"^{address},[^\n]+\n"
         # Delete lease for ip block
         data = re.sub(pattern, '', data)
 
     # Write new data to original lease_file
     with open(lease_file, 'w', encoding='utf-8') as f:
         f.write(data)
 
 def is_ip_in_leases(address):
     """
     Return True if address found in the lease file
     """
-    leases = IscDhcpLeases(lease_file)
+    leases = kea_parse_leases(lease_file)
     lease_ips = []
-    for lease in leases.get():
-        lease_ips.append(lease.ip)
-    if address not in lease_ips:
-        print(f'Address "{address}" not found in "{lease_file}"')
-        return False
-    return True
-
+    for lease in leases:
+        if address == lease['address']:
+            return True
+    print(f'Address "{address}" not found in "{lease_file}"')
+    return False
 
 if not config.exists(base):
     print('DHCP-server not configured!')
     exit(0)
 
 if config.exists(base + ['failover']):
     print('Lease cannot be reset in failover mode!')
     exit(0)
 
 
 if __name__ == '__main__':
     parser = argparse.ArgumentParser()
     parser.add_argument('--ip', help='IPv4 address', action='store', required=True)
 
     args = parser.parse_args()
     address = args.ip
 
     if not is_ip_in_leases(address):
         exit(1)
 
     if commit_in_progress():
         print('Cannot clear DHCP lease while a commit is in progress')
         exit(1)
 
     if not ask_yes_no(f'This will restart DHCP server.\nContinue?'):
         exit(1)
     else:
         del_lease_ip(address)
-        call('systemctl restart isc-dhcp-server.service')
+        call('systemctl restart kea-dhcp4-server.service')
diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py
index d6b8aa0b8..bd2c522ca 100755
--- a/src/op_mode/dhcp.py
+++ b/src/op_mode/dhcp.py
@@ -1,406 +1,402 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2022-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
 import sys
 import typing
 
 from datetime import datetime
 from glob import glob
 from ipaddress import ip_address
-from isc_dhcp_leases import IscDhcpLeases
 from tabulate import tabulate
 
 import vyos.opmode
 
 from vyos.base import Warning
 from vyos.configquery import ConfigTreeQuery
 
+from vyos.kea import kea_get_active_config
+from vyos.kea import kea_get_pool_from_subnet_id
+from vyos.kea import kea_parse_leases
 from vyos.utils.dict import dict_search
 from vyos.utils.file import read_file
 from vyos.utils.process import cmd
 from vyos.utils.process import is_systemd_service_running
 
 time_string = "%a %b %d %H:%M:%S %Z %Y"
 
 config = ConfigTreeQuery()
 lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup']
 sort_valid_inet = ['end', 'mac', 'hostname', 'ip', 'pool', 'remaining', 'start', 'state']
 sort_valid_inet6 = ['end', 'iaid_duid', 'ip', 'last_communication', 'pool', 'remaining', 'state', 'type']
 
 ArgFamily = typing.Literal['inet', 'inet6']
 ArgState = typing.Literal['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup']
 ArgOrigin = typing.Literal['local', 'remote']
 
 def _utc_to_local(utc_dt):
     return datetime.fromtimestamp((datetime.fromtimestamp(utc_dt) - datetime(1970, 1, 1)).total_seconds())
 
 
 def _format_hex_string(in_str):
     out_str = ""
     # if input is divisible by 2, add : every 2 chars
     if len(in_str) > 0 and len(in_str) % 2 == 0:
         out_str = ':'.join(a+b for a,b in zip(in_str[::2], in_str[1::2]))
     else:
         out_str = in_str
 
     return out_str
 
 
 def _find_list_of_dict_index(lst, key='ip', value='') -> int:
     """
     Find the index entry of list of dict matching the dict value
     Exampe:
         % lst = [{'ip': '192.0.2.1'}, {'ip': '192.0.2.2'}]
         % _find_list_of_dict_index(lst, key='ip', value='192.0.2.2')
         % 1
     """
     idx = next((index for (index, d) in enumerate(lst) if d[key] == value), None)
     return idx
 
 
 def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], origin=None) -> list:
     """
     Get DHCP server leases
     :return list
     """
-    lease_file = '/config/dhcpdv6.leases' if family == 'inet6' else '/config/dhcpd.leases'
+    lease_file = '/config/dhcp6.leases' if family == 'inet6' else '/config/dhcp4.leases'
     data = []
-    leases = IscDhcpLeases(lease_file).get()
+    leases = kea_parse_leases(lease_file)
 
     if pool is None:
         pool = _get_dhcp_pools(family=family)
-        aux = False
     else:
         pool = [pool]
-        aux = True
-
-    ## Search leases for every pool
-    for pool_name in pool:
-        for lease in leases:
-            if lease.sets.get('shared-networkname', '') == pool_name or lease.sets.get('shared-networkname', '') == '':
-            #if lease.sets.get('shared-networkname', '') == pool_name:
-                data_lease = {}
-                data_lease['ip'] = lease.ip
-                data_lease['state'] = lease.binding_state
-                #data_lease['pool'] = pool_name if lease.sets.get('shared-networkname', '') != '' else 'Fail-Over Server'
-                data_lease['pool'] = lease.sets.get('shared-networkname', '')
-                data_lease['end'] = lease.end.timestamp() if lease.end else None
-                data_lease['origin'] = 'local' if data_lease['pool'] != '' else 'remote'
-
-                if family == 'inet':
-                    data_lease['mac'] = lease.ethernet
-                    data_lease['start'] = lease.start.timestamp()
-                    data_lease['hostname'] = lease.hostname
-
-                if family == 'inet6':
-                    data_lease['last_communication'] = lease.last_communication.timestamp()
-                    data_lease['iaid_duid'] = _format_hex_string(lease.host_identifier_string)
-                    lease_types_long = {'na': 'non-temporary', 'ta': 'temporary', 'pd': 'prefix delegation'}
-                    data_lease['type'] = lease_types_long[lease.type]
-
-                data_lease['remaining'] = '-'
-
-                if lease.end:
-                    data_lease['remaining'] = lease.end - datetime.utcnow()
-
-                    if data_lease['remaining'].days >= 0:
-                        # substraction gives us a timedelta object which can't be formatted with strftime
-                        # so we use str(), split gets rid of the microseconds
-                        data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0]
-
-                # Do not add old leases
-                if data_lease['remaining'] != '' and data_lease['state'] != 'free':
-                    if not state or data_lease['state'] in state or state == 'all':
-                        if not origin or data_lease['origin'] in origin:
-                            if not aux or (aux and data_lease['pool'] == pool_name):
-                                data.append(data_lease)
-
-                # deduplicate
-                checked = []
-                for entry in data:
-                    addr = entry.get('ip')
-                    if addr not in checked:
-                        checked.append(addr)
-                    else:
-                        idx = _find_list_of_dict_index(data, key='ip', value=addr)
-                        data.pop(idx)
+
+    inet_suffix = '6' if family == 'inet6' else '4'
+    active_config = kea_get_active_config(inet_suffix)
+
+    for lease in leases:
+        data_lease = {}
+        data_lease['ip'] = lease['address']
+        lease_state_long = {'0': 'active', '1': 'rejected', '2': 'expired'}
+        data_lease['state'] = lease_state_long[lease['state']]
+        data_lease['pool'] = kea_get_pool_from_subnet_id(active_config, inet_suffix, lease['subnet_id']) if active_config else '-'
+        data_lease['end'] = lease['expire_timestamp'].timestamp() if lease['expire_timestamp'] else None
+        data_lease['origin'] = 'local' # TODO: Determine remote in HA
+
+        if family == 'inet':
+            data_lease['mac'] = lease['hwaddr']
+            data_lease['start'] = lease['start_timestamp']
+            data_lease['hostname'] = lease['hostname']
+
+        if family == 'inet6':
+            data_lease['last_communication'] = lease['start_timestamp']
+            data_lease['iaid_duid'] = _format_hex_string(lease['duid'])
+            lease_types_long = {'0': 'non-temporary', '1': 'temporary', '2': 'prefix delegation'}
+            data_lease['type'] = lease_types_long[lease['lease_type']]
+
+        data_lease['remaining'] = '-'
+
+        if lease['expire']:
+            data_lease['remaining'] = lease['expire_timestamp'] - datetime.utcnow()
+
+            if data_lease['remaining'].days >= 0:
+                # substraction gives us a timedelta object which can't be formatted with strftime
+                # so we use str(), split gets rid of the microseconds
+                data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0]
+
+        # Do not add old leases
+        if data_lease['remaining'] != '' and data_lease['pool'] in pool and data_lease['state'] != 'free':
+            if not state or data_lease['state'] in state:
+                data.append(data_lease)
+
+        # deduplicate
+        checked = []
+        for entry in data:
+            addr = entry.get('ip')
+            if addr not in checked:
+                checked.append(addr)
+            else:
+                idx = _find_list_of_dict_index(data, key='ip', value=addr)
+                data.pop(idx)
 
     if sorted:
         if sorted == 'ip':
             data.sort(key = lambda x:ip_address(x['ip']))
         else:
             data.sort(key = lambda x:x[sorted])
     return data
 
 
 def _get_formatted_server_leases(raw_data, family='inet'):
     data_entries = []
     if family == 'inet':
         for lease in raw_data:
             ipaddr = lease.get('ip')
             hw_addr = lease.get('mac')
             state = lease.get('state')
-            start = lease.get('start')
+            start = lease.get('start').timestamp()
             start =  _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S')
             end = lease.get('end')
             end =  _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') if end else '-'
             remain = lease.get('remaining')
             pool = lease.get('pool')
             hostname = lease.get('hostname')
             origin = lease.get('origin')
             data_entries.append([ipaddr, hw_addr, state, start, end, remain, pool, hostname, origin])
 
         headers = ['IP Address', 'MAC address', 'State', 'Lease start', 'Lease expiration', 'Remaining', 'Pool',
                    'Hostname', 'Origin']
 
     if family == 'inet6':
         for lease in raw_data:
             ipaddr = lease.get('ip')
             state = lease.get('state')
-            start = lease.get('last_communication')
+            start = lease.get('last_communication').timestamp()
             start =  _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S')
             end = lease.get('end')
             end =  _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S')
             remain = lease.get('remaining')
             lease_type = lease.get('type')
             pool = lease.get('pool')
             host_identifier = lease.get('iaid_duid')
             data_entries.append([ipaddr, state, start, end, remain, lease_type, pool, host_identifier])
 
         headers = ['IPv6 address', 'State', 'Last communication', 'Lease expiration', 'Remaining', 'Type', 'Pool',
                    'IAID_DUID']
 
     output = tabulate(data_entries, headers, numalign='left')
     return output
 
 
 def _get_dhcp_pools(family='inet') -> list:
     v = 'v6' if family == 'inet6' else ''
     pools = config.list_nodes(f'service dhcp{v}-server shared-network-name')
     return pools
 
 
 def _get_pool_size(pool, family='inet'):
     v = 'v6' if family == 'inet6' else ''
     base = f'service dhcp{v}-server shared-network-name {pool}'
     size = 0
     subnets = config.list_nodes(f'{base} subnet')
     for subnet in subnets:
         if family == 'inet6':
             ranges = config.list_nodes(f'{base} subnet {subnet} address-range start')
         else:
             ranges = config.list_nodes(f'{base} subnet {subnet} range')
         for range in ranges:
             if family == 'inet6':
                 start = config.list_nodes(f'{base} subnet {subnet} address-range start')[0]
                 stop = config.value(f'{base} subnet {subnet} address-range start {start} stop')
             else:
                 start = config.value(f'{base} subnet {subnet} range {range} start')
                 stop = config.value(f'{base} subnet {subnet} range {range} stop')
             # Add +1 because both range boundaries are inclusive
             size += int(ip_address(stop)) - int(ip_address(start)) + 1
     return size
 
 
 def _get_raw_pool_statistics(family='inet', pool=None):
     if pool is None:
         pool = _get_dhcp_pools(family=family)
     else:
         pool = [pool]
 
     v = 'v6' if family == 'inet6' else ''
     stats = []
     for p in pool:
         subnet = config.list_nodes(f'service dhcp{v}-server shared-network-name {p} subnet')
         size = _get_pool_size(family=family, pool=p)
         leases = len(_get_raw_server_leases(family=family, pool=p))
         use_percentage = round(leases / size * 100) if size != 0 else 0
         pool_stats = {'pool': p, 'size': size, 'leases': leases,
                       'available': (size - leases), 'use_percentage': use_percentage, 'subnet': subnet}
         stats.append(pool_stats)
     return stats
 
 
 def _get_formatted_pool_statistics(pool_data, family='inet'):
     data_entries = []
     for entry in pool_data:
         pool = entry.get('pool')
         size = entry.get('size')
         leases = entry.get('leases')
         available = entry.get('available')
         use_percentage = entry.get('use_percentage')
         use_percentage = f'{use_percentage}%'
         data_entries.append([pool, size, leases, available, use_percentage])
 
     headers = ['Pool', 'Size','Leases', 'Available', 'Usage']
     output = tabulate(data_entries, headers, numalign='left')
     return output
 
 
 def _verify(func):
     """Decorator checks if DHCP(v6) config exists"""
     from functools import wraps
 
     @wraps(func)
     def _wrapper(*args, **kwargs):
         config = ConfigTreeQuery()
         family = kwargs.get('family')
         v = 'v6' if family == 'inet6' else ''
         unconf_message = f'DHCP{v} server is not configured'
         # Check if config does not exist
         if not config.exists(f'service dhcp{v}-server'):
             raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
         return func(*args, **kwargs)
     return _wrapper
 
 
 @_verify
 def show_pool_statistics(raw: bool, family: ArgFamily, pool: typing.Optional[str]):
     pool_data = _get_raw_pool_statistics(family=family, pool=pool)
     if raw:
         return pool_data
     else:
         return _get_formatted_pool_statistics(pool_data, family=family)
 
 
 @_verify
 def show_server_leases(raw: bool, family: ArgFamily, pool: typing.Optional[str],
                        sorted: typing.Optional[str], state: typing.Optional[ArgState],
                        origin: typing.Optional[ArgOrigin] ):
     # if dhcp server is down, inactive leases may still be shown as active, so warn the user.
-    v = '6' if family == 'inet6' else ''
-    service_name = 'DHCPv6' if family == 'inet6' else 'DHCP'
-    if not is_systemd_service_running(f'isc-dhcp-server{v}.service'):
-        Warning(f'{service_name} server is configured but not started. Data may be stale.')
+    v = '6' if family == 'inet6' else '4'
+    if not is_systemd_service_running(f'kea-dhcp{v}-server.service'):
+        Warning('DHCP server is configured but not started. Data may be stale.')
 
     v = 'v6' if family == 'inet6' else ''
     if pool and pool not in _get_dhcp_pools(family=family):
         raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!')
 
     if state and state not in lease_valid_states:
         raise vyos.opmode.IncorrectValue(f'DHCP{v} state "{state}" is invalid!')
 
     sort_valid = sort_valid_inet6 if family == 'inet6' else sort_valid_inet
     if sorted and sorted not in sort_valid:
         raise vyos.opmode.IncorrectValue(f'DHCP{v} sort "{sorted}" is invalid!')
 
     lease_data = _get_raw_server_leases(family=family, pool=pool, sorted=sorted, state=state, origin=origin)
     if raw:
         return lease_data
     else:
         return _get_formatted_server_leases(lease_data, family=family)
 
 
 def _get_raw_client_leases(family='inet', interface=None):
     from time import mktime
     from datetime import datetime
     from vyos.defaults import directories
     from vyos.utils.network import get_interface_vrf
 
     lease_dir = directories['isc_dhclient_dir']
     lease_files = []
     lease_data = []
 
     if interface:
         tmp = f'{lease_dir}/dhclient_{interface}.lease'
         if os.path.exists(tmp):
             lease_files.append(tmp)
     else:
         # All DHCP leases
         lease_files = glob(f'{lease_dir}/dhclient_*.lease')
 
     for lease in lease_files:
         tmp = {}
         with open(lease, 'r') as f:
             for line in f.readlines():
                 line = line.rstrip()
                 if 'last_update' not in tmp:
                     # ISC dhcp client contains least_update timestamp in human readable
                     # format this makes less sense for an API and also the expiry
                     # timestamp is provided in UNIX time. Convert string (e.g. Sun Jul
                     # 30 18:13:44 CEST 2023) to UNIX time (1690733624)
                     tmp.update({'last_update' : int(mktime(datetime.strptime(line, time_string).timetuple()))})
                     continue
 
                 k, v = line.split('=')
                 tmp.update({k : v.replace("'", "")})
 
         if 'interface' in tmp:
             vrf = get_interface_vrf(tmp['interface'])
             if vrf: tmp.update({'vrf' : vrf})
 
         lease_data.append(tmp)
 
     return lease_data
 
 def _get_formatted_client_leases(lease_data, family):
     from time import localtime
     from time import strftime
 
     from vyos.utils.network import is_intf_addr_assigned
 
     data_entries = []
     for lease in lease_data:
         if not lease.get('new_ip_address'):
             continue
         data_entries.append(["Interface", lease['interface']])
         if 'new_ip_address' in lease:
             tmp = '[Active]' if is_intf_addr_assigned(lease['interface'], lease['new_ip_address']) else '[Inactive]'
             data_entries.append(["IP address", lease['new_ip_address'], tmp])
         if 'new_subnet_mask' in lease:
             data_entries.append(["Subnet Mask", lease['new_subnet_mask']])
         if 'new_domain_name' in lease:
             data_entries.append(["Domain Name", lease['new_domain_name']])
         if 'new_routers' in lease:
             data_entries.append(["Router", lease['new_routers']])
         if 'new_domain_name_servers' in lease:
             data_entries.append(["Name Server", lease['new_domain_name_servers']])
         if 'new_dhcp_server_identifier' in lease:
             data_entries.append(["DHCP Server", lease['new_dhcp_server_identifier']])
         if 'new_dhcp_lease_time' in lease:
             data_entries.append(["DHCP Server", lease['new_dhcp_lease_time']])
         if 'vrf' in lease:
             data_entries.append(["VRF", lease['vrf']])
         if 'last_update' in lease:
             tmp = strftime(time_string, localtime(int(lease['last_update'])))
             data_entries.append(["Last Update", tmp])
         if 'new_expiry' in lease:
             tmp = strftime(time_string, localtime(int(lease['new_expiry'])))
             data_entries.append(["Expiry", tmp])
 
         # Add empty marker
         data_entries.append([''])
 
     output = tabulate(data_entries, tablefmt='plain')
 
     return output
 
 def show_client_leases(raw: bool, family: ArgFamily, interface: typing.Optional[str]):
     lease_data = _get_raw_client_leases(family=family, interface=interface)
     if raw:
         return lease_data
     else:
         return _get_formatted_client_leases(lease_data, family=family)
 
 if __name__ == '__main__':
     try:
         res = vyos.opmode.run(sys.modules[__name__])
         if res:
             print(res)
     except (ValueError, vyos.opmode.Error) as e:
         print(e)
         sys.exit(1)
diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh
index 49e53d7e1..7b25bf338 100755
--- a/src/system/on-dhcp-event.sh
+++ b/src/system/on-dhcp-event.sh
@@ -1,53 +1,53 @@
 #!/bin/bash
 
 # This script came from ubnt.com forum user "bradd" in the following post
 # http://community.ubnt.com/t5/EdgeMAX/Automatic-DNS-resolution-of-DHCP-client-names/td-p/651311
 # It has been modified by Ubiquiti to update the /etc/host file
 # instead of adding to the CLI.
 # Thanks to forum user "itsmarcos" for bug fix & improvements
 # Thanks to forum user "ruudboon" for multiple domain fix
 # Thanks to forum user "chibby85" for expire patch and static-mapping
 
 if [ $# -lt 5 ]; then
   echo Invalid args
   logger -s -t on-dhcp-event "Invalid args \"$@\""
   exit 1
 fi
 
 action=$1
-client_name=$2
-client_ip=$3
-client_mac=$4
-domain=$5
+client_name=$LEASE4_HOSTNAME
+client_ip=$LEASE4_ADDRESS
+client_mac=$LEASE4_HWADDR
+domain=$(echo "$client_name" | cut -d"." -f2-)
 hostsd_client="/usr/bin/vyos-hostsd-client"
 
 case "$action" in
-  commit) # add mapping for new lease
+  leases4_renew|lease4_recover) # add mapping for new lease
     if [ -z "$client_name" ]; then
         logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead"
         client_name=$(echo "client-"$client_mac | tr : -)
     fi
 
-    if [ "$domain" == "..YYZ!" ]; then
+    if [ -z "$domain" ]; then
         client_fqdn_name=$client_name
         client_search_expr=$client_name
     else
         client_fqdn_name=$client_name.$domain
         client_search_expr="$client_name\\.$domain"
     fi
     $hostsd_client --add-hosts "$client_fqdn_name,$client_ip" --tag "dhcp-server-$client_ip" --apply
     exit 0
     ;;
 
-  release) # delete mapping for released address
+  lease4_release|lease4_expire) # delete mapping for released address)
     $hostsd_client --delete-hosts --tag "dhcp-server-$client_ip" --apply
     exit 0
     ;;
 
   *)
     logger -s -t on-dhcp-event "Invalid command \"$1\""
     exit 1
     ;;
 esac
 
 exit 0
diff --git a/src/systemd/isc-dhcp-server6.service b/src/systemd/isc-dhcp-server6.service
deleted file mode 100644
index 1345c5fc5..000000000
--- a/src/systemd/isc-dhcp-server6.service
+++ /dev/null
@@ -1,24 +0,0 @@
-[Unit]
-Description=ISC DHCP IPv6 server
-Documentation=man:dhcpd(8)
-RequiresMountsFor=/run
-ConditionPathExists=/run/dhcp-server/dhcpdv6.conf
-After=vyos-router.service
-
-[Service]
-Type=forking
-WorkingDirectory=/run/dhcp-server
-RuntimeDirectory=dhcp-server
-RuntimeDirectoryPreserve=yes
-Environment=PID_FILE=/run/dhcp-server/dhcpdv6.pid CONFIG_FILE=/run/dhcp-server/dhcpdv6.conf LEASE_FILE=/config/dhcpdv6.leases
-PIDFile=/run/dhcp-server/dhcpdv6.pid
-ExecStartPre=/bin/sh -ec '\
-touch ${LEASE_FILE}; \
-chown nobody:nogroup ${LEASE_FILE}* ; \
-chmod 664 ${LEASE_FILE}* ; \
-/usr/sbin/dhcpd -6 -t -T -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} '
-ExecStart=/usr/sbin/dhcpd -6 -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE}
-Restart=always
-
-[Install]
-WantedBy=multi-user.target