From 4ef110fd2c501b718344c72d495ad7e16d2bd465 Mon Sep 17 00:00:00 2001
From: Christian Breunig <christian@breunig.cc>
Date: Sat, 30 Dec 2023 23:25:20 +0100
Subject: T5474: establish common file name pattern for XML conf mode commands

We will use _ as CLI level divider. The XML definition filename and also
the Python helper should match the CLI node.

Example:
set interfaces ethernet -> interfaces_ethernet.xml.in
set interfaces bond -> interfaces_bond.xml.in
set service dhcp-server -> service_dhcp-server-xml.in
---
 src/conf_mode/arp.py                               |  74 ---
 src/conf_mode/bcast_relay.py                       | 111 ----
 src/conf_mode/config_mgmt.py                       |  96 ---
 src/conf_mode/conntrack.py                         | 243 -------
 src/conf_mode/conntrack_sync.py                    | 141 ----
 src/conf_mode/dhcp_relay.py                        | 104 ---
 src/conf_mode/dhcp_server.py                       | 385 -----------
 src/conf_mode/dhcpv6_relay.py                      | 106 ---
 src/conf_mode/dhcpv6_server.py                     | 222 -------
 src/conf_mode/dns_dynamic.py                       | 187 ------
 src/conf_mode/dns_forwarding.py                    | 358 ----------
 src/conf_mode/firewall.py                          |   3 -
 src/conf_mode/flow_accounting_conf.py              | 320 ---------
 src/conf_mode/host_name.py                         | 188 ------
 src/conf_mode/https.py                             | 335 ----------
 src/conf_mode/igmp_proxy.py                        | 113 ----
 src/conf_mode/intel_qat.py                         | 106 ---
 src/conf_mode/interfaces-bonding.py                | 294 ---------
 src/conf_mode/interfaces-bridge.py                 | 186 ------
 src/conf_mode/interfaces-dummy.py                  |  76 ---
 src/conf_mode/interfaces-ethernet.py               | 400 -----------
 src/conf_mode/interfaces-geneve.py                 | 102 ---
 src/conf_mode/interfaces-input.py                  |  70 --
 src/conf_mode/interfaces-l2tpv3.py                 | 112 ----
 src/conf_mode/interfaces-loopback.py               |  66 --
 src/conf_mode/interfaces-macsec.py                 | 207 ------
 src/conf_mode/interfaces-openvpn.py                | 732 ---------------------
 src/conf_mode/interfaces-pppoe.py                  | 148 -----
 src/conf_mode/interfaces-pseudo-ethernet.py        | 107 ---
 src/conf_mode/interfaces-sstpc.py                  | 145 ----
 src/conf_mode/interfaces-tunnel.py                 | 224 -------
 src/conf_mode/interfaces-virtual-ethernet.py       | 114 ----
 src/conf_mode/interfaces-vti.py                    |  68 --
 src/conf_mode/interfaces-vxlan.py                  | 236 -------
 src/conf_mode/interfaces-wireguard.py              | 133 ----
 src/conf_mode/interfaces-wireless.py               | 275 --------
 src/conf_mode/interfaces-wwan.py                   | 189 ------
 src/conf_mode/interfaces_bonding.py                | 294 +++++++++
 src/conf_mode/interfaces_bridge.py                 | 186 ++++++
 src/conf_mode/interfaces_dummy.py                  |  76 +++
 src/conf_mode/interfaces_ethernet.py               | 400 +++++++++++
 src/conf_mode/interfaces_geneve.py                 | 102 +++
 src/conf_mode/interfaces_input.py                  |  70 ++
 src/conf_mode/interfaces_l2tpv3.py                 | 112 ++++
 src/conf_mode/interfaces_loopback.py               |  66 ++
 src/conf_mode/interfaces_macsec.py                 | 207 ++++++
 src/conf_mode/interfaces_openvpn.py                | 732 +++++++++++++++++++++
 src/conf_mode/interfaces_pppoe.py                  | 148 +++++
 src/conf_mode/interfaces_pseudo-ethernet.py        | 107 +++
 src/conf_mode/interfaces_sstpc.py                  | 145 ++++
 src/conf_mode/interfaces_tunnel.py                 | 224 +++++++
 src/conf_mode/interfaces_virtual-ethernet.py       | 114 ++++
 src/conf_mode/interfaces_vti.py                    |  68 ++
 src/conf_mode/interfaces_vxlan.py                  | 236 +++++++
 src/conf_mode/interfaces_wireguard.py              | 133 ++++
 src/conf_mode/interfaces_wireless.py               | 275 ++++++++
 src/conf_mode/interfaces_wwan.py                   | 189 ++++++
 src/conf_mode/le_cert.py                           | 115 ----
 src/conf_mode/lldp.py                              | 123 ----
 src/conf_mode/load-balancing-haproxy.py            | 169 -----
 src/conf_mode/load-balancing-wan.py                | 151 -----
 src/conf_mode/load-balancing_reverse-proxy.py      | 169 +++++
 src/conf_mode/load-balancing_wan.py                | 151 +++++
 src/conf_mode/ntp.py                               | 136 ----
 src/conf_mode/pki.py                               |   8 +-
 src/conf_mode/policy-local-route.py                | 315 ---------
 src/conf_mode/policy-route.py                      | 195 ------
 src/conf_mode/policy_local-route.py                | 315 +++++++++
 src/conf_mode/policy_route.py                      | 195 ++++++
 src/conf_mode/protocols_igmp-proxy.py              | 113 ++++
 src/conf_mode/protocols_segment-routing.py         | 118 ++++
 src/conf_mode/protocols_segment_routing.py         | 118 ----
 src/conf_mode/protocols_static_arp.py              |  74 +++
 src/conf_mode/salt-minion.py                       | 118 ----
 src/conf_mode/service_broadcast-relay.py           | 111 ++++
 src/conf_mode/service_config-sync.py               | 105 +++
 src/conf_mode/service_config_sync.py               | 105 ---
 src/conf_mode/service_conntrack-sync.py            | 141 ++++
 src/conf_mode/service_dhcp-relay.py                | 104 +++
 src/conf_mode/service_dhcp-server.py               | 385 +++++++++++
 src/conf_mode/service_dhcpv6-relay.py              | 106 +++
 src/conf_mode/service_dhcpv6-server.py             | 222 +++++++
 src/conf_mode/service_dns_dynamic.py               | 187 ++++++
 src/conf_mode/service_dns_forwarding.py            | 358 ++++++++++
 src/conf_mode/service_event-handler.py             |  92 +++
 src/conf_mode/service_event_handler.py             |  92 ---
 src/conf_mode/service_https.py                     | 335 ++++++++++
 .../service_https_certificates_certbot.py          | 114 ++++
 src/conf_mode/service_ids_ddos-protection.py       | 104 +++
 src/conf_mode/service_ids_fastnetmon.py            | 104 ---
 src/conf_mode/service_lldp.py                      | 123 ++++
 src/conf_mode/service_mdns-repeater.py             | 146 ----
 src/conf_mode/service_mdns_repeater.py             | 146 ++++
 src/conf_mode/service_ntp.py                       | 136 ++++
 src/conf_mode/service_salt-minion.py               | 118 ++++
 src/conf_mode/service_snmp.py                      | 269 ++++++++
 src/conf_mode/service_ssh.py                       | 142 ++++
 src/conf_mode/service_tftp-server.py               | 142 ++++
 src/conf_mode/snmp.py                              | 269 --------
 src/conf_mode/ssh.py                               | 142 ----
 src/conf_mode/system-ip.py                         | 143 ----
 src/conf_mode/system-ipv6.py                       | 120 ----
 src/conf_mode/system-login-banner.py               | 107 ---
 src/conf_mode/system-login.py                      | 423 ------------
 src/conf_mode/system-logs.py                       |  79 ---
 src/conf_mode/system-option.py                     | 159 -----
 src/conf_mode/system-proxy.py                      |  71 --
 src/conf_mode/system-syslog.py                     | 103 ---
 src/conf_mode/system-timezone.py                   |  61 --
 src/conf_mode/system_acceleration.py               | 106 +++
 src/conf_mode/system_config-management.py          |  96 +++
 src/conf_mode/system_conntrack.py                  | 243 +++++++
 src/conf_mode/system_flow-accounting.py            | 320 +++++++++
 src/conf_mode/system_host-name.py                  | 188 ++++++
 src/conf_mode/system_ip.py                         | 143 ++++
 src/conf_mode/system_ipv6.py                       | 120 ++++
 src/conf_mode/system_login.py                      | 423 ++++++++++++
 src/conf_mode/system_login_banner.py               | 107 +++
 src/conf_mode/system_logs.py                       |  79 +++
 src/conf_mode/system_option.py                     | 159 +++++
 src/conf_mode/system_proxy.py                      |  71 ++
 src/conf_mode/system_syslog.py                     | 103 +++
 src/conf_mode/system_task-scheduler.py             | 153 +++++
 src/conf_mode/system_timezone.py                   |  61 ++
 src/conf_mode/system_update-check.py               |  93 +++
 src/conf_mode/system_update_check.py               |  93 ---
 src/conf_mode/task_scheduler.py                    | 153 -----
 src/conf_mode/tftp_server.py                       | 142 ----
 .../ip-down.d/98-vyos-pppoe-cleanup-nameservers    |   1 -
 .../ppp/ip-up.d/98-vyos-pppoe-setup-nameservers    |   1 -
 src/init/vyos-router                               |  10 +-
 src/migration-scripts/https/1-to-2                 |   2 +-
 src/op_mode/connect_disconnect.py                  |   2 +-
 src/system/keepalived-fifo.py                      |   2 +-
 src/tests/test_task_scheduler.py                   |   8 +-
 135 files changed, 10940 insertions(+), 10946 deletions(-)
 delete mode 100755 src/conf_mode/arp.py
 delete mode 100755 src/conf_mode/bcast_relay.py
 delete mode 100755 src/conf_mode/config_mgmt.py
 delete mode 100755 src/conf_mode/conntrack.py
 delete mode 100755 src/conf_mode/conntrack_sync.py
 delete mode 100755 src/conf_mode/dhcp_relay.py
 delete mode 100755 src/conf_mode/dhcp_server.py
 delete mode 100755 src/conf_mode/dhcpv6_relay.py
 delete mode 100755 src/conf_mode/dhcpv6_server.py
 delete mode 100755 src/conf_mode/dns_dynamic.py
 delete mode 100755 src/conf_mode/dns_forwarding.py
 delete mode 100755 src/conf_mode/flow_accounting_conf.py
 delete mode 100755 src/conf_mode/host_name.py
 delete mode 100755 src/conf_mode/https.py
 delete mode 100755 src/conf_mode/igmp_proxy.py
 delete mode 100755 src/conf_mode/intel_qat.py
 delete mode 100755 src/conf_mode/interfaces-bonding.py
 delete mode 100755 src/conf_mode/interfaces-bridge.py
 delete mode 100755 src/conf_mode/interfaces-dummy.py
 delete mode 100755 src/conf_mode/interfaces-ethernet.py
 delete mode 100755 src/conf_mode/interfaces-geneve.py
 delete mode 100755 src/conf_mode/interfaces-input.py
 delete mode 100755 src/conf_mode/interfaces-l2tpv3.py
 delete mode 100755 src/conf_mode/interfaces-loopback.py
 delete mode 100755 src/conf_mode/interfaces-macsec.py
 delete mode 100755 src/conf_mode/interfaces-openvpn.py
 delete mode 100755 src/conf_mode/interfaces-pppoe.py
 delete mode 100755 src/conf_mode/interfaces-pseudo-ethernet.py
 delete mode 100755 src/conf_mode/interfaces-sstpc.py
 delete mode 100755 src/conf_mode/interfaces-tunnel.py
 delete mode 100755 src/conf_mode/interfaces-virtual-ethernet.py
 delete mode 100755 src/conf_mode/interfaces-vti.py
 delete mode 100755 src/conf_mode/interfaces-vxlan.py
 delete mode 100755 src/conf_mode/interfaces-wireguard.py
 delete mode 100755 src/conf_mode/interfaces-wireless.py
 delete mode 100755 src/conf_mode/interfaces-wwan.py
 create mode 100755 src/conf_mode/interfaces_bonding.py
 create mode 100755 src/conf_mode/interfaces_bridge.py
 create mode 100755 src/conf_mode/interfaces_dummy.py
 create mode 100755 src/conf_mode/interfaces_ethernet.py
 create mode 100755 src/conf_mode/interfaces_geneve.py
 create mode 100755 src/conf_mode/interfaces_input.py
 create mode 100755 src/conf_mode/interfaces_l2tpv3.py
 create mode 100755 src/conf_mode/interfaces_loopback.py
 create mode 100755 src/conf_mode/interfaces_macsec.py
 create mode 100755 src/conf_mode/interfaces_openvpn.py
 create mode 100755 src/conf_mode/interfaces_pppoe.py
 create mode 100755 src/conf_mode/interfaces_pseudo-ethernet.py
 create mode 100755 src/conf_mode/interfaces_sstpc.py
 create mode 100755 src/conf_mode/interfaces_tunnel.py
 create mode 100755 src/conf_mode/interfaces_virtual-ethernet.py
 create mode 100755 src/conf_mode/interfaces_vti.py
 create mode 100755 src/conf_mode/interfaces_vxlan.py
 create mode 100755 src/conf_mode/interfaces_wireguard.py
 create mode 100755 src/conf_mode/interfaces_wireless.py
 create mode 100755 src/conf_mode/interfaces_wwan.py
 delete mode 100755 src/conf_mode/le_cert.py
 delete mode 100755 src/conf_mode/lldp.py
 delete mode 100755 src/conf_mode/load-balancing-haproxy.py
 delete mode 100755 src/conf_mode/load-balancing-wan.py
 create mode 100755 src/conf_mode/load-balancing_reverse-proxy.py
 create mode 100755 src/conf_mode/load-balancing_wan.py
 delete mode 100755 src/conf_mode/ntp.py
 delete mode 100755 src/conf_mode/policy-local-route.py
 delete mode 100755 src/conf_mode/policy-route.py
 create mode 100755 src/conf_mode/policy_local-route.py
 create mode 100755 src/conf_mode/policy_route.py
 create mode 100755 src/conf_mode/protocols_igmp-proxy.py
 create mode 100755 src/conf_mode/protocols_segment-routing.py
 delete mode 100755 src/conf_mode/protocols_segment_routing.py
 create mode 100755 src/conf_mode/protocols_static_arp.py
 delete mode 100755 src/conf_mode/salt-minion.py
 create mode 100755 src/conf_mode/service_broadcast-relay.py
 create mode 100755 src/conf_mode/service_config-sync.py
 delete mode 100755 src/conf_mode/service_config_sync.py
 create mode 100755 src/conf_mode/service_conntrack-sync.py
 create mode 100755 src/conf_mode/service_dhcp-relay.py
 create mode 100755 src/conf_mode/service_dhcp-server.py
 create mode 100755 src/conf_mode/service_dhcpv6-relay.py
 create mode 100755 src/conf_mode/service_dhcpv6-server.py
 create mode 100755 src/conf_mode/service_dns_dynamic.py
 create mode 100755 src/conf_mode/service_dns_forwarding.py
 create mode 100755 src/conf_mode/service_event-handler.py
 delete mode 100755 src/conf_mode/service_event_handler.py
 create mode 100755 src/conf_mode/service_https.py
 create mode 100755 src/conf_mode/service_https_certificates_certbot.py
 create mode 100755 src/conf_mode/service_ids_ddos-protection.py
 delete mode 100755 src/conf_mode/service_ids_fastnetmon.py
 create mode 100755 src/conf_mode/service_lldp.py
 delete mode 100755 src/conf_mode/service_mdns-repeater.py
 create mode 100755 src/conf_mode/service_mdns_repeater.py
 create mode 100755 src/conf_mode/service_ntp.py
 create mode 100755 src/conf_mode/service_salt-minion.py
 create mode 100755 src/conf_mode/service_snmp.py
 create mode 100755 src/conf_mode/service_ssh.py
 create mode 100755 src/conf_mode/service_tftp-server.py
 delete mode 100755 src/conf_mode/snmp.py
 delete mode 100755 src/conf_mode/ssh.py
 delete mode 100755 src/conf_mode/system-ip.py
 delete mode 100755 src/conf_mode/system-ipv6.py
 delete mode 100755 src/conf_mode/system-login-banner.py
 delete mode 100755 src/conf_mode/system-login.py
 delete mode 100755 src/conf_mode/system-logs.py
 delete mode 100755 src/conf_mode/system-option.py
 delete mode 100755 src/conf_mode/system-proxy.py
 delete mode 100755 src/conf_mode/system-syslog.py
 delete mode 100755 src/conf_mode/system-timezone.py
 create mode 100755 src/conf_mode/system_acceleration.py
 create mode 100755 src/conf_mode/system_config-management.py
 create mode 100755 src/conf_mode/system_conntrack.py
 create mode 100755 src/conf_mode/system_flow-accounting.py
 create mode 100755 src/conf_mode/system_host-name.py
 create mode 100755 src/conf_mode/system_ip.py
 create mode 100755 src/conf_mode/system_ipv6.py
 create mode 100755 src/conf_mode/system_login.py
 create mode 100755 src/conf_mode/system_login_banner.py
 create mode 100755 src/conf_mode/system_logs.py
 create mode 100755 src/conf_mode/system_option.py
 create mode 100755 src/conf_mode/system_proxy.py
 create mode 100755 src/conf_mode/system_syslog.py
 create mode 100755 src/conf_mode/system_task-scheduler.py
 create mode 100755 src/conf_mode/system_timezone.py
 create mode 100755 src/conf_mode/system_update-check.py
 delete mode 100755 src/conf_mode/system_update_check.py
 delete mode 100755 src/conf_mode/task_scheduler.py
 delete mode 100755 src/conf_mode/tftp_server.py

(limited to 'src')

diff --git a/src/conf_mode/arp.py b/src/conf_mode/arp.py
deleted file mode 100755
index b141f1141..000000000
--- a/src/conf_mode/arp.py
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2022 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/>.
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import node_changed
-from vyos.utils.process import call
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base = ['protocols', 'static', 'arp']
-    arp = conf.get_config_dict(base, get_first_key=True)
-
-    if 'interface' in arp:
-        for interface in arp['interface']:
-            tmp = node_changed(conf, base + ['interface', interface, 'address'], recursive=True)
-            if tmp: arp['interface'][interface].update({'address_old' : tmp})
-
-    return arp
-
-def verify(arp):
-    pass
-
-def generate(arp):
-    pass
-
-def apply(arp):
-    if not arp:
-        return None
-
-    if 'interface' in arp:
-        for interface, interface_config in arp['interface'].items():
-            # Delete old static ARP assignments first
-            if 'address_old' in interface_config:
-                for address in interface_config['address_old']:
-                    call(f'ip neigh del {address} dev {interface}')
-
-            # Add new static ARP entries to interface
-            if 'address' not in interface_config:
-                continue
-            for address, address_config in interface_config['address'].items():
-                mac = address_config['mac']
-                call(f'ip neigh replace {address} lladdr {mac} dev {interface}')
-
-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/bcast_relay.py b/src/conf_mode/bcast_relay.py
deleted file mode 100755
index 31c552f5a..000000000
--- a/src/conf_mode/bcast_relay.py
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2017-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 glob import glob
-from netifaces import AF_INET
-from sys import exit
-
-from vyos.config import Config
-from vyos.configverify import verify_interface_exists
-from vyos.template import render
-from vyos.utils.process import call
-from vyos.utils.network import is_afi_configured
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file_base = r'/etc/default/udp-broadcast-relay'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['service', 'broadcast-relay']
-
-    relay = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
-    return relay
-
-def verify(relay):
-    if not relay or 'disabled' in relay:
-        return None
-
-    for instance, config in relay.get('id', {}).items():
-        # we don't have to check this instance when it's disabled
-        if 'disabled' in config:
-            continue
-
-        # we certainly require a UDP port to listen to
-        if 'port' not in config:
-            raise ConfigError(f'Port number is mandatory for UDP broadcast relay "{instance}"')
-
-        # Relaying data without two interface is kinda senseless ...
-        if len(config.get('interface', [])) < 2:
-            raise ConfigError('At least two interfaces are required for UDP broadcast relay "{instance}"')
-
-        for interface in config.get('interface', []):
-            verify_interface_exists(interface)
-            if not is_afi_configured(interface, AF_INET):
-                raise ConfigError(f'Interface "{interface}" has no IPv4 address configured!')
-
-    return None
-
-def generate(relay):
-    if not relay or 'disabled' in relay:
-        return None
-
-    for config in glob(config_file_base + '*'):
-        os.remove(config)
-
-    for instance, config in relay.get('id').items():
-        # we don't have to check this instance when it's disabled
-        if 'disabled' in config:
-            continue
-
-        config['instance'] = instance
-        render(config_file_base + instance, 'bcast-relay/udp-broadcast-relay.j2',
-               config)
-
-    return None
-
-def apply(relay):
-    # first stop all running services
-    call('systemctl stop udp-broadcast-relay@*.service')
-
-    if not relay or 'disable' in relay:
-        return None
-
-    # start only required service instances
-    for instance, config in relay.get('id').items():
-        # we don't have to check this instance when it's disabled
-        if 'disabled' in config:
-            continue
-
-        call(f'systemctl start udp-broadcast-relay@{instance}.service')
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/config_mgmt.py b/src/conf_mode/config_mgmt.py
deleted file mode 100755
index c681a8405..000000000
--- a/src/conf_mode/config_mgmt.py
+++ /dev/null
@@ -1,96 +0,0 @@
-#!/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/>.
-
-import os
-import sys
-
-from vyos import ConfigError
-from vyos.config import Config
-from vyos.config_mgmt import ConfigMgmt
-from vyos.config_mgmt import commit_post_hook_dir, commit_hooks
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base = ['system', 'config-management']
-    if not conf.exists(base):
-        return None
-
-    mgmt = ConfigMgmt(config=conf)
-
-    return mgmt
-
-def verify(_mgmt):
-    return
-
-def generate(mgmt):
-    if mgmt is None:
-        return
-
-    mgmt.initialize_revision()
-
-def apply(mgmt):
-    if mgmt is None:
-        return
-
-    locations = mgmt.locations
-    archive_target = os.path.join(commit_post_hook_dir,
-                               commit_hooks['commit_archive'])
-    if locations:
-        try:
-            os.symlink('/usr/bin/config-mgmt', archive_target)
-        except FileExistsError:
-            pass
-        except OSError as exc:
-            raise ConfigError from exc
-    else:
-        try:
-            os.unlink(archive_target)
-        except FileNotFoundError:
-            pass
-        except OSError as exc:
-            raise ConfigError from exc
-
-    revisions = mgmt.max_revisions
-    revision_target = os.path.join(commit_post_hook_dir,
-                               commit_hooks['commit_revision'])
-    if revisions > 0:
-        try:
-            os.symlink('/usr/bin/config-mgmt', revision_target)
-        except FileExistsError:
-            pass
-        except OSError as exc:
-            raise ConfigError from exc
-    else:
-        try:
-            os.unlink(revision_target)
-        except FileNotFoundError:
-            pass
-        except OSError as exc:
-            raise ConfigError from exc
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        sys.exit(1)
diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py
deleted file mode 100755
index 7f6c71440..000000000
--- a/src/conf_mode/conntrack.py
+++ /dev/null
@@ -1,243 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2021-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 re
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdep import set_dependents, call_dependents
-from vyos.utils.process import process_named_running
-from vyos.utils.dict import dict_search
-from vyos.utils.dict import dict_search_args
-from vyos.utils.dict import dict_search_recursive
-from vyos.utils.process import cmd
-from vyos.utils.process import rc_cmd
-from vyos.utils.process import run
-from vyos.template import render
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-conntrack_config = r'/etc/modprobe.d/vyatta_nf_conntrack.conf'
-sysctl_file = r'/run/sysctl/10-vyos-conntrack.conf'
-nftables_ct_file = r'/run/nftables-ct.conf'
-
-# Every ALG (Application Layer Gateway) consists of either a Kernel Object
-# also called a Kernel Module/Driver or some rules present in iptables
-module_map = {
-    'ftp': {
-        'ko': ['nf_nat_ftp', 'nf_conntrack_ftp'],
-        'nftables': ['ct helper set "ftp_tcp" tcp dport {21} return']
-    },
-    'h323': {
-        'ko': ['nf_nat_h323', 'nf_conntrack_h323'],
-        'nftables': ['ct helper set "ras_udp" udp dport {1719} return',
-                     'ct helper set "q931_tcp" tcp dport {1720} return']
-    },
-    'nfs': {
-        'nftables': ['ct helper set "rpc_tcp" tcp dport {111} return',
-                     'ct helper set "rpc_udp" udp dport {111} return']
-    },
-    'pptp': {
-        'ko': ['nf_nat_pptp', 'nf_conntrack_pptp'],
-        'nftables': ['ct helper set "pptp_tcp" tcp dport {1723} return'],
-        'ipv4': True
-     },
-    'sip': {
-        'ko': ['nf_nat_sip', 'nf_conntrack_sip'],
-        'nftables': ['ct helper set "sip_tcp" tcp dport {5060,5061} return',
-                     'ct helper set "sip_udp" udp dport {5060,5061} return']
-     },
-    'sqlnet': {
-        'nftables': ['ct helper set "tns_tcp" tcp dport {1521,1525,1536} return']
-    },
-    'tftp': {
-        'ko': ['nf_nat_tftp', 'nf_conntrack_tftp'],
-        'nftables': ['ct helper set "tftp_udp" udp dport {69} return']
-     },
-}
-
-valid_groups = [
-    'address_group',
-    'domain_group',
-    'network_group',
-    'port_group'
-]
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['system', 'conntrack']
-
-    conntrack = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                     get_first_key=True,
-                                     with_recursive_defaults=True)
-
-    conntrack['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'),
-                                                 get_first_key=True,
-                                                 no_tag_node_value_mangle=True)
-
-    conntrack['ipv4_nat_action'] = 'accept' if conf.exists(['nat']) else 'return'
-    conntrack['ipv6_nat_action'] = 'accept' if conf.exists(['nat66']) else 'return'
-    conntrack['wlb_action'] = 'accept' if conf.exists(['load-balancing', 'wan']) else 'return'
-    conntrack['wlb_local_action'] = conf.exists(['load-balancing', 'wan', 'enable-local-traffic'])
-
-    conntrack['module_map'] = module_map
-
-    if conf.exists(['service', 'conntrack-sync']):
-        set_dependents('conntrack_sync', conf)
-
-    return conntrack
-
-def verify(conntrack):
-    for inet in ['ipv4', 'ipv6']:
-        if dict_search_args(conntrack, 'ignore', inet, 'rule') != None:
-            for rule, rule_config in conntrack['ignore'][inet]['rule'].items():
-                if dict_search('destination.port', rule_config) or \
-                   dict_search('destination.group.port_group', rule_config) or \
-                   dict_search('source.port', rule_config) or \
-                   dict_search('source.group.port_group', rule_config):
-                   if 'protocol' not in rule_config or rule_config['protocol'] not in ['tcp', 'udp']:
-                       raise ConfigError(f'Port requires tcp or udp as protocol in rule {rule}')
-
-                tcp_flags = dict_search_args(rule_config, 'tcp', 'flags')
-                if tcp_flags:
-                    if dict_search_args(rule_config, 'protocol') != 'tcp':
-                        raise ConfigError('Protocol must be tcp when specifying tcp flags')
-
-                    not_flags = dict_search_args(rule_config, 'tcp', 'flags', 'not')
-                    if not_flags:
-                        duplicates = [flag for flag in tcp_flags if flag in not_flags]
-                        if duplicates:
-                            raise ConfigError(f'Cannot match a tcp flag as set and not set')
-
-                for side in ['destination', 'source']:
-                    if side in rule_config:
-                        side_conf = rule_config[side]
-
-                        if 'group' in side_conf:
-                            if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1:
-                                raise ConfigError('Only one address-group, network-group or domain-group can be specified')
-
-                            for group in valid_groups:
-                                if group in side_conf['group']:
-                                    group_name = side_conf['group'][group]
-                                    error_group = group.replace("_", "-")
-
-                                    if group in ['address_group', 'network_group', 'domain_group']:
-                                        if 'address' in side_conf:
-                                            raise ConfigError(f'{error_group} and address cannot both be defined')
-
-                                    if group_name and group_name[0] == '!':
-                                        group_name = group_name[1:]
-
-                                    if inet == 'ipv6':
-                                        group = f'ipv6_{group}'
-
-                                    group_obj = dict_search_args(conntrack['firewall'], 'group', group, group_name)
-
-                                    if group_obj is None:
-                                        raise ConfigError(f'Invalid {error_group} "{group_name}" on ignore rule')
-
-                                    if not group_obj:
-                                        Warning(f'{error_group} "{group_name}" has no members!')
-
-        if dict_search_args(conntrack, 'timeout', 'custom', inet, 'rule') != None:
-            for rule, rule_config in conntrack['timeout']['custom'][inet]['rule'].items():
-                if 'protocol' not in rule_config:
-                    raise ConfigError(f'Conntrack custom timeout rule {rule} requires protocol tcp or udp')
-                else:
-                    if 'tcp' in rule_config['protocol'] and 'udp' in rule_config['protocol']:
-                        raise ConfigError(f'conntrack custom timeout rule {rule} - Cant use both tcp and udp protocol')
-    return None
-
-def generate(conntrack):
-    if not os.path.exists(nftables_ct_file):
-        conntrack['first_install'] = True
-
-    # Determine if conntrack is needed
-    conntrack['ipv4_firewall_action'] = 'return'
-    conntrack['ipv6_firewall_action'] = 'return'
-
-    for rules, path in dict_search_recursive(conntrack['firewall'], 'rule'):
-        if any(('state' in rule_conf or 'connection_status' in rule_conf or 'offload_target' in rule_conf) for rule_conf in rules.values()):
-            if path[0] == 'ipv4':
-                conntrack['ipv4_firewall_action'] = 'accept'
-            elif path[0] == 'ipv6':
-                conntrack['ipv6_firewall_action'] = 'accept'
-
-    render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.j2', conntrack)
-    render(sysctl_file, 'conntrack/sysctl.conf.j2', conntrack)
-    render(nftables_ct_file, 'conntrack/nftables-ct.j2', conntrack)
-    return None
-
-def apply(conntrack):
-    # Depending on the enable/disable state of the ALG (Application Layer Gateway)
-    # modules we need to either insmod or rmmod the helpers.
-    
-    add_modules = []
-    rm_modules = []
-
-    for module, module_config in module_map.items():
-        if dict_search_args(conntrack, 'modules', module) is None:
-            if 'ko' in module_config:
-                unloaded = [mod for mod in module_config['ko'] if os.path.exists(f'/sys/module/{mod}')]
-                rm_modules.extend(unloaded)
-        else:
-            if 'ko' in module_config:
-                add_modules.extend(module_config['ko'])
-
-    # Add modules before nftables uses them
-    if add_modules:
-        module_str = ' '.join(add_modules)
-        cmd(f'modprobe -a {module_str}')
-
-    # Load new nftables ruleset
-    install_result, output = rc_cmd(f'nft -f {nftables_ct_file}')
-    if install_result == 1:
-        raise ConfigError(f'Failed to apply configuration: {output}')
-
-    # Remove modules after nftables stops using them
-    if rm_modules:
-        module_str = ' '.join(rm_modules)
-        cmd(f'rmmod {module_str}')
-
-    try:
-        call_dependents()
-    except ConfigError:
-        # Ignore config errors on dependent due to being called too early. Example:
-        # ConfigError("ConfigError('Interface ethN requires an IP address!')")
-        pass
-
-    # We silently ignore all errors
-    # See: https://bugzilla.redhat.com/show_bug.cgi?id=1264080
-    cmd(f'sysctl -f {sysctl_file}')
-
-    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/conntrack_sync.py b/src/conf_mode/conntrack_sync.py
deleted file mode 100755
index 4fb2ce27f..000000000
--- a/src/conf_mode/conntrack_sync.py
+++ /dev/null
@@ -1,141 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 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
-
-from sys import exit
-from vyos.config import Config
-from vyos.configverify import verify_interface_exists
-from vyos.utils.dict import dict_search
-from vyos.utils.process import process_named_running
-from vyos.utils.file import read_file
-from vyos.utils.process import call
-from vyos.utils.process import run
-from vyos.template import render
-from vyos.template import get_ipv4
-from vyos.utils.network import is_addr_assigned
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = '/run/conntrackd/conntrackd.conf'
-
-def resync_vrrp():
-    tmp = run('/usr/libexec/vyos/conf_mode/high-availability.py')
-    if tmp > 0:
-        print('ERROR: error restarting VRRP daemon!')
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['service', 'conntrack-sync']
-    if not conf.exists(base):
-        return None
-
-    conntrack = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                     get_first_key=True, with_defaults=True)
-
-    conntrack['hash_size'] = read_file('/sys/module/nf_conntrack/parameters/hashsize')
-    conntrack['table_size'] = read_file('/proc/sys/net/netfilter/nf_conntrack_max')
-
-    conntrack['vrrp'] = conf.get_config_dict(['high-availability', 'vrrp', 'sync-group'],
-                                     get_first_key=True)
-
-    return conntrack
-
-def verify(conntrack):
-    if not conntrack:
-        return None
-
-    if 'interface' not in conntrack:
-        raise ConfigError('Interface not defined!')
-
-    has_peer = False
-    for interface, interface_config in conntrack['interface'].items():
-        verify_interface_exists(interface)
-        # Interface must not only exist, it must also carry an IP address
-        if len(get_ipv4(interface)) < 1:
-            raise ConfigError(f'Interface {interface} requires an IP address!')
-        if 'peer' in interface_config:
-            has_peer = True
-
-    # If one interface runs in unicast mode instead of multicast, so must all the
-    # others, else conntrackd will error out with: "cannot use UDP with other
-    # dedicated link protocols"
-    if has_peer:
-        for interface, interface_config in conntrack['interface'].items():
-            if 'peer' not in interface_config:
-                raise ConfigError('Can not mix unicast and multicast mode!')
-
-    if 'expect_sync' in conntrack:
-        if len(conntrack['expect_sync']) > 1 and 'all' in conntrack['expect_sync']:
-            raise ConfigError('Can not configure expect-sync "all" with other protocols!')
-
-    if 'listen_address' in conntrack:
-        for address in conntrack['listen_address']:
-            if not is_addr_assigned(address):
-                raise ConfigError(f'Specified listen-address {address} not assigned to any interface!')
-
-    vrrp_group = dict_search('failover_mechanism.vrrp.sync_group', conntrack)
-    if vrrp_group == None:
-        raise ConfigError(f'No VRRP sync-group defined!')
-    if vrrp_group not in conntrack['vrrp']:
-        raise ConfigError(f'VRRP sync-group {vrrp_group} not configured!')
-
-    return None
-
-def generate(conntrack):
-    if not conntrack:
-        if os.path.isfile(config_file):
-            os.unlink(config_file)
-        return None
-
-    render(config_file, 'conntrackd/conntrackd.conf.j2', conntrack)
-
-    return None
-
-def apply(conntrack):
-    systemd_service = 'conntrackd.service'
-    if not conntrack:
-        # Failover mechanism daemon should be indicated that it no longer needs
-        # to execute conntrackd actions on transition. This is only required
-        # once when conntrackd is stopped and taken out of service!
-        if process_named_running('conntrackd'):
-            resync_vrrp()
-
-        call(f'systemctl stop {systemd_service}')
-        return None
-
-    # Failover mechanism daemon should be indicated that it needs to execute
-    # conntrackd actions on transition. This is only required once when conntrackd
-    # is started the first time!
-    if not process_named_running('conntrackd'):
-        resync_vrrp()
-
-    call(f'systemctl reload-or-restart {systemd_service}')
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py
deleted file mode 100755
index 37d708847..000000000
--- a/src/conf_mode/dhcp_relay.py
+++ /dev/null
@@ -1,104 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 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 sys import exit
-
-from vyos.base import Warning
-from vyos.config import Config
-from vyos.template import render
-from vyos.base import Warning
-from vyos.utils.process import call
-from vyos.utils.dict import dict_search
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/run/dhcp-relay/dhcrelay.conf'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['service', 'dhcp-relay']
-    if not conf.exists(base):
-        return None
-
-    relay = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                 get_first_key=True,
-                                 with_recursive_defaults=True)
-
-    return relay
-
-def verify(relay):
-    # bail out early - looks like removal from running config
-    if not relay or 'disable' in relay:
-        return None
-
-    if 'lo' in (dict_search('interface', relay) or []):
-        raise ConfigError('DHCP relay does not support the loopback interface.')
-
-    if 'server' not in relay :
-        raise ConfigError('No DHCP relay server(s) configured.\n' \
-                          'At least one DHCP relay server required.')
-
-    if 'interface' in relay:
-        Warning('DHCP relay interface is DEPRECATED - please use upstream-interface and listen-interface instead!')
-        if 'upstream_interface' in relay or 'listen_interface' in relay:
-            raise ConfigError('<interface> configuration is not compatible with upstream/listen interface')
-        else:
-            Warning('<interface> is going to be deprecated.\n'  \
-                    'Please use <listen-interface> and <upstream-interface>')
-
-    if 'upstream_interface' in relay and 'listen_interface' not in relay:
-        raise ConfigError('No listen-interface configured')
-    if 'listen_interface' in relay and 'upstream_interface' not in relay:
-        raise ConfigError('No upstream-interface configured')
-
-    return None
-
-def generate(relay):
-    # bail out early - looks like removal from running config
-    if not relay or 'disable' in relay:
-        return None
-
-    render(config_file, 'dhcp-relay/dhcrelay.conf.j2', relay)
-    return None
-
-def apply(relay):
-    # bail out early - looks like removal from running config
-    service_name = 'isc-dhcp-relay.service'
-    if not relay or 'disable' in relay:
-        call(f'systemctl stop {service_name}')
-        if os.path.exists(config_file):
-            os.unlink(config_file)
-        return None
-
-    call(f'systemctl restart {service_name}')
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
deleted file mode 100755
index 7ebc560ba..000000000
--- a/src/conf_mode/dhcp_server.py
+++ /dev/null
@@ -1,385 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2023 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import os
-
-from ipaddress import ip_address
-from ipaddress import ip_network
-from netaddr import IPRange
-from sys import exit
-
-from vyos.config import Config
-from vyos.pki import wrap_certificate
-from vyos.pki import wrap_private_key
-from vyos.template import render
-from vyos.utils.dict import dict_search
-from vyos.utils.dict import dict_search_args
-from vyos.utils.file import chmod_775
-from vyos.utils.file import makedir
-from vyos.utils.file import write_file
-from vyos.utils.process import call
-from vyos.utils.network import is_subnet_connected
-from vyos.utils.network import is_addr_assigned
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-ctrl_config_file = '/run/kea/kea-ctrl-agent.conf'
-ctrl_socket = '/run/kea/dhcp4-ctrl-socket'
-config_file = '/run/kea/kea-dhcp4.conf'
-lease_file = '/config/dhcp/dhcp4-leases.csv'
-systemd_override = r'/run/systemd/system/kea-ctrl-agent.service.d/10-override.conf'
-user_group = '_kea'
-
-ca_cert_file = '/run/kea/kea-failover-ca.pem'
-cert_file = '/run/kea/kea-failover.pem'
-cert_key_file = '/run/kea/kea-failover-key.pem'
-
-def dhcp_slice_range(exclude_list, range_dict):
-    """
-    This function is intended to slice a DHCP range. What does it mean?
-
-    Lets assume we have a DHCP range from '192.0.2.1' to '192.0.2.100'
-    but want to exclude address '192.0.2.74' and '192.0.2.75'. We will
-    pass an input 'range_dict' in the format:
-      {'start' : '192.0.2.1', 'stop' : '192.0.2.100' }
-    and we will receive an output list of:
-      [{'start' : '192.0.2.1' , 'stop' : '192.0.2.73'  },
-       {'start' : '192.0.2.76', 'stop' : '192.0.2.100' }]
-    The resulting list can then be used in turn to build the proper dhcpd
-    configuration file.
-    """
-    output = []
-    # exclude list must be sorted for this to work
-    exclude_list = sorted(exclude_list)
-    range_start = range_dict['start']
-    range_stop = range_dict['stop']
-    range_last_exclude = ''
-
-    for e in exclude_list:
-        if (ip_address(e) >= ip_address(range_start)) and \
-           (ip_address(e) <= ip_address(range_stop)):
-            range_last_exclude = e
-
-    for e in exclude_list:
-        if (ip_address(e) >= ip_address(range_start)) and \
-           (ip_address(e) <= ip_address(range_stop)):
-
-            # Build new address range ending one address before exclude address
-            r = {
-                'start' : range_start,
-                'stop' : str(ip_address(e) -1)
-            }
-            # On the next run our address range will start one address after
-            # the exclude address
-            range_start = str(ip_address(e) + 1)
-
-            # on subsequent exclude addresses we can not
-            # append them to our output
-            if not (ip_address(r['start']) > ip_address(r['stop'])):
-                # Everything is fine, add range to result
-                output.append(r)
-
-            # Take care of last IP address range spanning from the last exclude
-            # address (+1) to the end of the initial configured range
-            if ip_address(e) == ip_address(range_last_exclude):
-                r = {
-                  'start': str(ip_address(e) + 1),
-                  'stop': str(range_stop)
-                }
-                if not (ip_address(r['start']) > ip_address(r['stop'])):
-                    output.append(r)
-        else:
-          # if the excluded address was not part of the range, we simply return
-          # the entire ranga again
-          if not range_last_exclude:
-              if range_dict not in output:
-                  output.append(range_dict)
-
-    return output
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['service', 'dhcp-server']
-    if not conf.exists(base):
-        return None
-
-    dhcp = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                no_tag_node_value_mangle=True,
-                                get_first_key=True,
-                                with_recursive_defaults=True)
-
-    if 'shared_network_name' in dhcp:
-        for network, network_config in dhcp['shared_network_name'].items():
-            if 'subnet' in network_config:
-                for subnet, subnet_config in network_config['subnet'].items():
-                    # If exclude IP addresses are defined we need to slice them out of
-                    # the defined ranges
-                    if {'exclude', 'range'} <= set(subnet_config):
-                        new_range_id = 0
-                        new_range_dict = {}
-                        for r, r_config in subnet_config['range'].items():
-                            for slice in dhcp_slice_range(subnet_config['exclude'], r_config):
-                                new_range_dict.update({new_range_id : slice})
-                                new_range_id +=1
-
-                        dhcp['shared_network_name'][network]['subnet'][subnet].update(
-                                {'range' : new_range_dict})
-
-    if dict_search('failover.certificate', dhcp):
-        dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
-
-    return dhcp
-
-def verify(dhcp):
-    # bail out early - looks like removal from running config
-    if not dhcp or 'disable' in dhcp:
-        return None
-
-    # If DHCP is enabled we need one share-network
-    if 'shared_network_name' not in dhcp:
-        raise ConfigError('No DHCP shared networks configured.\n' \
-                          'At least one DHCP shared network must be configured.')
-
-    # Inspect shared-network/subnet
-    listen_ok = False
-    subnets = []
-    failover_ok = False
-    shared_networks =  len(dhcp['shared_network_name'])
-    disabled_shared_networks = 0
-
-
-    # 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!')
-
-            # 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' not in mapping_config and 'duid' not in mapping_config) or \
-                            ('mac' in mapping_config and 'duid' in mapping_config):
-                            raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for '
-                                              f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!')
-
-            # There must be one subnet connected to a listen interface.
-            # This only counts if the network itself is not disabled!
-            if 'disable' not in network_config:
-                if is_subnet_connected(subnet, primary=False):
-                    listen_ok = True
-
-            # Subnets must be non overlapping
-            if subnet in subnets:
-                raise ConfigError(f'Configured subnets must be unique! Subnet "{subnet}"\n'
-                                   'defined multiple times!')
-            subnets.append(subnet)
-
-            # Check for overlapping subnets
-            net = ip_network(subnet)
-            for n in subnets:
-                net2 = ip_network(n)
-                if (net != net2):
-                    if net.overlaps(net2):
-                        raise ConfigError(f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
-
-    # Prevent 'disable' for shared-network if only one network is configured
-    if (shared_networks - disabled_shared_networks) < 1:
-        raise ConfigError(f'At least one shared network must be active!')
-
-    if 'failover' in dhcp:
-        for key in ['name', 'remote', 'source_address', 'status']:
-            if key not in dhcp['failover']:
-                tmp = key.replace('_', '-')
-                raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!')
-
-        if len({'certificate', 'ca_certificate'} & set(dhcp['failover'])) == 1:
-            raise ConfigError(f'DHCP secured failover requires both certificate and CA certificate')
-
-        if 'certificate' in dhcp['failover']:
-            cert_name = dhcp['failover']['certificate']
-
-            if cert_name not in dhcp['pki']['certificate']:
-                raise ConfigError(f'Invalid certificate specified for DHCP failover')
-
-            if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'certificate'):
-                raise ConfigError(f'Invalid certificate specified for DHCP failover')
-
-            if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'private', 'key'):
-                raise ConfigError(f'Missing private key on certificate specified for DHCP failover')
-
-        if 'ca_certificate' in dhcp['failover']:
-            ca_cert_name = dhcp['failover']['ca_certificate']
-            if ca_cert_name not in dhcp['pki']['ca']:
-                raise ConfigError(f'Invalid CA certificate specified for DHCP failover')
-
-            if not dict_search_args(dhcp['pki']['ca'], ca_cert_name, 'certificate'):
-                raise ConfigError(f'Invalid CA certificate specified for DHCP failover')
-
-    for address in (dict_search('listen_address', dhcp) or []):
-        if is_addr_assigned(address):
-            listen_ok = True
-            # no need to probe further networks, we have one that is valid
-            continue
-        else:
-            raise ConfigError(f'listen-address "{address}" not configured on any interface')
-
-
-    if not listen_ok:
-        raise ConfigError('None of the configured subnets have an appropriate primary IP address on any\n'
-                          'broadcast interface configured, nor was there an explicit listen-address\n'
-                          'configured for serving DHCP relay packets!')
-
-    return None
-
-def generate(dhcp):
-    # bail out early - looks like removal from running config
-    if not dhcp or 'disable' in dhcp:
-        return None
-
-    dhcp['lease_file'] = lease_file
-    dhcp['machine'] = os.uname().machine
-
-    # Create directory for lease file if necessary
-    lease_dir = os.path.dirname(lease_file)
-    if not os.path.isdir(lease_dir):
-        makedir(lease_dir, group='vyattacfg')
-        chmod_775(lease_dir)
-
-    # Create lease file if necessary and let kea own it - 'kea-lfc' expects it that way
-    if not os.path.exists(lease_file):
-        write_file(lease_file, '', user=user_group, group=user_group, mode=0o644)
-
-    for f in [cert_file, cert_key_file, ca_cert_file]:
-        if os.path.exists(f):
-            os.unlink(f)
-
-    if 'failover' in dhcp:
-        if 'certificate' in dhcp['failover']:
-            cert_name = dhcp['failover']['certificate']
-            cert_data = dhcp['pki']['certificate'][cert_name]['certificate']
-            key_data = dhcp['pki']['certificate'][cert_name]['private']['key']
-            write_file(cert_file, wrap_certificate(cert_data), user=user_group, mode=0o600)
-            write_file(cert_key_file, wrap_private_key(key_data), user=user_group, mode=0o600)
-
-            dhcp['failover']['cert_file'] = cert_file
-            dhcp['failover']['cert_key_file'] = cert_key_file
-
-        if 'ca_certificate' in dhcp['failover']:
-            ca_cert_name = dhcp['failover']['ca_certificate']
-            ca_cert_data = dhcp['pki']['ca'][ca_cert_name]['certificate']
-            write_file(ca_cert_file, wrap_certificate(ca_cert_data), user=user_group, mode=0o600)
-
-            dhcp['failover']['ca_cert_file'] = ca_cert_file
-
-        render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp)
-
-    render(ctrl_config_file, 'dhcp-server/kea-ctrl-agent.conf.j2', dhcp, user=user_group, group=user_group)
-    render(config_file, 'dhcp-server/kea-dhcp4.conf.j2', dhcp, user=user_group, group=user_group)
-
-    return None
-
-def apply(dhcp):
-    services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server']
-
-    if not dhcp or 'disable' in dhcp:
-        for service in services:
-            call(f'systemctl stop {service}.service')
-
-        if os.path.exists(config_file):
-            os.unlink(config_file)
-
-        return None
-
-    for service in services:
-        action = 'restart'
-
-        if service == 'kea-dhcp-ddns-server' and 'dynamic_dns_update' not in dhcp:
-            action = 'stop'
-
-        if service == 'kea-ctrl-agent' and 'failover' not in dhcp:
-            action = 'stop'
-
-        call(f'systemctl {action} {service}.service')
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py
deleted file mode 100755
index 6537ca3c2..000000000
--- a/src/conf_mode/dhcpv6_relay.py
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 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 sys import exit
-
-from vyos.config import Config
-from vyos.ifconfig import Interface
-from vyos.template import render
-from vyos.template import is_ipv6
-from vyos.utils.process import call
-from vyos.utils.network import is_ipv6_link_local
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = '/run/dhcp-relay/dhcrelay6.conf'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['service', 'dhcpv6-relay']
-    if not conf.exists(base):
-        return None
-
-    relay = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                 get_first_key=True,
-                                 with_recursive_defaults=True)
-
-    return relay
-
-def verify(relay):
-    # bail out early - looks like removal from running config
-    if not relay or 'disable' in relay:
-        return None
-
-    if 'upstream_interface' not in relay:
-        raise ConfigError('At least one upstream interface required!')
-    for interface, config in relay['upstream_interface'].items():
-        if 'address' not in config:
-            raise ConfigError('DHCPv6 server required for upstream ' \
-                              f'interface {interface}!')
-
-    if 'listen_interface' not in relay:
-        raise ConfigError('At least one listen interface required!')
-
-    # DHCPv6 relay requires at least one global unicat address assigned to the
-    # interface
-    for interface in relay['listen_interface']:
-        has_global = False
-        for addr in Interface(interface).get_addr():
-            if is_ipv6(addr) and not is_ipv6_link_local(addr):
-                has_global = True
-        if not has_global:
-            raise ConfigError(f'Interface {interface} does not have global '\
-                              'IPv6 address assigned!')
-
-    return None
-
-def generate(relay):
-    # bail out early - looks like removal from running config
-    if not relay or 'disable' in relay:
-        return None
-
-    render(config_file, 'dhcp-relay/dhcrelay6.conf.j2', relay)
-    return None
-
-def apply(relay):
-    # bail out early - looks like removal from running config
-    service_name = 'isc-dhcp-relay6.service'
-    if not relay or 'disable' in relay:
-        # DHCPv6 relay support is removed in the commit
-        call(f'systemctl stop {service_name}')
-        if os.path.exists(config_file):
-            os.unlink(config_file)
-        return None
-
-    call(f'systemctl restart {service_name}')
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
deleted file mode 100755
index 9cc57dbcf..000000000
--- a/src/conf_mode/dhcpv6_server.py
+++ /dev/null
@@ -1,222 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2023 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import os
-
-from ipaddress import ip_address
-from ipaddress import ip_network
-from sys import exit
-
-from vyos.config import Config
-from vyos.template import render
-from vyos.utils.process import call
-from vyos.utils.file import chmod_775
-from vyos.utils.file import makedir
-from vyos.utils.file import write_file
-from vyos.utils.dict import dict_search
-from vyos.utils.network import is_subnet_connected
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = '/run/kea/kea-dhcp6.conf'
-ctrl_socket = '/run/kea/dhcp6-ctrl-socket'
-lease_file = '/config/dhcp/dhcp6-leases.csv'
-user_group = '_kea'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['service', 'dhcpv6-server']
-    if not conf.exists(base):
-        return None
-
-    dhcpv6 = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                  get_first_key=True,
-                                  no_tag_node_value_mangle=True)
-    return dhcpv6
-
-def verify(dhcpv6):
-    # bail out early - looks like removal from running config
-    if not dhcpv6 or 'disable' in dhcpv6:
-        return None
-
-    # If DHCP is enabled we need one share-network
-    if 'shared_network_name' not in dhcpv6:
-        raise ConfigError('No DHCPv6 shared networks configured. At least '\
-                          'one DHCPv6 shared network must be configured.')
-
-    # Inspect shared-network/subnet
-    subnets = []
-    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 'prefix' not in subnet_config['prefix_delegation']:
-                    raise ConfigError('prefix-delegation prefix not defined!')
-
-                for prefix, prefix_config in subnet_config['prefix_delegation']['prefix'].items():
-                    if 'delegated_length' not in prefix_config:
-                        raise ConfigError(f'Delegated IPv6 prefix length for "{prefix}" '\
-                                          f'must be configured')
-
-                    if 'prefix_length' not in prefix_config:
-                        raise ConfigError('Length of delegated IPv6 prefix must be configured')
-
-                    if prefix_config['prefix_length'] > prefix_config['delegated_length']:
-                        raise ConfigError('Length of delegated IPv6 prefix must be within parent prefix')
-
-            # Static mappings don't require anything (but check if IP is in subnet if it's set)
-            if 'static_mapping' in subnet_config:
-                for mapping, mapping_config in subnet_config['static_mapping'].items():
-                    if 'ipv6_address' in mapping_config:
-                        # Static address must be in subnet
-                        if ip_address(mapping_config['ipv6_address']) not in ip_network(subnet):
-                            raise ConfigError(f'static-mapping address for mapping "{mapping}" is not in subnet "{subnet}"!')
-
-                        if ('mac' not in mapping_config and 'duid' not in mapping_config) or \
-                            ('mac' in mapping_config and 'duid' in mapping_config):
-                            raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for '
-                                              f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!')
-
-            if 'vendor_option' in subnet_config:
-                if len(dict_search('vendor_option.cisco.tftp_server', subnet_config)) > 2:
-                    raise ConfigError(f'No more then two Cisco tftp-servers should be defined for subnet "{subnet}"!')
-
-            # Subnets must be unique
-            if subnet in subnets:
-                raise ConfigError(f'DHCPv6 subnets must be unique! Subnet {subnet} defined multiple times!')
-            subnets.append(subnet)
-
-        # DHCPv6 requires at least one configured address range or one static mapping
-        # (FIXME: is not actually checked right now?)
-
-        # There must be one subnet connected to a listen interface if network is not disabled.
-        if 'disable' not in network_config:
-            if is_subnet_connected(subnet):
-                listen_ok = True
-
-            # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping
-            # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32"
-            net = ip_network(subnet)
-            for n in subnets:
-                net2 = ip_network(n)
-                if (net != net2):
-                    if net.overlaps(net2):
-                        raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2))
-
-    if not listen_ok:
-        raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on '\
-                          'this machine. At least one subnet6 must be connected such that '\
-                          'DHCPv6 listens on an interface!')
-
-
-    return None
-
-def generate(dhcpv6):
-    # bail out early - looks like removal from running config
-    if not dhcpv6 or 'disable' in dhcpv6:
-        return None
-
-    dhcpv6['lease_file'] = lease_file
-    dhcpv6['machine'] = os.uname().machine
-
-    # Create directory for lease file if necessary
-    lease_dir = os.path.dirname(lease_file)
-    if not os.path.isdir(lease_dir):
-        makedir(lease_dir, group='vyattacfg')
-        chmod_775(lease_dir)
-
-    # Create lease file if necessary and let kea own it - 'kea-lfc' expects it that way
-    if not os.path.exists(lease_file):
-        write_file(lease_file, '', user=user_group, group=user_group, mode=0o644)
-
-    render(config_file, 'dhcp-server/kea-dhcp6.conf.j2', dhcpv6, user=user_group, group=user_group)
-    return None
-
-def apply(dhcpv6):
-    # bail out early - looks like removal from running config
-    service_name = 'kea-dhcp6-server.service'
-    if not dhcpv6 or 'disable' in dhcpv6:
-        # DHCP server is removed in the commit
-        call(f'systemctl stop {service_name}')
-        if os.path.exists(config_file):
-            os.unlink(config_file)
-        return None
-
-    call(f'systemctl restart {service_name}')
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py
deleted file mode 100755
index 99fa8feee..000000000
--- a/src/conf_mode/dns_dynamic.py
+++ /dev/null
@@ -1,187 +0,0 @@
-#!/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
-import re
-from sys import exit
-
-from vyos.base import Warning
-from vyos.config import Config
-from vyos.configverify import verify_interface_exists
-from vyos.template import render
-from vyos.utils.process import call
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/run/ddclient/ddclient.conf'
-systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf'
-
-# Dynamic interfaces that might not exist when the configuration is loaded
-dynamic_interfaces = ('pppoe', 'sstpc')
-
-# Protocols that require zone
-zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi',
-                  'nfsn', 'nsupdate']
-zone_supported = zone_necessary + ['dnsexit2', 'zoneedit1']
-
-# Protocols that do not require username
-username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'digitalocean', 'dnsexit2',
-                        'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla',
-                        'nsupdate', 'regfishde']
-
-# Protocols that support TTL
-ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn',
-                 'nsupdate']
-
-# Protocols that support both IPv4 and IPv6
-dualstack_supported = ['cloudflare', 'digitalocean', 'dnsexit2', 'duckdns',
-                       'dyndns2', 'easydns', 'freedns', 'hetzner', 'infomaniak',
-                       'njalla']
-
-# dyndns2 protocol in ddclient honors dual stack for selective servers
-# because of the way it is implemented in ddclient
-dyndns_dualstack_servers = ['members.dyndns.org', 'dynv6.com']
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base = ['service', 'dns', 'dynamic']
-    if not conf.exists(base):
-        return None
-
-    dyndns = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                  no_tag_node_value_mangle=True,
-                                  get_first_key=True,
-                                  with_recursive_defaults=True)
-
-    dyndns['config_file'] = config_file
-    return dyndns
-
-def verify(dyndns):
-    # bail out early - looks like removal from running config
-    if not dyndns or 'name' not in dyndns:
-        return None
-
-    # Dynamic DNS service provider - configuration validation
-    for service, config in dyndns['name'].items():
-
-        error_msg_req = f'is required for Dynamic DNS service "{service}"'
-        error_msg_uns = f'is not supported for Dynamic DNS service "{service}"'
-
-        for field in ['protocol', 'address', 'host_name']:
-            if field not in config:
-                raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}')
-
-        # If dyndns address is an interface, ensure
-        # that the interface exists (or just warn if dynamic interface)
-        # and that web-options are not set
-        if config['address'] != 'web':
-            # exclude check interface for dynamic interfaces
-            if config['address'].startswith(dynamic_interfaces):
-                Warning(f'Interface "{config["address"]}" does not exist yet and cannot '
-                        f'be used for Dynamic DNS service "{service}" until it is up!')
-            else:
-                verify_interface_exists(config['address'])
-            if 'web_options' in config:
-                raise ConfigError(f'"web-options" is applicable only when using HTTP(S) '
-                                  f'web request to obtain the IP address')
-
-        # Warn if using checkip.dyndns.org, as it does not support HTTPS
-        # See: https://github.com/ddclient/ddclient/issues/597
-        if 'web_options' in config:
-            if 'url' not in config['web_options']:
-                raise ConfigError(f'"url" in "web-options" {error_msg_req} '
-                                  f'with protocol "{config["protocol"]}"')
-            elif re.search("^(https?://)?checkip\.dyndns\.org", config['web_options']['url']):
-                Warning(f'"checkip.dyndns.org" does not support HTTPS requests for IP address '
-                        f'lookup. Please use a different IP address lookup service.')
-
-        # RFC2136 uses 'key' instead of 'password'
-        if config['protocol'] != 'nsupdate' and 'password' not in config:
-            raise ConfigError(f'"password" {error_msg_req}')
-
-        # Other RFC2136 specific configuration validation
-        if config['protocol'] == 'nsupdate':
-            if 'password' in config:
-                raise ConfigError(f'"password" {error_msg_uns} with protocol "{config["protocol"]}"')
-            for field in ['server', 'key']:
-                if field not in config:
-                    raise ConfigError(f'"{field}" {error_msg_req} with protocol "{config["protocol"]}"')
-
-        if config['protocol'] in zone_necessary and 'zone' not in config:
-            raise ConfigError(f'"zone" {error_msg_req} with protocol "{config["protocol"]}"')
-
-        if config['protocol'] not in zone_supported and 'zone' in config:
-            raise ConfigError(f'"zone" {error_msg_uns} with protocol "{config["protocol"]}"')
-
-        if config['protocol'] not in username_unnecessary and 'username' not in config:
-            raise ConfigError(f'"username" {error_msg_req} with protocol "{config["protocol"]}"')
-
-        if config['protocol'] not in ttl_supported and 'ttl' in config:
-            raise ConfigError(f'"ttl" {error_msg_uns} with protocol "{config["protocol"]}"')
-
-        if config['ip_version'] == 'both':
-            if config['protocol'] not in dualstack_supported:
-                raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} '
-                                  f'with protocol "{config["protocol"]}"')
-            # dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org)
-            if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] not in dyndns_dualstack_servers:
-                raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} '
-                                  f'for "{config["server"]}" with protocol "{config["protocol"]}"')
-
-        if {'wait_time', 'expiry_time'} <= config.keys() and int(config['expiry_time']) < int(config['wait_time']):
-                raise ConfigError(f'"expiry-time" must be greater than "wait-time" for '
-                                  f'Dynamic DNS service "{service}"')
-
-    return None
-
-def generate(dyndns):
-    # bail out early - looks like removal from running config
-    if not dyndns or 'name' not in dyndns:
-        return None
-
-    render(config_file, 'dns-dynamic/ddclient.conf.j2', dyndns, permission=0o600)
-    render(systemd_override, 'dns-dynamic/override.conf.j2', dyndns)
-    return None
-
-def apply(dyndns):
-    systemd_service = 'ddclient.service'
-    # Reload systemd manager configuration
-    call('systemctl daemon-reload')
-
-    # bail out early - looks like removal from running config
-    if not dyndns or 'name' not in dyndns:
-        call(f'systemctl stop {systemd_service}')
-        if os.path.exists(config_file):
-            os.unlink(config_file)
-    else:
-        call(f'systemctl reload-or-restart {systemd_service}')
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
deleted file mode 100755
index c186f47af..000000000
--- a/src/conf_mode/dns_forwarding.py
+++ /dev/null
@@ -1,358 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2022 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 netifaces import interfaces
-from sys import exit
-from glob import glob
-
-from vyos.config import Config
-from vyos.hostsd_client import Client as hostsd_client
-from vyos.template import render
-from vyos.template import bracketize_ipv6
-from vyos.utils.process import call
-from vyos.utils.permission import chown
-from vyos.utils.dict import dict_search
-
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-pdns_rec_user = pdns_rec_group = 'pdns'
-pdns_rec_run_dir = '/run/powerdns'
-pdns_rec_lua_conf_file = f'{pdns_rec_run_dir}/recursor.conf.lua'
-pdns_rec_hostsd_lua_conf_file = f'{pdns_rec_run_dir}/recursor.vyos-hostsd.conf.lua'
-pdns_rec_hostsd_zones_file = f'{pdns_rec_run_dir}/recursor.forward-zones.conf'
-pdns_rec_config_file = f'{pdns_rec_run_dir}/recursor.conf'
-
-hostsd_tag = 'static'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['service', 'dns', 'forwarding']
-    if not conf.exists(base):
-        return None
-
-    dns = conf.get_config_dict(base, key_mangling=('-', '_'),
-                               no_tag_node_value_mangle=True,
-                               get_first_key=True,
-                               with_recursive_defaults=True)
-
-    # some additions to the default dictionary
-    if 'system' in dns:
-        base_nameservers = ['system', 'name-server']
-        if conf.exists(base_nameservers):
-            dns.update({'system_name_server': conf.return_values(base_nameservers)})
-
-    if 'authoritative_domain' in dns:
-        dns['authoritative_zones'] = []
-        dns['authoritative_zone_errors'] = []
-        for node in dns['authoritative_domain']:
-            zonedata = dns['authoritative_domain'][node]
-            if ('disable' in zonedata) or (not 'records' in zonedata):
-                continue
-            zone = {
-                'name': node,
-                'file': "{}/zone.{}.conf".format(pdns_rec_run_dir, node),
-                'records': [],
-            }
-
-            recorddata = zonedata['records']
-
-            for rtype in [ 'a', 'aaaa', 'cname', 'mx', 'ns', 'ptr', 'txt', 'spf', 'srv', 'naptr' ]:
-                if rtype not in recorddata:
-                    continue
-                for subnode in recorddata[rtype]:
-                    if 'disable' in recorddata[rtype][subnode]:
-                        continue
-
-                    rdata = recorddata[rtype][subnode]
-
-                    if rtype in [ 'a', 'aaaa' ]:
-                        if not 'address' in rdata:
-                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one address is required')
-                            continue
-
-                        if subnode == 'any':
-                            subnode = '*'
-
-                        for address in rdata['address']:
-                            zone['records'].append({
-                                'name': subnode,
-                                'type': rtype.upper(),
-                                'ttl': rdata['ttl'],
-                                'value': address
-                            })
-                    elif rtype in ['cname', 'ptr', 'ns']:
-                        if not 'target' in rdata:
-                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: target is required')
-                            continue
-
-                        zone['records'].append({
-                            'name': subnode,
-                            'type': rtype.upper(),
-                            'ttl': rdata['ttl'],
-                            'value': '{}.'.format(rdata['target'])
-                        })
-                    elif rtype == 'mx':
-                        if not 'server' in rdata:
-                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one server is required')
-                            continue
-
-                        for servername in rdata['server']:
-                            serverdata = rdata['server'][servername]
-                            zone['records'].append({
-                                'name': subnode,
-                                'type': rtype.upper(),
-                                'ttl': rdata['ttl'],
-                                'value': '{} {}.'.format(serverdata['priority'], servername)
-                            })
-                    elif rtype == 'txt':
-                        if not 'value' in rdata:
-                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one value is required')
-                            continue
-
-                        for value in rdata['value']:
-                            zone['records'].append({
-                                'name': subnode,
-                                'type': rtype.upper(),
-                                'ttl': rdata['ttl'],
-                                'value': "\"{}\"".format(value.replace("\"", "\\\""))
-                            })
-                    elif rtype == 'spf':
-                        if not 'value' in rdata:
-                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: value is required')
-                            continue
-
-                        zone['records'].append({
-                            'name': subnode,
-                            'type': rtype.upper(),
-                            'ttl': rdata['ttl'],
-                            'value': '"{}"'.format(rdata['value'].replace("\"", "\\\""))
-                        })
-                    elif rtype == 'srv':
-                        if not 'entry' in rdata:
-                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one entry is required')
-                            continue
-
-                        for entryno in rdata['entry']:
-                            entrydata = rdata['entry'][entryno]
-                            if not 'hostname' in entrydata:
-                                dns['authoritative_zone_errors'].append(f'{subnode}.{node}: hostname is required for entry {entryno}')
-                                continue
-
-                            if not 'port' in entrydata:
-                                dns['authoritative_zone_errors'].append(f'{subnode}.{node}: port is required for entry {entryno}')
-                                continue
-
-                            zone['records'].append({
-                                'name': subnode,
-                                'type': rtype.upper(),
-                                'ttl': rdata['ttl'],
-                                'value': '{} {} {} {}.'.format(entrydata['priority'], entrydata['weight'], entrydata['port'], entrydata['hostname'])
-                            })
-                    elif rtype == 'naptr':
-                        if not 'rule' in rdata:
-                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one rule is required')
-                            continue
-
-                        for ruleno in rdata['rule']:
-                            ruledata = rdata['rule'][ruleno]
-                            flags = ""
-                            if 'lookup-srv' in ruledata:
-                                flags += "S"
-                            if 'lookup-a' in ruledata:
-                                flags += "A"
-                            if 'resolve-uri' in ruledata:
-                                flags += "U"
-                            if 'protocol-specific' in ruledata:
-                                flags += "P"
-
-                            if 'order' in ruledata:
-                                order = ruledata['order']
-                            else:
-                                order = ruleno
-
-                            if 'regexp' in ruledata:
-                                regexp= ruledata['regexp'].replace("\"", "\\\"")
-                            else:
-                                regexp = ''
-
-                            if ruledata['replacement']:
-                                replacement = '{}.'.format(ruledata['replacement'])
-                            else:
-                                replacement = ''
-
-                            zone['records'].append({
-                                'name': subnode,
-                                'type': rtype.upper(),
-                                'ttl': rdata['ttl'],
-                                'value': '{} {} "{}" "{}" "{}" {}'.format(order, ruledata['preference'], flags, ruledata['service'], regexp, replacement)
-                            })
-
-            dns['authoritative_zones'].append(zone)
-
-    return dns
-
-def verify(dns):
-    # bail out early - looks like removal from running config
-    if not dns:
-        return None
-
-    if 'listen_address' not in dns:
-        raise ConfigError('DNS forwarding requires a listen-address')
-
-    if 'allow_from' not in dns:
-        raise ConfigError('DNS forwarding requires an allow-from network')
-
-    # we can not use dict_search() when testing for domain servers
-    # as a domain will contains dot's which is out dictionary delimiter.
-    if 'domain' in dns:
-        for domain in dns['domain']:
-            if 'name_server' not in dns['domain'][domain]:
-                raise ConfigError(f'No server configured for domain {domain}!')
-
-    if 'dns64_prefix' in dns:
-        dns_prefix = dns['dns64_prefix'].split('/')[1]
-        # RFC 6147 requires prefix /96
-        if int(dns_prefix) != 96:
-            raise ConfigError('DNS 6to4 prefix must be of length /96')
-
-    if ('authoritative_zone_errors' in dns) and dns['authoritative_zone_errors']:
-        for error in dns['authoritative_zone_errors']:
-            print(error)
-        raise ConfigError('Invalid authoritative records have been defined')
-
-    if 'system' in dns:
-        if not 'system_name_server' in dns:
-            print('Warning: No "system name-server" configured')
-
-    return None
-
-def generate(dns):
-    # bail out early - looks like removal from running config
-    if not dns:
-        return None
-
-    render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.j2',
-            dns, user=pdns_rec_user, group=pdns_rec_group)
-
-    render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.j2',
-            dns, user=pdns_rec_user, group=pdns_rec_group)
-
-    for zone_filename in glob(f'{pdns_rec_run_dir}/zone.*.conf'):
-        os.unlink(zone_filename)
-
-    if 'authoritative_zones' in dns:
-        for zone in dns['authoritative_zones']:
-            render(zone['file'], 'dns-forwarding/recursor.zone.conf.j2',
-                    zone, user=pdns_rec_user, group=pdns_rec_group)
-
-
-    # if vyos-hostsd didn't create its files yet, create them (empty)
-    for file in [pdns_rec_hostsd_lua_conf_file, pdns_rec_hostsd_zones_file]:
-        with open(file, 'a'):
-            pass
-        chown(file, user=pdns_rec_user, group=pdns_rec_group)
-
-    return None
-
-def apply(dns):
-    if not dns:
-        # DNS forwarding is removed in the commit
-        call('systemctl stop pdns-recursor.service')
-
-        if os.path.isfile(pdns_rec_config_file):
-            os.unlink(pdns_rec_config_file)
-
-        for zone_filename in glob(f'{pdns_rec_run_dir}/zone.*.conf'):
-            os.unlink(zone_filename)
-    else:
-        ### first apply vyos-hostsd config
-        hc = hostsd_client()
-
-        # add static nameservers to hostsd so they can be joined with other
-        # sources
-        hc.delete_name_servers([hostsd_tag])
-        if 'name_server' in dns:
-            # 'name_server' is of the form
-            # {'192.0.2.1': {'port': 53}, '2001:db8::1': {'port': 853}, ...}
-            # canonicalize them as ['192.0.2.1:53', '[2001:db8::1]:853', ...]
-            nslist = [(lambda h, p: f"{bracketize_ipv6(h)}:{p['port']}")(h, p)
-                      for (h, p) in dns['name_server'].items()]
-            hc.add_name_servers({hostsd_tag: nslist})
-
-        # delete all nameserver tags
-        hc.delete_name_server_tags_recursor(hc.get_name_server_tags_recursor())
-
-        ## add nameserver tags - the order determines the nameserver order!
-        # our own tag (static)
-        hc.add_name_server_tags_recursor([hostsd_tag])
-
-        if 'system' in dns:
-            hc.add_name_server_tags_recursor(['system'])
-        else:
-            hc.delete_name_server_tags_recursor(['system'])
-
-        # add dhcp nameserver tags for configured interfaces
-        if 'system_name_server' in dns:
-            for interface in dns['system_name_server']:
-                # system_name_server key contains both IP addresses and interface
-                # names (DHCP) to use DNS servers. We need to check if the
-                # value is an interface name - only if this is the case, add the
-                # interface based DNS forwarder.
-                if interface in interfaces():
-                    hc.add_name_server_tags_recursor(['dhcp-' + interface,
-                                                      'dhcpv6-' + interface ])
-
-        # hostsd will generate the forward-zones file
-        # the list and keys() are required as get returns a dict, not list
-        hc.delete_forward_zones(list(hc.get_forward_zones().keys()))
-        if 'domain' in dns:
-            zones = dns['domain']
-            for domain in zones.keys():
-                # 'name_server' is of the form
-                # {'192.0.2.1': {'port': 53}, '2001:db8::1': {'port': 853}, ...}
-                # canonicalize them as ['192.0.2.1:53', '[2001:db8::1]:853', ...]
-                zones[domain]['name_server'] = [(lambda h, p: f"{bracketize_ipv6(h)}:{p['port']}")(h, p)
-                                                for (h, p) in zones[domain]['name_server'].items()]
-            hc.add_forward_zones(zones)
-
-        # hostsd generates NTAs for the authoritative zones
-        # the list and keys() are required as get returns a dict, not list
-        hc.delete_authoritative_zones(list(hc.get_authoritative_zones()))
-        if 'authoritative_zones' in dns:
-            hc.add_authoritative_zones(list(map(lambda zone: zone['name'], dns['authoritative_zones'])))
-
-        # call hostsd to generate forward-zones and its lua-config-file
-        hc.apply()
-
-        ### finally (re)start pdns-recursor
-        call('systemctl restart pdns-recursor.service')
-
-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/firewall.py b/src/conf_mode/firewall.py
index da6724fde..acb7dfa41 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -42,9 +42,6 @@ from vyos import airbag
 
 airbag.enable()
 
-nat_conf_script = 'nat.py'
-policy_route_conf_script = 'policy-route.py'
-
 nftables_conf = '/run/nftables.conf'
 
 sysfs_config = {
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
deleted file mode 100755
index 206f513c8..000000000
--- a/src/conf_mode/flow_accounting_conf.py
+++ /dev/null
@@ -1,320 +0,0 @@
-#!/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
-import re
-
-from sys import exit
-from ipaddress import ip_address
-
-from vyos.base import Warning
-from vyos.config import Config
-from vyos.config import config_dict_merge
-from vyos.configverify import verify_vrf
-from vyos.ifconfig import Section
-from vyos.template import render
-from vyos.utils.process import call
-from vyos.utils.process import cmd
-from vyos.utils.process import run
-from vyos.utils.network import is_addr_assigned
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-uacctd_conf_path = '/run/pmacct/uacctd.conf'
-systemd_service = 'uacctd.service'
-systemd_override = f'/run/systemd/system/{systemd_service}.d/override.conf'
-nftables_nflog_table = 'raw'
-nftables_nflog_chain = 'VYOS_PREROUTING_HOOK'
-egress_nftables_nflog_table = 'inet mangle'
-egress_nftables_nflog_chain = 'FORWARD'
-
-# get nftables rule dict for chain in table
-def _nftables_get_nflog(chain, table):
-    # define list with rules
-    rules = []
-
-    # prepare regex for parsing rules
-    rule_pattern = '[io]ifname "(?P<interface>[\w\.\*\-]+)".*handle (?P<handle>[\d]+)'
-    rule_re = re.compile(rule_pattern)
-
-    # run nftables, save output and split it by lines
-    nftables_command = f'nft -a list chain {table} {chain}'
-    tmp = cmd(nftables_command, message='Failed to get flows list')
-    # parse each line and add information to list
-    for current_rule in tmp.splitlines():
-        if 'FLOW_ACCOUNTING_RULE' not in current_rule:
-            continue
-        current_rule_parsed = rule_re.search(current_rule)
-        if current_rule_parsed:
-            groups = current_rule_parsed.groupdict()
-            rules.append({ 'interface': groups["interface"], 'table': table, 'handle': groups["handle"] })
-
-    # return list with rules
-    return rules
-
-def _nftables_config(configured_ifaces, direction, length=None):
-    # define list of nftables commands to modify settings
-    nftable_commands = []
-    nftables_chain = nftables_nflog_chain
-    nftables_table = nftables_nflog_table
-
-    if direction == "egress":
-        nftables_chain = egress_nftables_nflog_chain
-        nftables_table = egress_nftables_nflog_table
-
-    # prepare extended list with configured interfaces
-    configured_ifaces_extended = []
-    for iface in configured_ifaces:
-        configured_ifaces_extended.append({ 'iface': iface })
-
-    # get currently configured interfaces with nftables rules
-    active_nflog_rules = _nftables_get_nflog(nftables_chain, nftables_table)
-
-    # compare current active list with configured one and delete excessive interfaces, add missed
-    active_nflog_ifaces = []
-    for rule in active_nflog_rules:
-        interface = rule['interface']
-        if interface not in configured_ifaces:
-            table = rule['table']
-            handle = rule['handle']
-            nftable_commands.append(f'nft delete rule {table} {nftables_chain} handle {handle}')
-        else:
-            active_nflog_ifaces.append({
-                'iface': interface,
-            })
-
-    # do not create new rules for already configured interfaces
-    for iface in active_nflog_ifaces:
-        if iface in active_nflog_ifaces and iface in configured_ifaces_extended:
-            configured_ifaces_extended.remove(iface)
-
-    # create missed rules
-    for iface_extended in configured_ifaces_extended:
-        iface = iface_extended['iface']
-        iface_prefix = "o" if direction == "egress" else "i"
-        rule_definition = f'{iface_prefix}ifname "{iface}" counter log group 2 snaplen {length} queue-threshold 100 comment "FLOW_ACCOUNTING_RULE"'
-        nftable_commands.append(f'nft insert rule {nftables_table} {nftables_chain} {rule_definition}')
-        # Also add IPv6 ingres logging
-        if nftables_table == nftables_nflog_table:
-            nftable_commands.append(f'nft insert rule ip6 {nftables_table} {nftables_chain} {rule_definition}')
-
-    # change nftables
-    for command in nftable_commands:
-        cmd(command, raising=ConfigError)
-
-
-def _nftables_trigger_setup(operation: str) -> None:
-    """Add a dummy rule to unlock the main pmacct loop with a packet-trigger
-
-    Args:
-        operation (str): 'add' or 'delete' a trigger
-    """
-    # check if a chain exists
-    table_exists = False
-    if run('nft -snj list table ip pmacct') == 0:
-        table_exists = True
-
-    if operation == 'delete' and table_exists:
-        nft_cmd: str = 'nft delete table ip pmacct'
-        cmd(nft_cmd, raising=ConfigError)
-    if operation == 'add' and not table_exists:
-        nft_cmds: list[str] = [
-            'nft add table ip pmacct',
-            'nft add chain ip pmacct pmacct_out { type filter hook output priority raw - 50 \\; policy accept \\; }',
-            'nft add rule ip pmacct pmacct_out oif lo ip daddr 127.0.254.0 counter log group 2 snaplen 1 queue-threshold 0 comment NFLOG_TRIGGER'
-        ]
-        for nft_cmd in nft_cmds:
-            cmd(nft_cmd, raising=ConfigError)
-
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['system', 'flow-accounting']
-    if not conf.exists(base):
-        return None
-
-    flow_accounting = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
-
-    # We have gathered the dict representation of the CLI, but there are
-    # default values which we need to conditionally update into the
-    # dictionary retrieved.
-    default_values = conf.get_config_defaults(**flow_accounting.kwargs,
-                                              recursive=True)
-
-    # delete individual flow type defaults - should only be added if user
-    # sets this feature
-    for flow_type in ['sflow', 'netflow']:
-        if flow_type not in flow_accounting and flow_type in default_values:
-            del default_values[flow_type]
-
-    flow_accounting = config_dict_merge(default_values, flow_accounting)
-
-    return flow_accounting
-
-def verify(flow_config):
-    if not flow_config:
-        return None
-
-    # check if at least one collector is enabled
-    if 'sflow' not in flow_config and 'netflow' not in flow_config and 'disable_imt' in flow_config:
-        raise ConfigError('You need to configure at least sFlow or NetFlow, ' \
-                          'or not set "disable-imt" for flow-accounting!')
-
-    # Check if at least one interface is configured
-    if 'interface' not in flow_config:
-        raise ConfigError('Flow accounting requires at least one interface to ' \
-                          'be configured!')
-
-    # check that all configured interfaces exists in the system
-    for interface in flow_config['interface']:
-        if interface not in Section.interfaces():
-            # Changed from error to warning to allow adding dynamic interfaces
-            # and interface templates
-            Warning(f'Interface "{interface}" is not presented in the system')
-
-    # check sFlow configuration
-    if 'sflow' in flow_config:
-        # check if at least one sFlow collector is configured
-        if 'server' not in flow_config['sflow']:
-            raise ConfigError('You need to configure at least one sFlow server!')
-
-        # check that all sFlow collectors use the same IP protocol version
-        sflow_collector_ipver = None
-        for server in flow_config['sflow']['server']:
-            if sflow_collector_ipver:
-                if sflow_collector_ipver != ip_address(server).version:
-                    raise ConfigError("All sFlow servers must use the same IP protocol")
-            else:
-                sflow_collector_ipver = ip_address(server).version
-
-        # check if vrf is defined for Sflow
-        verify_vrf(flow_config)
-        sflow_vrf = None
-        if 'vrf' in flow_config:
-            sflow_vrf = flow_config['vrf']
-
-        # check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa
-        for server in flow_config['sflow']['server']:
-            if 'agent_address' in flow_config['sflow']:
-                if ip_address(server).version != ip_address(flow_config['sflow']['agent_address']).version:
-                    raise ConfigError('IPv4 and IPv6 addresses can not be mixed in "sflow agent-address" and "sflow '\
-                                      'server". You need to set the same IP version for both "agent-address" and '\
-                                      'all sFlow servers')
-
-        if 'agent_address' in flow_config['sflow']:
-            tmp = flow_config['sflow']['agent_address']
-            if not is_addr_assigned(tmp, sflow_vrf):
-                raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!')
-
-        # Check if configured sflow source-address exist in the system
-        if 'source_address' in flow_config['sflow']:
-            if not is_addr_assigned(flow_config['sflow']['source_address'], sflow_vrf):
-                tmp = flow_config['sflow']['source_address']
-                raise ConfigError(f'Configured "sflow source-address {tmp}" does not exist on the system!')
-
-    # check NetFlow configuration
-    if 'netflow' in flow_config:
-        # check if vrf is defined for netflow
-        netflow_vrf = None
-        if 'vrf' in flow_config:
-            netflow_vrf = flow_config['vrf']
-
-        # check if at least one NetFlow collector is configured if NetFlow configuration is presented
-        if 'server' not in flow_config['netflow']:
-            raise ConfigError('You need to configure at least one NetFlow server!')
-
-        # Check if configured netflow source-address exist in the system
-        if 'source_address' in flow_config['netflow']:
-            if not is_addr_assigned(flow_config['netflow']['source_address'], netflow_vrf):
-                tmp = flow_config['netflow']['source_address']
-                raise ConfigError(f'Configured "netflow source-address {tmp}" does not exist on the system!')
-
-        # Check if engine-id compatible with selected protocol version
-        if 'engine_id' in flow_config['netflow']:
-            v5_filter = '^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]):(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$'
-            v9v10_filter = '^(\d|[1-9]\d{1,8}|[1-3]\d{9}|4[01]\d{8}|42[0-8]\d{7}|429[0-3]\d{6}|4294[0-8]\d{5}|42949[0-5]\d{4}|429496[0-6]\d{3}|4294967[01]\d{2}|42949672[0-8]\d|429496729[0-5])$'
-            engine_id = flow_config['netflow']['engine_id']
-            version = flow_config['netflow']['version']
-
-            if flow_config['netflow']['version'] == '5':
-                regex_filter = re.compile(v5_filter)
-                if not regex_filter.search(engine_id):
-                    raise ConfigError(f'You cannot use NetFlow engine-id "{engine_id}" '\
-                                      f'together with NetFlow protocol version "{version}"!')
-            else:
-                regex_filter = re.compile(v9v10_filter)
-                if not regex_filter.search(flow_config['netflow']['engine_id']):
-                    raise ConfigError(f'Can not use NetFlow engine-id "{engine_id}" together '\
-                                      f'with NetFlow protocol version "{version}"!')
-
-    # return True if all checks were passed
-    return True
-
-def generate(flow_config):
-    if not flow_config:
-        return None
-
-    render(uacctd_conf_path, 'pmacct/uacctd.conf.j2', flow_config)
-    render(systemd_override, 'pmacct/override.conf.j2', flow_config)
-    # Reload systemd manager configuration
-    call('systemctl daemon-reload')
-
-def apply(flow_config):
-    # Check if flow-accounting was removed and define command
-    if not flow_config:
-        _nftables_config([], 'ingress')
-        _nftables_config([], 'egress')
-
-        # Stop flow-accounting daemon and remove configuration file
-        call(f'systemctl stop {systemd_service}')
-        if os.path.exists(uacctd_conf_path):
-            os.unlink(uacctd_conf_path)
-
-        # must be done after systemctl
-        _nftables_trigger_setup('delete')
-
-        return
-
-    # Start/reload flow-accounting daemon
-    call(f'systemctl restart {systemd_service}')
-
-    # configure nftables rules for defined interfaces
-    if 'interface' in flow_config:
-        _nftables_config(flow_config['interface'], 'ingress', flow_config['packet_length'])
-
-        # configure egress the same way if configured otherwise remove it
-        if 'enable_egress' in flow_config:
-            _nftables_config(flow_config['interface'], 'egress', flow_config['packet_length'])
-        else:
-            _nftables_config([], 'egress')
-
-    # add a trigger for signal processing
-    _nftables_trigger_setup('add')
-
-
-if __name__ == '__main__':
-    try:
-        config = get_config()
-        verify(config)
-        generate(config)
-        apply(config)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
deleted file mode 100755
index 6204cf247..000000000
--- a/src/conf_mode/host_name.py
+++ /dev/null
@@ -1,188 +0,0 @@
-#!/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 re
-import sys
-import copy
-
-import vyos.hostsd_client
-
-from vyos.base import Warning
-from vyos.config import Config
-from vyos.ifconfig import Section
-from vyos.template import is_ip
-from vyos.utils.process import cmd
-from vyos.utils.process import call
-from vyos.utils.process import process_named_running
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-default_config_data = {
-    'hostname': 'vyos',
-    'domain_name': '',
-    'domain_search': [],
-    'nameserver': [],
-    'nameservers_dhcp_interfaces': {},
-    'static_host_mapping': {}
-}
-
-hostsd_tag = 'system'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    hosts = copy.deepcopy(default_config_data)
-
-    hosts['hostname'] = conf.return_value(['system', 'host-name'])
-
-    # This may happen if the config is not loaded yet,
-    # e.g. if run by cloud-init
-    if not hosts['hostname']:
-        hosts['hostname'] = default_config_data['hostname']
-
-    if conf.exists(['system', 'domain-name']):
-        hosts['domain_name'] = conf.return_value(['system', 'domain-name'])
-        hosts['domain_search'].append(hosts['domain_name'])
-
-    if conf.exists(['system', 'domain-search']):
-        for search in conf.return_values(['system', 'domain-search']):
-            hosts['domain_search'].append(search)
-
-    if conf.exists(['system', 'name-server']):
-        for ns in conf.return_values(['system', 'name-server']):
-            if is_ip(ns):
-                hosts['nameserver'].append(ns)
-            else:
-                tmp = ''
-                if_type = Section.section(ns)
-                if conf.exists(['interfaces', if_type, ns, 'address']):
-                    tmp = conf.return_values(['interfaces', if_type, ns, 'address'])
-
-                hosts['nameservers_dhcp_interfaces'].update({ ns : tmp })
-
-    # system static-host-mapping
-    for hn in conf.list_nodes(['system', 'static-host-mapping', 'host-name']):
-        hosts['static_host_mapping'][hn] = {}
-        hosts['static_host_mapping'][hn]['address'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'inet'])
-        hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'alias'])
-
-    return hosts
-
-
-def verify(hosts):
-    if hosts is None:
-        return None
-
-    # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)"
-    hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$")
-    if not hostname_regex.match(hosts['hostname']):
-        raise ConfigError('Invalid host name ' + hosts["hostname"])
-
-    # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length"
-    length = len(hosts['hostname'])
-    if length < 1 or length > 63:
-        raise ConfigError(
-            'Invalid host-name length, must be less than 63 characters')
-
-    all_static_host_mapping_addresses = []
-    # static mappings alias hostname
-    for host, hostprops in hosts['static_host_mapping'].items():
-        if not hostprops['address']:
-            raise ConfigError(f'IP address required for static-host-mapping "{host}"')
-        all_static_host_mapping_addresses.append(hostprops['address'])
-        for a in hostprops['aliases']:
-            if not hostname_regex.match(a) and len(a) != 0:
-                raise ConfigError(f'Invalid alias "{a}" in static-host-mapping "{host}"')
-
-    for interface, interface_config in hosts['nameservers_dhcp_interfaces'].items():
-        # Warnin user if interface does not have DHCP or DHCPv6 configured
-        if not set(interface_config).intersection(['dhcp', 'dhcpv6']):
-            Warning(f'"{interface}" is not a DHCP interface but uses DHCP name-server option!')
-
-    return None
-
-
-def generate(config):
-    pass
-
-def apply(config):
-    if config is None:
-        return None
-
-    ## Send the updated data to vyos-hostsd
-    try:
-        hc = vyos.hostsd_client.Client()
-
-        hc.set_host_name(config['hostname'], config['domain_name'])
-
-        hc.delete_search_domains([hostsd_tag])
-        if config['domain_search']:
-            hc.add_search_domains({hostsd_tag: config['domain_search']})
-
-        hc.delete_name_servers([hostsd_tag])
-        if config['nameserver']:
-            hc.add_name_servers({hostsd_tag: config['nameserver']})
-
-        # add our own tag's (system) nameservers and search to resolv.conf
-        hc.delete_name_server_tags_system(hc.get_name_server_tags_system())
-        hc.add_name_server_tags_system([hostsd_tag])
-
-        # this will add the dhcp client nameservers to resolv.conf
-        for intf in config['nameservers_dhcp_interfaces']:
-            hc.add_name_server_tags_system([f'dhcp-{intf}', f'dhcpv6-{intf}'])
-
-        hc.delete_hosts([hostsd_tag])
-        if config['static_host_mapping']:
-            hc.add_hosts({hostsd_tag: config['static_host_mapping']})
-
-        hc.apply()
-    except vyos.hostsd_client.VyOSHostsdError as e:
-        raise ConfigError(str(e))
-
-    ## Actually update the hostname -- vyos-hostsd doesn't do that
-
-    # No domain name -- the Debian way.
-    hostname_new = config['hostname']
-
-    # rsyslog runs into a race condition at boot time with systemd
-    # restart rsyslog only if the hostname changed.
-    hostname_old = cmd('hostnamectl --static')
-    call(f'hostnamectl set-hostname --static {hostname_new}')
-
-    # Restart services that use the hostname
-    if hostname_new != hostname_old:
-        call("systemctl restart rsyslog.service")
-
-    # If SNMP is running, restart it too
-    if process_named_running('snmpd'):
-        call('systemctl restart snmpd.service')
-
-    return None
-
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        sys.exit(1)
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
deleted file mode 100755
index 3dc5dfc01..000000000
--- a/src/conf_mode/https.py
+++ /dev/null
@@ -1,335 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-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 json
-
-from copy import deepcopy
-from time import sleep
-
-import vyos.defaults
-import vyos.certbot_util
-
-from vyos.base import Warning
-from vyos.config import Config
-from vyos.configdiff import get_config_diff
-from vyos.configverify import verify_vrf
-from vyos import ConfigError
-from vyos.pki import wrap_certificate
-from vyos.pki import wrap_private_key
-from vyos.template import render
-from vyos.utils.process import call
-from vyos.utils.process import is_systemd_service_running
-from vyos.utils.process import is_systemd_service_active
-from vyos.utils.network import check_port_availability
-from vyos.utils.network import is_listen_port_bind_service
-from vyos.utils.file import write_file
-
-from vyos import airbag
-airbag.enable()
-
-config_file = '/etc/nginx/sites-available/default'
-systemd_override = r'/run/systemd/system/nginx.service.d/override.conf'
-cert_dir = '/etc/ssl/certs'
-key_dir = '/etc/ssl/private'
-certbot_dir = vyos.defaults.directories['certbot']
-
-api_config_state = '/run/http-api-state'
-systemd_service = '/run/systemd/system/vyos-http-api.service'
-
-# https config needs to coordinate several subsystems: api, certbot,
-# self-signed certificate, as well as the virtual hosts defined within the
-# https config definition itself. Consequently, one needs a general dict,
-# encompassing the https and other configs, and a list of such virtual hosts
-# (server blocks in nginx terminology) to pass to the jinja2 template.
-default_server_block = {
-    'id'        : '',
-    'address'   : '*',
-    'port'      : '443',
-    'name'      : ['_'],
-    'api'       : False,
-    'vyos_cert' : {},
-    'certbot'   : False
-}
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base = ['service', 'https']
-    if not conf.exists(base):
-        return None
-
-    diff = get_config_diff(conf)
-
-    https = conf.get_config_dict(base, get_first_key=True)
-
-    if https:
-        https['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
-                                            no_tag_node_value_mangle=True,
-                                            get_first_key=True)
-
-    https['children_changed'] = diff.node_changed_children(base)
-    https['api_add_or_delete'] = diff.node_changed_presence(base + ['api'])
-
-    if 'api' not in https:
-        return https
-
-    http_api = conf.get_config_dict(base + ['api'], key_mangling=('-', '_'),
-                                    no_tag_node_value_mangle=True,
-                                    get_first_key=True,
-                                    with_recursive_defaults=True)
-
-    if http_api.from_defaults(['graphql']):
-        del http_api['graphql']
-
-    # Do we run inside a VRF context?
-    vrf_path = ['service', 'https', 'vrf']
-    if conf.exists(vrf_path):
-        http_api['vrf'] = conf.return_value(vrf_path)
-
-    https['api'] = http_api
-
-    return https
-
-def verify(https):
-    from vyos.utils.dict import dict_search
-
-    if https is None:
-        return None
-
-    if 'certificates' in https:
-        certificates = https['certificates']
-
-        if 'certificate' in certificates:
-            if not https['pki']:
-                raise ConfigError("PKI is not configured")
-
-            cert_name = certificates['certificate']
-
-            if cert_name not in https['pki']['certificate']:
-                raise ConfigError("Invalid certificate on https configuration")
-
-            pki_cert = https['pki']['certificate'][cert_name]
-
-            if 'certificate' not in pki_cert:
-                raise ConfigError("Missing certificate on https configuration")
-
-            if 'private' not in pki_cert or 'key' not in pki_cert['private']:
-                raise ConfigError("Missing certificate private key on https configuration")
-
-        if 'certbot' in https['certificates']:
-            vhost_names = []
-            for _, vh_conf in https.get('virtual-host', {}).items():
-                vhost_names += vh_conf.get('server-name', [])
-            domains = https['certificates']['certbot'].get('domain-name', [])
-            domains_found = [domain for domain in domains if domain in vhost_names]
-            if not domains_found:
-                raise ConfigError("At least one 'virtual-host <id> server-name' "
-                              "matching the 'certbot domain-name' is required.")
-
-    server_block_list = []
-
-    # organize by vhosts
-    vhost_dict = https.get('virtual-host', {})
-
-    if not vhost_dict:
-        # no specified virtual hosts (server blocks); use default
-        server_block_list.append(default_server_block)
-    else:
-        for vhost in list(vhost_dict):
-            server_block = deepcopy(default_server_block)
-            data = vhost_dict.get(vhost, {})
-            server_block['address'] = data.get('listen-address', '*')
-            server_block['port'] = data.get('port', '443')
-            server_block_list.append(server_block)
-
-    for entry in server_block_list:
-        _address = entry.get('address')
-        _address = '0.0.0.0' if _address == '*' else _address
-        _port = entry.get('port')
-        proto = 'tcp'
-        if check_port_availability(_address, int(_port), proto) is not True and \
-                not is_listen_port_bind_service(int(_port), 'nginx'):
-            raise ConfigError(f'"{proto}" port "{_port}" is used by another service')
-
-    verify_vrf(https)
-
-    # Verify API server settings, if present
-    if 'api' in https:
-        keys = dict_search('api.keys.id', https)
-        gql_auth_type = dict_search('api.graphql.authentication.type', https)
-
-        # If "api graphql" is not defined and `gql_auth_type` is None,
-        # there's certainly no JWT auth option, and keys are required
-        jwt_auth = (gql_auth_type == "token")
-
-        # Check for incomplete key configurations in every case
-        valid_keys_exist = False
-        if keys:
-            for k in keys:
-                if 'key' not in keys[k]:
-                    raise ConfigError(f'Missing HTTPS API key string for key id "{k}"')
-                else:
-                    valid_keys_exist = True
-
-        # If only key-based methods are enabled,
-        # fail the commit if no valid key configurations are found
-        if (not valid_keys_exist) and (not jwt_auth):
-            raise ConfigError('At least one HTTPS API key is required unless GraphQL token authentication is enabled')
-
-        if (not valid_keys_exist) and jwt_auth:
-            Warning(f'API keys are not configured: the classic (non-GraphQL) API will be unavailable.')
-
-    return None
-
-def generate(https):
-    if https is None:
-        return None
-
-    if 'api' not in https:
-        if os.path.exists(systemd_service):
-            os.unlink(systemd_service)
-    else:
-        render(systemd_service, 'https/vyos-http-api.service.j2', https['api'])
-        with open(api_config_state, 'w') as f:
-            json.dump(https['api'], f, indent=2)
-
-    server_block_list = []
-
-    # organize by vhosts
-
-    vhost_dict = https.get('virtual-host', {})
-
-    if not vhost_dict:
-        # no specified virtual hosts (server blocks); use default
-        server_block_list.append(default_server_block)
-    else:
-        for vhost in list(vhost_dict):
-            server_block = deepcopy(default_server_block)
-            server_block['id'] = vhost
-            data = vhost_dict.get(vhost, {})
-            server_block['address'] = data.get('listen-address', '*')
-            server_block['port'] = data.get('port', '443')
-            name = data.get('server-name', ['_'])
-            server_block['name'] = name
-            allow_client = data.get('allow-client', {})
-            server_block['allow_client'] = allow_client.get('address', [])
-            server_block_list.append(server_block)
-
-    # get certificate data
-
-    cert_dict = https.get('certificates', {})
-
-    if 'certificate' in cert_dict:
-        cert_name = cert_dict['certificate']
-        pki_cert = https['pki']['certificate'][cert_name]
-
-        cert_path = os.path.join(cert_dir, f'{cert_name}.pem')
-        key_path = os.path.join(key_dir, f'{cert_name}.pem')
-
-        server_cert = str(wrap_certificate(pki_cert['certificate']))
-        if 'ca-certificate' in cert_dict:
-            ca_cert = cert_dict['ca-certificate']
-            server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate']))
-
-        write_file(cert_path, server_cert)
-        write_file(key_path, wrap_private_key(pki_cert['private']['key']))
-
-        vyos_cert_data = {
-            'crt': cert_path,
-            'key': key_path
-        }
-
-        for block in server_block_list:
-            block['vyos_cert'] = vyos_cert_data
-
-    # letsencrypt certificate using certbot
-
-    certbot = False
-    cert_domains = cert_dict.get('certbot', {}).get('domain-name', [])
-    if cert_domains:
-        certbot = True
-        for domain in cert_domains:
-            sub_list = vyos.certbot_util.choose_server_block(server_block_list,
-                                                             domain)
-            if sub_list:
-                for sb in sub_list:
-                    sb['certbot'] = True
-                    sb['certbot_dir'] = certbot_dir
-                    # certbot organizes certificates by first domain
-                    sb['certbot_domain_dir'] = cert_domains[0]
-
-    if 'api' in list(https):
-        vhost_list = https.get('api-restrict', {}).get('virtual-host', [])
-        if not vhost_list:
-            for block in server_block_list:
-                block['api'] = True
-        else:
-            for block in server_block_list:
-                if block['id'] in vhost_list:
-                    block['api'] = True
-
-    data = {
-        'server_block_list': server_block_list,
-        'certbot': certbot
-    }
-
-    render(config_file, 'https/nginx.default.j2', data)
-    render(systemd_override, 'https/override.conf.j2', https)
-    return None
-
-def apply(https):
-    # Reload systemd manager configuration
-    call('systemctl daemon-reload')
-    http_api_service_name = 'vyos-http-api.service'
-    https_service_name = 'nginx.service'
-
-    if https is None:
-        if is_systemd_service_active(f'{http_api_service_name}'):
-            call(f'systemctl stop {http_api_service_name}')
-        call(f'systemctl stop {https_service_name}')
-        return
-
-    if 'api' in https['children_changed']:
-        if 'api' in https:
-            if is_systemd_service_running(f'{http_api_service_name}'):
-                call(f'systemctl reload {http_api_service_name}')
-            else:
-                call(f'systemctl restart {http_api_service_name}')
-            # Let uvicorn settle before (possibly) restarting nginx
-            sleep(1)
-        else:
-            if is_systemd_service_active(f'{http_api_service_name}'):
-                call(f'systemctl stop {http_api_service_name}')
-
-    if (not is_systemd_service_running(f'{https_service_name}') or
-        https['api_add_or_delete'] or
-        set(https['children_changed']) - set(['api'])):
-        call(f'systemctl restart {https_service_name}')
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        sys.exit(1)
diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py
deleted file mode 100755
index 40db417dd..000000000
--- a/src/conf_mode/igmp_proxy.py
+++ /dev/null
@@ -1,113 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 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 sys import exit
-from netifaces import interfaces
-
-from vyos.base import Warning
-from vyos.config import Config
-from vyos.template import render
-from vyos.utils.process import call
-from vyos.utils.dict import dict_search
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/etc/igmpproxy.conf'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base = ['protocols', 'igmp-proxy']
-    igmp_proxy = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                      get_first_key=True,
-                                      with_defaults=True)
-
-    if conf.exists(['protocols', 'igmp']):
-        igmp_proxy.update({'igmp_configured': ''})
-
-    if conf.exists(['protocols', 'pim']):
-        igmp_proxy.update({'pim_configured': ''})
-
-    return igmp_proxy
-
-def verify(igmp_proxy):
-    # bail out early - looks like removal from running config
-    if not igmp_proxy or 'disable' in igmp_proxy:
-        return None
-
-    if 'igmp_configured' in igmp_proxy or 'pim_configured' in igmp_proxy:
-        raise ConfigError('Can not configure both IGMP proxy and PIM '\
-                          'at the same time')
-
-    # at least two interfaces are required, one upstream and one downstream
-    if 'interface' not in igmp_proxy or len(igmp_proxy['interface']) < 2:
-        raise ConfigError('Must define exactly one upstream and at least one ' \
-                          'downstream interface!')
-
-    upstream = 0
-    for interface, config in igmp_proxy['interface'].items():
-        if interface not in interfaces():
-            raise ConfigError(f'Interface "{interface}" does not exist')
-        if dict_search('role', config) == 'upstream':
-            upstream += 1
-
-    if upstream == 0:
-        raise ConfigError('At least 1 upstream interface is required!')
-    elif upstream > 1:
-        raise ConfigError('Only 1 upstream interface allowed!')
-
-    return None
-
-def generate(igmp_proxy):
-    # bail out early - looks like removal from running config
-    if not igmp_proxy:
-        return None
-
-    # bail out early - service is disabled, but inform user
-    if 'disable' in igmp_proxy:
-        Warning('IGMP Proxy will be deactivated because it is disabled')
-        return None
-
-    render(config_file, 'igmp-proxy/igmpproxy.conf.j2', igmp_proxy)
-
-    return None
-
-def apply(igmp_proxy):
-    if not igmp_proxy or 'disable' in igmp_proxy:
-         # IGMP Proxy support is removed in the commit
-         call('systemctl stop igmpproxy.service')
-         if os.path.exists(config_file):
-             os.unlink(config_file)
-    else:
-        call('systemctl restart igmpproxy.service')
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/intel_qat.py b/src/conf_mode/intel_qat.py
deleted file mode 100755
index e4b248675..000000000
--- a/src/conf_mode/intel_qat.py
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-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 re
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.utils.process import popen
-from vyos.utils.process import run
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-qat_init_script = '/etc/init.d/qat_service'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    data = {}
-
-    if conf.exists(['system', 'acceleration', 'qat']):
-        data.update({'qat_enable' : ''})
-
-    if conf.exists(['vpn', 'ipsec']):
-        data.update({'ipsec' : ''})
-
-    if conf.exists(['interfaces', 'openvpn']):
-        data.update({'openvpn' : ''})
-
-    return data
-
-
-def vpn_control(action, force_ipsec=False):
-    # XXX: Should these commands report failure?
-    if action == 'restore' and force_ipsec:
-        return run('ipsec start')
-
-    return run(f'ipsec {action}')
-
-
-def verify(qat):
-    if 'qat_enable' not in qat:
-        return
-
-    # Check if QAT service installed
-    if not os.path.exists(qat_init_script):
-        raise ConfigError('QAT init script not found')
-
-    # Check if QAT device exist
-    output, err = popen('lspci -nn', decode='utf-8')
-    if not err:
-        # PCI id | Chipset
-        # 19e2 -> C3xx
-        # 37c8 -> C62x
-        # 0435 -> DH895
-        # 6f54 -> D15xx
-        # 18ee -> QAT_200XX
-        data = re.findall(
-            '(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)|(8086:18ee)', output)
-        # If QAT devices found
-        if not data:
-            raise ConfigError('No QAT acceleration device found')
-
-def apply(qat):
-    # Shutdown VPN service which can use QAT
-    if 'ipsec' in qat:
-        vpn_control('stop')
-
-    # Enable/Disable QAT service
-    if 'qat_enable' in qat:
-        run(f'{qat_init_script} start')
-    else:
-        run(f'{qat_init_script} stop')
-
-    # Recover VPN service
-    if 'ipsec' in qat:
-        vpn_control('start')
-
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        vpn_control('restore', force_ipsec=('ipsec' in c))
-        exit(1)
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
deleted file mode 100755
index 8184d8415..000000000
--- a/src/conf_mode/interfaces-bonding.py
+++ /dev/null
@@ -1,294 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-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 sys import exit
-from netifaces import interfaces
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configdict import is_node_changed
-from vyos.configdict import leaf_node_changed
-from vyos.configdict import is_member
-from vyos.configdict import is_source_interface
-from vyos.configverify import verify_address
-from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_dhcpv6
-from vyos.configverify import verify_mirror_redirect
-from vyos.configverify import verify_mtu_ipv6
-from vyos.configverify import verify_source_interface
-from vyos.configverify import verify_vlan_config
-from vyos.configverify import verify_vrf
-from vyos.ifconfig import BondIf
-from vyos.ifconfig.ethernet import EthernetIf
-from vyos.ifconfig import Section
-from vyos.template import render_to_string
-from vyos.utils.dict import dict_search
-from vyos.utils.dict import dict_to_paths_values
-from vyos.configdict import has_address_configured
-from vyos.configdict import has_vrf_configured
-from vyos.configdep import set_dependents, call_dependents
-from vyos import ConfigError
-from vyos import frr
-from vyos import airbag
-airbag.enable()
-
-def get_bond_mode(mode):
-    if mode == 'round-robin':
-        return 'balance-rr'
-    elif mode == 'active-backup':
-        return 'active-backup'
-    elif mode == 'xor-hash':
-        return 'balance-xor'
-    elif mode == 'broadcast':
-        return 'broadcast'
-    elif mode == '802.3ad':
-        return '802.3ad'
-    elif mode == 'transmit-load-balance':
-        return 'balance-tlb'
-    elif mode == 'adaptive-load-balance':
-        return 'balance-alb'
-    else:
-        raise ConfigError(f'invalid bond mode "{mode}"')
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'bonding']
-    ifname, bond = get_interface_dict(conf, base)
-
-    # To make our own life easier transfor the list of member interfaces
-    # into a dictionary - we will use this to add additional information
-    # later on for each member
-    if 'member' in bond and 'interface' in bond['member']:
-        # convert list of member interfaces to a dictionary
-        bond['member']['interface'] = {k: {} for k in bond['member']['interface']}
-
-    if 'mode' in bond:
-        bond['mode'] = get_bond_mode(bond['mode'])
-
-    tmp = is_node_changed(conf, base + [ifname, 'mode'])
-    if tmp: bond['shutdown_required'] = {}
-
-    tmp = is_node_changed(conf, base + [ifname, 'lacp-rate'])
-    if tmp: bond['shutdown_required'] = {}
-
-    # determine which members have been removed
-    interfaces_removed = leaf_node_changed(conf, base + [ifname, 'member', 'interface'])
-    # Reset config level to interfaces
-    old_level = conf.get_level()
-    conf.set_level(['interfaces'])
-
-    if interfaces_removed:
-        bond['shutdown_required'] = {}
-        if 'member' not in bond:
-            bond['member'] = {}
-
-        tmp = {}
-        for interface in interfaces_removed:
-            # if member is deleted from bond, add dependencies to call
-            # ethernet commit again in apply function
-            # to apply options under ethernet section
-            set_dependents('ethernet', conf, interface)
-            section = Section.section(interface) # this will be 'ethernet' for 'eth0'
-            if conf.exists([section, interface, 'disable']):
-                tmp[interface] = {'disable': ''}
-            else:
-                tmp[interface] = {}
-
-        # also present the interfaces to be removed from the bond as dictionary
-        bond['member']['interface_remove'] = tmp
-
-    # Restore existing config level
-    conf.set_level(old_level)
-
-    if dict_search('member.interface', bond):
-        for interface, interface_config in bond['member']['interface'].items():
-
-            interface_ethernet_config = conf.get_config_dict(
-                ['interfaces', 'ethernet', interface],
-                key_mangling=('-', '_'),
-                get_first_key=True,
-                no_tag_node_value_mangle=True,
-                with_defaults=False,
-                with_recursive_defaults=False)
-
-            interface_config['config_paths'] = dict_to_paths_values(interface_ethernet_config)
-
-            # Check if member interface is a new member
-            if not conf.exists_effective(base + [ifname, 'member', 'interface', interface]):
-                bond['shutdown_required'] = {}
-                interface_config['new_added'] = {}
-
-            # Check if member interface is disabled
-            conf.set_level(['interfaces'])
-
-            section = Section.section(interface) # this will be 'ethernet' for 'eth0'
-            if conf.exists([section, interface, 'disable']):
-                interface_config['disable'] = ''
-
-            conf.set_level(old_level)
-
-            # Check if member interface is already member of another bridge
-            tmp = is_member(conf, interface, 'bridge')
-            if tmp: interface_config['is_bridge_member'] = tmp
-
-            # Check if member interface is already member of a bond
-            tmp = is_member(conf, interface, 'bonding')
-            for tmp in is_member(conf, interface, 'bonding'):
-                if bond['ifname'] == tmp:
-                    continue
-                interface_config['is_bond_member'] = tmp
-
-            # Check if member interface is used as source-interface on another interface
-            tmp = is_source_interface(conf, interface)
-            if tmp: interface_config['is_source_interface'] = tmp
-
-            # bond members must not have an assigned address
-            tmp = has_address_configured(conf, interface)
-            if tmp: interface_config['has_address'] = {}
-
-            # bond members must not have a VRF attached
-            tmp = has_vrf_configured(conf, interface)
-            if tmp: interface_config['has_vrf'] = {}
-    return bond
-
-
-def verify(bond):
-    if 'deleted' in bond:
-        verify_bridge_delete(bond)
-        return None
-
-    if 'arp_monitor' in bond:
-        if 'target' in bond['arp_monitor'] and len(bond['arp_monitor']['target']) > 16:
-            raise ConfigError('The maximum number of arp-monitor targets is 16')
-
-        if 'interval' in bond['arp_monitor'] and int(bond['arp_monitor']['interval']) > 0:
-            if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']:
-                raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \
-                                  'transmit-load-balance or adaptive-load-balance')
-
-    if 'primary' in bond:
-        if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
-            raise ConfigError('Option primary - mode dependency failed, not'
-                              'supported in mode {mode}!'.format(**bond))
-
-    verify_mtu_ipv6(bond)
-    verify_address(bond)
-    verify_dhcpv6(bond)
-    verify_vrf(bond)
-    verify_mirror_redirect(bond)
-
-    # use common function to verify VLAN configuration
-    verify_vlan_config(bond)
-
-    bond_name = bond['ifname']
-    if dict_search('member.interface', bond):
-        for interface, interface_config in bond['member']['interface'].items():
-            error_msg = f'Can not add interface "{interface}" to bond, '
-
-            if interface == 'lo':
-                raise ConfigError('Loopback interface "lo" can not be added to a bond')
-
-            if interface not in interfaces():
-                raise ConfigError(error_msg + 'it does not exist!')
-
-            if 'is_bridge_member' in interface_config:
-                tmp = next(iter(interface_config['is_bridge_member']))
-                raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')
-
-            if 'is_bond_member' in interface_config:
-                tmp = next(iter(interface_config['is_bond_member']))
-                raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')
-
-            if 'is_source_interface' in interface_config:
-                tmp = interface_config['is_source_interface']
-                raise ConfigError(error_msg + f'it is the source-interface of "{tmp}"!')
-
-            if 'has_address' in interface_config:
-                raise ConfigError(error_msg + 'it has an address assigned!')
-
-            if 'has_vrf' in interface_config:
-                raise ConfigError(error_msg + 'it has a VRF assigned!')
-
-            if 'new_added' in interface_config and 'config_paths' in interface_config:
-                for option_path, option_value in interface_config['config_paths'].items():
-                    if option_path in EthernetIf.get_bond_member_allowed_options() :
-                        continue
-                    if option_path in BondIf.get_inherit_bond_options():
-                        continue
-                    raise ConfigError(error_msg + f'it has a "{option_path.replace(".", " ")}" assigned!')
-
-    if 'primary' in bond:
-        if bond['primary'] not in bond['member']['interface']:
-            raise ConfigError(f'Primary interface of bond "{bond_name}" must be a member interface')
-
-        if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
-            raise ConfigError('primary interface only works for mode active-backup, ' \
-                              'transmit-load-balance or adaptive-load-balance')
-
-    return None
-
-def generate(bond):
-    bond['frr_zebra_config'] = ''
-    if 'deleted' not in bond:
-        bond['frr_zebra_config'] = render_to_string('frr/evpn.mh.frr.j2', bond)
-    return None
-
-def apply(bond):
-    ifname = bond['ifname']
-    b = BondIf(ifname)
-    if 'deleted' in bond:
-        # delete interface
-        b.remove()
-    else:
-        b.update(bond)
-
-    if dict_search('member.interface_remove', bond):
-        try:
-            call_dependents()
-        except ConfigError:
-            raise ConfigError('Error in updating ethernet interface '
-                              'after deleting it from bond')
-
-    zebra_daemon = 'zebra'
-    # Save original configuration prior to starting any commit actions
-    frr_cfg = frr.FRRConfig()
-
-    # The route-map used for the FIB (zebra) is part of the zebra daemon
-    frr_cfg.load_configuration(zebra_daemon)
-    frr_cfg.modify_section(f'^interface {ifname}', stop_pattern='^exit', remove_stop_mark=True)
-    if 'frr_zebra_config' in bond:
-        frr_cfg.add_before(frr.default_add_before, bond['frr_zebra_config'])
-    frr_cfg.commit_configuration(zebra_daemon)
-
-    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/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
deleted file mode 100755
index 29991e2da..000000000
--- a/src/conf_mode/interfaces-bridge.py
+++ /dev/null
@@ -1,186 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-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/>.
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configdict import node_changed
-from vyos.configdict import is_member
-from vyos.configdict import is_source_interface
-from vyos.configdict import has_vlan_subinterface_configured
-from vyos.configverify import verify_dhcpv6
-from vyos.configverify import verify_mirror_redirect
-from vyos.configverify import verify_vrf
-from vyos.ifconfig import BridgeIf
-from vyos.configdict import has_address_configured
-from vyos.configdict import has_vrf_configured
-from vyos.configdep import set_dependents
-from vyos.configdep import call_dependents
-from vyos.utils.dict import dict_search
-from vyos import ConfigError
-
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'bridge']
-    ifname, bridge = get_interface_dict(conf, base)
-
-    # determine which members have been removed
-    tmp = node_changed(conf, base + [ifname, 'member', 'interface'])
-    if tmp:
-        if 'member' in bridge:
-            bridge['member'].update({'interface_remove' : tmp })
-        else:
-            bridge.update({'member' : {'interface_remove' : tmp }})
-
-    if dict_search('member.interface', bridge) is not None:
-        for interface in list(bridge['member']['interface']):
-            # Check if member interface is already member of another bridge
-            tmp = is_member(conf, interface, 'bridge')
-            if tmp and bridge['ifname'] not in tmp:
-                bridge['member']['interface'][interface].update({'is_bridge_member' : tmp})
-
-            # Check if member interface is already member of a bond
-            tmp = is_member(conf, interface, 'bonding')
-            if tmp: bridge['member']['interface'][interface].update({'is_bond_member' : tmp})
-
-            # Check if member interface is used as source-interface on another interface
-            tmp = is_source_interface(conf, interface)
-            if tmp: bridge['member']['interface'][interface].update({'is_source_interface' : tmp})
-
-            # Bridge members must not have an assigned address
-            tmp = has_address_configured(conf, interface)
-            if tmp: bridge['member']['interface'][interface].update({'has_address' : ''})
-
-            # Bridge members must not have a VRF attached
-            tmp = has_vrf_configured(conf, interface)
-            if tmp: bridge['member']['interface'][interface].update({'has_vrf' : ''})
-
-            # VLAN-aware bridge members must not have VLAN interface configuration
-            tmp = has_vlan_subinterface_configured(conf,interface)
-            if 'enable_vlan' in bridge and tmp:
-                bridge['member']['interface'][interface].update({'has_vlan' : ''})
-
-            # When using VXLAN member interfaces that are configured for Single
-            # VXLAN Device (SVD) we need to call the VXLAN conf-mode script to re-create
-            # VLAN to VNI mappings if required
-            if interface.startswith('vxlan'):
-                set_dependents('vxlan', conf, interface)
-
-    # delete empty dictionary keys - no need to run code paths if nothing is there to do
-    if 'member' in bridge:
-        if 'interface' in bridge['member'] and len(bridge['member']['interface']) == 0:
-            del bridge['member']['interface']
-
-        if len(bridge['member']) == 0:
-            del bridge['member']
-
-    return bridge
-
-def verify(bridge):
-    if 'deleted' in bridge:
-        return None
-
-    verify_dhcpv6(bridge)
-    verify_vrf(bridge)
-    verify_mirror_redirect(bridge)
-
-    ifname = bridge['ifname']
-
-    if dict_search('member.interface', bridge):
-        for interface, interface_config in bridge['member']['interface'].items():
-            error_msg = f'Can not add interface "{interface}" to bridge, '
-
-            if interface == 'lo':
-                raise ConfigError('Loopback interface "lo" can not be added to a bridge')
-
-            if 'is_bridge_member' in interface_config:
-                tmp = next(iter(interface_config['is_bridge_member']))
-                raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')
-
-            if 'is_bond_member' in interface_config:
-                tmp = next(iter(interface_config['is_bond_member']))
-                raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')
-
-            if 'is_source_interface' in interface_config:
-                tmp = interface_config['is_source_interface']
-                raise ConfigError(error_msg + f'it is the source-interface of "{tmp}"!')
-
-            if 'has_address' in interface_config:
-                raise ConfigError(error_msg + 'it has an address assigned!')
-
-            if 'has_vrf' in interface_config:
-                raise ConfigError(error_msg + 'it has a VRF assigned!')
-
-            if 'enable_vlan' in bridge:
-                if 'has_vlan' in interface_config:
-                    raise ConfigError(error_msg + 'it has VLAN subinterface(s) assigned!')
-
-                if 'wlan' in interface:
-                    raise ConfigError(error_msg + 'VLAN aware cannot be set!')
-            else:
-                for option in ['allowed_vlan', 'native_vlan']:
-                    if option in interface_config:
-                        raise ConfigError('Can not use VLAN options on non VLAN aware bridge')
-
-    if 'enable_vlan' in bridge:
-        if dict_search('vif.1', bridge):
-            raise ConfigError(f'VLAN 1 sub interface cannot be set for VLAN aware bridge {ifname}, and VLAN 1 is always the parent interface')
-    else:
-        if dict_search('vif', bridge):
-            raise ConfigError(f'You must first activate "enable-vlan" of {ifname} bridge to use "vif"')
-
-    return None
-
-def generate(bridge):
-    return None
-
-def apply(bridge):
-    br = BridgeIf(bridge['ifname'])
-    if 'deleted' in bridge:
-        # delete interface
-        br.remove()
-    else:
-        br.update(bridge)
-
-    for interface in dict_search('member.interface', bridge) or []:
-        if interface.startswith('vxlan'):
-            try:
-                call_dependents()
-            except ConfigError:
-                raise ConfigError('Error in updating VXLAN interface after changing bridge!')
-
-    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/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
deleted file mode 100755
index db768b94d..000000000
--- a/src/conf_mode/interfaces-dummy.py
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-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/>.
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configverify import verify_vrf
-from vyos.configverify import verify_address
-from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_mirror_redirect
-from vyos.ifconfig import DummyIf
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'dummy']
-    _, dummy = get_interface_dict(conf, base)
-    return dummy
-
-def verify(dummy):
-    if 'deleted' in dummy:
-        verify_bridge_delete(dummy)
-        return None
-
-    verify_vrf(dummy)
-    verify_address(dummy)
-    verify_mirror_redirect(dummy)
-
-    return None
-
-def generate(dummy):
-    return None
-
-def apply(dummy):
-    d = DummyIf(**dummy)
-
-    # Remove dummy interface
-    if 'deleted' in dummy:
-        d.remove()
-    else:
-        d.update(dummy)
-
-    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/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
deleted file mode 100755
index 7374a29f7..000000000
--- a/src/conf_mode/interfaces-ethernet.py
+++ /dev/null
@@ -1,400 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-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 pprint
-
-from glob import glob
-from sys import exit
-
-from vyos.base import Warning
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configdict import is_node_changed
-from vyos.configverify import verify_address
-from vyos.configverify import verify_dhcpv6
-from vyos.configverify import verify_eapol
-from vyos.configverify import verify_interface_exists
-from vyos.configverify import verify_mirror_redirect
-from vyos.configverify import verify_mtu
-from vyos.configverify import verify_mtu_ipv6
-from vyos.configverify import verify_vlan_config
-from vyos.configverify import verify_vrf
-from vyos.configverify import verify_bond_bridge_member
-from vyos.ethtool import Ethtool
-from vyos.ifconfig import EthernetIf
-from vyos.ifconfig import BondIf
-from vyos.pki import find_chain
-from vyos.pki import encode_certificate
-from vyos.pki import load_certificate
-from vyos.pki import wrap_private_key
-from vyos.template import render
-from vyos.utils.process import call
-from vyos.utils.dict import dict_search
-from vyos.utils.dict import dict_to_paths_values
-from vyos.utils.dict import dict_set
-from vyos.utils.dict import dict_delete
-from vyos.utils.file import write_file
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-# XXX: wpa_supplicant works on the source interface
-cfg_dir = '/run/wpa_supplicant'
-wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf'
-
-def update_bond_options(conf: Config, eth_conf: dict) -> list:
-    """
-    Return list of blocked options if interface is a bond member
-    :param conf: Config object
-    :type conf: Config
-    :param eth_conf: Ethernet config dictionary
-    :type eth_conf: dict
-    :return: List of blocked options
-    :rtype: list
-    """
-    blocked_list = []
-    bond_name = list(eth_conf['is_bond_member'].keys())[0]
-    config_without_defaults = conf.get_config_dict(
-        ['interfaces', 'ethernet', eth_conf['ifname']],
-        key_mangling=('-', '_'),
-        get_first_key=True,
-        no_tag_node_value_mangle=True,
-        with_defaults=False,
-        with_recursive_defaults=False)
-    config_with_defaults = conf.get_config_dict(
-        ['interfaces', 'ethernet', eth_conf['ifname']],
-        key_mangling=('-', '_'),
-        get_first_key=True,
-        no_tag_node_value_mangle=True,
-        with_defaults=True,
-        with_recursive_defaults=True)
-    bond_config_with_defaults = conf.get_config_dict(
-        ['interfaces', 'bonding', bond_name],
-        key_mangling=('-', '_'),
-        get_first_key=True,
-        no_tag_node_value_mangle=True,
-        with_defaults=True,
-        with_recursive_defaults=True)
-    eth_dict_paths = dict_to_paths_values(config_without_defaults)
-    eth_path_base = ['interfaces', 'ethernet', eth_conf['ifname']]
-
-    #if option is configured under ethernet section
-    for option_path, option_value in eth_dict_paths.items():
-        bond_option_value = dict_search(option_path, bond_config_with_defaults)
-
-        #If option is allowed for changing then continue
-        if option_path in EthernetIf.get_bond_member_allowed_options():
-            continue
-        # if option is inherited from bond then set valued from bond interface
-        if option_path in BondIf.get_inherit_bond_options():
-            # If option equals to bond option then do nothing
-            if option_value == bond_option_value:
-                continue
-            else:
-                # if ethernet has option and bond interface has
-                # then copy it from bond
-                if bond_option_value is not None:
-                    if is_node_changed(conf, eth_path_base + option_path.split('.')):
-                        Warning(
-                            f'Cannot apply "{option_path.replace(".", " ")}" to "{option_value}".' \
-                            f' Interface "{eth_conf["ifname"]}" is a bond member.' \
-                            f' Option is inherited from bond "{bond_name}"')
-                    dict_set(option_path, bond_option_value, eth_conf)
-                    continue
-                # if ethernet has option and bond interface does not have
-                # then delete it form dict and do not apply it
-                else:
-                    if is_node_changed(conf, eth_path_base + option_path.split('.')):
-                        Warning(
-                            f'Cannot apply "{option_path.replace(".", " ")}".' \
-                            f' Interface "{eth_conf["ifname"]}" is a bond member.' \
-                            f' Option is inherited from bond "{bond_name}"')
-                    dict_delete(option_path, eth_conf)
-        blocked_list.append(option_path)
-
-    # if inherited option is not configured under ethernet section but configured under bond section
-    for option_path in BondIf.get_inherit_bond_options():
-        bond_option_value = dict_search(option_path, bond_config_with_defaults)
-        if bond_option_value is not None:
-            if option_path not in eth_dict_paths:
-                if is_node_changed(conf, eth_path_base + option_path.split('.')):
-                    Warning(
-                        f'Cannot apply "{option_path.replace(".", " ")}" to "{dict_search(option_path, config_with_defaults)}".' \
-                        f' Interface "{eth_conf["ifname"]}" is a bond member. ' \
-                        f'Option is inherited from bond "{bond_name}"')
-                dict_set(option_path, bond_option_value, eth_conf)
-    eth_conf['bond_blocked_changes'] = blocked_list
-    return None
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    # This must be called prior to get_interface_dict(), as this function will
-    # alter the config level (config.set_level())
-    pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
-                               get_first_key=True, no_tag_node_value_mangle=True)
-
-    base = ['interfaces', 'ethernet']
-    ifname, ethernet = get_interface_dict(conf, base)
-    if 'is_bond_member' in ethernet:
-        update_bond_options(conf, ethernet)
-
-    if 'deleted' not in ethernet:
-       if pki: ethernet['pki'] = pki
-
-    tmp = is_node_changed(conf, base + [ifname, 'speed'])
-    if tmp: ethernet.update({'speed_duplex_changed': {}})
-
-    tmp = is_node_changed(conf, base + [ifname, 'duplex'])
-    if tmp: ethernet.update({'speed_duplex_changed': {}})
-
-    return ethernet
-
-
-
-def verify_speed_duplex(ethernet: dict, ethtool: Ethtool):
-    """
-     Verify speed and duplex
-    :param ethernet: dictionary which is received from get_interface_dict
-    :type ethernet: dict
-    :param ethtool: Ethernet object
-    :type ethtool: Ethtool
-    """
-    if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or
-            (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
-        raise ConfigError(
-            'Speed/Duplex missmatch. Must be both auto or manually configured')
-
-    if ethernet['speed'] != 'auto' and ethernet['duplex'] != 'auto':
-        # We need to verify if the requested speed and duplex setting is
-        # supported by the underlaying NIC.
-        speed = ethernet['speed']
-        duplex = ethernet['duplex']
-        if not ethtool.check_speed_duplex(speed, duplex):
-            raise ConfigError(
-                f'Adapter does not support changing speed ' \
-                f'and duplex settings to: {speed}/{duplex}!')
-
-
-def verify_flow_control(ethernet: dict, ethtool: Ethtool):
-    """
-     Verify flow control
-    :param ethernet: dictionary which is received from get_interface_dict
-    :type ethernet: dict
-    :param ethtool: Ethernet object
-    :type ethtool: Ethtool
-    """
-    if 'disable_flow_control' in ethernet:
-        if not ethtool.check_flow_control():
-            raise ConfigError(
-                'Adapter does not support changing flow-control settings!')
-
-
-def verify_ring_buffer(ethernet: dict, ethtool: Ethtool):
-    """
-     Verify ring buffer
-    :param ethernet: dictionary which is received from get_interface_dict
-    :type ethernet: dict
-    :param ethtool: Ethernet object
-    :type ethtool: Ethtool
-    """
-    if 'ring_buffer' in ethernet:
-        max_rx = ethtool.get_ring_buffer_max('rx')
-        if not max_rx:
-            raise ConfigError(
-                'Driver does not support RX ring-buffer configuration!')
-
-        max_tx = ethtool.get_ring_buffer_max('tx')
-        if not max_tx:
-            raise ConfigError(
-                'Driver does not support TX ring-buffer configuration!')
-
-        rx = dict_search('ring_buffer.rx', ethernet)
-        if rx and int(rx) > int(max_rx):
-            raise ConfigError(f'Driver only supports a maximum RX ring-buffer ' \
-                              f'size of "{max_rx}" bytes!')
-
-        tx = dict_search('ring_buffer.tx', ethernet)
-        if tx and int(tx) > int(max_tx):
-            raise ConfigError(f'Driver only supports a maximum TX ring-buffer ' \
-                              f'size of "{max_tx}" bytes!')
-
-
-def verify_offload(ethernet: dict, ethtool: Ethtool):
-    """
-     Verify offloading capabilities
-    :param ethernet: dictionary which is received from get_interface_dict
-    :type ethernet: dict
-    :param ethtool: Ethernet object
-    :type ethtool: Ethtool
-    """
-    if dict_search('offload.rps', ethernet) != None:
-        if not os.path.exists(f'/sys/class/net/{ethernet["ifname"]}/queues/rx-0/rps_cpus'):
-            raise ConfigError('Interface does not suport RPS!')
-    driver = ethtool.get_driver_name()
-    # T3342 - Xen driver requires special treatment
-    if driver == 'vif':
-        if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None:
-            raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\
-                              'for MTU size larger then 1500 bytes')
-
-
-def verify_allowedbond_changes(ethernet: dict):
-    """
-     Verify changed options if interface is in bonding
-    :param ethernet: dictionary which is received from get_interface_dict
-    :type ethernet: dict
-    """
-    if 'bond_blocked_changes' in ethernet:
-        for option in ethernet['bond_blocked_changes']:
-            raise ConfigError(f'Cannot configure "{option.replace(".", " ")}"' \
-                              f' on interface "{ethernet["ifname"]}".' \
-                              f' Interface is a bond member')
-
-
-def verify(ethernet):
-    if 'deleted' in ethernet:
-        return None
-    if 'is_bond_member' in ethernet:
-        verify_bond_member(ethernet)
-    else:
-        verify_ethernet(ethernet)
-
-
-def verify_bond_member(ethernet):
-    """
-     Verification function for ethernet interface which is in bonding
-    :param ethernet: dictionary which is received from get_interface_dict
-    :type ethernet: dict
-    """
-    ifname = ethernet['ifname']
-    verify_interface_exists(ifname)
-    verify_eapol(ethernet)
-    verify_mirror_redirect(ethernet)
-    ethtool = Ethtool(ifname)
-    verify_speed_duplex(ethernet, ethtool)
-    verify_flow_control(ethernet, ethtool)
-    verify_ring_buffer(ethernet, ethtool)
-    verify_offload(ethernet, ethtool)
-    verify_allowedbond_changes(ethernet)
-
-def verify_ethernet(ethernet):
-    """
-     Verification function for simple ethernet interface
-    :param ethernet: dictionary which is received from get_interface_dict
-    :type ethernet: dict
-    """
-    ifname = ethernet['ifname']
-    verify_interface_exists(ifname)
-    verify_mtu(ethernet)
-    verify_mtu_ipv6(ethernet)
-    verify_dhcpv6(ethernet)
-    verify_address(ethernet)
-    verify_vrf(ethernet)
-    verify_bond_bridge_member(ethernet)
-    verify_eapol(ethernet)
-    verify_mirror_redirect(ethernet)
-    ethtool = Ethtool(ifname)
-    # No need to check speed and duplex keys as both have default values.
-    verify_speed_duplex(ethernet, ethtool)
-    verify_flow_control(ethernet, ethtool)
-    verify_ring_buffer(ethernet, ethtool)
-    verify_offload(ethernet, ethtool)
-    # use common function to verify VLAN configuration
-    verify_vlan_config(ethernet)
-    return None
-
-
-def generate(ethernet):
-    # render real configuration file once
-    wpa_supplicant_conf = wpa_suppl_conf.format(**ethernet)
-
-    if 'deleted' in ethernet:
-        # delete configuration on interface removal
-        if os.path.isfile(wpa_supplicant_conf):
-            os.unlink(wpa_supplicant_conf)
-        return None
-
-    if 'eapol' in ethernet:
-        ifname = ethernet['ifname']
-
-        render(wpa_supplicant_conf, 'ethernet/wpa_supplicant.conf.j2', ethernet)
-
-        cert_file_path = os.path.join(cfg_dir, f'{ifname}_cert.pem')
-        cert_key_path = os.path.join(cfg_dir, f'{ifname}_cert.key')
-
-        cert_name = ethernet['eapol']['certificate']
-        pki_cert = ethernet['pki']['certificate'][cert_name]
-
-        loaded_pki_cert = load_certificate(pki_cert['certificate'])
-        loaded_ca_certs = {load_certificate(c['certificate'])
-            for c in ethernet['pki']['ca'].values()} if 'ca' in ethernet['pki'] else {}
-
-        cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs)
-
-        write_file(cert_file_path,
-                   '\n'.join(encode_certificate(c) for c in cert_full_chain))
-        write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))
-
-        if 'ca_certificate' in ethernet['eapol']:
-            ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem')
-            ca_chains = []
-
-            for ca_cert_name in ethernet['eapol']['ca_certificate']:
-                pki_ca_cert = ethernet['pki']['ca'][ca_cert_name]
-                loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
-                ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
-                ca_chains.append(
-                    '\n'.join(encode_certificate(c) for c in ca_full_chain))
-
-            write_file(ca_cert_file_path, '\n'.join(ca_chains))
-
-    return None
-
-def apply(ethernet):
-    ifname = ethernet['ifname']
-    # take care about EAPoL supplicant daemon
-    eapol_action='stop'
-
-    e = EthernetIf(ifname)
-    if 'deleted' in ethernet:
-        # delete interface
-        e.remove()
-    else:
-        e.update(ethernet)
-        if 'eapol' in ethernet:
-            eapol_action='reload-or-restart'
-
-    call(f'systemctl {eapol_action} wpa_supplicant-wired@{ifname}')
-
-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/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
deleted file mode 100755
index f6694ddde..000000000
--- a/src/conf_mode/interfaces-geneve.py
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-2022 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/>.
-
-from sys import exit
-from netifaces import interfaces
-
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configdict import is_node_changed
-from vyos.configverify import verify_address
-from vyos.configverify import verify_mtu_ipv6
-from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_mirror_redirect
-from vyos.configverify import verify_bond_bridge_member
-from vyos.ifconfig import GeneveIf
-from vyos import ConfigError
-
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'geneve']
-    ifname, geneve = get_interface_dict(conf, base)
-
-    # GENEVE interfaces are picky and require recreation if certain parameters
-    # change. But a GENEVE interface should - of course - not be re-created if
-    # it's description or IP address is adjusted. Feels somehow logic doesn't it?
-    for cli_option in ['remote', 'vni', 'parameters']:
-        if is_node_changed(conf, base + [ifname, cli_option]):
-            geneve.update({'rebuild_required': {}})
-
-    return geneve
-
-def verify(geneve):
-    if 'deleted' in geneve:
-        verify_bridge_delete(geneve)
-        return None
-
-    verify_mtu_ipv6(geneve)
-    verify_address(geneve)
-    verify_bond_bridge_member(geneve)
-    verify_mirror_redirect(geneve)
-
-    if 'remote' not in geneve:
-        raise ConfigError('Remote side must be configured')
-
-    if 'vni' not in geneve:
-        raise ConfigError('VNI must be configured')
-
-    return None
-
-
-def generate(geneve):
-    return None
-
-def apply(geneve):
-    # Check if GENEVE interface already exists
-    if 'rebuild_required' in geneve or 'delete' in geneve:
-        if geneve['ifname'] in interfaces():
-            g = GeneveIf(geneve['ifname'])
-            # GENEVE is super picky and the tunnel always needs to be recreated,
-            # thus we can simply always delete it first.
-            g.remove()
-
-    if 'deleted' not in geneve:
-        # Finally create the new interface
-        g = GeneveIf(**geneve)
-        g.update(geneve)
-
-    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/interfaces-input.py b/src/conf_mode/interfaces-input.py
deleted file mode 100755
index ad248843d..000000000
--- a/src/conf_mode/interfaces-input.py
+++ /dev/null
@@ -1,70 +0,0 @@
-#!/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/>.
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configverify import verify_mirror_redirect
-from vyos.ifconfig import InputIf
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at
-    least the interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'input']
-    _, ifb = get_interface_dict(conf, base)
-
-    return ifb
-
-def verify(ifb):
-    if 'deleted' in ifb:
-        return None
-
-    verify_mirror_redirect(ifb)
-    return None
-
-def generate(ifb):
-    return None
-
-def apply(ifb):
-    d = InputIf(ifb['ifname'])
-
-    # Remove input interface
-    if 'deleted' in ifb:
-        d.remove()
-    else:
-        d.update(ifb)
-
-    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/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
deleted file mode 100755
index e1db3206e..000000000
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-2020 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 sys import exit
-from netifaces import interfaces
-
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configdict import leaf_node_changed
-from vyos.configverify import verify_address
-from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_mtu_ipv6
-from vyos.configverify import verify_mirror_redirect
-from vyos.configverify import verify_bond_bridge_member
-from vyos.ifconfig import L2TPv3If
-from vyos.utils.kernel import check_kmod
-from vyos.utils.network import is_addr_assigned
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-k_mod = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6']
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'l2tpv3']
-    ifname, l2tpv3 = get_interface_dict(conf, base)
-
-    # To delete an l2tpv3 interface we need the current tunnel and session-id
-    if 'deleted' in l2tpv3:
-        tmp = leaf_node_changed(conf, base + [ifname, 'tunnel-id'])
-        # leaf_node_changed() returns a list
-        l2tpv3.update({'tunnel_id': tmp[0]})
-
-        tmp = leaf_node_changed(conf, base + [ifname, 'session-id'])
-        l2tpv3.update({'session_id': tmp[0]})
-
-    return l2tpv3
-
-def verify(l2tpv3):
-    if 'deleted' in l2tpv3:
-        verify_bridge_delete(l2tpv3)
-        return None
-
-    interface = l2tpv3['ifname']
-
-    for key in ['source_address', 'remote', 'tunnel_id', 'peer_tunnel_id',
-                'session_id', 'peer_session_id']:
-        if key not in l2tpv3:
-            tmp = key.replace('_', '-')
-            raise ConfigError(f'Missing mandatory L2TPv3 option: "{tmp}"!')
-
-    if not is_addr_assigned(l2tpv3['source_address']):
-        raise ConfigError('L2TPv3 source-address address "{source_address}" '
-                          'not configured on any interface!'.format(**l2tpv3))
-
-    verify_mtu_ipv6(l2tpv3)
-    verify_address(l2tpv3)
-    verify_bond_bridge_member(l2tpv3)
-    verify_mirror_redirect(l2tpv3)
-    return None
-
-def generate(l2tpv3):
-    return None
-
-def apply(l2tpv3):
-    # Check if L2TPv3 interface already exists
-    if l2tpv3['ifname'] in interfaces():
-        # L2TPv3 is picky when changing tunnels/sessions, thus we can simply
-        # always delete it first.
-        l = L2TPv3If(**l2tpv3)
-        l.remove()
-
-    if 'deleted' not in l2tpv3:
-        # Finally create the new interface
-        l = L2TPv3If(**l2tpv3)
-        l.update(l2tpv3)
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        check_kmod(k_mod)
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py
deleted file mode 100755
index 08d34477a..000000000
--- a/src/conf_mode/interfaces-loopback.py
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-2020 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 sys import exit
-
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configverify import verify_mirror_redirect
-from vyos.ifconfig import LoopbackIf
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'loopback']
-    _, loopback = get_interface_dict(conf, base)
-    return loopback
-
-def verify(loopback):
-    verify_mirror_redirect(loopback)
-    return None
-
-def generate(loopback):
-    return None
-
-def apply(loopback):
-    l = LoopbackIf(**loopback)
-    if 'deleted' in loopback:
-        l.remove()
-    else:
-        l.update(loopback)
-
-    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/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py
deleted file mode 100755
index 0a927ac88..000000000
--- a/src/conf_mode/interfaces-macsec.py
+++ /dev/null
@@ -1,207 +0,0 @@
-#!/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 netifaces import interfaces
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configdict import is_node_changed
-from vyos.configdict import is_source_interface
-from vyos.configverify import verify_vrf
-from vyos.configverify import verify_address
-from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_mtu_ipv6
-from vyos.configverify import verify_mirror_redirect
-from vyos.configverify import verify_source_interface
-from vyos.configverify import verify_bond_bridge_member
-from vyos.ifconfig import MACsecIf
-from vyos.ifconfig import Interface
-from vyos.template import render
-from vyos.utils.process import call
-from vyos.utils.dict import dict_search
-from vyos.utils.process import is_systemd_service_running
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-# XXX: wpa_supplicant works on the source interface
-wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf'
-
-# Constants
-## gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit
-GCM_AES_128_LEN: int = 32
-GCM_128_KEY_ERROR = 'gcm-aes-128 requires a 128bit long key!'
-## gcm-aes-256 requires a 256bit long key - 64 characters (string) = 32byte = 256bit
-GCM_AES_256_LEN: int = 64
-GCM_256_KEY_ERROR = 'gcm-aes-256 requires a 256bit long key!'
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'macsec']
-    ifname, macsec = get_interface_dict(conf, base)
-
-    # Check if interface has been removed
-    if 'deleted' in macsec:
-        source_interface = conf.return_effective_value(base + [ifname, 'source-interface'])
-        macsec.update({'source_interface': source_interface})
-
-    if is_node_changed(conf, base + [ifname, 'security']):
-        macsec.update({'shutdown_required': {}})
-
-    if is_node_changed(conf, base + [ifname, 'source_interface']):
-        macsec.update({'shutdown_required': {}})
-
-    if 'source_interface' in macsec:
-        tmp = is_source_interface(conf, macsec['source_interface'], ['macsec', 'pseudo-ethernet'])
-        if tmp and tmp != ifname: macsec.update({'is_source_interface' : tmp})
-
-    return macsec
-
-
-def verify(macsec):
-    if 'deleted' in macsec:
-        verify_bridge_delete(macsec)
-        return None
-
-    verify_source_interface(macsec)
-    verify_vrf(macsec)
-    verify_mtu_ipv6(macsec)
-    verify_address(macsec)
-    verify_bond_bridge_member(macsec)
-    verify_mirror_redirect(macsec)
-
-    if dict_search('security.cipher', macsec) == None:
-        raise ConfigError('Cipher suite must be set for MACsec "{ifname}"'.format(**macsec))
-
-    if dict_search('security.encrypt', macsec) != None:
-        # Check that only static or MKA config is present
-        if dict_search('security.static', macsec) != None and (dict_search('security.mka.cak', macsec) != None or dict_search('security.mka.ckn', macsec) != None):
-            raise ConfigError('Only static or MKA can be used!')
-
-        # Logic to check static configuration
-        if dict_search('security.static', macsec) != None:
-            # tx-key must be defined
-            if dict_search('security.static.key', macsec) == None:
-                raise ConfigError('Static MACsec tx-key must be defined.')
-
-            tx_len = len(dict_search('security.static.key', macsec))
-
-            if dict_search('security.cipher', macsec) == 'gcm-aes-128' and tx_len != GCM_AES_128_LEN:
-                raise ConfigError(GCM_128_KEY_ERROR)
-
-            if dict_search('security.cipher', macsec) == 'gcm-aes-256' and tx_len != GCM_AES_256_LEN:
-                raise ConfigError(GCM_256_KEY_ERROR)
-
-            # Make sure at least one peer is defined
-            if 'peer' not in macsec['security']['static']:
-                raise ConfigError('Must have at least one peer defined for static MACsec')
-
-            # For every enabled peer, make sure a MAC and rx-key is defined
-            for peer, peer_config in macsec['security']['static']['peer'].items():
-                if 'disable' not in peer_config and ('mac' not in peer_config or 'key' not in peer_config):
-                    raise ConfigError('Every enabled MACsec static peer must have a MAC address and rx-key defined.')
-
-                # check rx-key length against cipher suite
-                rx_len = len(peer_config['key'])
-
-                if dict_search('security.cipher', macsec) == 'gcm-aes-128' and rx_len != GCM_AES_128_LEN:
-                    raise ConfigError(GCM_128_KEY_ERROR)
-
-                if dict_search('security.cipher', macsec) == 'gcm-aes-256' and rx_len != GCM_AES_256_LEN:
-                    raise ConfigError(GCM_256_KEY_ERROR)
-
-        # Logic to check MKA configuration
-        else:
-            if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None:
-                raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!')
-
-            cak_len = len(dict_search('security.mka.cak', macsec))
-
-            if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != GCM_AES_128_LEN:
-                raise ConfigError(GCM_128_KEY_ERROR)
-
-            elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != GCM_AES_256_LEN:
-                raise ConfigError(GCM_256_KEY_ERROR)
-
-    if 'source_interface' in macsec:
-        # MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad
-        # and 802.1q) - we need to check the underlaying MTU if our configured
-        # MTU is at least 40 bytes less then the MTU of our physical interface.
-        lower_mtu = Interface(macsec['source_interface']).get_mtu()
-        if lower_mtu < (int(macsec['mtu']) + 40):
-            raise ConfigError('MACsec overhead does not fit into underlaying device MTU,\n' \
-                              f'{lower_mtu} bytes is too small!')
-
-    return None
-
-
-def generate(macsec):
-    # Only generate wpa_supplicant config if using MKA
-    if dict_search('security.mka.cak', macsec):
-        render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec)
-    return None
-
-
-def apply(macsec):
-    systemd_service = 'wpa_supplicant-macsec@{source_interface}'.format(**macsec)
-
-    # Remove macsec interface on deletion or mandatory parameter change
-    if 'deleted' in macsec or 'shutdown_required' in macsec:
-        call(f'systemctl stop {systemd_service}')
-
-        if macsec['ifname'] in interfaces():
-            tmp = MACsecIf(macsec['ifname'])
-            tmp.remove()
-
-        if 'deleted' in macsec:
-            # delete configuration on interface removal
-            if os.path.isfile(wpa_suppl_conf.format(**macsec)):
-                os.unlink(wpa_suppl_conf.format(**macsec))
-
-            return None
-
-    # It is safe to "re-create" the interface always, there is a sanity
-    # check that the interface will only be create if its non existent
-    i = MACsecIf(**macsec)
-    i.update(macsec)
-
-    # Only reload/restart if using MKA
-    if dict_search('security.mka.cak', macsec):
-        if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec:
-            call(f'systemctl reload-or-restart {systemd_service}')
-
-    return None
-
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
deleted file mode 100755
index bdeb44837..000000000
--- a/src/conf_mode/interfaces-openvpn.py
+++ /dev/null
@@ -1,732 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-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 re
-import tempfile
-
-from cryptography.hazmat.primitives.asymmetric import ec
-from glob import glob
-from sys import exit
-from ipaddress import IPv4Address
-from ipaddress import IPv4Network
-from ipaddress import IPv6Address
-from ipaddress import IPv6Network
-from ipaddress import summarize_address_range
-from netifaces import interfaces
-from secrets import SystemRandom
-from shutil import rmtree
-
-from vyos.base import DeprecationWarning
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configdict import is_node_changed
-from vyos.configverify import verify_vrf
-from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_mirror_redirect
-from vyos.configverify import verify_bond_bridge_member
-from vyos.ifconfig import VTunIf
-from vyos.pki import load_dh_parameters
-from vyos.pki import load_private_key
-from vyos.pki import sort_ca_chain
-from vyos.pki import verify_ca_chain
-from vyos.pki import wrap_certificate
-from vyos.pki import wrap_crl
-from vyos.pki import wrap_dh_parameters
-from vyos.pki import wrap_openvpn_key
-from vyos.pki import wrap_private_key
-from vyos.template import render
-from vyos.template import is_ipv4
-from vyos.template import is_ipv6
-from vyos.utils.dict import dict_search
-from vyos.utils.dict import dict_search_args
-from vyos.utils.list import is_list_equal
-from vyos.utils.file import makedir
-from vyos.utils.file import read_file
-from vyos.utils.file import write_file
-from vyos.utils.kernel import check_kmod
-from vyos.utils.kernel import unload_kmod
-from vyos.utils.process import call
-from vyos.utils.permission import chown
-from vyos.utils.process import cmd
-from vyos.utils.network import is_addr_assigned
-
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-user = 'openvpn'
-group = 'openvpn'
-
-cfg_dir = '/run/openvpn'
-cfg_file = '/run/openvpn/{ifname}.conf'
-otp_path = '/config/auth/openvpn'
-otp_file = '/config/auth/openvpn/{ifname}-otp-secrets'
-secret_chars = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')
-service_file = '/run/systemd/system/openvpn@{ifname}.service.d/20-override.conf'
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'openvpn']
-
-    ifname, openvpn = get_interface_dict(conf, base)
-    openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
-
-    if 'deleted' in openvpn:
-        return openvpn
-
-    openvpn['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
-                                        get_first_key=True,
-                                        no_tag_node_value_mangle=True)
-
-    if is_node_changed(conf, base + [ifname, 'openvpn-option']):
-        openvpn.update({'restart_required': {}})
-    if is_node_changed(conf, base + [ifname, 'enable-dco']):
-        openvpn.update({'restart_required': {}})
-
-    # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict'
-    # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there.
-    tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True)
-
-    # We have to cleanup the config dict, as default values could enable features
-    # which are not explicitly enabled on the CLI. Example: server mfa totp
-    # originate comes with defaults, which will enable the
-    # totp plugin, even when not set via CLI so we
-    # need to check this first and drop those keys
-    if dict_search('server.mfa.totp', tmp) == None:
-        del openvpn['server']['mfa']
-
-    # OpenVPN Data-Channel-Offload (DCO) is a Kernel module. If loaded it applies to all
-    # OpenVPN interfaces. Check if DCO is used by any other interface instance.
-    tmp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
-    for interface, interface_config in tmp.items():
-        # If one interface has DCO configured, enable it. No need to further check
-        # all other OpenVPN interfaces. We must use a dedicated key to indicate
-        # the Kernel module must be loaded or not. The per interface "offload.dco"
-        # key is required per OpenVPN interface instance.
-        if dict_search('offload.dco', interface_config) != None:
-            openvpn['module_load_dco'] = {}
-            break
-
-    return openvpn
-
-def is_ec_private_key(pki, cert_name):
-    if not pki or 'certificate' not in pki:
-        return False
-    if cert_name not in pki['certificate']:
-        return False
-
-    pki_cert = pki['certificate'][cert_name]
-    if 'private' not in pki_cert or 'key' not in pki_cert['private']:
-        return False
-
-    key = load_private_key(pki_cert['private']['key'])
-    return isinstance(key, ec.EllipticCurvePrivateKey)
-
-def verify_pki(openvpn):
-    pki = openvpn['pki']
-    interface = openvpn['ifname']
-    mode = openvpn['mode']
-    shared_secret_key = dict_search_args(openvpn, 'shared_secret_key')
-    tls = dict_search_args(openvpn, 'tls')
-
-    if not bool(shared_secret_key) ^ bool(tls): #  xor check if only one is set
-        raise ConfigError('Must specify only one of "shared-secret-key" and "tls"')
-
-    if mode in ['server', 'client'] and not tls:
-        raise ConfigError('Must specify "tls" for server and client modes')
-
-    if not pki:
-        raise ConfigError('PKI is not configured')
-
-    if shared_secret_key:
-        if not dict_search_args(pki, 'openvpn', 'shared_secret'):
-            raise ConfigError('There are no openvpn shared-secrets in PKI configuration')
-
-        if shared_secret_key not in pki['openvpn']['shared_secret']:
-            raise ConfigError(f'Invalid shared-secret on openvpn interface {interface}')
-
-        # If PSK settings are correct, warn about its deprecation
-        DeprecationWarning("OpenVPN shared-secret support will be removed in future VyOS versions.\n\
-        Please migrate your site-to-site tunnels to TLS.\n\
-        You can use self-signed certificates with peer fingerprint verification, consult the documentation for details.")
-
-    if tls:
-        if (mode in ['server', 'client']) and ('ca_certificate' not in tls):
-            raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface},\
-              it is required in server and client modes')
-        else:
-            if ('ca_certificate' not in tls) and ('peer_fingerprint' not in tls):
-                raise ConfigError('Either "tls ca-certificate" or "tls peer-fingerprint" is required\
-                  on openvpn interface {interface} in site-to-site mode')
-
-        if 'ca_certificate' in tls:
-            for ca_name in tls['ca_certificate']:
-                if ca_name not in pki['ca']:
-                    raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}')
-
-            if len(tls['ca_certificate']) > 1:
-                sorted_chain = sort_ca_chain(tls['ca_certificate'], pki['ca'])
-                if not verify_ca_chain(sorted_chain, pki['ca']):
-                    raise ConfigError(f'CA certificates are not a valid chain')
-
-        if mode != 'client' and 'auth_key' not in tls:
-            if 'certificate' not in tls:
-                raise ConfigError(f'Missing "tls certificate" on openvpn interface {interface}')
-
-        if 'certificate' in tls:
-            if tls['certificate'] not in pki['certificate']:
-                raise ConfigError(f'Invalid certificate on openvpn interface {interface}')
-
-            if dict_search_args(pki, 'certificate', tls['certificate'], 'private', 'password_protected') is not None:
-                raise ConfigError(f'Cannot use encrypted private key on openvpn interface {interface}')
-
-        if 'dh_params' in tls:
-            pki_dh = pki['dh'][tls['dh_params']]
-            dh_params = load_dh_parameters(pki_dh['parameters'])
-            dh_numbers = dh_params.parameter_numbers()
-            dh_bits = dh_numbers.p.bit_length()
-
-            if dh_bits < 2048:
-                raise ConfigError(f'Minimum DH key-size is 2048 bits')
-
-
-        if 'auth_key' in tls or 'crypt_key' in tls:
-            if not dict_search_args(pki, 'openvpn', 'shared_secret'):
-                raise ConfigError('There are no openvpn shared-secrets in PKI configuration')
-
-        if 'auth_key' in tls:
-            if tls['auth_key'] not in pki['openvpn']['shared_secret']:
-                raise ConfigError(f'Invalid auth-key on openvpn interface {interface}')
-
-        if 'crypt_key' in tls:
-            if tls['crypt_key'] not in pki['openvpn']['shared_secret']:
-                raise ConfigError(f'Invalid crypt-key on openvpn interface {interface}')
-
-def verify(openvpn):
-    if 'deleted' in openvpn:
-        # remove totp secrets file if totp is not configured
-        if os.path.isfile(otp_file.format(**openvpn)):
-            os.remove(otp_file.format(**openvpn))
-
-        verify_bridge_delete(openvpn)
-        return None
-
-    if 'mode' not in openvpn:
-        raise ConfigError('Must specify OpenVPN operation mode!')
-
-    #
-    # OpenVPN client mode - VERIFY
-    #
-    if openvpn['mode'] == 'client':
-        if 'local_port' in openvpn:
-            raise ConfigError('Cannot specify "local-port" in client mode')
-
-        if 'local_host' in openvpn:
-            raise ConfigError('Cannot specify "local-host" in client mode')
-
-        if 'remote_host' not in openvpn:
-            raise ConfigError('Must specify "remote-host" in client mode')
-
-        if openvpn['protocol'] == 'tcp-passive':
-            raise ConfigError('Protocol "tcp-passive" is not valid in client mode')
-
-        if dict_search('tls.dh_params', openvpn):
-            raise ConfigError('Cannot specify "tls dh-params" in client mode')
-
-    #
-    # OpenVPN site-to-site - VERIFY
-    #
-    elif openvpn['mode'] == 'site-to-site':
-        if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn:
-            raise ConfigError('Must specify "local-address" or add interface to bridge')
-
-        if 'local_address' in openvpn:
-            if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1:
-                raise ConfigError('Only one IPv4 local-address can be specified')
-
-            if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1:
-                raise ConfigError('Only one IPv6 local-address can be specified')
-
-        if openvpn['device_type'] == 'tun':
-            if 'remote_address' not in openvpn:
-                raise ConfigError('Must specify "remote-address"')
-
-        if 'remote_address' in openvpn:
-            if len([addr for addr in openvpn['remote_address'] if is_ipv4(addr)]) > 1:
-                raise ConfigError('Only one IPv4 remote-address can be specified')
-
-            if len([addr for addr in openvpn['remote_address'] if is_ipv6(addr)]) > 1:
-                raise ConfigError('Only one IPv6 remote-address can be specified')
-
-            if not 'local_address' in openvpn:
-                raise ConfigError('"remote-address" requires "local-address"')
-
-            v4loAddr = [addr for addr in openvpn['local_address'] if is_ipv4(addr)]
-            v4remAddr = [addr for addr in openvpn['remote_address'] if is_ipv4(addr)]
-            if v4loAddr and not v4remAddr:
-                raise ConfigError('IPv4 "local-address" requires IPv4 "remote-address"')
-            elif v4remAddr and not v4loAddr:
-                raise ConfigError('IPv4 "remote-address" requires IPv4 "local-address"')
-
-            v6remAddr = [addr for addr in openvpn['remote_address'] if is_ipv6(addr)]
-            v6loAddr = [addr for addr in openvpn['local_address'] if is_ipv6(addr)]
-            if v6loAddr and not v6remAddr:
-                raise ConfigError('IPv6 "local-address" requires IPv6 "remote-address"')
-            elif v6remAddr and not v6loAddr:
-                raise ConfigError('IPv6 "remote-address" requires IPv6 "local-address"')
-
-            if is_list_equal(v4loAddr, v4remAddr) or is_list_equal(v6loAddr, v6remAddr):
-                raise ConfigError('"local-address" and "remote-address" cannot be the same')
-
-            if dict_search('local_host', openvpn) in dict_search('local_address', openvpn):
-                raise ConfigError('"local-address" cannot be the same as "local-host"')
-
-            if dict_search('remote_host', openvpn) in dict_search('remote_address', openvpn):
-                raise ConfigError('"remote-address" and "remote-host" can not be the same')
-
-        if openvpn['device_type'] == 'tap' and 'local_address' in openvpn:
-            # we can only have one local_address, this is ensured above
-            v4addr = None
-            for laddr in openvpn['local_address']:
-                if is_ipv4(laddr):
-                    v4addr = laddr
-                    break
-
-            if v4addr in openvpn['local_address'] and 'subnet_mask' not in openvpn['local_address'][v4addr]:
-                raise ConfigError('Must specify IPv4 "subnet-mask" for local-address')
-
-        if dict_search('encryption.ncp_ciphers', openvpn):
-            raise ConfigError('NCP ciphers can only be used in client or server mode')
-
-    else:
-        # checks for client-server or site-to-site bridged
-        if 'local_address' in openvpn or 'remote_address' in openvpn:
-            raise ConfigError('Cannot specify "local-address" or "remote-address" ' \
-                              'in client/server or bridge mode')
-
-    #
-    # OpenVPN server mode - VERIFY
-    #
-    if openvpn['mode'] == 'server':
-        if openvpn['protocol'] == 'tcp-active':
-            raise ConfigError('Protocol "tcp-active" is not valid in server mode')
-
-        if dict_search('authentication.username', openvpn) or dict_search('authentication.password', openvpn):
-            raise ConfigError('Cannot specify "authentication" in server mode')
-
-        if 'remote_port' in openvpn:
-            raise ConfigError('Cannot specify "remote-port" in server mode')
-
-        if 'remote_host' in openvpn:
-            raise ConfigError('Cannot specify "remote-host" in server mode')
-
-        tmp = dict_search('server.subnet', openvpn)
-        if tmp:
-            v4_subnets = len([subnet for subnet in tmp if is_ipv4(subnet)])
-            v6_subnets = len([subnet for subnet in tmp if is_ipv6(subnet)])
-            if v4_subnets > 1:
-                raise ConfigError('Cannot specify more than 1 IPv4 server subnet')
-            if v6_subnets > 1:
-                raise ConfigError('Cannot specify more than 1 IPv6 server subnet')
-
-            for subnet in tmp:
-                if is_ipv4(subnet):
-                    subnet = IPv4Network(subnet)
-
-                    if openvpn['device_type'] == 'tun' and subnet.prefixlen > 29:
-                        raise ConfigError('Server subnets smaller than /29 with device type "tun" are not supported')
-                    elif openvpn['device_type'] == 'tap' and subnet.prefixlen > 30:
-                        raise ConfigError('Server subnets smaller than /30 with device type "tap" are not supported')
-
-                    for client in (dict_search('client', openvpn) or []):
-                        if client['ip'] and not IPv4Address(client['ip'][0]) in subnet:
-                            raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}')
-
-        else:
-            if 'is_bridge_member' not in openvpn:
-                raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode')
-
-        if hasattr(dict_search('server.client', openvpn), '__iter__'):
-            for client_k, client_v in dict_search('server.client', openvpn).items():
-                if (client_v.get('ip') and len(client_v['ip']) > 1) or (client_v.get('ipv6_ip') and len(client_v['ipv6_ip']) > 1):
-                    raise ConfigError(f'Server client "{client_k}": cannot specify more than 1 IPv4 and 1 IPv6 IP')
-
-        if dict_search('server.client_ip_pool', openvpn):
-            if not (dict_search('server.client_ip_pool.start', openvpn) and dict_search('server.client_ip_pool.stop', openvpn)):
-                raise ConfigError('Server client-ip-pool requires both start and stop addresses')
-            else:
-                v4PoolStart = IPv4Address(dict_search('server.client_ip_pool.start', openvpn))
-                v4PoolStop = IPv4Address(dict_search('server.client_ip_pool.stop', openvpn))
-                if v4PoolStart > v4PoolStop:
-                    raise ConfigError(f'Server client-ip-pool start address {v4PoolStart} is larger than stop address {v4PoolStop}')
-
-                v4PoolSize = int(v4PoolStop) - int(v4PoolStart)
-                if v4PoolSize >= 65536:
-                    raise ConfigError(f'Server client-ip-pool is too large [{v4PoolStart} -> {v4PoolStop} = {v4PoolSize}], maximum is 65536 addresses.')
-
-                v4PoolNets = list(summarize_address_range(v4PoolStart, v4PoolStop))
-                for client in (dict_search('client', openvpn) or []):
-                    if client['ip']:
-                        for v4PoolNet in v4PoolNets:
-                            if IPv4Address(client['ip'][0]) in v4PoolNet:
-                                print(f'Warning: Client "{client["name"]}" IP {client["ip"][0]} is in server IP pool, it is not reserved for this client.')
-            # configuring a client_ip_pool will set 'server ... nopool' which is currently incompatible with 'server-ipv6' (probably to be fixed upstream)
-            for subnet in (dict_search('server.subnet', openvpn) or []):
-                if is_ipv6(subnet):
-                    raise ConfigError(f'Setting client-ip-pool is incompatible having an IPv6 server subnet.')
-
-        for subnet in (dict_search('server.subnet', openvpn) or []):
-            if is_ipv6(subnet):
-                tmp = dict_search('client_ipv6_pool.base', openvpn)
-                if tmp:
-                    if not dict_search('server.client_ip_pool', openvpn):
-                        raise ConfigError('IPv6 server pool requires an IPv4 server pool')
-
-                    if int(tmp.split('/')[1]) >= 112:
-                        raise ConfigError('IPv6 server pool must be larger than /112')
-
-                    #
-                    # todo - weird logic
-                    #
-                    v6PoolStart = IPv6Address(tmp)
-                    v6PoolStop = IPv6Network((v6PoolStart, openvpn['server_ipv6_pool_prefixlen']), strict=False)[-1] # don't remove the parentheses, it's a 2-tuple
-                    v6PoolSize = int(v6PoolStop) - int(v6PoolStart) if int(openvpn['server_ipv6_pool_prefixlen']) > 96 else 65536
-                    if v6PoolSize < v4PoolSize:
-                        raise ConfigError(f'IPv6 server pool must be at least as large as the IPv4 pool (current sizes: IPv6={v6PoolSize} IPv4={v4PoolSize})')
-
-                    v6PoolNets = list(summarize_address_range(v6PoolStart, v6PoolStop))
-                    for client in (dict_search('client', openvpn) or []):
-                        if client['ipv6_ip']:
-                            for v6PoolNet in v6PoolNets:
-                                if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet:
-                                    print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.')
-
-        # add mfa users to the file the mfa plugin uses
-        if dict_search('server.mfa.totp', openvpn):
-            user_data = ''
-            if not os.path.isfile(otp_file.format(**openvpn)):
-                write_file(otp_file.format(**openvpn), user_data,
-                           user=user, group=group, mode=0o644)
-
-            ovpn_users = read_file(otp_file.format(**openvpn))
-            for client in (dict_search('server.client', openvpn) or []):
-                exists = None
-                for ovpn_user in ovpn_users.split('\n'):
-                    if re.search('^' + client + ' ', ovpn_user):
-                        user_data += f'{ovpn_user}\n'
-                        exists = 'true'
-
-                if not exists:
-                    random = SystemRandom()
-                    totp_secret = ''.join(random.choice(secret_chars) for _ in range(16))
-                    user_data += f'{client} otp totp:sha1:base32:{totp_secret}::xxx *\n'
-
-            write_file(otp_file.format(**openvpn), user_data,
-                           user=user, group=group, mode=0o644)
-
-    else:
-        # checks for both client and site-to-site go here
-        if dict_search('server.reject_unconfigured_clients', openvpn):
-            raise ConfigError('Option reject-unconfigured-clients only supported in server mode')
-
-        if 'replace_default_route' in openvpn and 'remote_host' not in openvpn:
-            raise ConfigError('Cannot set "replace-default-route" without "remote-host"')
-
-    #
-    # OpenVPN common verification section
-    # not depending on any operation mode
-    #
-
-    # verify specified IP address is present on any interface on this system
-    if 'local_host' in openvpn:
-        if not is_addr_assigned(openvpn['local_host']):
-            print('local-host IP address "{local_host}" not assigned' \
-                  ' to any interface'.format(**openvpn))
-
-    # TCP active
-    if openvpn['protocol'] == 'tcp-active':
-        if 'local_port' in openvpn:
-            raise ConfigError('Cannot specify "local-port" with "tcp-active"')
-
-        if 'remote_host' not in openvpn:
-            raise ConfigError('Must specify "remote-host" with "tcp-active"')
-
-    #
-    # TLS/encryption
-    #
-    if 'shared_secret_key' in openvpn:
-        if dict_search('encryption.cipher', openvpn) in ['aes128gcm', 'aes192gcm', 'aes256gcm']:
-            raise ConfigError('GCM encryption with shared-secret-key not supported')
-
-    if 'tls' in openvpn:
-        if {'auth_key', 'crypt_key'} <= set(openvpn['tls']):
-            raise ConfigError('TLS auth and crypt keys are mutually exclusive')
-
-        tmp = dict_search('tls.role', openvpn)
-        if tmp:
-            if openvpn['mode'] in ['client', 'server']:
-                if not dict_search('tls.auth_key', openvpn):
-                    raise ConfigError('Cannot specify "tls role" in client-server mode')
-
-            if tmp == 'active':
-                if openvpn['protocol'] == 'tcp-passive':
-                    raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"')
-
-                if dict_search('tls.dh_params', openvpn):
-                    raise ConfigError('Cannot specify "tls dh-params" when "tls role" is "active"')
-
-            elif tmp == 'passive':
-                if openvpn['protocol'] == 'tcp-active':
-                    raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"')
-
-        if 'certificate' in openvpn['tls'] and is_ec_private_key(openvpn['pki'], openvpn['tls']['certificate']):
-            if 'dh_params' in openvpn['tls']:
-                print('Warning: using dh-params and EC keys simultaneously will ' \
-                      'lead to DH ciphers being used instead of ECDH')
-
-    if dict_search('encryption.cipher', openvpn) == 'none':
-        print('Warning: "encryption none" was specified!')
-        print('No encryption will be performed and data is transmitted in ' \
-              'plain text over the network!')
-
-    verify_pki(openvpn)
-
-    #
-    # Auth user/pass
-    #
-    if (dict_search('authentication.username', openvpn) and not
-        dict_search('authentication.password', openvpn)):
-            raise ConfigError('Password for authentication is missing')
-
-    if (dict_search('authentication.password', openvpn) and not
-        dict_search('authentication.username', openvpn)):
-            raise ConfigError('Username for authentication is missing')
-
-    verify_vrf(openvpn)
-    verify_bond_bridge_member(openvpn)
-    verify_mirror_redirect(openvpn)
-
-    return None
-
-def generate_pki_files(openvpn):
-    pki = openvpn['pki']
-    if not pki:
-        return None
-
-    interface = openvpn['ifname']
-    shared_secret_key = dict_search_args(openvpn, 'shared_secret_key')
-    tls = dict_search_args(openvpn, 'tls')
-
-    if shared_secret_key:
-        pki_key = pki['openvpn']['shared_secret'][shared_secret_key]
-        key_path = os.path.join(cfg_dir, f'{interface}_shared.key')
-        write_file(key_path, wrap_openvpn_key(pki_key['key']),
-                   user=user, group=group)
-
-    if tls:
-        if 'ca_certificate' in tls:
-            cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem')
-            crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem')
-
-            if os.path.exists(cert_path):
-                os.unlink(cert_path)
-
-            if os.path.exists(crl_path):
-                os.unlink(crl_path)
-
-            for cert_name in sort_ca_chain(tls['ca_certificate'], pki['ca']):
-                pki_ca = pki['ca'][cert_name]
-
-                if 'certificate' in pki_ca:
-                    write_file(cert_path, wrap_certificate(pki_ca['certificate']) + "\n",
-                               user=user, group=group, mode=0o600, append=True)
-
-                if 'crl' in pki_ca:
-                    for crl in pki_ca['crl']:
-                        write_file(crl_path, wrap_crl(crl) + "\n", user=user, group=group,
-                                   mode=0o600, append=True)
-
-                    openvpn['tls']['crl'] = True
-
-        if 'certificate' in tls:
-            cert_name = tls['certificate']
-            pki_cert = pki['certificate'][cert_name]
-
-            if 'certificate' in pki_cert:
-                cert_path = os.path.join(cfg_dir, f'{interface}_cert.pem')
-                write_file(cert_path, wrap_certificate(pki_cert['certificate']),
-                           user=user, group=group, mode=0o600)
-
-            if 'private' in pki_cert and 'key' in pki_cert['private']:
-                key_path = os.path.join(cfg_dir, f'{interface}_cert.key')
-                write_file(key_path, wrap_private_key(pki_cert['private']['key']),
-                           user=user, group=group, mode=0o600)
-
-                openvpn['tls']['private_key'] = True
-
-        if 'dh_params' in tls:
-            dh_name = tls['dh_params']
-            pki_dh = pki['dh'][dh_name]
-
-            if 'parameters' in pki_dh:
-                dh_path = os.path.join(cfg_dir, f'{interface}_dh.pem')
-                write_file(dh_path, wrap_dh_parameters(pki_dh['parameters']),
-                           user=user, group=group, mode=0o600)
-
-        if 'auth_key' in tls:
-            key_name = tls['auth_key']
-            pki_key = pki['openvpn']['shared_secret'][key_name]
-
-            if 'key' in pki_key:
-                key_path = os.path.join(cfg_dir, f'{interface}_auth.key')
-                write_file(key_path, wrap_openvpn_key(pki_key['key']),
-                           user=user, group=group, mode=0o600)
-
-        if 'crypt_key' in tls:
-            key_name = tls['crypt_key']
-            pki_key = pki['openvpn']['shared_secret'][key_name]
-
-            if 'key' in pki_key:
-                key_path = os.path.join(cfg_dir, f'{interface}_crypt.key')
-                write_file(key_path, wrap_openvpn_key(pki_key['key']),
-                           user=user, group=group, mode=0o600)
-
-
-def generate(openvpn):
-    interface = openvpn['ifname']
-    directory = os.path.dirname(cfg_file.format(**openvpn))
-    openvpn['plugin_dir'] = '/usr/lib/openvpn'
-    # create base config directory on demand
-    makedir(directory, user, group)
-    # enforce proper permissions on /run/openvpn
-    chown(directory, user, group)
-
-    # we can't know in advance which clients have been removed,
-    # thus all client configs will be removed and re-added on demand
-    ccd_dir = os.path.join(directory, 'ccd', interface)
-    if os.path.isdir(ccd_dir):
-        rmtree(ccd_dir, ignore_errors=True)
-
-    # Remove systemd directories with overrides
-    service_dir = os.path.dirname(service_file.format(**openvpn))
-    if os.path.isdir(service_dir):
-        rmtree(service_dir, ignore_errors=True)
-
-    if 'deleted' in openvpn or 'disable' in openvpn:
-        return None
-
-    # create client config directory on demand
-    makedir(ccd_dir, user, group)
-
-    # Fix file permissons for keys
-    generate_pki_files(openvpn)
-
-    # Generate User/Password authentication file
-    if 'authentication' in openvpn:
-        render(openvpn['auth_user_pass_file'], 'openvpn/auth.pw.j2', openvpn,
-               user=user, group=group, permission=0o600)
-    else:
-        # delete old auth file if present
-        if os.path.isfile(openvpn['auth_user_pass_file']):
-            os.remove(openvpn['auth_user_pass_file'])
-
-    # Generate client specific configuration
-    server_client = dict_search_args(openvpn, 'server', 'client')
-    if server_client:
-        for client, client_config in server_client.items():
-            client_file = os.path.join(ccd_dir, client)
-
-            # Our client need's to know its subnet mask ...
-            client_config['server_subnet'] = dict_search('server.subnet', openvpn)
-
-            render(client_file, 'openvpn/client.conf.j2', client_config,
-                   user=user, group=group)
-
-    # we need to support quoting of raw parameters from OpenVPN CLI
-    # see https://vyos.dev/T1632
-    render(cfg_file.format(**openvpn), 'openvpn/server.conf.j2', openvpn,
-           formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
-
-    # Render 20-override.conf for OpenVPN service
-    render(service_file.format(**openvpn), 'openvpn/service-override.conf.j2', openvpn,
-           formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
-    # Reload systemd services config to apply an override
-    call(f'systemctl daemon-reload')
-
-    return None
-
-def apply(openvpn):
-    interface = openvpn['ifname']
-
-    # Do some cleanup when OpenVPN is disabled/deleted
-    if 'deleted' in openvpn or 'disable' in openvpn:
-        call(f'systemctl stop openvpn@{interface}.service')
-        for cleanup_file in glob(f'/run/openvpn/{interface}.*'):
-            if os.path.isfile(cleanup_file):
-                os.unlink(cleanup_file)
-
-        if interface in interfaces():
-            VTunIf(interface).remove()
-
-    # dynamically load/unload DCO Kernel extension if requested
-    dco_module = 'ovpn_dco_v2'
-    if 'module_load_dco' in openvpn:
-        check_kmod(dco_module)
-    else:
-        unload_kmod(dco_module)
-
-    # Now bail out early if interface is disabled or got deleted
-    if 'deleted' in openvpn or 'disable' in openvpn:
-        return None
-
-    # verify specified IP address is present on any interface on this system
-    # Allow to bind service to nonlocal address, if it virtaual-vrrp address
-    # or if address will be assign later
-    if 'local_host' in openvpn:
-        if not is_addr_assigned(openvpn['local_host']):
-            cmd('sysctl -w net.ipv4.ip_nonlocal_bind=1')
-
-    # No matching OpenVPN process running - maybe it got killed or none
-    # existed - nevertheless, spawn new OpenVPN process
-    action = 'reload-or-restart'
-    if 'restart_required' in openvpn:
-        action = 'restart'
-    call(f'systemctl {action} openvpn@{interface}.service')
-
-    o = VTunIf(**openvpn)
-    o.update(openvpn)
-
-    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/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
deleted file mode 100755
index 42f084309..000000000
--- a/src/conf_mode/interfaces-pppoe.py
+++ /dev/null
@@ -1,148 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-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
-
-from sys import exit
-from copy import deepcopy
-from netifaces import interfaces
-
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configdict import is_node_changed
-from vyos.configdict import get_pppoe_interfaces
-from vyos.configverify import verify_authentication
-from vyos.configverify import verify_source_interface
-from vyos.configverify import verify_interface_exists
-from vyos.configverify import verify_vrf
-from vyos.configverify import verify_mtu_ipv6
-from vyos.configverify import verify_mirror_redirect
-from vyos.ifconfig import PPPoEIf
-from vyos.template import render
-from vyos.utils.process import call
-from vyos.utils.process import is_systemd_service_running
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'pppoe']
-    ifname, pppoe = get_interface_dict(conf, base)
-
-    # We should only terminate the PPPoE session if critical parameters change.
-    # All parameters that can be changed on-the-fly (like interface description)
-    # should not lead to a reconnect!
-    for options in ['access-concentrator', 'connect-on-demand', 'service-name',
-                    'source-interface', 'vrf', 'no-default-route',
-                    'authentication', 'host_uniq']:
-        if is_node_changed(conf, base + [ifname, options]):
-            pppoe.update({'shutdown_required': {}})
-            # bail out early - no need to further process other nodes
-            break
-
-    if 'deleted' not in pppoe:
-        # We always set the MRU value to the MTU size. This code path only re-creates
-        # the old behavior if MRU is not set on the CLI.
-        if 'mru' not in pppoe:
-            pppoe['mru'] = pppoe['mtu']
-
-    return pppoe
-
-def verify(pppoe):
-    if 'deleted' in pppoe:
-        # bail out early
-        return None
-
-    verify_source_interface(pppoe)
-    verify_authentication(pppoe)
-    verify_vrf(pppoe)
-    verify_mtu_ipv6(pppoe)
-    verify_mirror_redirect(pppoe)
-
-    if {'connect_on_demand', 'vrf'} <= set(pppoe):
-        raise ConfigError('On-demand dialing and VRF can not be used at the same time')
-
-    # both MTU and MRU have default values, thus we do not need to check
-    # if the key exists
-    if int(pppoe['mru']) > int(pppoe['mtu']):
-        raise ConfigError('PPPoE MRU needs to be lower then MTU!')
-
-    return None
-
-def generate(pppoe):
-    # set up configuration file path variables where our templates will be
-    # rendered into
-    ifname = pppoe['ifname']
-    config_pppoe = f'/etc/ppp/peers/{ifname}'
-
-    if 'deleted' in pppoe or 'disable' in pppoe:
-        if os.path.exists(config_pppoe):
-            os.unlink(config_pppoe)
-
-        return None
-
-    # Create PPP configuration files
-    render(config_pppoe, 'pppoe/peer.j2', pppoe, permission=0o640)
-
-    return None
-
-def apply(pppoe):
-    ifname = pppoe['ifname']
-    if 'deleted' in pppoe or 'disable' in pppoe:
-        if os.path.isdir(f'/sys/class/net/{ifname}'):
-            p = PPPoEIf(ifname)
-            p.remove()
-        call(f'systemctl stop ppp@{ifname}.service')
-        return None
-
-    # reconnect should only be necessary when certain config options change,
-    # like ACS name, authentication ... (see get_config() for details)
-    if ((not is_systemd_service_running(f'ppp@{ifname}.service')) or
-        'shutdown_required' in pppoe):
-
-        # cleanup system (e.g. FRR routes first)
-        if os.path.isdir(f'/sys/class/net/{ifname}'):
-            p = PPPoEIf(ifname)
-            p.remove()
-
-        call(f'systemctl restart ppp@{ifname}.service')
-        # When interface comes "live" a hook is called:
-        # /etc/ppp/ip-up.d/99-vyos-pppoe-callback
-        # which triggers PPPoEIf.update()
-    else:
-        if os.path.isdir(f'/sys/class/net/{ifname}'):
-            p = PPPoEIf(ifname)
-            p.update(pppoe)
-
-    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/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
deleted file mode 100755
index dce5c2358..000000000
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ /dev/null
@@ -1,107 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-2022 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/>.
-
-from sys import exit
-from netifaces import interfaces
-
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configdict import is_node_changed
-from vyos.configdict import is_source_interface
-from vyos.configdict import is_node_changed
-from vyos.configverify import verify_vrf
-from vyos.configverify import verify_address
-from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_source_interface
-from vyos.configverify import verify_vlan_config
-from vyos.configverify import verify_mtu_parent
-from vyos.configverify import verify_mirror_redirect
-from vyos.configverify import verify_bond_bridge_member
-from vyos.ifconfig import MACVLANIf
-from vyos import ConfigError
-
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at
-    least the interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'pseudo-ethernet']
-    ifname, peth = get_interface_dict(conf, base)
-
-    mode = is_node_changed(conf, ['mode'])
-    if mode: peth.update({'shutdown_required' : {}})
-
-    if is_node_changed(conf, base + [ifname, 'mode']):
-        peth.update({'rebuild_required': {}})
-
-    if 'source_interface' in peth:
-        _, peth['parent'] = get_interface_dict(conf, ['interfaces', 'ethernet'],
-                                               peth['source_interface'])
-        # test if source-interface is maybe already used by another interface
-        tmp = is_source_interface(conf, peth['source_interface'], ['macsec'])
-        if tmp and tmp != ifname: peth.update({'is_source_interface' : tmp})
-
-    return peth
-
-def verify(peth):
-    if 'deleted' in peth:
-        verify_bridge_delete(peth)
-        return None
-
-    verify_source_interface(peth)
-    verify_vrf(peth)
-    verify_address(peth)
-    verify_mtu_parent(peth, peth['parent'])
-    verify_mirror_redirect(peth)
-    # use common function to verify VLAN configuration
-    verify_vlan_config(peth)
-
-    return None
-
-def generate(peth):
-    return None
-
-def apply(peth):
-    # Check if the MACVLAN interface already exists
-    if 'rebuild_required' in peth or 'deleted' in peth:
-        if peth['ifname'] in interfaces():
-            p = MACVLANIf(peth['ifname'])
-            # MACVLAN is always needs to be recreated,
-            # thus we can simply always delete it first.
-            p.remove()
-
-    if 'deleted' not in peth:
-        p = MACVLANIf(**peth)
-        p.update(peth)
-
-    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/interfaces-sstpc.py b/src/conf_mode/interfaces-sstpc.py
deleted file mode 100755
index b588910dc..000000000
--- a/src/conf_mode/interfaces-sstpc.py
+++ /dev/null
@@ -1,145 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2022 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 sys import exit
-
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configdict import is_node_changed
-from vyos.configverify import verify_authentication
-from vyos.configverify import verify_vrf
-from vyos.ifconfig import SSTPCIf
-from vyos.pki import encode_certificate
-from vyos.pki import find_chain
-from vyos.pki import load_certificate
-from vyos.template import render
-from vyos.utils.process import call
-from vyos.utils.dict import dict_search
-from vyos.utils.process import is_systemd_service_running
-from vyos.utils.file import write_file
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'sstpc']
-    ifname, sstpc = get_interface_dict(conf, base)
-
-    # We should only terminate the SSTP client session if critical parameters
-    # change. All parameters that can be changed on-the-fly (like interface
-    # description) should not lead to a reconnect!
-    for options in ['authentication', 'no_peer_dns', 'no_default_route',
-                    'server', 'ssl']:
-        if is_node_changed(conf, base + [ifname, options]):
-            sstpc.update({'shutdown_required': {}})
-            # bail out early - no need to further process other nodes
-            break
-
-    # Load PKI certificates for later processing
-    sstpc['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
-                                        get_first_key=True,
-                                        no_tag_node_value_mangle=True)
-    return sstpc
-
-def verify(sstpc):
-    if 'deleted' in sstpc:
-        return None
-
-    verify_authentication(sstpc)
-    verify_vrf(sstpc)
-
-    if not dict_search('server', sstpc):
-        raise ConfigError('Remote SSTP server must be specified!')
-
-    if not dict_search('ssl.ca_certificate', sstpc):
-        raise ConfigError('Missing mandatory CA certificate!')
-
-    return None
-
-def generate(sstpc):
-    ifname = sstpc['ifname']
-    config_sstpc = f'/etc/ppp/peers/{ifname}'
-
-    sstpc['ca_file_path'] = f'/run/sstpc/{ifname}_ca-cert.pem'
-
-    if 'deleted' in sstpc:
-        for file in [sstpc['ca_file_path'], config_sstpc]:
-            if os.path.exists(file):
-                os.unlink(file)
-        return None
-
-    ca_name = sstpc['ssl']['ca_certificate']
-    pki_ca_cert = sstpc['pki']['ca'][ca_name]
-
-    loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
-    loaded_ca_certs = {load_certificate(c['certificate'])
-            for c in sstpc['pki']['ca'].values()} if 'ca' in sstpc['pki'] else {}
-
-    ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
-
-    write_file(sstpc['ca_file_path'], '\n'.join(encode_certificate(c) for c in ca_full_chain))
-    render(config_sstpc, 'sstp-client/peer.j2', sstpc, permission=0o640)
-
-    return None
-
-def apply(sstpc):
-    ifname = sstpc['ifname']
-    if 'deleted' in sstpc or 'disable' in sstpc:
-        if os.path.isdir(f'/sys/class/net/{ifname}'):
-            p = SSTPCIf(ifname)
-            p.remove()
-        call(f'systemctl stop ppp@{ifname}.service')
-        return None
-
-    # reconnect should only be necessary when specific options change,
-    # like server, authentication ... (see get_config() for details)
-    if ((not is_systemd_service_running(f'ppp@{ifname}.service')) or
-        'shutdown_required' in sstpc):
-
-        # cleanup system (e.g. FRR routes first)
-        if os.path.isdir(f'/sys/class/net/{ifname}'):
-            p = SSTPCIf(ifname)
-            p.remove()
-
-        call(f'systemctl restart ppp@{ifname}.service')
-        # When interface comes "live" a hook is called:
-        # /etc/ppp/ip-up.d/96-vyos-sstpc-callback
-        # which triggers SSTPCIf.update()
-    else:
-        if os.path.isdir(f'/sys/class/net/{ifname}'):
-            p = SSTPCIf(ifname)
-            p.update(sstpc)
-
-    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/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
deleted file mode 100755
index 91aed9cc3..000000000
--- a/src/conf_mode/interfaces-tunnel.py
+++ /dev/null
@@ -1,224 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2022 yOS 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 sys import exit
-from netifaces import interfaces
-
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configdict import is_node_changed
-from vyos.configverify import verify_address
-from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_interface_exists
-from vyos.configverify import verify_mtu_ipv6
-from vyos.configverify import verify_mirror_redirect
-from vyos.configverify import verify_vrf
-from vyos.configverify import verify_tunnel
-from vyos.configverify import verify_bond_bridge_member
-from vyos.ifconfig import Interface
-from vyos.ifconfig import Section
-from vyos.ifconfig import TunnelIf
-from vyos.utils.network import get_interface_config
-from vyos.utils.dict import dict_search
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least
-    the interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'tunnel']
-    ifname, tunnel = get_interface_dict(conf, base)
-
-    if 'deleted' not in tunnel:
-        tmp = is_node_changed(conf, base + [ifname, 'encapsulation'])
-        if tmp: tunnel.update({'encapsulation_changed': {}})
-
-        tmp = is_node_changed(conf, base + [ifname, 'parameters', 'ip', 'key'])
-        if tmp: tunnel.update({'key_changed': {}})
-
-        # We also need to inspect other configured tunnels as there are Kernel
-        # restrictions where we need to comply. E.g. GRE tunnel key can't be used
-        # twice, or with multiple GRE tunnels to the same location we must specify
-        # a GRE key
-        conf.set_level(base)
-        tunnel['other_tunnels'] = conf.get_config_dict([], key_mangling=('-', '_'),
-                                                      get_first_key=True,
-                                                      no_tag_node_value_mangle=True)
-        # delete our own instance from this dict
-        ifname = tunnel['ifname']
-        del tunnel['other_tunnels'][ifname]
-        # if only one tunnel is present on the system, no need to keep this key
-        if len(tunnel['other_tunnels']) == 0:
-            del tunnel['other_tunnels']
-
-    # We must check if our interface is configured to be a DMVPN member
-    nhrp_base = ['protocols', 'nhrp', 'tunnel']
-    conf.set_level(nhrp_base)
-    nhrp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
-    if nhrp: tunnel.update({'nhrp' : list(nhrp.keys())})
-
-    if 'encapsulation' in tunnel and tunnel['encapsulation'] not in ['erspan', 'ip6erspan']:
-        del tunnel['parameters']['erspan']
-
-    return tunnel
-
-def verify(tunnel):
-    if 'deleted' in tunnel:
-        verify_bridge_delete(tunnel)
-
-        if 'nhrp' in tunnel and tunnel['ifname'] in tunnel['nhrp']:
-            raise ConfigError('Tunnel used for NHRP, it can not be deleted!')
-
-        return None
-
-    verify_tunnel(tunnel)
-
-    if tunnel['encapsulation'] in ['erspan', 'ip6erspan']:
-        if dict_search('parameters.ip.key', tunnel) == None:
-            raise ConfigError('ERSPAN requires ip key parameter!')
-
-        # this is a default field
-        ver = int(tunnel['parameters']['erspan']['version'])
-        if ver == 1:
-            if 'hw_id' in tunnel['parameters']['erspan']:
-                raise ConfigError('ERSPAN version 1 does not support hw-id!')
-            if 'direction' in tunnel['parameters']['erspan']:
-                raise ConfigError('ERSPAN version 1 does not support direction!')
-        elif ver == 2:
-            if 'idx' in tunnel['parameters']['erspan']:
-                raise ConfigError('ERSPAN version 2 does not index parameter!')
-            if 'direction' not in tunnel['parameters']['erspan']:
-                raise ConfigError('ERSPAN version 2 requires direction to be set!')
-
-    # If tunnel source is any and gre key is not set
-    interface = tunnel['ifname']
-    if tunnel['encapsulation'] in ['gre'] and \
-       dict_search('source_address', tunnel) == '0.0.0.0' and \
-       dict_search('parameters.ip.key', tunnel) == None:
-        raise ConfigError(f'"parameters ip key" must be set for {interface} when '\
-                           'encapsulation is GRE!')
-
-    gre_encapsulations = ['gre', 'gretap']
-    if tunnel['encapsulation'] in gre_encapsulations and 'other_tunnels' in tunnel:
-        # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
-        # Prevent the same key for 2 tunnels with same source-address/encap. T2920
-        for o_tunnel, o_tunnel_conf in tunnel['other_tunnels'].items():
-            # no match on encapsulation - bail out
-            our_encapsulation = tunnel['encapsulation']
-            their_encapsulation = o_tunnel_conf['encapsulation']
-            if our_encapsulation in gre_encapsulations and their_encapsulation \
-                not in gre_encapsulations:
-                continue
-
-            our_address = dict_search('source_address', tunnel)
-            our_key = dict_search('parameters.ip.key', tunnel)
-            their_address = dict_search('source_address', o_tunnel_conf)
-            their_key = dict_search('parameters.ip.key', o_tunnel_conf)
-            if our_key != None:
-                if their_address == our_address and their_key == our_key:
-                    raise ConfigError(f'Key "{our_key}" for source-address "{our_address}" ' \
-                                      f'is already used for tunnel "{o_tunnel}"!')
-            else:
-                our_source_if = dict_search('source_interface', tunnel)
-                their_source_if = dict_search('source_interface', o_tunnel_conf)
-                our_remote = dict_search('remote', tunnel)
-                their_remote = dict_search('remote', o_tunnel_conf)
-                # If no IP GRE key is defined we can not have more then one GRE tunnel
-                # bound to any one interface/IP address and the same remote. This will
-                # result in a OS  PermissionError: add tunnel "gre0" failed: File exists
-                if (their_address == our_address or our_source_if == their_source_if) and \
-                    our_remote == their_remote:
-                    raise ConfigError(f'Missing required "ip key" parameter when '\
-                                       'running more then one GRE based tunnel on the '\
-                                       'same source-interface/source-address')
-
-    # Keys are not allowed with ipip and sit tunnels
-    if tunnel['encapsulation'] in ['ipip', 'sit']:
-        if dict_search('parameters.ip.key', tunnel) != None:
-            raise ConfigError('Keys are not allowed with ipip and sit tunnels!')
-
-    verify_mtu_ipv6(tunnel)
-    verify_address(tunnel)
-    verify_vrf(tunnel)
-    verify_bond_bridge_member(tunnel)
-    verify_mirror_redirect(tunnel)
-
-    if 'source_interface' in tunnel:
-        verify_interface_exists(tunnel['source_interface'])
-
-    # TTL != 0 and nopmtudisc are incompatible, parameters and ip use default
-    # values, thus the keys are always present.
-    if dict_search('parameters.ip.no_pmtu_discovery', tunnel) != None:
-        if dict_search('parameters.ip.ttl', tunnel) != '0':
-            raise ConfigError('Disabled PMTU requires TTL set to "0"!')
-        if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']:
-            raise ConfigError('Can not disable PMTU discovery for given encapsulation')
-
-    if dict_search('parameters.ip.ignore_df', tunnel) != None:
-        if tunnel['encapsulation'] not in ['gretap']:
-            raise ConfigError('Option ignore-df can only be used on GRETAP tunnels!')
-
-        if dict_search('parameters.ip.no_pmtu_discovery', tunnel) == None:
-            raise ConfigError('Option ignore-df requires path MTU discovery to be disabled!')
-
-
-def generate(tunnel):
-    return None
-
-def apply(tunnel):
-    interface = tunnel['ifname']
-    # If a gretap tunnel is already existing we can not "simply" change local or
-    # remote addresses. This returns "Operation not supported" by the Kernel.
-    # There is no other solution to destroy and recreate the tunnel.
-    encap = ''
-    remote = ''
-    tmp = get_interface_config(interface)
-    if tmp:
-        encap = dict_search('linkinfo.info_kind', tmp)
-        remote = dict_search('linkinfo.info_data.remote', tmp)
-
-    if ('deleted' in tunnel or 'encapsulation_changed' in tunnel or encap in
-        ['gretap', 'ip6gretap', 'erspan', 'ip6erspan'] or remote in ['any'] or
-        'key_changed' in tunnel):
-        if interface in interfaces():
-            tmp = Interface(interface)
-            tmp.remove()
-        if 'deleted' in tunnel:
-            return None
-
-    tun = TunnelIf(**tunnel)
-    tun.update(tunnel)
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        generate(c)
-        verify(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/interfaces-virtual-ethernet.py b/src/conf_mode/interfaces-virtual-ethernet.py
deleted file mode 100755
index 8efe89c41..000000000
--- a/src/conf_mode/interfaces-virtual-ethernet.py
+++ /dev/null
@@ -1,114 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2022 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/>.
-
-from sys import exit
-
-from netifaces import interfaces
-from vyos import ConfigError
-from vyos import airbag
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configverify import verify_address
-from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_vrf
-from vyos.ifconfig import VethIf
-
-airbag.enable()
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at
-    least the interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'virtual-ethernet']
-    ifname, veth = get_interface_dict(conf, base)
-
-    # We need to know all other veth related interfaces as veth requires a 1:1
-    # mapping for the peer-names. The Linux kernel automatically creates both
-    # interfaces, the local one and the peer-name, but VyOS also needs a peer
-    # interfaces configrued on the CLI so we can assign proper IP addresses etc.
-    veth['other_interfaces'] = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                     get_first_key=True, no_tag_node_value_mangle=True)
-
-    return veth
-
-
-def verify(veth):
-    if 'deleted' in veth:
-        verify_bridge_delete(veth)
-        # Prevent to delete veth interface which used for another "vethX peer-name"
-        for iface, iface_config in veth['other_interfaces'].items():
-            if veth['ifname'] in iface_config['peer_name']:
-                ifname = veth['ifname']
-                raise ConfigError(
-                    f'Cannot delete "{ifname}" used for "interface {iface} peer-name"'
-                )
-        return None
-
-    verify_vrf(veth)
-    verify_address(veth)
-
-    if 'peer_name' not in veth:
-        raise ConfigError(f'Remote peer name must be set for "{veth["ifname"]}"!')
-
-    peer_name = veth['peer_name']
-    ifname = veth['ifname']
-
-    if veth['peer_name'] not in veth['other_interfaces']:
-        raise ConfigError(f'Used peer-name "{peer_name}" on interface "{ifname}" ' \
-                          'is not configured!')
-
-    if veth['other_interfaces'][peer_name]['peer_name'] != ifname:
-        raise ConfigError(
-            f'Configuration mismatch between "{ifname}" and "{peer_name}"!')
-
-    if peer_name == ifname:
-        raise ConfigError(
-            f'Peer-name "{peer_name}" cannot be the same as interface "{ifname}"!')
-
-    return None
-
-
-def generate(peth):
-    return None
-
-def apply(veth):
-    # Check if the Veth interface already exists
-    if 'rebuild_required' in veth or 'deleted' in veth:
-        if veth['ifname'] in interfaces():
-            p = VethIf(veth['ifname'])
-            p.remove()
-
-    if 'deleted' not in veth:
-        p = VethIf(**veth)
-        p.update(veth)
-
-    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/interfaces-vti.py b/src/conf_mode/interfaces-vti.py
deleted file mode 100755
index 9871810ae..000000000
--- a/src/conf_mode/interfaces-vti.py
+++ /dev/null
@@ -1,68 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 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/>.
-
-from netifaces import interfaces
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configverify import verify_mirror_redirect
-from vyos.ifconfig import VTIIf
-from vyos.utils.dict import dict_search
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'vti']
-    _, vti = get_interface_dict(conf, base)
-    return vti
-
-def verify(vti):
-    verify_mirror_redirect(vti)
-    return None
-
-def generate(vti):
-    return None
-
-def apply(vti):
-    # Remove macsec interface
-    if 'deleted' in vti:
-        VTIIf(**vti).remove()
-        return None
-
-    tmp = VTIIf(**vti)
-    tmp.update(vti)
-
-    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/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
deleted file mode 100755
index 4251e611b..000000000
--- a/src/conf_mode/interfaces-vxlan.py
+++ /dev/null
@@ -1,236 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-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 sys import exit
-from netifaces import interfaces
-
-from vyos.base import Warning
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configdict import leaf_node_changed
-from vyos.configdict import is_node_changed
-from vyos.configdict import node_changed
-from vyos.configverify import verify_address
-from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_mtu_ipv6
-from vyos.configverify import verify_mirror_redirect
-from vyos.configverify import verify_source_interface
-from vyos.configverify import verify_bond_bridge_member
-from vyos.ifconfig import Interface
-from vyos.ifconfig import VXLANIf
-from vyos.template import is_ipv6
-from vyos.utils.dict import dict_search
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least
-    the interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'vxlan']
-    ifname, vxlan = get_interface_dict(conf, base)
-
-    # VXLAN interfaces are picky and require recreation if certain parameters
-    # change. But a VXLAN interface should - of course - not be re-created if
-    # it's description or IP address is adjusted. Feels somehow logic doesn't it?
-    for cli_option in ['parameters', 'gpe', 'group', 'port', 'remote',
-                       'source-address', 'source-interface', 'vni']:
-        if is_node_changed(conf, base + [ifname, cli_option]):
-            vxlan.update({'rebuild_required': {}})
-            break
-
-    # When dealing with VNI filtering we need to know what VNI was actually removed,
-    # so build up a dict matching the vlan_to_vni structure but with removed values.
-    tmp = node_changed(conf, base + [ifname, 'vlan-to-vni'], recursive=True)
-    if tmp:
-        vxlan.update({'vlan_to_vni_removed': {}})
-        for vlan in tmp:
-            vni = leaf_node_changed(conf, base + [ifname, 'vlan-to-vni', vlan, 'vni'])
-            vxlan['vlan_to_vni_removed'].update({vlan : {'vni' : vni[0]}})
-
-    # We need to verify that no other VXLAN tunnel is configured when external
-    # mode is in use - Linux Kernel limitation
-    conf.set_level(base)
-    vxlan['other_tunnels'] = conf.get_config_dict([], key_mangling=('-', '_'),
-                                                  get_first_key=True,
-                                                  no_tag_node_value_mangle=True)
-
-    # This if-clause is just to be sure - it will always evaluate to true
-    ifname = vxlan['ifname']
-    if ifname in vxlan['other_tunnels']:
-        del vxlan['other_tunnels'][ifname]
-    if len(vxlan['other_tunnels']) == 0:
-        del vxlan['other_tunnels']
-
-    return vxlan
-
-def verify(vxlan):
-    if 'deleted' in vxlan:
-        verify_bridge_delete(vxlan)
-        return None
-
-    if int(vxlan['mtu']) < 1500:
-        Warning('RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU')
-
-    if 'group' in vxlan:
-        if 'source_interface' not in vxlan:
-            raise ConfigError('Multicast VXLAN requires an underlaying interface')
-        verify_source_interface(vxlan)
-
-    if not any(tmp in ['group', 'remote', 'source_address', 'source_interface'] for tmp in vxlan):
-        raise ConfigError('Group, remote, source-address or source-interface must be configured')
-
-    if 'vni' not in vxlan and dict_search('parameters.external', vxlan) == None:
-        raise ConfigError('Must either configure VXLAN "vni" or use "external" CLI option!')
-
-    if dict_search('parameters.external', vxlan) != None:
-        if 'vni' in vxlan:
-            raise ConfigError('Can not specify both "external" and "VNI"!')
-
-        if 'other_tunnels' in vxlan:
-            # When multiple VXLAN interfaces are defined and "external" is used,
-            # all VXLAN interfaces need to have vni-filter enabled!
-            # See Linux Kernel commit f9c4bb0b245cee35ef66f75bf409c9573d934cf9
-            other_vni_filter = False
-            for tunnel, tunnel_config in vxlan['other_tunnels'].items():
-                if dict_search('parameters.vni_filter', tunnel_config) != None:
-                    other_vni_filter = True
-                    break
-            # eqivalent of the C foo ? 'a' : 'b' statement
-            vni_filter = True and (dict_search('parameters.vni_filter', vxlan) != None) or False
-            # If either one is enabled, so must be the other. Both can be off and both can be on
-            if (vni_filter and not other_vni_filter) or (not vni_filter and other_vni_filter):
-                raise ConfigError(f'Using multiple VXLAN interfaces with "external" '\
-                    'requires all VXLAN interfaces to have "vni-filter" configured!')
-
-            if not vni_filter and not other_vni_filter:
-                other_tunnels = ', '.join(vxlan['other_tunnels'])
-                raise ConfigError(f'Only one VXLAN tunnel is supported when "external" '\
-                                f'CLI option is used and "vni-filter" is unset. '\
-                                f'Additional tunnels: {other_tunnels}')
-
-    if 'gpe' in vxlan and 'external' not in vxlan:
-        raise ConfigError(f'VXLAN-GPE is only supported when "external" '\
-                          f'CLI option is used.')
-
-    if 'source_interface' in vxlan:
-        # VXLAN adds at least an overhead of 50 byte - we need to check the
-        # underlaying device if our VXLAN package is not going to be fragmented!
-        vxlan_overhead = 50
-        if 'source_address' in vxlan and is_ipv6(vxlan['source_address']):
-            # IPv6 adds an extra 20 bytes overhead because the IPv6 header is 20
-            # bytes larger than the IPv4 header - assuming no extra options are
-            # in use.
-            vxlan_overhead += 20
-
-        # If source_address is not used - check IPv6 'remote' list
-        elif 'remote' in vxlan:
-            if any(is_ipv6(a) for a in vxlan['remote']):
-                vxlan_overhead += 20
-
-        lower_mtu = Interface(vxlan['source_interface']).get_mtu()
-        if lower_mtu < (int(vxlan['mtu']) + vxlan_overhead):
-            raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\
-                              f'bytes) for VXLAN overhead ({vxlan_overhead} bytes!)')
-
-    # Check for mixed IPv4 and IPv6 addresses
-    protocol = None
-    if 'source_address' in vxlan:
-        if is_ipv6(vxlan['source_address']):
-            protocol = 'ipv6'
-        else:
-            protocol = 'ipv4'
-
-    if 'remote' in vxlan:
-        error_msg = 'Can not mix both IPv4 and IPv6 for VXLAN underlay'
-        for remote in vxlan['remote']:
-            if is_ipv6(remote):
-                if protocol == 'ipv4':
-                    raise ConfigError(error_msg)
-                protocol = 'ipv6'
-            else:
-                if protocol == 'ipv6':
-                    raise ConfigError(error_msg)
-                protocol = 'ipv4'
-
-    if 'vlan_to_vni' in vxlan:
-        if 'is_bridge_member' not in vxlan:
-            raise ConfigError('VLAN to VNI mapping requires that VXLAN interface '\
-                              'is member of a bridge interface!')
-
-        vnis_used = []
-        for vif, vif_config in vxlan['vlan_to_vni'].items():
-            if 'vni' not in vif_config:
-                raise ConfigError(f'Must define VNI for VLAN "{vif}"!')
-            vni = vif_config['vni']
-            if vni in vnis_used:
-                raise ConfigError(f'VNI "{vni}" is already assigned to a different VLAN!')
-            vnis_used.append(vni)
-
-    if dict_search('parameters.neighbor_suppress', vxlan) != None:
-        if 'is_bridge_member' not in vxlan:
-            raise ConfigError('Neighbor suppression requires that VXLAN interface '\
-                              'is member of a bridge interface!')
-
-    verify_mtu_ipv6(vxlan)
-    verify_address(vxlan)
-    verify_bond_bridge_member(vxlan)
-    verify_mirror_redirect(vxlan)
-
-    # We use a defaultValue for port, thus it's always safe to use
-    if vxlan['port'] == '8472':
-        Warning('Starting from VyOS 1.4, the default port for VXLAN '\
-                'has been changed to 4789. This matches the IANA assigned '\
-                'standard port number!')
-
-    return None
-
-def generate(vxlan):
-    return None
-
-def apply(vxlan):
-    # Check if the VXLAN interface already exists
-    if 'rebuild_required' in vxlan or 'delete' in vxlan:
-        if vxlan['ifname'] in interfaces():
-            v = VXLANIf(vxlan['ifname'])
-            # VXLAN is super picky and the tunnel always needs to be recreated,
-            # thus we can simply always delete it first.
-            v.remove()
-
-    if 'deleted' not in vxlan:
-        # Finally create the new interface
-        v = VXLANIf(**vxlan)
-        v.update(vxlan)
-
-    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/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
deleted file mode 100755
index 79e5d3f44..000000000
--- a/src/conf_mode/interfaces-wireguard.py
+++ /dev/null
@@ -1,133 +0,0 @@
-#!/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/>.
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.configdict import get_interface_dict
-from vyos.configdict import is_node_changed
-from vyos.configverify import verify_vrf
-from vyos.configverify import verify_address
-from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_mtu_ipv6
-from vyos.configverify import verify_mirror_redirect
-from vyos.configverify import verify_bond_bridge_member
-from vyos.ifconfig import WireGuardIf
-from vyos.utils.kernel import check_kmod
-from vyos.utils.network import check_port_availability
-from vyos.utils.network import is_wireguard_key_pair
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'wireguard']
-    ifname, wireguard = get_interface_dict(conf, base)
-
-    # Check if a port was changed
-    tmp = is_node_changed(conf, base + [ifname, 'port'])
-    if tmp: wireguard['port_changed'] = {}
-
-    # T4702: If anything on a peer changes we remove the peer first and re-add it
-    if is_node_changed(conf, base + [ifname, 'peer']):
-        wireguard.update({'rebuild_required': {}})
-
-    return wireguard
-
-def verify(wireguard):
-    if 'deleted' in wireguard:
-        verify_bridge_delete(wireguard)
-        return None
-
-    verify_mtu_ipv6(wireguard)
-    verify_address(wireguard)
-    verify_vrf(wireguard)
-    verify_bond_bridge_member(wireguard)
-    verify_mirror_redirect(wireguard)
-
-    if 'private_key' not in wireguard:
-        raise ConfigError('Wireguard private-key not defined')
-
-    if 'peer' not in wireguard:
-        raise ConfigError('At least one Wireguard peer is required!')
-
-    if 'port' in wireguard and 'port_changed' in wireguard:
-        listen_port = int(wireguard['port'])
-        if check_port_availability('0.0.0.0', listen_port, 'udp') is not True:
-            raise ConfigError(f'UDP port {listen_port} is busy or unavailable and '
-                               'cannot be used for the interface!')
-
-    # run checks on individual configured WireGuard peer
-    public_keys = []
-    for tmp in wireguard['peer']:
-        peer = wireguard['peer'][tmp]
-
-        if 'allowed_ips' not in peer:
-            raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!')
-
-        if 'public_key' not in peer:
-            raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!')
-
-        if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer):
-            raise ConfigError('Both Wireguard port and address must be defined '
-                              f'for peer "{tmp}" if either one of them is set!')
-
-        if peer['public_key'] in public_keys:
-            raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"')
-
-        if 'disable' not in peer:
-            if is_wireguard_key_pair(wireguard['private_key'], peer['public_key']):
-                raise ConfigError(f'Peer "{tmp}" has the same public key as the interface "{wireguard["ifname"]}"')
-
-        public_keys.append(peer['public_key'])
-
-def apply(wireguard):
-    if 'rebuild_required' in wireguard or 'deleted' in wireguard:
-        wg = WireGuardIf(**wireguard)
-        # WireGuard only supports peer removal based on the configured public-key,
-        # by deleting the entire interface this is the shortcut instead of parsing
-        # out all peers and removing them one by one.
-        #
-        # Peer reconfiguration will always come with a short downtime while the
-        # WireGuard interface is recreated (see below)
-        wg.remove()
-
-    # Create the new interface if required
-    if 'deleted' not in wireguard:
-        wg = WireGuardIf(**wireguard)
-        wg.update(wireguard)
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        check_kmod('wireguard')
-        c = get_config()
-        verify(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
deleted file mode 100755
index 02b4a2500..000000000
--- a/src/conf_mode/interfaces-wireless.py
+++ /dev/null
@@ -1,275 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-2020 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 sys import exit
-from re import findall
-from netaddr import EUI, mac_unix_expanded
-
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configdict import dict_merge
-from vyos.configverify import verify_address
-from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_mirror_redirect
-from vyos.configverify import verify_vlan_config
-from vyos.configverify import verify_vrf
-from vyos.configverify import verify_bond_bridge_member
-from vyos.ifconfig import WiFiIf
-from vyos.template import render
-from vyos.utils.process import call
-from vyos.utils.dict import dict_search
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-# XXX: wpa_supplicant works on the source interface
-wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf'
-hostapd_conf = '/run/hostapd/{ifname}.conf'
-hostapd_accept_station_conf = '/run/hostapd/{ifname}_station_accept.conf'
-hostapd_deny_station_conf = '/run/hostapd/{ifname}_station_deny.conf'
-
-def find_other_stations(conf, base, ifname):
-    """
-    Only one wireless interface per phy can be in station mode -
-    find all interfaces attached to a phy which run in station mode
-    """
-    old_level = conf.get_level()
-    conf.set_level(base)
-    dict = {}
-    for phy in os.listdir('/sys/class/ieee80211'):
-        list = []
-        for interface in conf.list_nodes([]):
-            if interface == ifname:
-                continue
-            # the following node is mandatory
-            if conf.exists([interface, 'physical-device', phy]):
-                tmp = conf.return_value([interface, 'type'])
-                if tmp == 'station':
-                    list.append(interface)
-        if list:
-            dict.update({phy: list})
-    conf.set_level(old_level)
-    return dict
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'wireless']
-
-    ifname, wifi = get_interface_dict(conf, base)
-
-    if 'deleted' not in wifi:
-        # then get_interface_dict provides default keys
-        if wifi.from_defaults(['security', 'wep']): # if not set by user
-            del wifi['security']['wep']
-        if wifi.from_defaults(['security', 'wpa']): # if not set by user
-            del wifi['security']['wpa']
-
-    if dict_search('security.wpa', wifi) != None:
-        wpa_cipher = wifi['security']['wpa'].get('cipher')
-        wpa_mode = wifi['security']['wpa'].get('mode')
-        if not wpa_cipher:
-            tmp = None
-            if wpa_mode == 'wpa':
-                tmp = {'security': {'wpa': {'cipher' : ['TKIP', 'CCMP']}}}
-            elif wpa_mode == 'wpa2':
-                tmp = {'security': {'wpa': {'cipher' : ['CCMP']}}}
-            elif wpa_mode == 'both':
-                tmp = {'security': {'wpa': {'cipher' : ['CCMP', 'TKIP']}}}
-
-            if tmp: wifi = dict_merge(tmp, wifi)
-
-    # Only one wireless interface per phy can be in station mode
-    tmp = find_other_stations(conf, base, wifi['ifname'])
-    if tmp: wifi['station_interfaces'] = tmp
-
-    # used in hostapt.conf.j2
-    wifi['hostapd_accept_station_conf'] = hostapd_accept_station_conf.format(**wifi)
-    wifi['hostapd_deny_station_conf'] = hostapd_deny_station_conf.format(**wifi)
-
-    return wifi
-
-def verify(wifi):
-    if 'deleted' in wifi:
-        verify_bridge_delete(wifi)
-        return None
-
-    if 'physical_device' not in wifi:
-        raise ConfigError('You must specify a physical-device "phy"')
-
-    if 'type' not in wifi:
-        raise ConfigError('You must specify a WiFi mode')
-
-    if 'ssid' not in wifi and wifi['type'] != 'monitor':
-        raise ConfigError('SSID must be configured unless type is set to "monitor"!')
-
-    if wifi['type'] == 'access-point':
-        if 'country_code' not in wifi:
-            raise ConfigError('Wireless country-code is mandatory')
-
-        if 'channel' not in wifi:
-            raise ConfigError('Wireless channel must be configured!')
-
-    if 'security' in wifi:
-        if {'wep', 'wpa'} <= set(wifi.get('security', {})):
-            raise ConfigError('Must either use WEP or WPA security!')
-
-        if 'wep' in wifi['security']:
-            if 'key' in wifi['security']['wep'] and len(wifi['security']['wep']) > 4:
-                raise ConfigError('No more then 4 WEP keys configurable')
-            elif 'key' not in wifi['security']['wep']:
-                raise ConfigError('Security WEP configured - missing WEP keys!')
-
-        elif 'wpa' in wifi['security']:
-            wpa = wifi['security']['wpa']
-            if not any(i in ['passphrase', 'radius'] for i in wpa):
-                raise ConfigError('Misssing WPA key or RADIUS server')
-
-            if 'radius' in wpa:
-                if 'server' in wpa['radius']:
-                    for server in wpa['radius']['server']:
-                        if 'key' not in wpa['radius']['server'][server]:
-                            raise ConfigError(f'Misssing RADIUS shared secret key for server: {server}')
-
-    if 'capabilities' in wifi:
-        capabilities = wifi['capabilities']
-        if 'vht' in capabilities:
-            if 'ht' not in capabilities:
-                raise ConfigError('Specify HT flags if you want to use VHT!')
-
-            if {'beamform', 'antenna_count'} <= set(capabilities.get('vht', {})):
-                if capabilities['vht']['antenna_count'] == '1':
-                    raise ConfigError('Cannot use beam forming with just one antenna!')
-
-                if capabilities['vht']['beamform'] == 'single-user-beamformer':
-                    if int(capabilities['vht']['antenna_count']) < 3:
-                        # Nasty Gotcha: see https://w1.fi/cgit/hostap/plain/hostapd/hostapd.conf lines 692-705
-                        raise ConfigError('Single-user beam former requires at least 3 antennas!')
-
-    if 'station_interfaces' in wifi and wifi['type'] == 'station':
-        phy = wifi['physical_device']
-        if phy in wifi['station_interfaces']:
-            if len(wifi['station_interfaces'][phy]) > 0:
-                raise ConfigError('Only one station per wireless physical interface possible!')
-
-    verify_address(wifi)
-    verify_vrf(wifi)
-    verify_bond_bridge_member(wifi)
-    verify_mirror_redirect(wifi)
-
-    # use common function to verify VLAN configuration
-    verify_vlan_config(wifi)
-
-    return None
-
-def generate(wifi):
-    interface = wifi['ifname']
-
-    # always stop hostapd service first before reconfiguring it
-    call(f'systemctl stop hostapd@{interface}.service')
-    # always stop wpa_supplicant service first before reconfiguring it
-    call(f'systemctl stop wpa_supplicant@{interface}.service')
-
-    # Delete config files if interface is removed
-    if 'deleted' in wifi:
-        if os.path.isfile(hostapd_conf.format(**wifi)):
-            os.unlink(hostapd_conf.format(**wifi))
-        if os.path.isfile(hostapd_accept_station_conf.format(**wifi)):
-            os.unlink(hostapd_accept_station_conf.format(**wifi))
-        if os.path.isfile(hostapd_deny_station_conf.format(**wifi)):
-            os.unlink(hostapd_deny_station_conf.format(**wifi))
-        if os.path.isfile(wpa_suppl_conf.format(**wifi)):
-            os.unlink(wpa_suppl_conf.format(**wifi))
-
-        return None
-
-    if 'mac' not in wifi:
-        # http://wiki.stocksy.co.uk/wiki/Multiple_SSIDs_with_hostapd
-        # generate locally administered MAC address from used phy interface
-        with open('/sys/class/ieee80211/{physical_device}/addresses'.format(**wifi), 'r') as f:
-            # some PHYs tend to have multiple interfaces and thus supply multiple MAC
-            # addresses - we only need the first one for our calculation
-            tmp = f.readline().rstrip()
-            tmp = EUI(tmp).value
-            # mask last nibble from the MAC address
-            tmp &= 0xfffffffffff0
-            # set locally administered bit in MAC address
-            tmp |= 0x020000000000
-            # we now need to add an offset to our MAC address indicating this
-            # subinterfaces index
-            tmp += int(findall(r'\d+', interface)[0])
-
-            # convert integer to "real" MAC address representation
-            mac = EUI(hex(tmp).split('x')[-1])
-            # change dialect to use : as delimiter instead of -
-            mac.dialect = mac_unix_expanded
-            wifi['mac'] = str(mac)
-
-    # XXX: Jinja2 can not operate on a dictionary key when it starts of with a number
-    if '40mhz_incapable' in (dict_search('capabilities.ht', wifi) or []):
-        wifi['capabilities']['ht']['fourtymhz_incapable'] = wifi['capabilities']['ht']['40mhz_incapable']
-        del wifi['capabilities']['ht']['40mhz_incapable']
-
-    # render appropriate new config files depending on access-point or station mode
-    if wifi['type'] == 'access-point':
-        render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.j2', wifi)
-        render(hostapd_accept_station_conf.format(**wifi), 'wifi/hostapd_accept_station.conf.j2', wifi)
-        render(hostapd_deny_station_conf.format(**wifi), 'wifi/hostapd_deny_station.conf.j2', wifi)
-
-    elif wifi['type'] == 'station':
-        render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.j2', wifi)
-
-    return None
-
-def apply(wifi):
-    interface = wifi['ifname']
-    if 'deleted' in wifi:
-        WiFiIf(interface).remove()
-    else:
-        # Finally create the new interface
-        w = WiFiIf(**wifi)
-        w.update(wifi)
-
-        # Enable/Disable interface - interface is always placed in
-        # administrative down state in WiFiIf class
-        if 'disable' not in wifi:
-            # Physical interface is now configured. Proceed by starting hostapd or
-            # wpa_supplicant daemon. When type is monitor we can just skip this.
-            if wifi['type'] == 'access-point':
-                call(f'systemctl start hostapd@{interface}.service')
-
-            elif wifi['type'] == 'station':
-                call(f'systemctl start wpa_supplicant@{interface}.service')
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py
deleted file mode 100755
index 2515dc838..000000000
--- a/src/conf_mode/interfaces-wwan.py
+++ /dev/null
@@ -1,189 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2020-2022 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 sys import exit
-from time import sleep
-
-from vyos.config import Config
-from vyos.configdict import get_interface_dict
-from vyos.configdict import is_node_changed
-from vyos.configverify import verify_authentication
-from vyos.configverify import verify_interface_exists
-from vyos.configverify import verify_mirror_redirect
-from vyos.configverify import verify_vrf
-from vyos.ifconfig import WWANIf
-from vyos.utils.dict import dict_search
-from vyos.utils.process import cmd
-from vyos.utils.process import call
-from vyos.utils.process import DEVNULL
-from vyos.utils.process import is_systemd_service_active
-from vyos.utils.file import write_file
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-service_name = 'ModemManager.service'
-cron_script = '/etc/cron.d/vyos-wwan'
-
-def get_config(config=None):
-    """
-    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
-    interface name will be added or a deleted flag
-    """
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['interfaces', 'wwan']
-    ifname, wwan = get_interface_dict(conf, base)
-
-    # We should only terminate the WWAN session if critical parameters change.
-    # All parameters that can be changed on-the-fly (like interface description)
-    # should not lead to a reconnect!
-    tmp = is_node_changed(conf, base + [ifname, 'address'])
-    if tmp: wwan.update({'shutdown_required': {}})
-
-    tmp = is_node_changed(conf, base + [ifname, 'apn'])
-    if tmp: wwan.update({'shutdown_required': {}})
-
-    tmp = is_node_changed(conf, base + [ifname, 'disable'])
-    if tmp: wwan.update({'shutdown_required': {}})
-
-    tmp = is_node_changed(conf, base + [ifname, 'vrf'])
-    if tmp: wwan.update({'shutdown_required': {}})
-
-    tmp = is_node_changed(conf, base + [ifname, 'authentication'])
-    if tmp: wwan.update({'shutdown_required': {}})
-
-    tmp = is_node_changed(conf, base + [ifname, 'ipv6', 'address', 'autoconf'])
-    if tmp: wwan.update({'shutdown_required': {}})
-
-    # We need to know the amount of other WWAN interfaces as ModemManager needs
-    # to be started or stopped.
-    wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'),
-                                                       get_first_key=True,
-                                                       no_tag_node_value_mangle=True)
-
-    # This if-clause is just to be sure - it will always evaluate to true
-    if ifname in wwan['other_interfaces']:
-        del wwan['other_interfaces'][ifname]
-    if len(wwan['other_interfaces']) == 0:
-        del wwan['other_interfaces']
-
-    return wwan
-
-def verify(wwan):
-    if 'deleted' in wwan:
-        return None
-
-    ifname = wwan['ifname']
-    if not 'apn' in wwan:
-        raise ConfigError(f'No APN configured for "{ifname}"!')
-
-    verify_interface_exists(ifname)
-    verify_authentication(wwan)
-    verify_vrf(wwan)
-    verify_mirror_redirect(wwan)
-
-    return None
-
-def generate(wwan):
-    if 'deleted' in wwan:
-        # We are the last WWAN interface - there are no other ones remaining
-        # thus the cronjob needs to go away, too
-        if 'other_interfaces' not in wwan:
-            if os.path.exists(cron_script):
-                os.unlink(cron_script)
-        return None
-
-    # Install cron triggered helper script to re-dial WWAN interfaces on
-    # disconnect - e.g. happens during RF signal loss. The script watches every
-    # WWAN interface - so there is only one instance.
-    if not os.path.exists(cron_script):
-        write_file(cron_script, '*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py\n')
-
-    return None
-
-def apply(wwan):
-    # ModemManager is required to dial WWAN connections - one instance is
-    # required to serve all modems. Activate ModemManager on first invocation
-    # of any WWAN interface.
-    if not is_systemd_service_active(service_name):
-        cmd(f'systemctl start {service_name}')
-
-        counter = 100
-        # Wait until a modem is detected and then we can continue
-        while counter > 0:
-            counter -= 1
-            tmp = cmd('mmcli -L')
-            if tmp != 'No modems were found':
-                break
-            sleep(0.250)
-
-    if 'shutdown_required' in wwan:
-        # we only need the modem number. wwan0 -> 0, wwan1 -> 1
-        modem = wwan['ifname'].lstrip('wwan')
-        base_cmd = f'mmcli --modem {modem}'
-        # Number of bearers is limited - always disconnect first
-        cmd(f'{base_cmd} --simple-disconnect')
-
-    w = WWANIf(wwan['ifname'])
-    if 'deleted' in wwan or 'disable' in wwan:
-        w.remove()
-
-        # We are the last WWAN interface - there are no other WWAN interfaces
-        # remaining, thus we can stop ModemManager and free resources.
-        if 'other_interfaces' not in wwan:
-            cmd(f'systemctl stop {service_name}')
-            # Clean CRON helper script which is used for to re-connect when
-            # RF signal is lost
-            if os.path.exists(cron_script):
-                os.unlink(cron_script)
-
-        return None
-
-    if 'shutdown_required' in wwan:
-        ip_type = 'ipv4'
-        slaac = dict_search('ipv6.address.autoconf', wwan) != None
-        if 'address' in wwan:
-            if 'dhcp' in wwan['address'] and ('dhcpv6' in wwan['address'] or slaac):
-                ip_type = 'ipv4v6'
-            elif 'dhcpv6' in wwan['address'] or slaac:
-                ip_type = 'ipv6'
-            elif 'dhcp' in wwan['address']:
-                ip_type = 'ipv4'
-
-        options = f'ip-type={ip_type},apn=' + wwan['apn']
-        if 'authentication' in wwan:
-            options += ',user={username},password={password}'.format(**wwan['authentication'])
-
-        command = f'{base_cmd} --simple-connect="{options}"'
-        call(command, stdout=DEVNULL)
-
-    w.update(wwan)
-    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/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py
new file mode 100755
index 000000000..8184d8415
--- /dev/null
+++ b/src/conf_mode/interfaces_bonding.py
@@ -0,0 +1,294 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-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 sys import exit
+from netifaces import interfaces
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
+from vyos.configdict import leaf_node_changed
+from vyos.configdict import is_member
+from vyos.configdict import is_source_interface
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_dhcpv6
+from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_vlan_config
+from vyos.configverify import verify_vrf
+from vyos.ifconfig import BondIf
+from vyos.ifconfig.ethernet import EthernetIf
+from vyos.ifconfig import Section
+from vyos.template import render_to_string
+from vyos.utils.dict import dict_search
+from vyos.utils.dict import dict_to_paths_values
+from vyos.configdict import has_address_configured
+from vyos.configdict import has_vrf_configured
+from vyos.configdep import set_dependents, call_dependents
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+airbag.enable()
+
+def get_bond_mode(mode):
+    if mode == 'round-robin':
+        return 'balance-rr'
+    elif mode == 'active-backup':
+        return 'active-backup'
+    elif mode == 'xor-hash':
+        return 'balance-xor'
+    elif mode == 'broadcast':
+        return 'broadcast'
+    elif mode == '802.3ad':
+        return '802.3ad'
+    elif mode == 'transmit-load-balance':
+        return 'balance-tlb'
+    elif mode == 'adaptive-load-balance':
+        return 'balance-alb'
+    else:
+        raise ConfigError(f'invalid bond mode "{mode}"')
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'bonding']
+    ifname, bond = get_interface_dict(conf, base)
+
+    # To make our own life easier transfor the list of member interfaces
+    # into a dictionary - we will use this to add additional information
+    # later on for each member
+    if 'member' in bond and 'interface' in bond['member']:
+        # convert list of member interfaces to a dictionary
+        bond['member']['interface'] = {k: {} for k in bond['member']['interface']}
+
+    if 'mode' in bond:
+        bond['mode'] = get_bond_mode(bond['mode'])
+
+    tmp = is_node_changed(conf, base + [ifname, 'mode'])
+    if tmp: bond['shutdown_required'] = {}
+
+    tmp = is_node_changed(conf, base + [ifname, 'lacp-rate'])
+    if tmp: bond['shutdown_required'] = {}
+
+    # determine which members have been removed
+    interfaces_removed = leaf_node_changed(conf, base + [ifname, 'member', 'interface'])
+    # Reset config level to interfaces
+    old_level = conf.get_level()
+    conf.set_level(['interfaces'])
+
+    if interfaces_removed:
+        bond['shutdown_required'] = {}
+        if 'member' not in bond:
+            bond['member'] = {}
+
+        tmp = {}
+        for interface in interfaces_removed:
+            # if member is deleted from bond, add dependencies to call
+            # ethernet commit again in apply function
+            # to apply options under ethernet section
+            set_dependents('ethernet', conf, interface)
+            section = Section.section(interface) # this will be 'ethernet' for 'eth0'
+            if conf.exists([section, interface, 'disable']):
+                tmp[interface] = {'disable': ''}
+            else:
+                tmp[interface] = {}
+
+        # also present the interfaces to be removed from the bond as dictionary
+        bond['member']['interface_remove'] = tmp
+
+    # Restore existing config level
+    conf.set_level(old_level)
+
+    if dict_search('member.interface', bond):
+        for interface, interface_config in bond['member']['interface'].items():
+
+            interface_ethernet_config = conf.get_config_dict(
+                ['interfaces', 'ethernet', interface],
+                key_mangling=('-', '_'),
+                get_first_key=True,
+                no_tag_node_value_mangle=True,
+                with_defaults=False,
+                with_recursive_defaults=False)
+
+            interface_config['config_paths'] = dict_to_paths_values(interface_ethernet_config)
+
+            # Check if member interface is a new member
+            if not conf.exists_effective(base + [ifname, 'member', 'interface', interface]):
+                bond['shutdown_required'] = {}
+                interface_config['new_added'] = {}
+
+            # Check if member interface is disabled
+            conf.set_level(['interfaces'])
+
+            section = Section.section(interface) # this will be 'ethernet' for 'eth0'
+            if conf.exists([section, interface, 'disable']):
+                interface_config['disable'] = ''
+
+            conf.set_level(old_level)
+
+            # Check if member interface is already member of another bridge
+            tmp = is_member(conf, interface, 'bridge')
+            if tmp: interface_config['is_bridge_member'] = tmp
+
+            # Check if member interface is already member of a bond
+            tmp = is_member(conf, interface, 'bonding')
+            for tmp in is_member(conf, interface, 'bonding'):
+                if bond['ifname'] == tmp:
+                    continue
+                interface_config['is_bond_member'] = tmp
+
+            # Check if member interface is used as source-interface on another interface
+            tmp = is_source_interface(conf, interface)
+            if tmp: interface_config['is_source_interface'] = tmp
+
+            # bond members must not have an assigned address
+            tmp = has_address_configured(conf, interface)
+            if tmp: interface_config['has_address'] = {}
+
+            # bond members must not have a VRF attached
+            tmp = has_vrf_configured(conf, interface)
+            if tmp: interface_config['has_vrf'] = {}
+    return bond
+
+
+def verify(bond):
+    if 'deleted' in bond:
+        verify_bridge_delete(bond)
+        return None
+
+    if 'arp_monitor' in bond:
+        if 'target' in bond['arp_monitor'] and len(bond['arp_monitor']['target']) > 16:
+            raise ConfigError('The maximum number of arp-monitor targets is 16')
+
+        if 'interval' in bond['arp_monitor'] and int(bond['arp_monitor']['interval']) > 0:
+            if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']:
+                raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \
+                                  'transmit-load-balance or adaptive-load-balance')
+
+    if 'primary' in bond:
+        if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
+            raise ConfigError('Option primary - mode dependency failed, not'
+                              'supported in mode {mode}!'.format(**bond))
+
+    verify_mtu_ipv6(bond)
+    verify_address(bond)
+    verify_dhcpv6(bond)
+    verify_vrf(bond)
+    verify_mirror_redirect(bond)
+
+    # use common function to verify VLAN configuration
+    verify_vlan_config(bond)
+
+    bond_name = bond['ifname']
+    if dict_search('member.interface', bond):
+        for interface, interface_config in bond['member']['interface'].items():
+            error_msg = f'Can not add interface "{interface}" to bond, '
+
+            if interface == 'lo':
+                raise ConfigError('Loopback interface "lo" can not be added to a bond')
+
+            if interface not in interfaces():
+                raise ConfigError(error_msg + 'it does not exist!')
+
+            if 'is_bridge_member' in interface_config:
+                tmp = next(iter(interface_config['is_bridge_member']))
+                raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')
+
+            if 'is_bond_member' in interface_config:
+                tmp = next(iter(interface_config['is_bond_member']))
+                raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')
+
+            if 'is_source_interface' in interface_config:
+                tmp = interface_config['is_source_interface']
+                raise ConfigError(error_msg + f'it is the source-interface of "{tmp}"!')
+
+            if 'has_address' in interface_config:
+                raise ConfigError(error_msg + 'it has an address assigned!')
+
+            if 'has_vrf' in interface_config:
+                raise ConfigError(error_msg + 'it has a VRF assigned!')
+
+            if 'new_added' in interface_config and 'config_paths' in interface_config:
+                for option_path, option_value in interface_config['config_paths'].items():
+                    if option_path in EthernetIf.get_bond_member_allowed_options() :
+                        continue
+                    if option_path in BondIf.get_inherit_bond_options():
+                        continue
+                    raise ConfigError(error_msg + f'it has a "{option_path.replace(".", " ")}" assigned!')
+
+    if 'primary' in bond:
+        if bond['primary'] not in bond['member']['interface']:
+            raise ConfigError(f'Primary interface of bond "{bond_name}" must be a member interface')
+
+        if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
+            raise ConfigError('primary interface only works for mode active-backup, ' \
+                              'transmit-load-balance or adaptive-load-balance')
+
+    return None
+
+def generate(bond):
+    bond['frr_zebra_config'] = ''
+    if 'deleted' not in bond:
+        bond['frr_zebra_config'] = render_to_string('frr/evpn.mh.frr.j2', bond)
+    return None
+
+def apply(bond):
+    ifname = bond['ifname']
+    b = BondIf(ifname)
+    if 'deleted' in bond:
+        # delete interface
+        b.remove()
+    else:
+        b.update(bond)
+
+    if dict_search('member.interface_remove', bond):
+        try:
+            call_dependents()
+        except ConfigError:
+            raise ConfigError('Error in updating ethernet interface '
+                              'after deleting it from bond')
+
+    zebra_daemon = 'zebra'
+    # Save original configuration prior to starting any commit actions
+    frr_cfg = frr.FRRConfig()
+
+    # The route-map used for the FIB (zebra) is part of the zebra daemon
+    frr_cfg.load_configuration(zebra_daemon)
+    frr_cfg.modify_section(f'^interface {ifname}', stop_pattern='^exit', remove_stop_mark=True)
+    if 'frr_zebra_config' in bond:
+        frr_cfg.add_before(frr.default_add_before, bond['frr_zebra_config'])
+    frr_cfg.commit_configuration(zebra_daemon)
+
+    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/interfaces_bridge.py b/src/conf_mode/interfaces_bridge.py
new file mode 100755
index 000000000..29991e2da
--- /dev/null
+++ b/src/conf_mode/interfaces_bridge.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-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/>.
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import node_changed
+from vyos.configdict import is_member
+from vyos.configdict import is_source_interface
+from vyos.configdict import has_vlan_subinterface_configured
+from vyos.configverify import verify_dhcpv6
+from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_vrf
+from vyos.ifconfig import BridgeIf
+from vyos.configdict import has_address_configured
+from vyos.configdict import has_vrf_configured
+from vyos.configdep import set_dependents
+from vyos.configdep import call_dependents
+from vyos.utils.dict import dict_search
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'bridge']
+    ifname, bridge = get_interface_dict(conf, base)
+
+    # determine which members have been removed
+    tmp = node_changed(conf, base + [ifname, 'member', 'interface'])
+    if tmp:
+        if 'member' in bridge:
+            bridge['member'].update({'interface_remove' : tmp })
+        else:
+            bridge.update({'member' : {'interface_remove' : tmp }})
+
+    if dict_search('member.interface', bridge) is not None:
+        for interface in list(bridge['member']['interface']):
+            # Check if member interface is already member of another bridge
+            tmp = is_member(conf, interface, 'bridge')
+            if tmp and bridge['ifname'] not in tmp:
+                bridge['member']['interface'][interface].update({'is_bridge_member' : tmp})
+
+            # Check if member interface is already member of a bond
+            tmp = is_member(conf, interface, 'bonding')
+            if tmp: bridge['member']['interface'][interface].update({'is_bond_member' : tmp})
+
+            # Check if member interface is used as source-interface on another interface
+            tmp = is_source_interface(conf, interface)
+            if tmp: bridge['member']['interface'][interface].update({'is_source_interface' : tmp})
+
+            # Bridge members must not have an assigned address
+            tmp = has_address_configured(conf, interface)
+            if tmp: bridge['member']['interface'][interface].update({'has_address' : ''})
+
+            # Bridge members must not have a VRF attached
+            tmp = has_vrf_configured(conf, interface)
+            if tmp: bridge['member']['interface'][interface].update({'has_vrf' : ''})
+
+            # VLAN-aware bridge members must not have VLAN interface configuration
+            tmp = has_vlan_subinterface_configured(conf,interface)
+            if 'enable_vlan' in bridge and tmp:
+                bridge['member']['interface'][interface].update({'has_vlan' : ''})
+
+            # When using VXLAN member interfaces that are configured for Single
+            # VXLAN Device (SVD) we need to call the VXLAN conf-mode script to re-create
+            # VLAN to VNI mappings if required
+            if interface.startswith('vxlan'):
+                set_dependents('vxlan', conf, interface)
+
+    # delete empty dictionary keys - no need to run code paths if nothing is there to do
+    if 'member' in bridge:
+        if 'interface' in bridge['member'] and len(bridge['member']['interface']) == 0:
+            del bridge['member']['interface']
+
+        if len(bridge['member']) == 0:
+            del bridge['member']
+
+    return bridge
+
+def verify(bridge):
+    if 'deleted' in bridge:
+        return None
+
+    verify_dhcpv6(bridge)
+    verify_vrf(bridge)
+    verify_mirror_redirect(bridge)
+
+    ifname = bridge['ifname']
+
+    if dict_search('member.interface', bridge):
+        for interface, interface_config in bridge['member']['interface'].items():
+            error_msg = f'Can not add interface "{interface}" to bridge, '
+
+            if interface == 'lo':
+                raise ConfigError('Loopback interface "lo" can not be added to a bridge')
+
+            if 'is_bridge_member' in interface_config:
+                tmp = next(iter(interface_config['is_bridge_member']))
+                raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')
+
+            if 'is_bond_member' in interface_config:
+                tmp = next(iter(interface_config['is_bond_member']))
+                raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')
+
+            if 'is_source_interface' in interface_config:
+                tmp = interface_config['is_source_interface']
+                raise ConfigError(error_msg + f'it is the source-interface of "{tmp}"!')
+
+            if 'has_address' in interface_config:
+                raise ConfigError(error_msg + 'it has an address assigned!')
+
+            if 'has_vrf' in interface_config:
+                raise ConfigError(error_msg + 'it has a VRF assigned!')
+
+            if 'enable_vlan' in bridge:
+                if 'has_vlan' in interface_config:
+                    raise ConfigError(error_msg + 'it has VLAN subinterface(s) assigned!')
+
+                if 'wlan' in interface:
+                    raise ConfigError(error_msg + 'VLAN aware cannot be set!')
+            else:
+                for option in ['allowed_vlan', 'native_vlan']:
+                    if option in interface_config:
+                        raise ConfigError('Can not use VLAN options on non VLAN aware bridge')
+
+    if 'enable_vlan' in bridge:
+        if dict_search('vif.1', bridge):
+            raise ConfigError(f'VLAN 1 sub interface cannot be set for VLAN aware bridge {ifname}, and VLAN 1 is always the parent interface')
+    else:
+        if dict_search('vif', bridge):
+            raise ConfigError(f'You must first activate "enable-vlan" of {ifname} bridge to use "vif"')
+
+    return None
+
+def generate(bridge):
+    return None
+
+def apply(bridge):
+    br = BridgeIf(bridge['ifname'])
+    if 'deleted' in bridge:
+        # delete interface
+        br.remove()
+    else:
+        br.update(bridge)
+
+    for interface in dict_search('member.interface', bridge) or []:
+        if interface.startswith('vxlan'):
+            try:
+                call_dependents()
+            except ConfigError:
+                raise ConfigError('Error in updating VXLAN interface after changing bridge!')
+
+    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/interfaces_dummy.py b/src/conf_mode/interfaces_dummy.py
new file mode 100755
index 000000000..db768b94d
--- /dev/null
+++ b/src/conf_mode/interfaces_dummy.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-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/>.
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mirror_redirect
+from vyos.ifconfig import DummyIf
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'dummy']
+    _, dummy = get_interface_dict(conf, base)
+    return dummy
+
+def verify(dummy):
+    if 'deleted' in dummy:
+        verify_bridge_delete(dummy)
+        return None
+
+    verify_vrf(dummy)
+    verify_address(dummy)
+    verify_mirror_redirect(dummy)
+
+    return None
+
+def generate(dummy):
+    return None
+
+def apply(dummy):
+    d = DummyIf(**dummy)
+
+    # Remove dummy interface
+    if 'deleted' in dummy:
+        d.remove()
+    else:
+        d.update(dummy)
+
+    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/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py
new file mode 100755
index 000000000..7374a29f7
--- /dev/null
+++ b/src/conf_mode/interfaces_ethernet.py
@@ -0,0 +1,400 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-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 pprint
+
+from glob import glob
+from sys import exit
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_address
+from vyos.configverify import verify_dhcpv6
+from vyos.configverify import verify_eapol
+from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_mtu
+from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_vlan_config
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_bond_bridge_member
+from vyos.ethtool import Ethtool
+from vyos.ifconfig import EthernetIf
+from vyos.ifconfig import BondIf
+from vyos.pki import find_chain
+from vyos.pki import encode_certificate
+from vyos.pki import load_certificate
+from vyos.pki import wrap_private_key
+from vyos.template import render
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
+from vyos.utils.dict import dict_to_paths_values
+from vyos.utils.dict import dict_set
+from vyos.utils.dict import dict_delete
+from vyos.utils.file import write_file
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+# XXX: wpa_supplicant works on the source interface
+cfg_dir = '/run/wpa_supplicant'
+wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf'
+
+def update_bond_options(conf: Config, eth_conf: dict) -> list:
+    """
+    Return list of blocked options if interface is a bond member
+    :param conf: Config object
+    :type conf: Config
+    :param eth_conf: Ethernet config dictionary
+    :type eth_conf: dict
+    :return: List of blocked options
+    :rtype: list
+    """
+    blocked_list = []
+    bond_name = list(eth_conf['is_bond_member'].keys())[0]
+    config_without_defaults = conf.get_config_dict(
+        ['interfaces', 'ethernet', eth_conf['ifname']],
+        key_mangling=('-', '_'),
+        get_first_key=True,
+        no_tag_node_value_mangle=True,
+        with_defaults=False,
+        with_recursive_defaults=False)
+    config_with_defaults = conf.get_config_dict(
+        ['interfaces', 'ethernet', eth_conf['ifname']],
+        key_mangling=('-', '_'),
+        get_first_key=True,
+        no_tag_node_value_mangle=True,
+        with_defaults=True,
+        with_recursive_defaults=True)
+    bond_config_with_defaults = conf.get_config_dict(
+        ['interfaces', 'bonding', bond_name],
+        key_mangling=('-', '_'),
+        get_first_key=True,
+        no_tag_node_value_mangle=True,
+        with_defaults=True,
+        with_recursive_defaults=True)
+    eth_dict_paths = dict_to_paths_values(config_without_defaults)
+    eth_path_base = ['interfaces', 'ethernet', eth_conf['ifname']]
+
+    #if option is configured under ethernet section
+    for option_path, option_value in eth_dict_paths.items():
+        bond_option_value = dict_search(option_path, bond_config_with_defaults)
+
+        #If option is allowed for changing then continue
+        if option_path in EthernetIf.get_bond_member_allowed_options():
+            continue
+        # if option is inherited from bond then set valued from bond interface
+        if option_path in BondIf.get_inherit_bond_options():
+            # If option equals to bond option then do nothing
+            if option_value == bond_option_value:
+                continue
+            else:
+                # if ethernet has option and bond interface has
+                # then copy it from bond
+                if bond_option_value is not None:
+                    if is_node_changed(conf, eth_path_base + option_path.split('.')):
+                        Warning(
+                            f'Cannot apply "{option_path.replace(".", " ")}" to "{option_value}".' \
+                            f' Interface "{eth_conf["ifname"]}" is a bond member.' \
+                            f' Option is inherited from bond "{bond_name}"')
+                    dict_set(option_path, bond_option_value, eth_conf)
+                    continue
+                # if ethernet has option and bond interface does not have
+                # then delete it form dict and do not apply it
+                else:
+                    if is_node_changed(conf, eth_path_base + option_path.split('.')):
+                        Warning(
+                            f'Cannot apply "{option_path.replace(".", " ")}".' \
+                            f' Interface "{eth_conf["ifname"]}" is a bond member.' \
+                            f' Option is inherited from bond "{bond_name}"')
+                    dict_delete(option_path, eth_conf)
+        blocked_list.append(option_path)
+
+    # if inherited option is not configured under ethernet section but configured under bond section
+    for option_path in BondIf.get_inherit_bond_options():
+        bond_option_value = dict_search(option_path, bond_config_with_defaults)
+        if bond_option_value is not None:
+            if option_path not in eth_dict_paths:
+                if is_node_changed(conf, eth_path_base + option_path.split('.')):
+                    Warning(
+                        f'Cannot apply "{option_path.replace(".", " ")}" to "{dict_search(option_path, config_with_defaults)}".' \
+                        f' Interface "{eth_conf["ifname"]}" is a bond member. ' \
+                        f'Option is inherited from bond "{bond_name}"')
+                dict_set(option_path, bond_option_value, eth_conf)
+    eth_conf['bond_blocked_changes'] = blocked_list
+    return None
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    # This must be called prior to get_interface_dict(), as this function will
+    # alter the config level (config.set_level())
+    pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+                               get_first_key=True, no_tag_node_value_mangle=True)
+
+    base = ['interfaces', 'ethernet']
+    ifname, ethernet = get_interface_dict(conf, base)
+    if 'is_bond_member' in ethernet:
+        update_bond_options(conf, ethernet)
+
+    if 'deleted' not in ethernet:
+       if pki: ethernet['pki'] = pki
+
+    tmp = is_node_changed(conf, base + [ifname, 'speed'])
+    if tmp: ethernet.update({'speed_duplex_changed': {}})
+
+    tmp = is_node_changed(conf, base + [ifname, 'duplex'])
+    if tmp: ethernet.update({'speed_duplex_changed': {}})
+
+    return ethernet
+
+
+
+def verify_speed_duplex(ethernet: dict, ethtool: Ethtool):
+    """
+     Verify speed and duplex
+    :param ethernet: dictionary which is received from get_interface_dict
+    :type ethernet: dict
+    :param ethtool: Ethernet object
+    :type ethtool: Ethtool
+    """
+    if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or
+            (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
+        raise ConfigError(
+            'Speed/Duplex missmatch. Must be both auto or manually configured')
+
+    if ethernet['speed'] != 'auto' and ethernet['duplex'] != 'auto':
+        # We need to verify if the requested speed and duplex setting is
+        # supported by the underlaying NIC.
+        speed = ethernet['speed']
+        duplex = ethernet['duplex']
+        if not ethtool.check_speed_duplex(speed, duplex):
+            raise ConfigError(
+                f'Adapter does not support changing speed ' \
+                f'and duplex settings to: {speed}/{duplex}!')
+
+
+def verify_flow_control(ethernet: dict, ethtool: Ethtool):
+    """
+     Verify flow control
+    :param ethernet: dictionary which is received from get_interface_dict
+    :type ethernet: dict
+    :param ethtool: Ethernet object
+    :type ethtool: Ethtool
+    """
+    if 'disable_flow_control' in ethernet:
+        if not ethtool.check_flow_control():
+            raise ConfigError(
+                'Adapter does not support changing flow-control settings!')
+
+
+def verify_ring_buffer(ethernet: dict, ethtool: Ethtool):
+    """
+     Verify ring buffer
+    :param ethernet: dictionary which is received from get_interface_dict
+    :type ethernet: dict
+    :param ethtool: Ethernet object
+    :type ethtool: Ethtool
+    """
+    if 'ring_buffer' in ethernet:
+        max_rx = ethtool.get_ring_buffer_max('rx')
+        if not max_rx:
+            raise ConfigError(
+                'Driver does not support RX ring-buffer configuration!')
+
+        max_tx = ethtool.get_ring_buffer_max('tx')
+        if not max_tx:
+            raise ConfigError(
+                'Driver does not support TX ring-buffer configuration!')
+
+        rx = dict_search('ring_buffer.rx', ethernet)
+        if rx and int(rx) > int(max_rx):
+            raise ConfigError(f'Driver only supports a maximum RX ring-buffer ' \
+                              f'size of "{max_rx}" bytes!')
+
+        tx = dict_search('ring_buffer.tx', ethernet)
+        if tx and int(tx) > int(max_tx):
+            raise ConfigError(f'Driver only supports a maximum TX ring-buffer ' \
+                              f'size of "{max_tx}" bytes!')
+
+
+def verify_offload(ethernet: dict, ethtool: Ethtool):
+    """
+     Verify offloading capabilities
+    :param ethernet: dictionary which is received from get_interface_dict
+    :type ethernet: dict
+    :param ethtool: Ethernet object
+    :type ethtool: Ethtool
+    """
+    if dict_search('offload.rps', ethernet) != None:
+        if not os.path.exists(f'/sys/class/net/{ethernet["ifname"]}/queues/rx-0/rps_cpus'):
+            raise ConfigError('Interface does not suport RPS!')
+    driver = ethtool.get_driver_name()
+    # T3342 - Xen driver requires special treatment
+    if driver == 'vif':
+        if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None:
+            raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\
+                              'for MTU size larger then 1500 bytes')
+
+
+def verify_allowedbond_changes(ethernet: dict):
+    """
+     Verify changed options if interface is in bonding
+    :param ethernet: dictionary which is received from get_interface_dict
+    :type ethernet: dict
+    """
+    if 'bond_blocked_changes' in ethernet:
+        for option in ethernet['bond_blocked_changes']:
+            raise ConfigError(f'Cannot configure "{option.replace(".", " ")}"' \
+                              f' on interface "{ethernet["ifname"]}".' \
+                              f' Interface is a bond member')
+
+
+def verify(ethernet):
+    if 'deleted' in ethernet:
+        return None
+    if 'is_bond_member' in ethernet:
+        verify_bond_member(ethernet)
+    else:
+        verify_ethernet(ethernet)
+
+
+def verify_bond_member(ethernet):
+    """
+     Verification function for ethernet interface which is in bonding
+    :param ethernet: dictionary which is received from get_interface_dict
+    :type ethernet: dict
+    """
+    ifname = ethernet['ifname']
+    verify_interface_exists(ifname)
+    verify_eapol(ethernet)
+    verify_mirror_redirect(ethernet)
+    ethtool = Ethtool(ifname)
+    verify_speed_duplex(ethernet, ethtool)
+    verify_flow_control(ethernet, ethtool)
+    verify_ring_buffer(ethernet, ethtool)
+    verify_offload(ethernet, ethtool)
+    verify_allowedbond_changes(ethernet)
+
+def verify_ethernet(ethernet):
+    """
+     Verification function for simple ethernet interface
+    :param ethernet: dictionary which is received from get_interface_dict
+    :type ethernet: dict
+    """
+    ifname = ethernet['ifname']
+    verify_interface_exists(ifname)
+    verify_mtu(ethernet)
+    verify_mtu_ipv6(ethernet)
+    verify_dhcpv6(ethernet)
+    verify_address(ethernet)
+    verify_vrf(ethernet)
+    verify_bond_bridge_member(ethernet)
+    verify_eapol(ethernet)
+    verify_mirror_redirect(ethernet)
+    ethtool = Ethtool(ifname)
+    # No need to check speed and duplex keys as both have default values.
+    verify_speed_duplex(ethernet, ethtool)
+    verify_flow_control(ethernet, ethtool)
+    verify_ring_buffer(ethernet, ethtool)
+    verify_offload(ethernet, ethtool)
+    # use common function to verify VLAN configuration
+    verify_vlan_config(ethernet)
+    return None
+
+
+def generate(ethernet):
+    # render real configuration file once
+    wpa_supplicant_conf = wpa_suppl_conf.format(**ethernet)
+
+    if 'deleted' in ethernet:
+        # delete configuration on interface removal
+        if os.path.isfile(wpa_supplicant_conf):
+            os.unlink(wpa_supplicant_conf)
+        return None
+
+    if 'eapol' in ethernet:
+        ifname = ethernet['ifname']
+
+        render(wpa_supplicant_conf, 'ethernet/wpa_supplicant.conf.j2', ethernet)
+
+        cert_file_path = os.path.join(cfg_dir, f'{ifname}_cert.pem')
+        cert_key_path = os.path.join(cfg_dir, f'{ifname}_cert.key')
+
+        cert_name = ethernet['eapol']['certificate']
+        pki_cert = ethernet['pki']['certificate'][cert_name]
+
+        loaded_pki_cert = load_certificate(pki_cert['certificate'])
+        loaded_ca_certs = {load_certificate(c['certificate'])
+            for c in ethernet['pki']['ca'].values()} if 'ca' in ethernet['pki'] else {}
+
+        cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs)
+
+        write_file(cert_file_path,
+                   '\n'.join(encode_certificate(c) for c in cert_full_chain))
+        write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))
+
+        if 'ca_certificate' in ethernet['eapol']:
+            ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem')
+            ca_chains = []
+
+            for ca_cert_name in ethernet['eapol']['ca_certificate']:
+                pki_ca_cert = ethernet['pki']['ca'][ca_cert_name]
+                loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
+                ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
+                ca_chains.append(
+                    '\n'.join(encode_certificate(c) for c in ca_full_chain))
+
+            write_file(ca_cert_file_path, '\n'.join(ca_chains))
+
+    return None
+
+def apply(ethernet):
+    ifname = ethernet['ifname']
+    # take care about EAPoL supplicant daemon
+    eapol_action='stop'
+
+    e = EthernetIf(ifname)
+    if 'deleted' in ethernet:
+        # delete interface
+        e.remove()
+    else:
+        e.update(ethernet)
+        if 'eapol' in ethernet:
+            eapol_action='reload-or-restart'
+
+    call(f'systemctl {eapol_action} wpa_supplicant-wired@{ifname}')
+
+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/interfaces_geneve.py b/src/conf_mode/interfaces_geneve.py
new file mode 100755
index 000000000..f6694ddde
--- /dev/null
+++ b/src/conf_mode/interfaces_geneve.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2022 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/>.
+
+from sys import exit
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_address
+from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_bond_bridge_member
+from vyos.ifconfig import GeneveIf
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'geneve']
+    ifname, geneve = get_interface_dict(conf, base)
+
+    # GENEVE interfaces are picky and require recreation if certain parameters
+    # change. But a GENEVE interface should - of course - not be re-created if
+    # it's description or IP address is adjusted. Feels somehow logic doesn't it?
+    for cli_option in ['remote', 'vni', 'parameters']:
+        if is_node_changed(conf, base + [ifname, cli_option]):
+            geneve.update({'rebuild_required': {}})
+
+    return geneve
+
+def verify(geneve):
+    if 'deleted' in geneve:
+        verify_bridge_delete(geneve)
+        return None
+
+    verify_mtu_ipv6(geneve)
+    verify_address(geneve)
+    verify_bond_bridge_member(geneve)
+    verify_mirror_redirect(geneve)
+
+    if 'remote' not in geneve:
+        raise ConfigError('Remote side must be configured')
+
+    if 'vni' not in geneve:
+        raise ConfigError('VNI must be configured')
+
+    return None
+
+
+def generate(geneve):
+    return None
+
+def apply(geneve):
+    # Check if GENEVE interface already exists
+    if 'rebuild_required' in geneve or 'delete' in geneve:
+        if geneve['ifname'] in interfaces():
+            g = GeneveIf(geneve['ifname'])
+            # GENEVE is super picky and the tunnel always needs to be recreated,
+            # thus we can simply always delete it first.
+            g.remove()
+
+    if 'deleted' not in geneve:
+        # Finally create the new interface
+        g = GeneveIf(**geneve)
+        g.update(geneve)
+
+    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/interfaces_input.py b/src/conf_mode/interfaces_input.py
new file mode 100755
index 000000000..ad248843d
--- /dev/null
+++ b/src/conf_mode/interfaces_input.py
@@ -0,0 +1,70 @@
+#!/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/>.
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_mirror_redirect
+from vyos.ifconfig import InputIf
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at
+    least the interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'input']
+    _, ifb = get_interface_dict(conf, base)
+
+    return ifb
+
+def verify(ifb):
+    if 'deleted' in ifb:
+        return None
+
+    verify_mirror_redirect(ifb)
+    return None
+
+def generate(ifb):
+    return None
+
+def apply(ifb):
+    d = InputIf(ifb['ifname'])
+
+    # Remove input interface
+    if 'deleted' in ifb:
+        d.remove()
+    else:
+        d.update(ifb)
+
+    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/interfaces_l2tpv3.py b/src/conf_mode/interfaces_l2tpv3.py
new file mode 100755
index 000000000..e1db3206e
--- /dev/null
+++ b/src/conf_mode/interfaces_l2tpv3.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2020 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 sys import exit
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_bond_bridge_member
+from vyos.ifconfig import L2TPv3If
+from vyos.utils.kernel import check_kmod
+from vyos.utils.network import is_addr_assigned
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+k_mod = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6']
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'l2tpv3']
+    ifname, l2tpv3 = get_interface_dict(conf, base)
+
+    # To delete an l2tpv3 interface we need the current tunnel and session-id
+    if 'deleted' in l2tpv3:
+        tmp = leaf_node_changed(conf, base + [ifname, 'tunnel-id'])
+        # leaf_node_changed() returns a list
+        l2tpv3.update({'tunnel_id': tmp[0]})
+
+        tmp = leaf_node_changed(conf, base + [ifname, 'session-id'])
+        l2tpv3.update({'session_id': tmp[0]})
+
+    return l2tpv3
+
+def verify(l2tpv3):
+    if 'deleted' in l2tpv3:
+        verify_bridge_delete(l2tpv3)
+        return None
+
+    interface = l2tpv3['ifname']
+
+    for key in ['source_address', 'remote', 'tunnel_id', 'peer_tunnel_id',
+                'session_id', 'peer_session_id']:
+        if key not in l2tpv3:
+            tmp = key.replace('_', '-')
+            raise ConfigError(f'Missing mandatory L2TPv3 option: "{tmp}"!')
+
+    if not is_addr_assigned(l2tpv3['source_address']):
+        raise ConfigError('L2TPv3 source-address address "{source_address}" '
+                          'not configured on any interface!'.format(**l2tpv3))
+
+    verify_mtu_ipv6(l2tpv3)
+    verify_address(l2tpv3)
+    verify_bond_bridge_member(l2tpv3)
+    verify_mirror_redirect(l2tpv3)
+    return None
+
+def generate(l2tpv3):
+    return None
+
+def apply(l2tpv3):
+    # Check if L2TPv3 interface already exists
+    if l2tpv3['ifname'] in interfaces():
+        # L2TPv3 is picky when changing tunnels/sessions, thus we can simply
+        # always delete it first.
+        l = L2TPv3If(**l2tpv3)
+        l.remove()
+
+    if 'deleted' not in l2tpv3:
+        # Finally create the new interface
+        l = L2TPv3If(**l2tpv3)
+        l.update(l2tpv3)
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        check_kmod(k_mod)
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/interfaces_loopback.py b/src/conf_mode/interfaces_loopback.py
new file mode 100755
index 000000000..08d34477a
--- /dev/null
+++ b/src/conf_mode/interfaces_loopback.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2020 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 sys import exit
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_mirror_redirect
+from vyos.ifconfig import LoopbackIf
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'loopback']
+    _, loopback = get_interface_dict(conf, base)
+    return loopback
+
+def verify(loopback):
+    verify_mirror_redirect(loopback)
+    return None
+
+def generate(loopback):
+    return None
+
+def apply(loopback):
+    l = LoopbackIf(**loopback)
+    if 'deleted' in loopback:
+        l.remove()
+    else:
+        l.update(loopback)
+
+    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/interfaces_macsec.py b/src/conf_mode/interfaces_macsec.py
new file mode 100755
index 000000000..0a927ac88
--- /dev/null
+++ b/src/conf_mode/interfaces_macsec.py
@@ -0,0 +1,207 @@
+#!/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 netifaces import interfaces
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
+from vyos.configdict import is_source_interface
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_bond_bridge_member
+from vyos.ifconfig import MACsecIf
+from vyos.ifconfig import Interface
+from vyos.template import render
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
+from vyos.utils.process import is_systemd_service_running
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+# XXX: wpa_supplicant works on the source interface
+wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf'
+
+# Constants
+## gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit
+GCM_AES_128_LEN: int = 32
+GCM_128_KEY_ERROR = 'gcm-aes-128 requires a 128bit long key!'
+## gcm-aes-256 requires a 256bit long key - 64 characters (string) = 32byte = 256bit
+GCM_AES_256_LEN: int = 64
+GCM_256_KEY_ERROR = 'gcm-aes-256 requires a 256bit long key!'
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'macsec']
+    ifname, macsec = get_interface_dict(conf, base)
+
+    # Check if interface has been removed
+    if 'deleted' in macsec:
+        source_interface = conf.return_effective_value(base + [ifname, 'source-interface'])
+        macsec.update({'source_interface': source_interface})
+
+    if is_node_changed(conf, base + [ifname, 'security']):
+        macsec.update({'shutdown_required': {}})
+
+    if is_node_changed(conf, base + [ifname, 'source_interface']):
+        macsec.update({'shutdown_required': {}})
+
+    if 'source_interface' in macsec:
+        tmp = is_source_interface(conf, macsec['source_interface'], ['macsec', 'pseudo-ethernet'])
+        if tmp and tmp != ifname: macsec.update({'is_source_interface' : tmp})
+
+    return macsec
+
+
+def verify(macsec):
+    if 'deleted' in macsec:
+        verify_bridge_delete(macsec)
+        return None
+
+    verify_source_interface(macsec)
+    verify_vrf(macsec)
+    verify_mtu_ipv6(macsec)
+    verify_address(macsec)
+    verify_bond_bridge_member(macsec)
+    verify_mirror_redirect(macsec)
+
+    if dict_search('security.cipher', macsec) == None:
+        raise ConfigError('Cipher suite must be set for MACsec "{ifname}"'.format(**macsec))
+
+    if dict_search('security.encrypt', macsec) != None:
+        # Check that only static or MKA config is present
+        if dict_search('security.static', macsec) != None and (dict_search('security.mka.cak', macsec) != None or dict_search('security.mka.ckn', macsec) != None):
+            raise ConfigError('Only static or MKA can be used!')
+
+        # Logic to check static configuration
+        if dict_search('security.static', macsec) != None:
+            # tx-key must be defined
+            if dict_search('security.static.key', macsec) == None:
+                raise ConfigError('Static MACsec tx-key must be defined.')
+
+            tx_len = len(dict_search('security.static.key', macsec))
+
+            if dict_search('security.cipher', macsec) == 'gcm-aes-128' and tx_len != GCM_AES_128_LEN:
+                raise ConfigError(GCM_128_KEY_ERROR)
+
+            if dict_search('security.cipher', macsec) == 'gcm-aes-256' and tx_len != GCM_AES_256_LEN:
+                raise ConfigError(GCM_256_KEY_ERROR)
+
+            # Make sure at least one peer is defined
+            if 'peer' not in macsec['security']['static']:
+                raise ConfigError('Must have at least one peer defined for static MACsec')
+
+            # For every enabled peer, make sure a MAC and rx-key is defined
+            for peer, peer_config in macsec['security']['static']['peer'].items():
+                if 'disable' not in peer_config and ('mac' not in peer_config or 'key' not in peer_config):
+                    raise ConfigError('Every enabled MACsec static peer must have a MAC address and rx-key defined.')
+
+                # check rx-key length against cipher suite
+                rx_len = len(peer_config['key'])
+
+                if dict_search('security.cipher', macsec) == 'gcm-aes-128' and rx_len != GCM_AES_128_LEN:
+                    raise ConfigError(GCM_128_KEY_ERROR)
+
+                if dict_search('security.cipher', macsec) == 'gcm-aes-256' and rx_len != GCM_AES_256_LEN:
+                    raise ConfigError(GCM_256_KEY_ERROR)
+
+        # Logic to check MKA configuration
+        else:
+            if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None:
+                raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!')
+
+            cak_len = len(dict_search('security.mka.cak', macsec))
+
+            if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != GCM_AES_128_LEN:
+                raise ConfigError(GCM_128_KEY_ERROR)
+
+            elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != GCM_AES_256_LEN:
+                raise ConfigError(GCM_256_KEY_ERROR)
+
+    if 'source_interface' in macsec:
+        # MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad
+        # and 802.1q) - we need to check the underlaying MTU if our configured
+        # MTU is at least 40 bytes less then the MTU of our physical interface.
+        lower_mtu = Interface(macsec['source_interface']).get_mtu()
+        if lower_mtu < (int(macsec['mtu']) + 40):
+            raise ConfigError('MACsec overhead does not fit into underlaying device MTU,\n' \
+                              f'{lower_mtu} bytes is too small!')
+
+    return None
+
+
+def generate(macsec):
+    # Only generate wpa_supplicant config if using MKA
+    if dict_search('security.mka.cak', macsec):
+        render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec)
+    return None
+
+
+def apply(macsec):
+    systemd_service = 'wpa_supplicant-macsec@{source_interface}'.format(**macsec)
+
+    # Remove macsec interface on deletion or mandatory parameter change
+    if 'deleted' in macsec or 'shutdown_required' in macsec:
+        call(f'systemctl stop {systemd_service}')
+
+        if macsec['ifname'] in interfaces():
+            tmp = MACsecIf(macsec['ifname'])
+            tmp.remove()
+
+        if 'deleted' in macsec:
+            # delete configuration on interface removal
+            if os.path.isfile(wpa_suppl_conf.format(**macsec)):
+                os.unlink(wpa_suppl_conf.format(**macsec))
+
+            return None
+
+    # It is safe to "re-create" the interface always, there is a sanity
+    # check that the interface will only be create if its non existent
+    i = MACsecIf(**macsec)
+    i.update(macsec)
+
+    # Only reload/restart if using MKA
+    if dict_search('security.mka.cak', macsec):
+        if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec:
+            call(f'systemctl reload-or-restart {systemd_service}')
+
+    return None
+
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py
new file mode 100755
index 000000000..bdeb44837
--- /dev/null
+++ b/src/conf_mode/interfaces_openvpn.py
@@ -0,0 +1,732 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-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 re
+import tempfile
+
+from cryptography.hazmat.primitives.asymmetric import ec
+from glob import glob
+from sys import exit
+from ipaddress import IPv4Address
+from ipaddress import IPv4Network
+from ipaddress import IPv6Address
+from ipaddress import IPv6Network
+from ipaddress import summarize_address_range
+from netifaces import interfaces
+from secrets import SystemRandom
+from shutil import rmtree
+
+from vyos.base import DeprecationWarning
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_bond_bridge_member
+from vyos.ifconfig import VTunIf
+from vyos.pki import load_dh_parameters
+from vyos.pki import load_private_key
+from vyos.pki import sort_ca_chain
+from vyos.pki import verify_ca_chain
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_crl
+from vyos.pki import wrap_dh_parameters
+from vyos.pki import wrap_openvpn_key
+from vyos.pki import wrap_private_key
+from vyos.template import render
+from vyos.template import is_ipv4
+from vyos.template import is_ipv6
+from vyos.utils.dict import dict_search
+from vyos.utils.dict import dict_search_args
+from vyos.utils.list import is_list_equal
+from vyos.utils.file import makedir
+from vyos.utils.file import read_file
+from vyos.utils.file import write_file
+from vyos.utils.kernel import check_kmod
+from vyos.utils.kernel import unload_kmod
+from vyos.utils.process import call
+from vyos.utils.permission import chown
+from vyos.utils.process import cmd
+from vyos.utils.network import is_addr_assigned
+
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+user = 'openvpn'
+group = 'openvpn'
+
+cfg_dir = '/run/openvpn'
+cfg_file = '/run/openvpn/{ifname}.conf'
+otp_path = '/config/auth/openvpn'
+otp_file = '/config/auth/openvpn/{ifname}-otp-secrets'
+secret_chars = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')
+service_file = '/run/systemd/system/openvpn@{ifname}.service.d/20-override.conf'
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'openvpn']
+
+    ifname, openvpn = get_interface_dict(conf, base)
+    openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
+
+    if 'deleted' in openvpn:
+        return openvpn
+
+    openvpn['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+                                        get_first_key=True,
+                                        no_tag_node_value_mangle=True)
+
+    if is_node_changed(conf, base + [ifname, 'openvpn-option']):
+        openvpn.update({'restart_required': {}})
+    if is_node_changed(conf, base + [ifname, 'enable-dco']):
+        openvpn.update({'restart_required': {}})
+
+    # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict'
+    # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there.
+    tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True)
+
+    # We have to cleanup the config dict, as default values could enable features
+    # which are not explicitly enabled on the CLI. Example: server mfa totp
+    # originate comes with defaults, which will enable the
+    # totp plugin, even when not set via CLI so we
+    # need to check this first and drop those keys
+    if dict_search('server.mfa.totp', tmp) == None:
+        del openvpn['server']['mfa']
+
+    # OpenVPN Data-Channel-Offload (DCO) is a Kernel module. If loaded it applies to all
+    # OpenVPN interfaces. Check if DCO is used by any other interface instance.
+    tmp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+    for interface, interface_config in tmp.items():
+        # If one interface has DCO configured, enable it. No need to further check
+        # all other OpenVPN interfaces. We must use a dedicated key to indicate
+        # the Kernel module must be loaded or not. The per interface "offload.dco"
+        # key is required per OpenVPN interface instance.
+        if dict_search('offload.dco', interface_config) != None:
+            openvpn['module_load_dco'] = {}
+            break
+
+    return openvpn
+
+def is_ec_private_key(pki, cert_name):
+    if not pki or 'certificate' not in pki:
+        return False
+    if cert_name not in pki['certificate']:
+        return False
+
+    pki_cert = pki['certificate'][cert_name]
+    if 'private' not in pki_cert or 'key' not in pki_cert['private']:
+        return False
+
+    key = load_private_key(pki_cert['private']['key'])
+    return isinstance(key, ec.EllipticCurvePrivateKey)
+
+def verify_pki(openvpn):
+    pki = openvpn['pki']
+    interface = openvpn['ifname']
+    mode = openvpn['mode']
+    shared_secret_key = dict_search_args(openvpn, 'shared_secret_key')
+    tls = dict_search_args(openvpn, 'tls')
+
+    if not bool(shared_secret_key) ^ bool(tls): #  xor check if only one is set
+        raise ConfigError('Must specify only one of "shared-secret-key" and "tls"')
+
+    if mode in ['server', 'client'] and not tls:
+        raise ConfigError('Must specify "tls" for server and client modes')
+
+    if not pki:
+        raise ConfigError('PKI is not configured')
+
+    if shared_secret_key:
+        if not dict_search_args(pki, 'openvpn', 'shared_secret'):
+            raise ConfigError('There are no openvpn shared-secrets in PKI configuration')
+
+        if shared_secret_key not in pki['openvpn']['shared_secret']:
+            raise ConfigError(f'Invalid shared-secret on openvpn interface {interface}')
+
+        # If PSK settings are correct, warn about its deprecation
+        DeprecationWarning("OpenVPN shared-secret support will be removed in future VyOS versions.\n\
+        Please migrate your site-to-site tunnels to TLS.\n\
+        You can use self-signed certificates with peer fingerprint verification, consult the documentation for details.")
+
+    if tls:
+        if (mode in ['server', 'client']) and ('ca_certificate' not in tls):
+            raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface},\
+              it is required in server and client modes')
+        else:
+            if ('ca_certificate' not in tls) and ('peer_fingerprint' not in tls):
+                raise ConfigError('Either "tls ca-certificate" or "tls peer-fingerprint" is required\
+                  on openvpn interface {interface} in site-to-site mode')
+
+        if 'ca_certificate' in tls:
+            for ca_name in tls['ca_certificate']:
+                if ca_name not in pki['ca']:
+                    raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}')
+
+            if len(tls['ca_certificate']) > 1:
+                sorted_chain = sort_ca_chain(tls['ca_certificate'], pki['ca'])
+                if not verify_ca_chain(sorted_chain, pki['ca']):
+                    raise ConfigError(f'CA certificates are not a valid chain')
+
+        if mode != 'client' and 'auth_key' not in tls:
+            if 'certificate' not in tls:
+                raise ConfigError(f'Missing "tls certificate" on openvpn interface {interface}')
+
+        if 'certificate' in tls:
+            if tls['certificate'] not in pki['certificate']:
+                raise ConfigError(f'Invalid certificate on openvpn interface {interface}')
+
+            if dict_search_args(pki, 'certificate', tls['certificate'], 'private', 'password_protected') is not None:
+                raise ConfigError(f'Cannot use encrypted private key on openvpn interface {interface}')
+
+        if 'dh_params' in tls:
+            pki_dh = pki['dh'][tls['dh_params']]
+            dh_params = load_dh_parameters(pki_dh['parameters'])
+            dh_numbers = dh_params.parameter_numbers()
+            dh_bits = dh_numbers.p.bit_length()
+
+            if dh_bits < 2048:
+                raise ConfigError(f'Minimum DH key-size is 2048 bits')
+
+
+        if 'auth_key' in tls or 'crypt_key' in tls:
+            if not dict_search_args(pki, 'openvpn', 'shared_secret'):
+                raise ConfigError('There are no openvpn shared-secrets in PKI configuration')
+
+        if 'auth_key' in tls:
+            if tls['auth_key'] not in pki['openvpn']['shared_secret']:
+                raise ConfigError(f'Invalid auth-key on openvpn interface {interface}')
+
+        if 'crypt_key' in tls:
+            if tls['crypt_key'] not in pki['openvpn']['shared_secret']:
+                raise ConfigError(f'Invalid crypt-key on openvpn interface {interface}')
+
+def verify(openvpn):
+    if 'deleted' in openvpn:
+        # remove totp secrets file if totp is not configured
+        if os.path.isfile(otp_file.format(**openvpn)):
+            os.remove(otp_file.format(**openvpn))
+
+        verify_bridge_delete(openvpn)
+        return None
+
+    if 'mode' not in openvpn:
+        raise ConfigError('Must specify OpenVPN operation mode!')
+
+    #
+    # OpenVPN client mode - VERIFY
+    #
+    if openvpn['mode'] == 'client':
+        if 'local_port' in openvpn:
+            raise ConfigError('Cannot specify "local-port" in client mode')
+
+        if 'local_host' in openvpn:
+            raise ConfigError('Cannot specify "local-host" in client mode')
+
+        if 'remote_host' not in openvpn:
+            raise ConfigError('Must specify "remote-host" in client mode')
+
+        if openvpn['protocol'] == 'tcp-passive':
+            raise ConfigError('Protocol "tcp-passive" is not valid in client mode')
+
+        if dict_search('tls.dh_params', openvpn):
+            raise ConfigError('Cannot specify "tls dh-params" in client mode')
+
+    #
+    # OpenVPN site-to-site - VERIFY
+    #
+    elif openvpn['mode'] == 'site-to-site':
+        if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn:
+            raise ConfigError('Must specify "local-address" or add interface to bridge')
+
+        if 'local_address' in openvpn:
+            if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1:
+                raise ConfigError('Only one IPv4 local-address can be specified')
+
+            if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1:
+                raise ConfigError('Only one IPv6 local-address can be specified')
+
+        if openvpn['device_type'] == 'tun':
+            if 'remote_address' not in openvpn:
+                raise ConfigError('Must specify "remote-address"')
+
+        if 'remote_address' in openvpn:
+            if len([addr for addr in openvpn['remote_address'] if is_ipv4(addr)]) > 1:
+                raise ConfigError('Only one IPv4 remote-address can be specified')
+
+            if len([addr for addr in openvpn['remote_address'] if is_ipv6(addr)]) > 1:
+                raise ConfigError('Only one IPv6 remote-address can be specified')
+
+            if not 'local_address' in openvpn:
+                raise ConfigError('"remote-address" requires "local-address"')
+
+            v4loAddr = [addr for addr in openvpn['local_address'] if is_ipv4(addr)]
+            v4remAddr = [addr for addr in openvpn['remote_address'] if is_ipv4(addr)]
+            if v4loAddr and not v4remAddr:
+                raise ConfigError('IPv4 "local-address" requires IPv4 "remote-address"')
+            elif v4remAddr and not v4loAddr:
+                raise ConfigError('IPv4 "remote-address" requires IPv4 "local-address"')
+
+            v6remAddr = [addr for addr in openvpn['remote_address'] if is_ipv6(addr)]
+            v6loAddr = [addr for addr in openvpn['local_address'] if is_ipv6(addr)]
+            if v6loAddr and not v6remAddr:
+                raise ConfigError('IPv6 "local-address" requires IPv6 "remote-address"')
+            elif v6remAddr and not v6loAddr:
+                raise ConfigError('IPv6 "remote-address" requires IPv6 "local-address"')
+
+            if is_list_equal(v4loAddr, v4remAddr) or is_list_equal(v6loAddr, v6remAddr):
+                raise ConfigError('"local-address" and "remote-address" cannot be the same')
+
+            if dict_search('local_host', openvpn) in dict_search('local_address', openvpn):
+                raise ConfigError('"local-address" cannot be the same as "local-host"')
+
+            if dict_search('remote_host', openvpn) in dict_search('remote_address', openvpn):
+                raise ConfigError('"remote-address" and "remote-host" can not be the same')
+
+        if openvpn['device_type'] == 'tap' and 'local_address' in openvpn:
+            # we can only have one local_address, this is ensured above
+            v4addr = None
+            for laddr in openvpn['local_address']:
+                if is_ipv4(laddr):
+                    v4addr = laddr
+                    break
+
+            if v4addr in openvpn['local_address'] and 'subnet_mask' not in openvpn['local_address'][v4addr]:
+                raise ConfigError('Must specify IPv4 "subnet-mask" for local-address')
+
+        if dict_search('encryption.ncp_ciphers', openvpn):
+            raise ConfigError('NCP ciphers can only be used in client or server mode')
+
+    else:
+        # checks for client-server or site-to-site bridged
+        if 'local_address' in openvpn or 'remote_address' in openvpn:
+            raise ConfigError('Cannot specify "local-address" or "remote-address" ' \
+                              'in client/server or bridge mode')
+
+    #
+    # OpenVPN server mode - VERIFY
+    #
+    if openvpn['mode'] == 'server':
+        if openvpn['protocol'] == 'tcp-active':
+            raise ConfigError('Protocol "tcp-active" is not valid in server mode')
+
+        if dict_search('authentication.username', openvpn) or dict_search('authentication.password', openvpn):
+            raise ConfigError('Cannot specify "authentication" in server mode')
+
+        if 'remote_port' in openvpn:
+            raise ConfigError('Cannot specify "remote-port" in server mode')
+
+        if 'remote_host' in openvpn:
+            raise ConfigError('Cannot specify "remote-host" in server mode')
+
+        tmp = dict_search('server.subnet', openvpn)
+        if tmp:
+            v4_subnets = len([subnet for subnet in tmp if is_ipv4(subnet)])
+            v6_subnets = len([subnet for subnet in tmp if is_ipv6(subnet)])
+            if v4_subnets > 1:
+                raise ConfigError('Cannot specify more than 1 IPv4 server subnet')
+            if v6_subnets > 1:
+                raise ConfigError('Cannot specify more than 1 IPv6 server subnet')
+
+            for subnet in tmp:
+                if is_ipv4(subnet):
+                    subnet = IPv4Network(subnet)
+
+                    if openvpn['device_type'] == 'tun' and subnet.prefixlen > 29:
+                        raise ConfigError('Server subnets smaller than /29 with device type "tun" are not supported')
+                    elif openvpn['device_type'] == 'tap' and subnet.prefixlen > 30:
+                        raise ConfigError('Server subnets smaller than /30 with device type "tap" are not supported')
+
+                    for client in (dict_search('client', openvpn) or []):
+                        if client['ip'] and not IPv4Address(client['ip'][0]) in subnet:
+                            raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}')
+
+        else:
+            if 'is_bridge_member' not in openvpn:
+                raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode')
+
+        if hasattr(dict_search('server.client', openvpn), '__iter__'):
+            for client_k, client_v in dict_search('server.client', openvpn).items():
+                if (client_v.get('ip') and len(client_v['ip']) > 1) or (client_v.get('ipv6_ip') and len(client_v['ipv6_ip']) > 1):
+                    raise ConfigError(f'Server client "{client_k}": cannot specify more than 1 IPv4 and 1 IPv6 IP')
+
+        if dict_search('server.client_ip_pool', openvpn):
+            if not (dict_search('server.client_ip_pool.start', openvpn) and dict_search('server.client_ip_pool.stop', openvpn)):
+                raise ConfigError('Server client-ip-pool requires both start and stop addresses')
+            else:
+                v4PoolStart = IPv4Address(dict_search('server.client_ip_pool.start', openvpn))
+                v4PoolStop = IPv4Address(dict_search('server.client_ip_pool.stop', openvpn))
+                if v4PoolStart > v4PoolStop:
+                    raise ConfigError(f'Server client-ip-pool start address {v4PoolStart} is larger than stop address {v4PoolStop}')
+
+                v4PoolSize = int(v4PoolStop) - int(v4PoolStart)
+                if v4PoolSize >= 65536:
+                    raise ConfigError(f'Server client-ip-pool is too large [{v4PoolStart} -> {v4PoolStop} = {v4PoolSize}], maximum is 65536 addresses.')
+
+                v4PoolNets = list(summarize_address_range(v4PoolStart, v4PoolStop))
+                for client in (dict_search('client', openvpn) or []):
+                    if client['ip']:
+                        for v4PoolNet in v4PoolNets:
+                            if IPv4Address(client['ip'][0]) in v4PoolNet:
+                                print(f'Warning: Client "{client["name"]}" IP {client["ip"][0]} is in server IP pool, it is not reserved for this client.')
+            # configuring a client_ip_pool will set 'server ... nopool' which is currently incompatible with 'server-ipv6' (probably to be fixed upstream)
+            for subnet in (dict_search('server.subnet', openvpn) or []):
+                if is_ipv6(subnet):
+                    raise ConfigError(f'Setting client-ip-pool is incompatible having an IPv6 server subnet.')
+
+        for subnet in (dict_search('server.subnet', openvpn) or []):
+            if is_ipv6(subnet):
+                tmp = dict_search('client_ipv6_pool.base', openvpn)
+                if tmp:
+                    if not dict_search('server.client_ip_pool', openvpn):
+                        raise ConfigError('IPv6 server pool requires an IPv4 server pool')
+
+                    if int(tmp.split('/')[1]) >= 112:
+                        raise ConfigError('IPv6 server pool must be larger than /112')
+
+                    #
+                    # todo - weird logic
+                    #
+                    v6PoolStart = IPv6Address(tmp)
+                    v6PoolStop = IPv6Network((v6PoolStart, openvpn['server_ipv6_pool_prefixlen']), strict=False)[-1] # don't remove the parentheses, it's a 2-tuple
+                    v6PoolSize = int(v6PoolStop) - int(v6PoolStart) if int(openvpn['server_ipv6_pool_prefixlen']) > 96 else 65536
+                    if v6PoolSize < v4PoolSize:
+                        raise ConfigError(f'IPv6 server pool must be at least as large as the IPv4 pool (current sizes: IPv6={v6PoolSize} IPv4={v4PoolSize})')
+
+                    v6PoolNets = list(summarize_address_range(v6PoolStart, v6PoolStop))
+                    for client in (dict_search('client', openvpn) or []):
+                        if client['ipv6_ip']:
+                            for v6PoolNet in v6PoolNets:
+                                if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet:
+                                    print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.')
+
+        # add mfa users to the file the mfa plugin uses
+        if dict_search('server.mfa.totp', openvpn):
+            user_data = ''
+            if not os.path.isfile(otp_file.format(**openvpn)):
+                write_file(otp_file.format(**openvpn), user_data,
+                           user=user, group=group, mode=0o644)
+
+            ovpn_users = read_file(otp_file.format(**openvpn))
+            for client in (dict_search('server.client', openvpn) or []):
+                exists = None
+                for ovpn_user in ovpn_users.split('\n'):
+                    if re.search('^' + client + ' ', ovpn_user):
+                        user_data += f'{ovpn_user}\n'
+                        exists = 'true'
+
+                if not exists:
+                    random = SystemRandom()
+                    totp_secret = ''.join(random.choice(secret_chars) for _ in range(16))
+                    user_data += f'{client} otp totp:sha1:base32:{totp_secret}::xxx *\n'
+
+            write_file(otp_file.format(**openvpn), user_data,
+                           user=user, group=group, mode=0o644)
+
+    else:
+        # checks for both client and site-to-site go here
+        if dict_search('server.reject_unconfigured_clients', openvpn):
+            raise ConfigError('Option reject-unconfigured-clients only supported in server mode')
+
+        if 'replace_default_route' in openvpn and 'remote_host' not in openvpn:
+            raise ConfigError('Cannot set "replace-default-route" without "remote-host"')
+
+    #
+    # OpenVPN common verification section
+    # not depending on any operation mode
+    #
+
+    # verify specified IP address is present on any interface on this system
+    if 'local_host' in openvpn:
+        if not is_addr_assigned(openvpn['local_host']):
+            print('local-host IP address "{local_host}" not assigned' \
+                  ' to any interface'.format(**openvpn))
+
+    # TCP active
+    if openvpn['protocol'] == 'tcp-active':
+        if 'local_port' in openvpn:
+            raise ConfigError('Cannot specify "local-port" with "tcp-active"')
+
+        if 'remote_host' not in openvpn:
+            raise ConfigError('Must specify "remote-host" with "tcp-active"')
+
+    #
+    # TLS/encryption
+    #
+    if 'shared_secret_key' in openvpn:
+        if dict_search('encryption.cipher', openvpn) in ['aes128gcm', 'aes192gcm', 'aes256gcm']:
+            raise ConfigError('GCM encryption with shared-secret-key not supported')
+
+    if 'tls' in openvpn:
+        if {'auth_key', 'crypt_key'} <= set(openvpn['tls']):
+            raise ConfigError('TLS auth and crypt keys are mutually exclusive')
+
+        tmp = dict_search('tls.role', openvpn)
+        if tmp:
+            if openvpn['mode'] in ['client', 'server']:
+                if not dict_search('tls.auth_key', openvpn):
+                    raise ConfigError('Cannot specify "tls role" in client-server mode')
+
+            if tmp == 'active':
+                if openvpn['protocol'] == 'tcp-passive':
+                    raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"')
+
+                if dict_search('tls.dh_params', openvpn):
+                    raise ConfigError('Cannot specify "tls dh-params" when "tls role" is "active"')
+
+            elif tmp == 'passive':
+                if openvpn['protocol'] == 'tcp-active':
+                    raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"')
+
+        if 'certificate' in openvpn['tls'] and is_ec_private_key(openvpn['pki'], openvpn['tls']['certificate']):
+            if 'dh_params' in openvpn['tls']:
+                print('Warning: using dh-params and EC keys simultaneously will ' \
+                      'lead to DH ciphers being used instead of ECDH')
+
+    if dict_search('encryption.cipher', openvpn) == 'none':
+        print('Warning: "encryption none" was specified!')
+        print('No encryption will be performed and data is transmitted in ' \
+              'plain text over the network!')
+
+    verify_pki(openvpn)
+
+    #
+    # Auth user/pass
+    #
+    if (dict_search('authentication.username', openvpn) and not
+        dict_search('authentication.password', openvpn)):
+            raise ConfigError('Password for authentication is missing')
+
+    if (dict_search('authentication.password', openvpn) and not
+        dict_search('authentication.username', openvpn)):
+            raise ConfigError('Username for authentication is missing')
+
+    verify_vrf(openvpn)
+    verify_bond_bridge_member(openvpn)
+    verify_mirror_redirect(openvpn)
+
+    return None
+
+def generate_pki_files(openvpn):
+    pki = openvpn['pki']
+    if not pki:
+        return None
+
+    interface = openvpn['ifname']
+    shared_secret_key = dict_search_args(openvpn, 'shared_secret_key')
+    tls = dict_search_args(openvpn, 'tls')
+
+    if shared_secret_key:
+        pki_key = pki['openvpn']['shared_secret'][shared_secret_key]
+        key_path = os.path.join(cfg_dir, f'{interface}_shared.key')
+        write_file(key_path, wrap_openvpn_key(pki_key['key']),
+                   user=user, group=group)
+
+    if tls:
+        if 'ca_certificate' in tls:
+            cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem')
+            crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem')
+
+            if os.path.exists(cert_path):
+                os.unlink(cert_path)
+
+            if os.path.exists(crl_path):
+                os.unlink(crl_path)
+
+            for cert_name in sort_ca_chain(tls['ca_certificate'], pki['ca']):
+                pki_ca = pki['ca'][cert_name]
+
+                if 'certificate' in pki_ca:
+                    write_file(cert_path, wrap_certificate(pki_ca['certificate']) + "\n",
+                               user=user, group=group, mode=0o600, append=True)
+
+                if 'crl' in pki_ca:
+                    for crl in pki_ca['crl']:
+                        write_file(crl_path, wrap_crl(crl) + "\n", user=user, group=group,
+                                   mode=0o600, append=True)
+
+                    openvpn['tls']['crl'] = True
+
+        if 'certificate' in tls:
+            cert_name = tls['certificate']
+            pki_cert = pki['certificate'][cert_name]
+
+            if 'certificate' in pki_cert:
+                cert_path = os.path.join(cfg_dir, f'{interface}_cert.pem')
+                write_file(cert_path, wrap_certificate(pki_cert['certificate']),
+                           user=user, group=group, mode=0o600)
+
+            if 'private' in pki_cert and 'key' in pki_cert['private']:
+                key_path = os.path.join(cfg_dir, f'{interface}_cert.key')
+                write_file(key_path, wrap_private_key(pki_cert['private']['key']),
+                           user=user, group=group, mode=0o600)
+
+                openvpn['tls']['private_key'] = True
+
+        if 'dh_params' in tls:
+            dh_name = tls['dh_params']
+            pki_dh = pki['dh'][dh_name]
+
+            if 'parameters' in pki_dh:
+                dh_path = os.path.join(cfg_dir, f'{interface}_dh.pem')
+                write_file(dh_path, wrap_dh_parameters(pki_dh['parameters']),
+                           user=user, group=group, mode=0o600)
+
+        if 'auth_key' in tls:
+            key_name = tls['auth_key']
+            pki_key = pki['openvpn']['shared_secret'][key_name]
+
+            if 'key' in pki_key:
+                key_path = os.path.join(cfg_dir, f'{interface}_auth.key')
+                write_file(key_path, wrap_openvpn_key(pki_key['key']),
+                           user=user, group=group, mode=0o600)
+
+        if 'crypt_key' in tls:
+            key_name = tls['crypt_key']
+            pki_key = pki['openvpn']['shared_secret'][key_name]
+
+            if 'key' in pki_key:
+                key_path = os.path.join(cfg_dir, f'{interface}_crypt.key')
+                write_file(key_path, wrap_openvpn_key(pki_key['key']),
+                           user=user, group=group, mode=0o600)
+
+
+def generate(openvpn):
+    interface = openvpn['ifname']
+    directory = os.path.dirname(cfg_file.format(**openvpn))
+    openvpn['plugin_dir'] = '/usr/lib/openvpn'
+    # create base config directory on demand
+    makedir(directory, user, group)
+    # enforce proper permissions on /run/openvpn
+    chown(directory, user, group)
+
+    # we can't know in advance which clients have been removed,
+    # thus all client configs will be removed and re-added on demand
+    ccd_dir = os.path.join(directory, 'ccd', interface)
+    if os.path.isdir(ccd_dir):
+        rmtree(ccd_dir, ignore_errors=True)
+
+    # Remove systemd directories with overrides
+    service_dir = os.path.dirname(service_file.format(**openvpn))
+    if os.path.isdir(service_dir):
+        rmtree(service_dir, ignore_errors=True)
+
+    if 'deleted' in openvpn or 'disable' in openvpn:
+        return None
+
+    # create client config directory on demand
+    makedir(ccd_dir, user, group)
+
+    # Fix file permissons for keys
+    generate_pki_files(openvpn)
+
+    # Generate User/Password authentication file
+    if 'authentication' in openvpn:
+        render(openvpn['auth_user_pass_file'], 'openvpn/auth.pw.j2', openvpn,
+               user=user, group=group, permission=0o600)
+    else:
+        # delete old auth file if present
+        if os.path.isfile(openvpn['auth_user_pass_file']):
+            os.remove(openvpn['auth_user_pass_file'])
+
+    # Generate client specific configuration
+    server_client = dict_search_args(openvpn, 'server', 'client')
+    if server_client:
+        for client, client_config in server_client.items():
+            client_file = os.path.join(ccd_dir, client)
+
+            # Our client need's to know its subnet mask ...
+            client_config['server_subnet'] = dict_search('server.subnet', openvpn)
+
+            render(client_file, 'openvpn/client.conf.j2', client_config,
+                   user=user, group=group)
+
+    # we need to support quoting of raw parameters from OpenVPN CLI
+    # see https://vyos.dev/T1632
+    render(cfg_file.format(**openvpn), 'openvpn/server.conf.j2', openvpn,
+           formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
+
+    # Render 20-override.conf for OpenVPN service
+    render(service_file.format(**openvpn), 'openvpn/service-override.conf.j2', openvpn,
+           formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
+    # Reload systemd services config to apply an override
+    call(f'systemctl daemon-reload')
+
+    return None
+
+def apply(openvpn):
+    interface = openvpn['ifname']
+
+    # Do some cleanup when OpenVPN is disabled/deleted
+    if 'deleted' in openvpn or 'disable' in openvpn:
+        call(f'systemctl stop openvpn@{interface}.service')
+        for cleanup_file in glob(f'/run/openvpn/{interface}.*'):
+            if os.path.isfile(cleanup_file):
+                os.unlink(cleanup_file)
+
+        if interface in interfaces():
+            VTunIf(interface).remove()
+
+    # dynamically load/unload DCO Kernel extension if requested
+    dco_module = 'ovpn_dco_v2'
+    if 'module_load_dco' in openvpn:
+        check_kmod(dco_module)
+    else:
+        unload_kmod(dco_module)
+
+    # Now bail out early if interface is disabled or got deleted
+    if 'deleted' in openvpn or 'disable' in openvpn:
+        return None
+
+    # verify specified IP address is present on any interface on this system
+    # Allow to bind service to nonlocal address, if it virtaual-vrrp address
+    # or if address will be assign later
+    if 'local_host' in openvpn:
+        if not is_addr_assigned(openvpn['local_host']):
+            cmd('sysctl -w net.ipv4.ip_nonlocal_bind=1')
+
+    # No matching OpenVPN process running - maybe it got killed or none
+    # existed - nevertheless, spawn new OpenVPN process
+    action = 'reload-or-restart'
+    if 'restart_required' in openvpn:
+        action = 'restart'
+    call(f'systemctl {action} openvpn@{interface}.service')
+
+    o = VTunIf(**openvpn)
+    o.update(openvpn)
+
+    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/interfaces_pppoe.py b/src/conf_mode/interfaces_pppoe.py
new file mode 100755
index 000000000..42f084309
--- /dev/null
+++ b/src/conf_mode/interfaces_pppoe.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-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
+
+from sys import exit
+from copy import deepcopy
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
+from vyos.configdict import get_pppoe_interfaces
+from vyos.configverify import verify_authentication
+from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
+from vyos.ifconfig import PPPoEIf
+from vyos.template import render
+from vyos.utils.process import call
+from vyos.utils.process import is_systemd_service_running
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'pppoe']
+    ifname, pppoe = get_interface_dict(conf, base)
+
+    # We should only terminate the PPPoE session if critical parameters change.
+    # All parameters that can be changed on-the-fly (like interface description)
+    # should not lead to a reconnect!
+    for options in ['access-concentrator', 'connect-on-demand', 'service-name',
+                    'source-interface', 'vrf', 'no-default-route',
+                    'authentication', 'host_uniq']:
+        if is_node_changed(conf, base + [ifname, options]):
+            pppoe.update({'shutdown_required': {}})
+            # bail out early - no need to further process other nodes
+            break
+
+    if 'deleted' not in pppoe:
+        # We always set the MRU value to the MTU size. This code path only re-creates
+        # the old behavior if MRU is not set on the CLI.
+        if 'mru' not in pppoe:
+            pppoe['mru'] = pppoe['mtu']
+
+    return pppoe
+
+def verify(pppoe):
+    if 'deleted' in pppoe:
+        # bail out early
+        return None
+
+    verify_source_interface(pppoe)
+    verify_authentication(pppoe)
+    verify_vrf(pppoe)
+    verify_mtu_ipv6(pppoe)
+    verify_mirror_redirect(pppoe)
+
+    if {'connect_on_demand', 'vrf'} <= set(pppoe):
+        raise ConfigError('On-demand dialing and VRF can not be used at the same time')
+
+    # both MTU and MRU have default values, thus we do not need to check
+    # if the key exists
+    if int(pppoe['mru']) > int(pppoe['mtu']):
+        raise ConfigError('PPPoE MRU needs to be lower then MTU!')
+
+    return None
+
+def generate(pppoe):
+    # set up configuration file path variables where our templates will be
+    # rendered into
+    ifname = pppoe['ifname']
+    config_pppoe = f'/etc/ppp/peers/{ifname}'
+
+    if 'deleted' in pppoe or 'disable' in pppoe:
+        if os.path.exists(config_pppoe):
+            os.unlink(config_pppoe)
+
+        return None
+
+    # Create PPP configuration files
+    render(config_pppoe, 'pppoe/peer.j2', pppoe, permission=0o640)
+
+    return None
+
+def apply(pppoe):
+    ifname = pppoe['ifname']
+    if 'deleted' in pppoe or 'disable' in pppoe:
+        if os.path.isdir(f'/sys/class/net/{ifname}'):
+            p = PPPoEIf(ifname)
+            p.remove()
+        call(f'systemctl stop ppp@{ifname}.service')
+        return None
+
+    # reconnect should only be necessary when certain config options change,
+    # like ACS name, authentication ... (see get_config() for details)
+    if ((not is_systemd_service_running(f'ppp@{ifname}.service')) or
+        'shutdown_required' in pppoe):
+
+        # cleanup system (e.g. FRR routes first)
+        if os.path.isdir(f'/sys/class/net/{ifname}'):
+            p = PPPoEIf(ifname)
+            p.remove()
+
+        call(f'systemctl restart ppp@{ifname}.service')
+        # When interface comes "live" a hook is called:
+        # /etc/ppp/ip-up.d/99-vyos-pppoe-callback
+        # which triggers PPPoEIf.update()
+    else:
+        if os.path.isdir(f'/sys/class/net/{ifname}'):
+            p = PPPoEIf(ifname)
+            p.update(pppoe)
+
+    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/interfaces_pseudo-ethernet.py b/src/conf_mode/interfaces_pseudo-ethernet.py
new file mode 100755
index 000000000..dce5c2358
--- /dev/null
+++ b/src/conf_mode/interfaces_pseudo-ethernet.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2022 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/>.
+
+from sys import exit
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
+from vyos.configdict import is_source_interface
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_vlan_config
+from vyos.configverify import verify_mtu_parent
+from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_bond_bridge_member
+from vyos.ifconfig import MACVLANIf
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at
+    least the interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'pseudo-ethernet']
+    ifname, peth = get_interface_dict(conf, base)
+
+    mode = is_node_changed(conf, ['mode'])
+    if mode: peth.update({'shutdown_required' : {}})
+
+    if is_node_changed(conf, base + [ifname, 'mode']):
+        peth.update({'rebuild_required': {}})
+
+    if 'source_interface' in peth:
+        _, peth['parent'] = get_interface_dict(conf, ['interfaces', 'ethernet'],
+                                               peth['source_interface'])
+        # test if source-interface is maybe already used by another interface
+        tmp = is_source_interface(conf, peth['source_interface'], ['macsec'])
+        if tmp and tmp != ifname: peth.update({'is_source_interface' : tmp})
+
+    return peth
+
+def verify(peth):
+    if 'deleted' in peth:
+        verify_bridge_delete(peth)
+        return None
+
+    verify_source_interface(peth)
+    verify_vrf(peth)
+    verify_address(peth)
+    verify_mtu_parent(peth, peth['parent'])
+    verify_mirror_redirect(peth)
+    # use common function to verify VLAN configuration
+    verify_vlan_config(peth)
+
+    return None
+
+def generate(peth):
+    return None
+
+def apply(peth):
+    # Check if the MACVLAN interface already exists
+    if 'rebuild_required' in peth or 'deleted' in peth:
+        if peth['ifname'] in interfaces():
+            p = MACVLANIf(peth['ifname'])
+            # MACVLAN is always needs to be recreated,
+            # thus we can simply always delete it first.
+            p.remove()
+
+    if 'deleted' not in peth:
+        p = MACVLANIf(**peth)
+        p.update(peth)
+
+    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/interfaces_sstpc.py b/src/conf_mode/interfaces_sstpc.py
new file mode 100755
index 000000000..b588910dc
--- /dev/null
+++ b/src/conf_mode/interfaces_sstpc.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 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 sys import exit
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_authentication
+from vyos.configverify import verify_vrf
+from vyos.ifconfig import SSTPCIf
+from vyos.pki import encode_certificate
+from vyos.pki import find_chain
+from vyos.pki import load_certificate
+from vyos.template import render
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
+from vyos.utils.process import is_systemd_service_running
+from vyos.utils.file import write_file
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'sstpc']
+    ifname, sstpc = get_interface_dict(conf, base)
+
+    # We should only terminate the SSTP client session if critical parameters
+    # change. All parameters that can be changed on-the-fly (like interface
+    # description) should not lead to a reconnect!
+    for options in ['authentication', 'no_peer_dns', 'no_default_route',
+                    'server', 'ssl']:
+        if is_node_changed(conf, base + [ifname, options]):
+            sstpc.update({'shutdown_required': {}})
+            # bail out early - no need to further process other nodes
+            break
+
+    # Load PKI certificates for later processing
+    sstpc['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+                                        get_first_key=True,
+                                        no_tag_node_value_mangle=True)
+    return sstpc
+
+def verify(sstpc):
+    if 'deleted' in sstpc:
+        return None
+
+    verify_authentication(sstpc)
+    verify_vrf(sstpc)
+
+    if not dict_search('server', sstpc):
+        raise ConfigError('Remote SSTP server must be specified!')
+
+    if not dict_search('ssl.ca_certificate', sstpc):
+        raise ConfigError('Missing mandatory CA certificate!')
+
+    return None
+
+def generate(sstpc):
+    ifname = sstpc['ifname']
+    config_sstpc = f'/etc/ppp/peers/{ifname}'
+
+    sstpc['ca_file_path'] = f'/run/sstpc/{ifname}_ca-cert.pem'
+
+    if 'deleted' in sstpc:
+        for file in [sstpc['ca_file_path'], config_sstpc]:
+            if os.path.exists(file):
+                os.unlink(file)
+        return None
+
+    ca_name = sstpc['ssl']['ca_certificate']
+    pki_ca_cert = sstpc['pki']['ca'][ca_name]
+
+    loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
+    loaded_ca_certs = {load_certificate(c['certificate'])
+            for c in sstpc['pki']['ca'].values()} if 'ca' in sstpc['pki'] else {}
+
+    ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
+
+    write_file(sstpc['ca_file_path'], '\n'.join(encode_certificate(c) for c in ca_full_chain))
+    render(config_sstpc, 'sstp-client/peer.j2', sstpc, permission=0o640)
+
+    return None
+
+def apply(sstpc):
+    ifname = sstpc['ifname']
+    if 'deleted' in sstpc or 'disable' in sstpc:
+        if os.path.isdir(f'/sys/class/net/{ifname}'):
+            p = SSTPCIf(ifname)
+            p.remove()
+        call(f'systemctl stop ppp@{ifname}.service')
+        return None
+
+    # reconnect should only be necessary when specific options change,
+    # like server, authentication ... (see get_config() for details)
+    if ((not is_systemd_service_running(f'ppp@{ifname}.service')) or
+        'shutdown_required' in sstpc):
+
+        # cleanup system (e.g. FRR routes first)
+        if os.path.isdir(f'/sys/class/net/{ifname}'):
+            p = SSTPCIf(ifname)
+            p.remove()
+
+        call(f'systemctl restart ppp@{ifname}.service')
+        # When interface comes "live" a hook is called:
+        # /etc/ppp/ip-up.d/96-vyos-sstpc-callback
+        # which triggers SSTPCIf.update()
+    else:
+        if os.path.isdir(f'/sys/class/net/{ifname}'):
+            p = SSTPCIf(ifname)
+            p.update(sstpc)
+
+    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/interfaces_tunnel.py b/src/conf_mode/interfaces_tunnel.py
new file mode 100755
index 000000000..91aed9cc3
--- /dev/null
+++ b/src/conf_mode/interfaces_tunnel.py
@@ -0,0 +1,224 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2022 yOS 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 sys import exit
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_tunnel
+from vyos.configverify import verify_bond_bridge_member
+from vyos.ifconfig import Interface
+from vyos.ifconfig import Section
+from vyos.ifconfig import TunnelIf
+from vyos.utils.network import get_interface_config
+from vyos.utils.dict import dict_search
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least
+    the interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'tunnel']
+    ifname, tunnel = get_interface_dict(conf, base)
+
+    if 'deleted' not in tunnel:
+        tmp = is_node_changed(conf, base + [ifname, 'encapsulation'])
+        if tmp: tunnel.update({'encapsulation_changed': {}})
+
+        tmp = is_node_changed(conf, base + [ifname, 'parameters', 'ip', 'key'])
+        if tmp: tunnel.update({'key_changed': {}})
+
+        # We also need to inspect other configured tunnels as there are Kernel
+        # restrictions where we need to comply. E.g. GRE tunnel key can't be used
+        # twice, or with multiple GRE tunnels to the same location we must specify
+        # a GRE key
+        conf.set_level(base)
+        tunnel['other_tunnels'] = conf.get_config_dict([], key_mangling=('-', '_'),
+                                                      get_first_key=True,
+                                                      no_tag_node_value_mangle=True)
+        # delete our own instance from this dict
+        ifname = tunnel['ifname']
+        del tunnel['other_tunnels'][ifname]
+        # if only one tunnel is present on the system, no need to keep this key
+        if len(tunnel['other_tunnels']) == 0:
+            del tunnel['other_tunnels']
+
+    # We must check if our interface is configured to be a DMVPN member
+    nhrp_base = ['protocols', 'nhrp', 'tunnel']
+    conf.set_level(nhrp_base)
+    nhrp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
+    if nhrp: tunnel.update({'nhrp' : list(nhrp.keys())})
+
+    if 'encapsulation' in tunnel and tunnel['encapsulation'] not in ['erspan', 'ip6erspan']:
+        del tunnel['parameters']['erspan']
+
+    return tunnel
+
+def verify(tunnel):
+    if 'deleted' in tunnel:
+        verify_bridge_delete(tunnel)
+
+        if 'nhrp' in tunnel and tunnel['ifname'] in tunnel['nhrp']:
+            raise ConfigError('Tunnel used for NHRP, it can not be deleted!')
+
+        return None
+
+    verify_tunnel(tunnel)
+
+    if tunnel['encapsulation'] in ['erspan', 'ip6erspan']:
+        if dict_search('parameters.ip.key', tunnel) == None:
+            raise ConfigError('ERSPAN requires ip key parameter!')
+
+        # this is a default field
+        ver = int(tunnel['parameters']['erspan']['version'])
+        if ver == 1:
+            if 'hw_id' in tunnel['parameters']['erspan']:
+                raise ConfigError('ERSPAN version 1 does not support hw-id!')
+            if 'direction' in tunnel['parameters']['erspan']:
+                raise ConfigError('ERSPAN version 1 does not support direction!')
+        elif ver == 2:
+            if 'idx' in tunnel['parameters']['erspan']:
+                raise ConfigError('ERSPAN version 2 does not index parameter!')
+            if 'direction' not in tunnel['parameters']['erspan']:
+                raise ConfigError('ERSPAN version 2 requires direction to be set!')
+
+    # If tunnel source is any and gre key is not set
+    interface = tunnel['ifname']
+    if tunnel['encapsulation'] in ['gre'] and \
+       dict_search('source_address', tunnel) == '0.0.0.0' and \
+       dict_search('parameters.ip.key', tunnel) == None:
+        raise ConfigError(f'"parameters ip key" must be set for {interface} when '\
+                           'encapsulation is GRE!')
+
+    gre_encapsulations = ['gre', 'gretap']
+    if tunnel['encapsulation'] in gre_encapsulations and 'other_tunnels' in tunnel:
+        # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
+        # Prevent the same key for 2 tunnels with same source-address/encap. T2920
+        for o_tunnel, o_tunnel_conf in tunnel['other_tunnels'].items():
+            # no match on encapsulation - bail out
+            our_encapsulation = tunnel['encapsulation']
+            their_encapsulation = o_tunnel_conf['encapsulation']
+            if our_encapsulation in gre_encapsulations and their_encapsulation \
+                not in gre_encapsulations:
+                continue
+
+            our_address = dict_search('source_address', tunnel)
+            our_key = dict_search('parameters.ip.key', tunnel)
+            their_address = dict_search('source_address', o_tunnel_conf)
+            their_key = dict_search('parameters.ip.key', o_tunnel_conf)
+            if our_key != None:
+                if their_address == our_address and their_key == our_key:
+                    raise ConfigError(f'Key "{our_key}" for source-address "{our_address}" ' \
+                                      f'is already used for tunnel "{o_tunnel}"!')
+            else:
+                our_source_if = dict_search('source_interface', tunnel)
+                their_source_if = dict_search('source_interface', o_tunnel_conf)
+                our_remote = dict_search('remote', tunnel)
+                their_remote = dict_search('remote', o_tunnel_conf)
+                # If no IP GRE key is defined we can not have more then one GRE tunnel
+                # bound to any one interface/IP address and the same remote. This will
+                # result in a OS  PermissionError: add tunnel "gre0" failed: File exists
+                if (their_address == our_address or our_source_if == their_source_if) and \
+                    our_remote == their_remote:
+                    raise ConfigError(f'Missing required "ip key" parameter when '\
+                                       'running more then one GRE based tunnel on the '\
+                                       'same source-interface/source-address')
+
+    # Keys are not allowed with ipip and sit tunnels
+    if tunnel['encapsulation'] in ['ipip', 'sit']:
+        if dict_search('parameters.ip.key', tunnel) != None:
+            raise ConfigError('Keys are not allowed with ipip and sit tunnels!')
+
+    verify_mtu_ipv6(tunnel)
+    verify_address(tunnel)
+    verify_vrf(tunnel)
+    verify_bond_bridge_member(tunnel)
+    verify_mirror_redirect(tunnel)
+
+    if 'source_interface' in tunnel:
+        verify_interface_exists(tunnel['source_interface'])
+
+    # TTL != 0 and nopmtudisc are incompatible, parameters and ip use default
+    # values, thus the keys are always present.
+    if dict_search('parameters.ip.no_pmtu_discovery', tunnel) != None:
+        if dict_search('parameters.ip.ttl', tunnel) != '0':
+            raise ConfigError('Disabled PMTU requires TTL set to "0"!')
+        if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']:
+            raise ConfigError('Can not disable PMTU discovery for given encapsulation')
+
+    if dict_search('parameters.ip.ignore_df', tunnel) != None:
+        if tunnel['encapsulation'] not in ['gretap']:
+            raise ConfigError('Option ignore-df can only be used on GRETAP tunnels!')
+
+        if dict_search('parameters.ip.no_pmtu_discovery', tunnel) == None:
+            raise ConfigError('Option ignore-df requires path MTU discovery to be disabled!')
+
+
+def generate(tunnel):
+    return None
+
+def apply(tunnel):
+    interface = tunnel['ifname']
+    # If a gretap tunnel is already existing we can not "simply" change local or
+    # remote addresses. This returns "Operation not supported" by the Kernel.
+    # There is no other solution to destroy and recreate the tunnel.
+    encap = ''
+    remote = ''
+    tmp = get_interface_config(interface)
+    if tmp:
+        encap = dict_search('linkinfo.info_kind', tmp)
+        remote = dict_search('linkinfo.info_data.remote', tmp)
+
+    if ('deleted' in tunnel or 'encapsulation_changed' in tunnel or encap in
+        ['gretap', 'ip6gretap', 'erspan', 'ip6erspan'] or remote in ['any'] or
+        'key_changed' in tunnel):
+        if interface in interfaces():
+            tmp = Interface(interface)
+            tmp.remove()
+        if 'deleted' in tunnel:
+            return None
+
+    tun = TunnelIf(**tunnel)
+    tun.update(tunnel)
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        generate(c)
+        verify(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/interfaces_virtual-ethernet.py b/src/conf_mode/interfaces_virtual-ethernet.py
new file mode 100755
index 000000000..8efe89c41
--- /dev/null
+++ b/src/conf_mode/interfaces_virtual-ethernet.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 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/>.
+
+from sys import exit
+
+from netifaces import interfaces
+from vyos import ConfigError
+from vyos import airbag
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_vrf
+from vyos.ifconfig import VethIf
+
+airbag.enable()
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at
+    least the interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'virtual-ethernet']
+    ifname, veth = get_interface_dict(conf, base)
+
+    # We need to know all other veth related interfaces as veth requires a 1:1
+    # mapping for the peer-names. The Linux kernel automatically creates both
+    # interfaces, the local one and the peer-name, but VyOS also needs a peer
+    # interfaces configrued on the CLI so we can assign proper IP addresses etc.
+    veth['other_interfaces'] = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                     get_first_key=True, no_tag_node_value_mangle=True)
+
+    return veth
+
+
+def verify(veth):
+    if 'deleted' in veth:
+        verify_bridge_delete(veth)
+        # Prevent to delete veth interface which used for another "vethX peer-name"
+        for iface, iface_config in veth['other_interfaces'].items():
+            if veth['ifname'] in iface_config['peer_name']:
+                ifname = veth['ifname']
+                raise ConfigError(
+                    f'Cannot delete "{ifname}" used for "interface {iface} peer-name"'
+                )
+        return None
+
+    verify_vrf(veth)
+    verify_address(veth)
+
+    if 'peer_name' not in veth:
+        raise ConfigError(f'Remote peer name must be set for "{veth["ifname"]}"!')
+
+    peer_name = veth['peer_name']
+    ifname = veth['ifname']
+
+    if veth['peer_name'] not in veth['other_interfaces']:
+        raise ConfigError(f'Used peer-name "{peer_name}" on interface "{ifname}" ' \
+                          'is not configured!')
+
+    if veth['other_interfaces'][peer_name]['peer_name'] != ifname:
+        raise ConfigError(
+            f'Configuration mismatch between "{ifname}" and "{peer_name}"!')
+
+    if peer_name == ifname:
+        raise ConfigError(
+            f'Peer-name "{peer_name}" cannot be the same as interface "{ifname}"!')
+
+    return None
+
+
+def generate(peth):
+    return None
+
+def apply(veth):
+    # Check if the Veth interface already exists
+    if 'rebuild_required' in veth or 'deleted' in veth:
+        if veth['ifname'] in interfaces():
+            p = VethIf(veth['ifname'])
+            p.remove()
+
+    if 'deleted' not in veth:
+        p = VethIf(**veth)
+        p.update(veth)
+
+    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/interfaces_vti.py b/src/conf_mode/interfaces_vti.py
new file mode 100755
index 000000000..9871810ae
--- /dev/null
+++ b/src/conf_mode/interfaces_vti.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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/>.
+
+from netifaces import interfaces
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_mirror_redirect
+from vyos.ifconfig import VTIIf
+from vyos.utils.dict import dict_search
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'vti']
+    _, vti = get_interface_dict(conf, base)
+    return vti
+
+def verify(vti):
+    verify_mirror_redirect(vti)
+    return None
+
+def generate(vti):
+    return None
+
+def apply(vti):
+    # Remove macsec interface
+    if 'deleted' in vti:
+        VTIIf(**vti).remove()
+        return None
+
+    tmp = VTIIf(**vti)
+    tmp.update(vti)
+
+    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/interfaces_vxlan.py b/src/conf_mode/interfaces_vxlan.py
new file mode 100755
index 000000000..4251e611b
--- /dev/null
+++ b/src/conf_mode/interfaces_vxlan.py
@@ -0,0 +1,236 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-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 sys import exit
+from netifaces import interfaces
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
+from vyos.configdict import is_node_changed
+from vyos.configdict import node_changed
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_bond_bridge_member
+from vyos.ifconfig import Interface
+from vyos.ifconfig import VXLANIf
+from vyos.template import is_ipv6
+from vyos.utils.dict import dict_search
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least
+    the interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'vxlan']
+    ifname, vxlan = get_interface_dict(conf, base)
+
+    # VXLAN interfaces are picky and require recreation if certain parameters
+    # change. But a VXLAN interface should - of course - not be re-created if
+    # it's description or IP address is adjusted. Feels somehow logic doesn't it?
+    for cli_option in ['parameters', 'gpe', 'group', 'port', 'remote',
+                       'source-address', 'source-interface', 'vni']:
+        if is_node_changed(conf, base + [ifname, cli_option]):
+            vxlan.update({'rebuild_required': {}})
+            break
+
+    # When dealing with VNI filtering we need to know what VNI was actually removed,
+    # so build up a dict matching the vlan_to_vni structure but with removed values.
+    tmp = node_changed(conf, base + [ifname, 'vlan-to-vni'], recursive=True)
+    if tmp:
+        vxlan.update({'vlan_to_vni_removed': {}})
+        for vlan in tmp:
+            vni = leaf_node_changed(conf, base + [ifname, 'vlan-to-vni', vlan, 'vni'])
+            vxlan['vlan_to_vni_removed'].update({vlan : {'vni' : vni[0]}})
+
+    # We need to verify that no other VXLAN tunnel is configured when external
+    # mode is in use - Linux Kernel limitation
+    conf.set_level(base)
+    vxlan['other_tunnels'] = conf.get_config_dict([], key_mangling=('-', '_'),
+                                                  get_first_key=True,
+                                                  no_tag_node_value_mangle=True)
+
+    # This if-clause is just to be sure - it will always evaluate to true
+    ifname = vxlan['ifname']
+    if ifname in vxlan['other_tunnels']:
+        del vxlan['other_tunnels'][ifname]
+    if len(vxlan['other_tunnels']) == 0:
+        del vxlan['other_tunnels']
+
+    return vxlan
+
+def verify(vxlan):
+    if 'deleted' in vxlan:
+        verify_bridge_delete(vxlan)
+        return None
+
+    if int(vxlan['mtu']) < 1500:
+        Warning('RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU')
+
+    if 'group' in vxlan:
+        if 'source_interface' not in vxlan:
+            raise ConfigError('Multicast VXLAN requires an underlaying interface')
+        verify_source_interface(vxlan)
+
+    if not any(tmp in ['group', 'remote', 'source_address', 'source_interface'] for tmp in vxlan):
+        raise ConfigError('Group, remote, source-address or source-interface must be configured')
+
+    if 'vni' not in vxlan and dict_search('parameters.external', vxlan) == None:
+        raise ConfigError('Must either configure VXLAN "vni" or use "external" CLI option!')
+
+    if dict_search('parameters.external', vxlan) != None:
+        if 'vni' in vxlan:
+            raise ConfigError('Can not specify both "external" and "VNI"!')
+
+        if 'other_tunnels' in vxlan:
+            # When multiple VXLAN interfaces are defined and "external" is used,
+            # all VXLAN interfaces need to have vni-filter enabled!
+            # See Linux Kernel commit f9c4bb0b245cee35ef66f75bf409c9573d934cf9
+            other_vni_filter = False
+            for tunnel, tunnel_config in vxlan['other_tunnels'].items():
+                if dict_search('parameters.vni_filter', tunnel_config) != None:
+                    other_vni_filter = True
+                    break
+            # eqivalent of the C foo ? 'a' : 'b' statement
+            vni_filter = True and (dict_search('parameters.vni_filter', vxlan) != None) or False
+            # If either one is enabled, so must be the other. Both can be off and both can be on
+            if (vni_filter and not other_vni_filter) or (not vni_filter and other_vni_filter):
+                raise ConfigError(f'Using multiple VXLAN interfaces with "external" '\
+                    'requires all VXLAN interfaces to have "vni-filter" configured!')
+
+            if not vni_filter and not other_vni_filter:
+                other_tunnels = ', '.join(vxlan['other_tunnels'])
+                raise ConfigError(f'Only one VXLAN tunnel is supported when "external" '\
+                                f'CLI option is used and "vni-filter" is unset. '\
+                                f'Additional tunnels: {other_tunnels}')
+
+    if 'gpe' in vxlan and 'external' not in vxlan:
+        raise ConfigError(f'VXLAN-GPE is only supported when "external" '\
+                          f'CLI option is used.')
+
+    if 'source_interface' in vxlan:
+        # VXLAN adds at least an overhead of 50 byte - we need to check the
+        # underlaying device if our VXLAN package is not going to be fragmented!
+        vxlan_overhead = 50
+        if 'source_address' in vxlan and is_ipv6(vxlan['source_address']):
+            # IPv6 adds an extra 20 bytes overhead because the IPv6 header is 20
+            # bytes larger than the IPv4 header - assuming no extra options are
+            # in use.
+            vxlan_overhead += 20
+
+        # If source_address is not used - check IPv6 'remote' list
+        elif 'remote' in vxlan:
+            if any(is_ipv6(a) for a in vxlan['remote']):
+                vxlan_overhead += 20
+
+        lower_mtu = Interface(vxlan['source_interface']).get_mtu()
+        if lower_mtu < (int(vxlan['mtu']) + vxlan_overhead):
+            raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\
+                              f'bytes) for VXLAN overhead ({vxlan_overhead} bytes!)')
+
+    # Check for mixed IPv4 and IPv6 addresses
+    protocol = None
+    if 'source_address' in vxlan:
+        if is_ipv6(vxlan['source_address']):
+            protocol = 'ipv6'
+        else:
+            protocol = 'ipv4'
+
+    if 'remote' in vxlan:
+        error_msg = 'Can not mix both IPv4 and IPv6 for VXLAN underlay'
+        for remote in vxlan['remote']:
+            if is_ipv6(remote):
+                if protocol == 'ipv4':
+                    raise ConfigError(error_msg)
+                protocol = 'ipv6'
+            else:
+                if protocol == 'ipv6':
+                    raise ConfigError(error_msg)
+                protocol = 'ipv4'
+
+    if 'vlan_to_vni' in vxlan:
+        if 'is_bridge_member' not in vxlan:
+            raise ConfigError('VLAN to VNI mapping requires that VXLAN interface '\
+                              'is member of a bridge interface!')
+
+        vnis_used = []
+        for vif, vif_config in vxlan['vlan_to_vni'].items():
+            if 'vni' not in vif_config:
+                raise ConfigError(f'Must define VNI for VLAN "{vif}"!')
+            vni = vif_config['vni']
+            if vni in vnis_used:
+                raise ConfigError(f'VNI "{vni}" is already assigned to a different VLAN!')
+            vnis_used.append(vni)
+
+    if dict_search('parameters.neighbor_suppress', vxlan) != None:
+        if 'is_bridge_member' not in vxlan:
+            raise ConfigError('Neighbor suppression requires that VXLAN interface '\
+                              'is member of a bridge interface!')
+
+    verify_mtu_ipv6(vxlan)
+    verify_address(vxlan)
+    verify_bond_bridge_member(vxlan)
+    verify_mirror_redirect(vxlan)
+
+    # We use a defaultValue for port, thus it's always safe to use
+    if vxlan['port'] == '8472':
+        Warning('Starting from VyOS 1.4, the default port for VXLAN '\
+                'has been changed to 4789. This matches the IANA assigned '\
+                'standard port number!')
+
+    return None
+
+def generate(vxlan):
+    return None
+
+def apply(vxlan):
+    # Check if the VXLAN interface already exists
+    if 'rebuild_required' in vxlan or 'delete' in vxlan:
+        if vxlan['ifname'] in interfaces():
+            v = VXLANIf(vxlan['ifname'])
+            # VXLAN is super picky and the tunnel always needs to be recreated,
+            # thus we can simply always delete it first.
+            v.remove()
+
+    if 'deleted' not in vxlan:
+        # Finally create the new interface
+        v = VXLANIf(**vxlan)
+        v.update(vxlan)
+
+    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/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py
new file mode 100755
index 000000000..79e5d3f44
--- /dev/null
+++ b/src/conf_mode/interfaces_wireguard.py
@@ -0,0 +1,133 @@
+#!/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/>.
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_bond_bridge_member
+from vyos.ifconfig import WireGuardIf
+from vyos.utils.kernel import check_kmod
+from vyos.utils.network import check_port_availability
+from vyos.utils.network import is_wireguard_key_pair
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'wireguard']
+    ifname, wireguard = get_interface_dict(conf, base)
+
+    # Check if a port was changed
+    tmp = is_node_changed(conf, base + [ifname, 'port'])
+    if tmp: wireguard['port_changed'] = {}
+
+    # T4702: If anything on a peer changes we remove the peer first and re-add it
+    if is_node_changed(conf, base + [ifname, 'peer']):
+        wireguard.update({'rebuild_required': {}})
+
+    return wireguard
+
+def verify(wireguard):
+    if 'deleted' in wireguard:
+        verify_bridge_delete(wireguard)
+        return None
+
+    verify_mtu_ipv6(wireguard)
+    verify_address(wireguard)
+    verify_vrf(wireguard)
+    verify_bond_bridge_member(wireguard)
+    verify_mirror_redirect(wireguard)
+
+    if 'private_key' not in wireguard:
+        raise ConfigError('Wireguard private-key not defined')
+
+    if 'peer' not in wireguard:
+        raise ConfigError('At least one Wireguard peer is required!')
+
+    if 'port' in wireguard and 'port_changed' in wireguard:
+        listen_port = int(wireguard['port'])
+        if check_port_availability('0.0.0.0', listen_port, 'udp') is not True:
+            raise ConfigError(f'UDP port {listen_port} is busy or unavailable and '
+                               'cannot be used for the interface!')
+
+    # run checks on individual configured WireGuard peer
+    public_keys = []
+    for tmp in wireguard['peer']:
+        peer = wireguard['peer'][tmp]
+
+        if 'allowed_ips' not in peer:
+            raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!')
+
+        if 'public_key' not in peer:
+            raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!')
+
+        if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer):
+            raise ConfigError('Both Wireguard port and address must be defined '
+                              f'for peer "{tmp}" if either one of them is set!')
+
+        if peer['public_key'] in public_keys:
+            raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"')
+
+        if 'disable' not in peer:
+            if is_wireguard_key_pair(wireguard['private_key'], peer['public_key']):
+                raise ConfigError(f'Peer "{tmp}" has the same public key as the interface "{wireguard["ifname"]}"')
+
+        public_keys.append(peer['public_key'])
+
+def apply(wireguard):
+    if 'rebuild_required' in wireguard or 'deleted' in wireguard:
+        wg = WireGuardIf(**wireguard)
+        # WireGuard only supports peer removal based on the configured public-key,
+        # by deleting the entire interface this is the shortcut instead of parsing
+        # out all peers and removing them one by one.
+        #
+        # Peer reconfiguration will always come with a short downtime while the
+        # WireGuard interface is recreated (see below)
+        wg.remove()
+
+    # Create the new interface if required
+    if 'deleted' not in wireguard:
+        wg = WireGuardIf(**wireguard)
+        wg.update(wireguard)
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        check_kmod('wireguard')
+        c = get_config()
+        verify(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py
new file mode 100755
index 000000000..02b4a2500
--- /dev/null
+++ b/src/conf_mode/interfaces_wireless.py
@@ -0,0 +1,275 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2020 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 sys import exit
+from re import findall
+from netaddr import EUI, mac_unix_expanded
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import dict_merge
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_vlan_config
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_bond_bridge_member
+from vyos.ifconfig import WiFiIf
+from vyos.template import render
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+# XXX: wpa_supplicant works on the source interface
+wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf'
+hostapd_conf = '/run/hostapd/{ifname}.conf'
+hostapd_accept_station_conf = '/run/hostapd/{ifname}_station_accept.conf'
+hostapd_deny_station_conf = '/run/hostapd/{ifname}_station_deny.conf'
+
+def find_other_stations(conf, base, ifname):
+    """
+    Only one wireless interface per phy can be in station mode -
+    find all interfaces attached to a phy which run in station mode
+    """
+    old_level = conf.get_level()
+    conf.set_level(base)
+    dict = {}
+    for phy in os.listdir('/sys/class/ieee80211'):
+        list = []
+        for interface in conf.list_nodes([]):
+            if interface == ifname:
+                continue
+            # the following node is mandatory
+            if conf.exists([interface, 'physical-device', phy]):
+                tmp = conf.return_value([interface, 'type'])
+                if tmp == 'station':
+                    list.append(interface)
+        if list:
+            dict.update({phy: list})
+    conf.set_level(old_level)
+    return dict
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'wireless']
+
+    ifname, wifi = get_interface_dict(conf, base)
+
+    if 'deleted' not in wifi:
+        # then get_interface_dict provides default keys
+        if wifi.from_defaults(['security', 'wep']): # if not set by user
+            del wifi['security']['wep']
+        if wifi.from_defaults(['security', 'wpa']): # if not set by user
+            del wifi['security']['wpa']
+
+    if dict_search('security.wpa', wifi) != None:
+        wpa_cipher = wifi['security']['wpa'].get('cipher')
+        wpa_mode = wifi['security']['wpa'].get('mode')
+        if not wpa_cipher:
+            tmp = None
+            if wpa_mode == 'wpa':
+                tmp = {'security': {'wpa': {'cipher' : ['TKIP', 'CCMP']}}}
+            elif wpa_mode == 'wpa2':
+                tmp = {'security': {'wpa': {'cipher' : ['CCMP']}}}
+            elif wpa_mode == 'both':
+                tmp = {'security': {'wpa': {'cipher' : ['CCMP', 'TKIP']}}}
+
+            if tmp: wifi = dict_merge(tmp, wifi)
+
+    # Only one wireless interface per phy can be in station mode
+    tmp = find_other_stations(conf, base, wifi['ifname'])
+    if tmp: wifi['station_interfaces'] = tmp
+
+    # used in hostapt.conf.j2
+    wifi['hostapd_accept_station_conf'] = hostapd_accept_station_conf.format(**wifi)
+    wifi['hostapd_deny_station_conf'] = hostapd_deny_station_conf.format(**wifi)
+
+    return wifi
+
+def verify(wifi):
+    if 'deleted' in wifi:
+        verify_bridge_delete(wifi)
+        return None
+
+    if 'physical_device' not in wifi:
+        raise ConfigError('You must specify a physical-device "phy"')
+
+    if 'type' not in wifi:
+        raise ConfigError('You must specify a WiFi mode')
+
+    if 'ssid' not in wifi and wifi['type'] != 'monitor':
+        raise ConfigError('SSID must be configured unless type is set to "monitor"!')
+
+    if wifi['type'] == 'access-point':
+        if 'country_code' not in wifi:
+            raise ConfigError('Wireless country-code is mandatory')
+
+        if 'channel' not in wifi:
+            raise ConfigError('Wireless channel must be configured!')
+
+    if 'security' in wifi:
+        if {'wep', 'wpa'} <= set(wifi.get('security', {})):
+            raise ConfigError('Must either use WEP or WPA security!')
+
+        if 'wep' in wifi['security']:
+            if 'key' in wifi['security']['wep'] and len(wifi['security']['wep']) > 4:
+                raise ConfigError('No more then 4 WEP keys configurable')
+            elif 'key' not in wifi['security']['wep']:
+                raise ConfigError('Security WEP configured - missing WEP keys!')
+
+        elif 'wpa' in wifi['security']:
+            wpa = wifi['security']['wpa']
+            if not any(i in ['passphrase', 'radius'] for i in wpa):
+                raise ConfigError('Misssing WPA key or RADIUS server')
+
+            if 'radius' in wpa:
+                if 'server' in wpa['radius']:
+                    for server in wpa['radius']['server']:
+                        if 'key' not in wpa['radius']['server'][server]:
+                            raise ConfigError(f'Misssing RADIUS shared secret key for server: {server}')
+
+    if 'capabilities' in wifi:
+        capabilities = wifi['capabilities']
+        if 'vht' in capabilities:
+            if 'ht' not in capabilities:
+                raise ConfigError('Specify HT flags if you want to use VHT!')
+
+            if {'beamform', 'antenna_count'} <= set(capabilities.get('vht', {})):
+                if capabilities['vht']['antenna_count'] == '1':
+                    raise ConfigError('Cannot use beam forming with just one antenna!')
+
+                if capabilities['vht']['beamform'] == 'single-user-beamformer':
+                    if int(capabilities['vht']['antenna_count']) < 3:
+                        # Nasty Gotcha: see https://w1.fi/cgit/hostap/plain/hostapd/hostapd.conf lines 692-705
+                        raise ConfigError('Single-user beam former requires at least 3 antennas!')
+
+    if 'station_interfaces' in wifi and wifi['type'] == 'station':
+        phy = wifi['physical_device']
+        if phy in wifi['station_interfaces']:
+            if len(wifi['station_interfaces'][phy]) > 0:
+                raise ConfigError('Only one station per wireless physical interface possible!')
+
+    verify_address(wifi)
+    verify_vrf(wifi)
+    verify_bond_bridge_member(wifi)
+    verify_mirror_redirect(wifi)
+
+    # use common function to verify VLAN configuration
+    verify_vlan_config(wifi)
+
+    return None
+
+def generate(wifi):
+    interface = wifi['ifname']
+
+    # always stop hostapd service first before reconfiguring it
+    call(f'systemctl stop hostapd@{interface}.service')
+    # always stop wpa_supplicant service first before reconfiguring it
+    call(f'systemctl stop wpa_supplicant@{interface}.service')
+
+    # Delete config files if interface is removed
+    if 'deleted' in wifi:
+        if os.path.isfile(hostapd_conf.format(**wifi)):
+            os.unlink(hostapd_conf.format(**wifi))
+        if os.path.isfile(hostapd_accept_station_conf.format(**wifi)):
+            os.unlink(hostapd_accept_station_conf.format(**wifi))
+        if os.path.isfile(hostapd_deny_station_conf.format(**wifi)):
+            os.unlink(hostapd_deny_station_conf.format(**wifi))
+        if os.path.isfile(wpa_suppl_conf.format(**wifi)):
+            os.unlink(wpa_suppl_conf.format(**wifi))
+
+        return None
+
+    if 'mac' not in wifi:
+        # http://wiki.stocksy.co.uk/wiki/Multiple_SSIDs_with_hostapd
+        # generate locally administered MAC address from used phy interface
+        with open('/sys/class/ieee80211/{physical_device}/addresses'.format(**wifi), 'r') as f:
+            # some PHYs tend to have multiple interfaces and thus supply multiple MAC
+            # addresses - we only need the first one for our calculation
+            tmp = f.readline().rstrip()
+            tmp = EUI(tmp).value
+            # mask last nibble from the MAC address
+            tmp &= 0xfffffffffff0
+            # set locally administered bit in MAC address
+            tmp |= 0x020000000000
+            # we now need to add an offset to our MAC address indicating this
+            # subinterfaces index
+            tmp += int(findall(r'\d+', interface)[0])
+
+            # convert integer to "real" MAC address representation
+            mac = EUI(hex(tmp).split('x')[-1])
+            # change dialect to use : as delimiter instead of -
+            mac.dialect = mac_unix_expanded
+            wifi['mac'] = str(mac)
+
+    # XXX: Jinja2 can not operate on a dictionary key when it starts of with a number
+    if '40mhz_incapable' in (dict_search('capabilities.ht', wifi) or []):
+        wifi['capabilities']['ht']['fourtymhz_incapable'] = wifi['capabilities']['ht']['40mhz_incapable']
+        del wifi['capabilities']['ht']['40mhz_incapable']
+
+    # render appropriate new config files depending on access-point or station mode
+    if wifi['type'] == 'access-point':
+        render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.j2', wifi)
+        render(hostapd_accept_station_conf.format(**wifi), 'wifi/hostapd_accept_station.conf.j2', wifi)
+        render(hostapd_deny_station_conf.format(**wifi), 'wifi/hostapd_deny_station.conf.j2', wifi)
+
+    elif wifi['type'] == 'station':
+        render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.j2', wifi)
+
+    return None
+
+def apply(wifi):
+    interface = wifi['ifname']
+    if 'deleted' in wifi:
+        WiFiIf(interface).remove()
+    else:
+        # Finally create the new interface
+        w = WiFiIf(**wifi)
+        w.update(wifi)
+
+        # Enable/Disable interface - interface is always placed in
+        # administrative down state in WiFiIf class
+        if 'disable' not in wifi:
+            # Physical interface is now configured. Proceed by starting hostapd or
+            # wpa_supplicant daemon. When type is monitor we can just skip this.
+            if wifi['type'] == 'access-point':
+                call(f'systemctl start hostapd@{interface}.service')
+
+            elif wifi['type'] == 'station':
+                call(f'systemctl start wpa_supplicant@{interface}.service')
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/interfaces_wwan.py b/src/conf_mode/interfaces_wwan.py
new file mode 100755
index 000000000..2515dc838
--- /dev/null
+++ b/src/conf_mode/interfaces_wwan.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2022 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 sys import exit
+from time import sleep
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_authentication
+from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_mirror_redirect
+from vyos.configverify import verify_vrf
+from vyos.ifconfig import WWANIf
+from vyos.utils.dict import dict_search
+from vyos.utils.process import cmd
+from vyos.utils.process import call
+from vyos.utils.process import DEVNULL
+from vyos.utils.process import is_systemd_service_active
+from vyos.utils.file import write_file
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+service_name = 'ModemManager.service'
+cron_script = '/etc/cron.d/vyos-wwan'
+
+def get_config(config=None):
+    """
+    Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+    interface name will be added or a deleted flag
+    """
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['interfaces', 'wwan']
+    ifname, wwan = get_interface_dict(conf, base)
+
+    # We should only terminate the WWAN session if critical parameters change.
+    # All parameters that can be changed on-the-fly (like interface description)
+    # should not lead to a reconnect!
+    tmp = is_node_changed(conf, base + [ifname, 'address'])
+    if tmp: wwan.update({'shutdown_required': {}})
+
+    tmp = is_node_changed(conf, base + [ifname, 'apn'])
+    if tmp: wwan.update({'shutdown_required': {}})
+
+    tmp = is_node_changed(conf, base + [ifname, 'disable'])
+    if tmp: wwan.update({'shutdown_required': {}})
+
+    tmp = is_node_changed(conf, base + [ifname, 'vrf'])
+    if tmp: wwan.update({'shutdown_required': {}})
+
+    tmp = is_node_changed(conf, base + [ifname, 'authentication'])
+    if tmp: wwan.update({'shutdown_required': {}})
+
+    tmp = is_node_changed(conf, base + [ifname, 'ipv6', 'address', 'autoconf'])
+    if tmp: wwan.update({'shutdown_required': {}})
+
+    # We need to know the amount of other WWAN interfaces as ModemManager needs
+    # to be started or stopped.
+    wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'),
+                                                       get_first_key=True,
+                                                       no_tag_node_value_mangle=True)
+
+    # This if-clause is just to be sure - it will always evaluate to true
+    if ifname in wwan['other_interfaces']:
+        del wwan['other_interfaces'][ifname]
+    if len(wwan['other_interfaces']) == 0:
+        del wwan['other_interfaces']
+
+    return wwan
+
+def verify(wwan):
+    if 'deleted' in wwan:
+        return None
+
+    ifname = wwan['ifname']
+    if not 'apn' in wwan:
+        raise ConfigError(f'No APN configured for "{ifname}"!')
+
+    verify_interface_exists(ifname)
+    verify_authentication(wwan)
+    verify_vrf(wwan)
+    verify_mirror_redirect(wwan)
+
+    return None
+
+def generate(wwan):
+    if 'deleted' in wwan:
+        # We are the last WWAN interface - there are no other ones remaining
+        # thus the cronjob needs to go away, too
+        if 'other_interfaces' not in wwan:
+            if os.path.exists(cron_script):
+                os.unlink(cron_script)
+        return None
+
+    # Install cron triggered helper script to re-dial WWAN interfaces on
+    # disconnect - e.g. happens during RF signal loss. The script watches every
+    # WWAN interface - so there is only one instance.
+    if not os.path.exists(cron_script):
+        write_file(cron_script, '*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py\n')
+
+    return None
+
+def apply(wwan):
+    # ModemManager is required to dial WWAN connections - one instance is
+    # required to serve all modems. Activate ModemManager on first invocation
+    # of any WWAN interface.
+    if not is_systemd_service_active(service_name):
+        cmd(f'systemctl start {service_name}')
+
+        counter = 100
+        # Wait until a modem is detected and then we can continue
+        while counter > 0:
+            counter -= 1
+            tmp = cmd('mmcli -L')
+            if tmp != 'No modems were found':
+                break
+            sleep(0.250)
+
+    if 'shutdown_required' in wwan:
+        # we only need the modem number. wwan0 -> 0, wwan1 -> 1
+        modem = wwan['ifname'].lstrip('wwan')
+        base_cmd = f'mmcli --modem {modem}'
+        # Number of bearers is limited - always disconnect first
+        cmd(f'{base_cmd} --simple-disconnect')
+
+    w = WWANIf(wwan['ifname'])
+    if 'deleted' in wwan or 'disable' in wwan:
+        w.remove()
+
+        # We are the last WWAN interface - there are no other WWAN interfaces
+        # remaining, thus we can stop ModemManager and free resources.
+        if 'other_interfaces' not in wwan:
+            cmd(f'systemctl stop {service_name}')
+            # Clean CRON helper script which is used for to re-connect when
+            # RF signal is lost
+            if os.path.exists(cron_script):
+                os.unlink(cron_script)
+
+        return None
+
+    if 'shutdown_required' in wwan:
+        ip_type = 'ipv4'
+        slaac = dict_search('ipv6.address.autoconf', wwan) != None
+        if 'address' in wwan:
+            if 'dhcp' in wwan['address'] and ('dhcpv6' in wwan['address'] or slaac):
+                ip_type = 'ipv4v6'
+            elif 'dhcpv6' in wwan['address'] or slaac:
+                ip_type = 'ipv6'
+            elif 'dhcp' in wwan['address']:
+                ip_type = 'ipv4'
+
+        options = f'ip-type={ip_type},apn=' + wwan['apn']
+        if 'authentication' in wwan:
+            options += ',user={username},password={password}'.format(**wwan['authentication'])
+
+        command = f'{base_cmd} --simple-connect="{options}"'
+        call(command, stdout=DEVNULL)
+
+    w.update(wwan)
+    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/le_cert.py b/src/conf_mode/le_cert.py
deleted file mode 100755
index 06c7e7b72..000000000
--- a/src/conf_mode/le_cert.py
+++ /dev/null
@@ -1,115 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-2020 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 sys
-import os
-
-import vyos.defaults
-from vyos.config import Config
-from vyos import ConfigError
-from vyos.utils.process import cmd
-from vyos.utils.process import call
-from vyos.utils.process import is_systemd_service_running
-
-from vyos import airbag
-airbag.enable()
-
-vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode']
-vyos_certbot_dir = vyos.defaults.directories['certbot']
-
-dependencies = [
-    'https.py',
-]
-
-def request_certbot(cert):
-    email = cert.get('email')
-    if email is not None:
-        email_flag = '-m {0}'.format(email)
-    else:
-        email_flag = ''
-
-    domains = cert.get('domains')
-    if domains is not None:
-        domain_flag = '-d ' + ' -d '.join(domains)
-    else:
-        domain_flag = ''
-
-    certbot_cmd = f'certbot certonly --config-dir {vyos_certbot_dir} -n --nginx --agree-tos --no-eff-email --expand {email_flag} {domain_flag}'
-
-    cmd(certbot_cmd,
-        raising=ConfigError,
-        message="The certbot request failed for the specified domains.")
-
-def get_config():
-    conf = Config()
-    if not conf.exists('service https certificates certbot'):
-        return None
-    else:
-        conf.set_level('service https certificates certbot')
-
-    cert = {}
-
-    if conf.exists('domain-name'):
-        cert['domains'] = conf.return_values('domain-name')
-
-    if conf.exists('email'):
-        cert['email'] = conf.return_value('email')
-
-    return cert
-
-def verify(cert):
-    if cert is None:
-        return None
-
-    if 'domains' not in cert:
-        raise ConfigError("At least one domain name is required to"
-                          " request a letsencrypt certificate.")
-
-    if 'email' not in cert:
-        raise ConfigError("An email address is required to request"
-                          " a letsencrypt certificate.")
-
-def generate(cert):
-    if cert is None:
-        return None
-
-    # certbot will attempt to reload nginx, even with 'certonly';
-    # start nginx if not active
-    if not is_systemd_service_running('nginx.service'):
-        call('systemctl start nginx.service')
-
-    request_certbot(cert)
-
-def apply(cert):
-    if cert is not None:
-        call('systemctl restart certbot.timer')
-    else:
-        call('systemctl stop certbot.timer')
-        return None
-
-    for dep in dependencies:
-        cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError)
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        sys.exit(1)
-
diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py
deleted file mode 100755
index 3c647a0e8..000000000
--- a/src/conf_mode/lldp.py
+++ /dev/null
@@ -1,123 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2017-2022 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 sys import exit
-
-from vyos.base import Warning
-from vyos.config import Config
-from vyos.utils.network import is_addr_assigned
-from vyos.utils.network import is_loopback_addr
-from vyos.version import get_version_data
-from vyos.utils.process import call
-from vyos.utils.dict import dict_search
-from vyos.template import render
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = "/etc/default/lldpd"
-vyos_config_file = "/etc/lldpd.d/01-vyos.conf"
-base = ['service', 'lldp']
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    if not conf.exists(base):
-        return {}
-
-    lldp = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                no_tag_node_value_mangle=True,
-                                get_first_key=True,
-                                with_recursive_defaults=True)
-
-    if conf.exists(['service', 'snmp']):
-        lldp['system_snmp_enabled'] = ''
-
-    version_data = get_version_data()
-    lldp['version'] = version_data['version']
-
-    # prune location information if not set by user
-    for interface in lldp.get('interface', []):
-        if lldp.from_defaults(['interface', interface, 'location']):
-            del lldp['interface'][interface]['location']
-        elif lldp.from_defaults(['interface', interface, 'location','coordinate_based']):
-            del lldp['interface'][interface]['location']['coordinate_based']
-
-    return lldp
-
-def verify(lldp):
-    # bail out early - looks like removal from running config
-    if lldp is None:
-        return
-
-    if 'management_address' in lldp:
-        for address in lldp['management_address']:
-            message = f'LLDP management address "{address}" is invalid'
-            if is_loopback_addr(address):
-                Warning(f'{message} - loopback address')
-            elif not is_addr_assigned(address):
-                Warning(f'{message} - not assigned to any interface')
-
-    if 'interface' in lldp:
-        for interface, interface_config in lldp['interface'].items():
-            # bail out early if no location info present in interface config
-            if 'location' not in interface_config:
-                continue
-            if 'coordinate_based' in interface_config['location']:
-                if not {'latitude', 'latitude'} <= set(interface_config['location']['coordinate_based']):
-                    raise ConfigError(f'Must define both longitude and latitude for "{interface}" location!')
-
-    # check options
-    if 'snmp' in lldp:
-        if 'system_snmp_enabled' not in lldp:
-            raise ConfigError('SNMP must be configured to enable LLDP SNMP!')
-
-
-def generate(lldp):
-    # bail out early - looks like removal from running config
-    if lldp is None:
-        return
-
-    render(config_file, 'lldp/lldpd.j2', lldp)
-    render(vyos_config_file, 'lldp/vyos.conf.j2', lldp)
-
-def apply(lldp):
-    systemd_service = 'lldpd.service'
-    if lldp:
-        # start/restart lldp service
-        call(f'systemctl restart {systemd_service}')
-    else:
-        # LLDP service has been terminated
-        call(f'systemctl stop {systemd_service}')
-        if os.path.isfile(config_file):
-            os.unlink(config_file)
-        if os.path.isfile(vyos_config_file):
-            os.unlink(vyos_config_file)
-
-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/load-balancing-haproxy.py b/src/conf_mode/load-balancing-haproxy.py
deleted file mode 100755
index 333ebc66c..000000000
--- a/src/conf_mode/load-balancing-haproxy.py
+++ /dev/null
@@ -1,169 +0,0 @@
-#!/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/>.
-
-import os
-
-from sys import exit
-from shutil import rmtree
-
-from vyos.config import Config
-from vyos.utils.process import call
-from vyos.utils.network import check_port_availability
-from vyos.utils.network import is_listen_port_bind_service
-from vyos.pki import wrap_certificate
-from vyos.pki import wrap_private_key
-from vyos.template import render
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-load_balancing_dir = '/run/haproxy'
-load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg'
-systemd_service = 'haproxy.service'
-systemd_override = r'/run/systemd/system/haproxy.service.d/10-override.conf'
-
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base = ['load-balancing', 'reverse-proxy']
-    lb = conf.get_config_dict(base,
-                              get_first_key=True,
-                              key_mangling=('-', '_'),
-                              no_tag_node_value_mangle=True)
-
-    if lb:
-        lb['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
-                                    get_first_key=True, no_tag_node_value_mangle=True)
-
-    if lb:
-        lb = conf.merge_defaults(lb, recursive=True)
-
-    return lb
-
-
-def verify(lb):
-    if not lb:
-        return None
-
-    if 'backend' not in lb or 'service' not in lb:
-        raise ConfigError(f'"service" and "backend" must be configured!')
-
-    for front, front_config in lb['service'].items():
-        if 'port' not in front_config:
-            raise ConfigError(f'"{front} service port" must be configured!')
-
-        # Check if bind address:port are used by another service
-        tmp_address = front_config.get('address', '0.0.0.0')
-        tmp_port = front_config['port']
-        if check_port_availability(tmp_address, int(tmp_port), 'tcp') is not True and \
-                not is_listen_port_bind_service(int(tmp_port), 'haproxy'):
-            raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service')
-
-    for back, back_config in lb['backend'].items():
-        if 'server' not in back_config:
-            raise ConfigError(f'"{back} server" must be configured!')
-        for bk_server, bk_server_conf in back_config['server'].items():
-            if 'address' not in bk_server_conf or 'port' not in bk_server_conf:
-                raise ConfigError(f'"backend {back} server {bk_server} address and port" must be configured!')
-
-            if {'send_proxy', 'send_proxy_v2'} <= set(bk_server_conf):
-                raise ConfigError(f'Cannot use both "send-proxy" and "send-proxy-v2" for server "{bk_server}"')
-
-def generate(lb):
-    if not lb:
-        # Delete /run/haproxy/haproxy.cfg
-        config_files = [load_balancing_conf_file, systemd_override]
-        for file in config_files:
-            if os.path.isfile(file):
-                os.unlink(file)
-        # Delete old directories
-        if os.path.isdir(load_balancing_dir):
-            rmtree(load_balancing_dir, ignore_errors=True)
-
-        return None
-
-    # Create load-balance dir
-    if not os.path.isdir(load_balancing_dir):
-        os.mkdir(load_balancing_dir)
-
-    # SSL Certificates for frontend
-    for front, front_config in lb['service'].items():
-        if 'ssl' in front_config:
-
-            if 'certificate' in front_config['ssl']:
-                cert_names = front_config['ssl']['certificate']
-
-                for cert_name in cert_names:
-                    pki_cert = lb['pki']['certificate'][cert_name]
-                    cert_file_path = os.path.join(load_balancing_dir, f'{cert_name}.pem')
-                    cert_key_path = os.path.join(load_balancing_dir, f'{cert_name}.pem.key')
-
-                    with open(cert_file_path, 'w') as f:
-                        f.write(wrap_certificate(pki_cert['certificate']))
-
-                    if 'private' in pki_cert and 'key' in pki_cert['private']:
-                        with open(cert_key_path, 'w') as f:
-                            f.write(wrap_private_key(pki_cert['private']['key']))
-
-            if 'ca_certificate' in front_config['ssl']:
-                ca_name = front_config['ssl']['ca_certificate']
-                pki_ca_cert = lb['pki']['ca'][ca_name]
-                ca_cert_file_path = os.path.join(load_balancing_dir, f'{ca_name}.pem')
-
-                with open(ca_cert_file_path, 'w') as f:
-                    f.write(wrap_certificate(pki_ca_cert['certificate']))
-
-    # SSL Certificates for backend
-    for back, back_config in lb['backend'].items():
-        if 'ssl' in back_config:
-
-            if 'ca_certificate' in back_config['ssl']:
-                ca_name = back_config['ssl']['ca_certificate']
-                pki_ca_cert = lb['pki']['ca'][ca_name]
-                ca_cert_file_path = os.path.join(load_balancing_dir, f'{ca_name}.pem')
-
-                with open(ca_cert_file_path, 'w') as f:
-                    f.write(wrap_certificate(pki_ca_cert['certificate']))
-
-    render(load_balancing_conf_file, 'load-balancing/haproxy.cfg.j2', lb)
-    render(systemd_override, 'load-balancing/override_haproxy.conf.j2', lb)
-
-    return None
-
-
-def apply(lb):
-    call('systemctl daemon-reload')
-    if not lb:
-        call(f'systemctl stop {systemd_service}')
-    else:
-        call(f'systemctl reload-or-restart {systemd_service}')
-
-    return None
-
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/load-balancing-wan.py b/src/conf_mode/load-balancing-wan.py
deleted file mode 100755
index 5da0b906b..000000000
--- a/src/conf_mode/load-balancing-wan.py
+++ /dev/null
@@ -1,151 +0,0 @@
-#!/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/>.
-
-import os
-
-from sys import exit
-from shutil import rmtree
-
-from vyos.base import Warning
-from vyos.config import Config
-from vyos.configdep import set_dependents, call_dependents
-from vyos.utils.process import cmd
-from vyos.template import render
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-load_balancing_dir = '/run/load-balance'
-load_balancing_conf_file = f'{load_balancing_dir}/wlb.conf'
-systemd_service = 'vyos-wan-load-balance.service'
-
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base = ['load-balancing', 'wan']
-    lb = conf.get_config_dict(base, key_mangling=('-', '_'),
-                              no_tag_node_value_mangle=True,
-                              get_first_key=True,
-                              with_recursive_defaults=True)
-
-    # prune limit key if not set by user
-    for rule in lb.get('rule', []):
-        if lb.from_defaults(['rule', rule, 'limit']):
-            del lb['rule'][rule]['limit']
-
-    set_dependents('conntrack', conf)
-
-    return lb
-
-
-def verify(lb):
-    if not lb:
-        return None
-
-    if 'interface_health' not in lb:
-        raise ConfigError(
-            'A valid WAN load-balance configuration requires an interface with a nexthop!'
-        )
-
-    for interface, interface_config in lb['interface_health'].items():
-        if 'nexthop' not in interface_config:
-            raise ConfigError(
-                f'interface-health {interface} nexthop must be specified!')
-
-        if 'test' in interface_config:
-            for test_rule, test_config in interface_config['test'].items():
-                if 'type' in test_config:
-                    if test_config['type'] == 'user-defined' and 'test_script' not in test_config:
-                        raise ConfigError(
-                            f'test {test_rule} script must be defined for test-script!'
-                        )
-
-    if 'rule' not in lb:
-        Warning(
-            'At least one rule with an (outbound) interface must be defined for WAN load balancing to be active!'
-        )
-    else:
-        for rule, rule_config in lb['rule'].items():
-            if 'inbound_interface' not in rule_config:
-                raise ConfigError(f'rule {rule} inbound-interface must be specified!')
-            if {'failover', 'exclude'} <= set(rule_config):
-                raise ConfigError(f'rule {rule} failover cannot be configured with exclude!')
-            if {'limit', 'exclude'} <= set(rule_config):
-                raise ConfigError(f'rule {rule} limit cannot be used with exclude!')
-            if 'interface' not in rule_config:
-                if 'exclude' not in rule_config:
-                    Warning(
-                        f'rule {rule} will be inactive because no (outbound) interfaces have been defined for this rule'
-                    )
-            for direction in {'source', 'destination'}:
-                if direction in rule_config:
-                    if 'protocol' in rule_config and 'port' in rule_config[
-                            direction]:
-                        if rule_config['protocol'] not in {'tcp', 'udp'}:
-                            raise ConfigError('ports can only be specified when protocol is "tcp" or "udp"')
-
-
-def generate(lb):
-    if not lb:
-        # Delete /run/load-balance/wlb.conf
-        if os.path.isfile(load_balancing_conf_file):
-            os.unlink(load_balancing_conf_file)
-        # Delete old directories
-        if os.path.isdir(load_balancing_dir):
-            rmtree(load_balancing_dir, ignore_errors=True)
-        if os.path.exists('/var/run/load-balance/wlb.out'):
-            os.unlink('/var/run/load-balance/wlb.out')
-
-        return None
-
-    # Create load-balance dir
-    if not os.path.isdir(load_balancing_dir):
-        os.mkdir(load_balancing_dir)
-
-    render(load_balancing_conf_file, 'load-balancing/wlb.conf.j2', lb)
-
-    return None
-
-
-def apply(lb):
-    if not lb:
-        try:
-            cmd(f'systemctl stop {systemd_service}')
-        except Exception as e:
-            print(f"Error message: {e}")
-
-    else:
-        cmd('sudo sysctl -w net.netfilter.nf_conntrack_acct=1')
-        cmd(f'systemctl restart {systemd_service}')
-
-    call_dependents()
-
-    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/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_reverse-proxy.py
new file mode 100755
index 000000000..333ebc66c
--- /dev/null
+++ b/src/conf_mode/load-balancing_reverse-proxy.py
@@ -0,0 +1,169 @@
+#!/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/>.
+
+import os
+
+from sys import exit
+from shutil import rmtree
+
+from vyos.config import Config
+from vyos.utils.process import call
+from vyos.utils.network import check_port_availability
+from vyos.utils.network import is_listen_port_bind_service
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_private_key
+from vyos.template import render
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+load_balancing_dir = '/run/haproxy'
+load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg'
+systemd_service = 'haproxy.service'
+systemd_override = r'/run/systemd/system/haproxy.service.d/10-override.conf'
+
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base = ['load-balancing', 'reverse-proxy']
+    lb = conf.get_config_dict(base,
+                              get_first_key=True,
+                              key_mangling=('-', '_'),
+                              no_tag_node_value_mangle=True)
+
+    if lb:
+        lb['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+                                    get_first_key=True, no_tag_node_value_mangle=True)
+
+    if lb:
+        lb = conf.merge_defaults(lb, recursive=True)
+
+    return lb
+
+
+def verify(lb):
+    if not lb:
+        return None
+
+    if 'backend' not in lb or 'service' not in lb:
+        raise ConfigError(f'"service" and "backend" must be configured!')
+
+    for front, front_config in lb['service'].items():
+        if 'port' not in front_config:
+            raise ConfigError(f'"{front} service port" must be configured!')
+
+        # Check if bind address:port are used by another service
+        tmp_address = front_config.get('address', '0.0.0.0')
+        tmp_port = front_config['port']
+        if check_port_availability(tmp_address, int(tmp_port), 'tcp') is not True and \
+                not is_listen_port_bind_service(int(tmp_port), 'haproxy'):
+            raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service')
+
+    for back, back_config in lb['backend'].items():
+        if 'server' not in back_config:
+            raise ConfigError(f'"{back} server" must be configured!')
+        for bk_server, bk_server_conf in back_config['server'].items():
+            if 'address' not in bk_server_conf or 'port' not in bk_server_conf:
+                raise ConfigError(f'"backend {back} server {bk_server} address and port" must be configured!')
+
+            if {'send_proxy', 'send_proxy_v2'} <= set(bk_server_conf):
+                raise ConfigError(f'Cannot use both "send-proxy" and "send-proxy-v2" for server "{bk_server}"')
+
+def generate(lb):
+    if not lb:
+        # Delete /run/haproxy/haproxy.cfg
+        config_files = [load_balancing_conf_file, systemd_override]
+        for file in config_files:
+            if os.path.isfile(file):
+                os.unlink(file)
+        # Delete old directories
+        if os.path.isdir(load_balancing_dir):
+            rmtree(load_balancing_dir, ignore_errors=True)
+
+        return None
+
+    # Create load-balance dir
+    if not os.path.isdir(load_balancing_dir):
+        os.mkdir(load_balancing_dir)
+
+    # SSL Certificates for frontend
+    for front, front_config in lb['service'].items():
+        if 'ssl' in front_config:
+
+            if 'certificate' in front_config['ssl']:
+                cert_names = front_config['ssl']['certificate']
+
+                for cert_name in cert_names:
+                    pki_cert = lb['pki']['certificate'][cert_name]
+                    cert_file_path = os.path.join(load_balancing_dir, f'{cert_name}.pem')
+                    cert_key_path = os.path.join(load_balancing_dir, f'{cert_name}.pem.key')
+
+                    with open(cert_file_path, 'w') as f:
+                        f.write(wrap_certificate(pki_cert['certificate']))
+
+                    if 'private' in pki_cert and 'key' in pki_cert['private']:
+                        with open(cert_key_path, 'w') as f:
+                            f.write(wrap_private_key(pki_cert['private']['key']))
+
+            if 'ca_certificate' in front_config['ssl']:
+                ca_name = front_config['ssl']['ca_certificate']
+                pki_ca_cert = lb['pki']['ca'][ca_name]
+                ca_cert_file_path = os.path.join(load_balancing_dir, f'{ca_name}.pem')
+
+                with open(ca_cert_file_path, 'w') as f:
+                    f.write(wrap_certificate(pki_ca_cert['certificate']))
+
+    # SSL Certificates for backend
+    for back, back_config in lb['backend'].items():
+        if 'ssl' in back_config:
+
+            if 'ca_certificate' in back_config['ssl']:
+                ca_name = back_config['ssl']['ca_certificate']
+                pki_ca_cert = lb['pki']['ca'][ca_name]
+                ca_cert_file_path = os.path.join(load_balancing_dir, f'{ca_name}.pem')
+
+                with open(ca_cert_file_path, 'w') as f:
+                    f.write(wrap_certificate(pki_ca_cert['certificate']))
+
+    render(load_balancing_conf_file, 'load-balancing/haproxy.cfg.j2', lb)
+    render(systemd_override, 'load-balancing/override_haproxy.conf.j2', lb)
+
+    return None
+
+
+def apply(lb):
+    call('systemctl daemon-reload')
+    if not lb:
+        call(f'systemctl stop {systemd_service}')
+    else:
+        call(f'systemctl reload-or-restart {systemd_service}')
+
+    return None
+
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/load-balancing_wan.py b/src/conf_mode/load-balancing_wan.py
new file mode 100755
index 000000000..5da0b906b
--- /dev/null
+++ b/src/conf_mode/load-balancing_wan.py
@@ -0,0 +1,151 @@
+#!/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/>.
+
+import os
+
+from sys import exit
+from shutil import rmtree
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.configdep import set_dependents, call_dependents
+from vyos.utils.process import cmd
+from vyos.template import render
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+load_balancing_dir = '/run/load-balance'
+load_balancing_conf_file = f'{load_balancing_dir}/wlb.conf'
+systemd_service = 'vyos-wan-load-balance.service'
+
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base = ['load-balancing', 'wan']
+    lb = conf.get_config_dict(base, key_mangling=('-', '_'),
+                              no_tag_node_value_mangle=True,
+                              get_first_key=True,
+                              with_recursive_defaults=True)
+
+    # prune limit key if not set by user
+    for rule in lb.get('rule', []):
+        if lb.from_defaults(['rule', rule, 'limit']):
+            del lb['rule'][rule]['limit']
+
+    set_dependents('conntrack', conf)
+
+    return lb
+
+
+def verify(lb):
+    if not lb:
+        return None
+
+    if 'interface_health' not in lb:
+        raise ConfigError(
+            'A valid WAN load-balance configuration requires an interface with a nexthop!'
+        )
+
+    for interface, interface_config in lb['interface_health'].items():
+        if 'nexthop' not in interface_config:
+            raise ConfigError(
+                f'interface-health {interface} nexthop must be specified!')
+
+        if 'test' in interface_config:
+            for test_rule, test_config in interface_config['test'].items():
+                if 'type' in test_config:
+                    if test_config['type'] == 'user-defined' and 'test_script' not in test_config:
+                        raise ConfigError(
+                            f'test {test_rule} script must be defined for test-script!'
+                        )
+
+    if 'rule' not in lb:
+        Warning(
+            'At least one rule with an (outbound) interface must be defined for WAN load balancing to be active!'
+        )
+    else:
+        for rule, rule_config in lb['rule'].items():
+            if 'inbound_interface' not in rule_config:
+                raise ConfigError(f'rule {rule} inbound-interface must be specified!')
+            if {'failover', 'exclude'} <= set(rule_config):
+                raise ConfigError(f'rule {rule} failover cannot be configured with exclude!')
+            if {'limit', 'exclude'} <= set(rule_config):
+                raise ConfigError(f'rule {rule} limit cannot be used with exclude!')
+            if 'interface' not in rule_config:
+                if 'exclude' not in rule_config:
+                    Warning(
+                        f'rule {rule} will be inactive because no (outbound) interfaces have been defined for this rule'
+                    )
+            for direction in {'source', 'destination'}:
+                if direction in rule_config:
+                    if 'protocol' in rule_config and 'port' in rule_config[
+                            direction]:
+                        if rule_config['protocol'] not in {'tcp', 'udp'}:
+                            raise ConfigError('ports can only be specified when protocol is "tcp" or "udp"')
+
+
+def generate(lb):
+    if not lb:
+        # Delete /run/load-balance/wlb.conf
+        if os.path.isfile(load_balancing_conf_file):
+            os.unlink(load_balancing_conf_file)
+        # Delete old directories
+        if os.path.isdir(load_balancing_dir):
+            rmtree(load_balancing_dir, ignore_errors=True)
+        if os.path.exists('/var/run/load-balance/wlb.out'):
+            os.unlink('/var/run/load-balance/wlb.out')
+
+        return None
+
+    # Create load-balance dir
+    if not os.path.isdir(load_balancing_dir):
+        os.mkdir(load_balancing_dir)
+
+    render(load_balancing_conf_file, 'load-balancing/wlb.conf.j2', lb)
+
+    return None
+
+
+def apply(lb):
+    if not lb:
+        try:
+            cmd(f'systemctl stop {systemd_service}')
+        except Exception as e:
+            print(f"Error message: {e}")
+
+    else:
+        cmd('sudo sysctl -w net.netfilter.nf_conntrack_acct=1')
+        cmd(f'systemctl restart {systemd_service}')
+
+    call_dependents()
+
+    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/ntp.py b/src/conf_mode/ntp.py
deleted file mode 100755
index 1cc23a7df..000000000
--- a/src/conf_mode/ntp.py
+++ /dev/null
@@ -1,136 +0,0 @@
-#!/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 vyos.config import Config
-from vyos.configdict import is_node_changed
-from vyos.configverify import verify_vrf
-from vyos.configverify import verify_interface_exists
-from vyos.utils.process import call
-from vyos.utils.permission import chmod_750
-from vyos.utils.network import get_interface_config
-from vyos.template import render
-from vyos.template import is_ipv4
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/run/chrony/chrony.conf'
-systemd_override = r'/run/systemd/system/chrony.service.d/override.conf'
-user_group = '_chrony'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['service', 'ntp']
-    if not conf.exists(base):
-        return None
-
-    ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
-    ntp['config_file'] = config_file
-    ntp['user'] = user_group
-
-    tmp = is_node_changed(conf, base + ['vrf'])
-    if tmp: ntp.update({'restart_required': {}})
-
-    return ntp
-
-def verify(ntp):
-    # bail out early - looks like removal from running config
-    if not ntp:
-        return None
-
-    if 'server' not in ntp:
-        raise ConfigError('NTP server not configured')
-
-    verify_vrf(ntp)
-
-    if 'interface' in ntp:
-        # If ntpd should listen on a given interface, ensure it exists
-        interface = ntp['interface']
-        verify_interface_exists(interface)
-
-        # If we run in a VRF, our interface must belong to this VRF, too
-        if 'vrf' in ntp:
-            tmp = get_interface_config(interface)
-            vrf_name = ntp['vrf']
-            if 'master' not in tmp or tmp['master'] != vrf_name:
-                raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{interface}" '\
-                                  f'does not belong to this VRF!')
-
-    if 'listen_address' in ntp:
-        ipv4_addresses = 0
-        ipv6_addresses = 0
-        for address in ntp['listen_address']:
-            if is_ipv4(address):
-                ipv4_addresses += 1
-            else:
-                ipv6_addresses += 1
-        if ipv4_addresses > 1:
-            raise ConfigError(f'NTP Only admits one ipv4 value for listen-address parameter ')
-        if ipv6_addresses > 1:
-            raise ConfigError(f'NTP Only admits one ipv6 value for listen-address parameter ')
-
-    return None
-
-def generate(ntp):
-    # bail out early - looks like removal from running config
-    if not ntp:
-        return None
-
-    render(config_file, 'chrony/chrony.conf.j2', ntp, user=user_group, group=user_group)
-    render(systemd_override, 'chrony/override.conf.j2', ntp, user=user_group, group=user_group)
-
-    # Ensure proper permission for chrony command socket
-    config_dir = os.path.dirname(config_file)
-    chmod_750(config_dir)
-
-    return None
-
-def apply(ntp):
-    systemd_service = 'chrony.service'
-    # Reload systemd manager configuration
-    call('systemctl daemon-reload')
-
-    if not ntp:
-        # NTP support is removed in the commit
-        call(f'systemctl stop {systemd_service}')
-        if os.path.exists(config_file):
-            os.unlink(config_file)
-        if os.path.isfile(systemd_override):
-            os.unlink(systemd_override)
-        return
-
-    # we need to restart the service if e.g. the VRF name changed
-    systemd_action = 'reload-or-restart'
-    if 'restart_required' in ntp:
-        systemd_action = 'restart'
-
-    call(f'systemctl {systemd_action} {systemd_service}')
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index 34ba2fe69..f7e14aa16 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -36,22 +36,22 @@ sync_search = [
     {
         'keys': ['certificate'],
         'path': ['service', 'https'],
-        'script': '/usr/libexec/vyos/conf_mode/https.py'
+        'script': '/usr/libexec/vyos/conf_mode/service_https.py'
     },
     {
         'keys': ['certificate', 'ca_certificate'],
         'path': ['interfaces', 'ethernet'],
-        'script': '/usr/libexec/vyos/conf_mode/interfaces-ethernet.py'
+        'script': '/usr/libexec/vyos/conf_mode/interfaces_ethernet.py'
     },
     {
         'keys': ['certificate', 'ca_certificate', 'dh_params', 'shared_secret_key', 'auth_key', 'crypt_key'],
         'path': ['interfaces', 'openvpn'],
-        'script': '/usr/libexec/vyos/conf_mode/interfaces-openvpn.py'
+        'script': '/usr/libexec/vyos/conf_mode/interfaces_openvpn.py'
     },
     {
         'keys': ['ca_certificate'],
         'path': ['interfaces', 'sstpc'],
-        'script': '/usr/libexec/vyos/conf_mode/interfaces-sstpc.py'
+        'script': '/usr/libexec/vyos/conf_mode/interfaces_sstpc.py'
     },
     {
         'keys': ['certificate', 'ca_certificate', 'local_key', 'remote_key'],
diff --git a/src/conf_mode/policy-local-route.py b/src/conf_mode/policy-local-route.py
deleted file mode 100755
index 91e4fce2c..000000000
--- a/src/conf_mode/policy-local-route.py
+++ /dev/null
@@ -1,315 +0,0 @@
-#!/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 itertools import product
-from sys import exit
-
-from netifaces import interfaces
-from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.configdict import node_changed
-from vyos.configdict import leaf_node_changed
-from vyos.template import render
-from vyos.utils.process import call
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-
-def get_config(config=None):
-
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['policy']
-
-    pbr = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
-
-    for route in ['local_route', 'local_route6']:
-        dict_id = 'rule_remove' if route == 'local_route' else 'rule6_remove'
-        route_key = 'local-route' if route == 'local_route' else 'local-route6'
-        base_rule = base + [route_key, 'rule']
-
-        # delete policy local-route
-        dict = {}
-        tmp = node_changed(conf, base_rule, key_mangling=('-', '_'))
-        if tmp:
-            for rule in (tmp or []):
-                src = leaf_node_changed(conf, base_rule + [rule, 'source', 'address'])
-                src_port = leaf_node_changed(conf, base_rule + [rule, 'source', 'port'])
-                fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark'])
-                iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface'])
-                dst = leaf_node_changed(conf, base_rule + [rule, 'destination', 'address'])
-                dst_port = leaf_node_changed(conf, base_rule + [rule, 'destination', 'port'])
-                table = leaf_node_changed(conf, base_rule + [rule, 'set', 'table'])
-                proto = leaf_node_changed(conf, base_rule + [rule, 'protocol'])
-                rule_def = {}
-                if src:
-                    rule_def = dict_merge({'source': {'address': src}}, rule_def)
-                if src_port:
-                    rule_def = dict_merge({'source': {'port': src_port}}, rule_def)
-                if fwmk:
-                    rule_def = dict_merge({'fwmark' : fwmk}, rule_def)
-                if iif:
-                    rule_def = dict_merge({'inbound_interface' : iif}, rule_def)
-                if dst:
-                    rule_def = dict_merge({'destination': {'address': dst}}, rule_def)
-                if dst_port:
-                    rule_def = dict_merge({'destination': {'port': dst_port}}, rule_def)
-                if table:
-                    rule_def = dict_merge({'table' : table}, rule_def)
-                if proto:
-                    rule_def = dict_merge({'protocol' : proto}, rule_def)
-                dict = dict_merge({dict_id : {rule : rule_def}}, dict)
-                pbr.update(dict)
-
-        if not route in pbr:
-            continue
-
-        # delete policy local-route rule x source x.x.x.x
-        # delete policy local-route rule x fwmark x
-        # delete policy local-route rule x destination x.x.x.x
-        if 'rule' in pbr[route]:
-            for rule, rule_config in pbr[route]['rule'].items():
-                src = leaf_node_changed(conf, base_rule + [rule, 'source', 'address'])
-                src_port = leaf_node_changed(conf, base_rule + [rule, 'source', 'port'])
-                fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark'])
-                iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface'])
-                dst = leaf_node_changed(conf, base_rule + [rule, 'destination', 'address'])
-                dst_port = leaf_node_changed(conf, base_rule + [rule, 'destination', 'port'])
-                table = leaf_node_changed(conf, base_rule + [rule, 'set', 'table'])
-                proto = leaf_node_changed(conf, base_rule + [rule, 'protocol'])
-                # keep track of changes in configuration
-                # otherwise we might remove an existing node although nothing else has changed
-                changed = False
-
-                rule_def = {}
-                # src is None if there are no changes to src
-                if src is None:
-                    # if src hasn't changed, include it in the removal selector
-                    # if a new selector is added, we have to remove all previous rules without this selector
-                    # to make sure we remove all previous rules with this source(s), it will be included
-                    if 'source' in rule_config:
-                        if 'address' in rule_config['source']:
-                            rule_def = dict_merge({'source': {'address': rule_config['source']['address']}}, rule_def)
-                else:
-                    # if src is not None, it's previous content will be returned
-                    # this can be an empty array if it's just being set, or the previous value
-                    # either way, something has to be changed and we only want to remove previous values
-                    changed = True
-                    # set the old value for removal if it's not empty
-                    if len(src) > 0:
-                        rule_def = dict_merge({'source': {'address': src}}, rule_def)
-
-                # source port
-                if src_port is None:
-                    if 'source' in rule_config:
-                        if 'port' in rule_config['source']:
-                            tmp = rule_config['source']['port']
-                            if isinstance(tmp, str):
-                                tmp = [tmp]
-                            rule_def = dict_merge({'source': {'port': tmp}}, rule_def)
-                else:
-                    changed = True
-                    if len(src_port) > 0:
-                        rule_def = dict_merge({'source': {'port': src_port}}, rule_def)
-
-                # fwmark
-                if fwmk is None:
-                    if 'fwmark' in rule_config:
-                        tmp = rule_config['fwmark']
-                        if isinstance(tmp, str):
-                            tmp = [tmp]
-                        rule_def = dict_merge({'fwmark': tmp}, rule_def)
-                else:
-                    changed = True
-                    if len(fwmk) > 0:
-                        rule_def = dict_merge({'fwmark' : fwmk}, rule_def)
-
-                # inbound-interface
-                if iif is None:
-                    if 'inbound_interface' in rule_config:
-                        rule_def = dict_merge({'inbound_interface': rule_config['inbound_interface']}, rule_def)
-                else:
-                    changed = True
-                    if len(iif) > 0:
-                        rule_def = dict_merge({'inbound_interface' : iif}, rule_def)
-
-                # destination address
-                if dst is None:
-                    if 'destination' in rule_config:
-                        if 'address' in rule_config['destination']:
-                            rule_def = dict_merge({'destination': {'address': rule_config['destination']['address']}}, rule_def)
-                else:
-                    changed = True
-                    if len(dst) > 0:
-                        rule_def = dict_merge({'destination': {'address': dst}}, rule_def)
-
-                # destination port
-                if dst_port is None:
-                    if 'destination' in rule_config:
-                        if 'port' in rule_config['destination']:
-                            tmp = rule_config['destination']['port']
-                            if isinstance(tmp, str):
-                                tmp = [tmp]
-                            rule_def = dict_merge({'destination': {'port': tmp}}, rule_def)
-                else:
-                    changed = True
-                    if len(dst_port) > 0:
-                        rule_def = dict_merge({'destination': {'port': dst_port}}, rule_def)
-
-                # table
-                if table is None:
-                    if 'set' in rule_config and 'table' in rule_config['set']:
-                        rule_def = dict_merge({'table': [rule_config['set']['table']]}, rule_def)
-                else:
-                    changed = True
-                    if len(table) > 0:
-                        rule_def = dict_merge({'table' : table}, rule_def)
-
-                # protocol
-                if proto is None:
-                    if 'protocol' in rule_config:
-                        tmp = rule_config['protocol']
-                        if isinstance(tmp, str):
-                            tmp = [tmp]
-                        rule_def = dict_merge({'protocol': tmp}, rule_def)
-                else:
-                    changed = True
-                    if len(proto) > 0:
-                        rule_def = dict_merge({'protocol' : proto}, rule_def)
-
-                if changed:
-                    dict = dict_merge({dict_id : {rule : rule_def}}, dict)
-                    pbr.update(dict)
-
-    return pbr
-
-def verify(pbr):
-    # bail out early - looks like removal from running config
-    if not pbr:
-        return None
-
-    for route in ['local_route', 'local_route6']:
-        if not route in pbr:
-            continue
-
-        pbr_route = pbr[route]
-        if 'rule' in pbr_route:
-            for rule in pbr_route['rule']:
-                if (
-                    'source' not in pbr_route['rule'][rule] and
-                    'destination' not in pbr_route['rule'][rule] and
-                    'fwmark' not in pbr_route['rule'][rule] and
-                    'inbound_interface' not in pbr_route['rule'][rule] and
-                    'protocol' not in pbr_route['rule'][rule]
-                ):
-                    raise ConfigError('Source or destination address or fwmark or inbound-interface or protocol is required!')
-
-                if 'set' not in pbr_route['rule'][rule] or 'table' not in pbr_route['rule'][rule]['set']:
-                    raise ConfigError('Table set is required!')
-
-                if 'inbound_interface' in pbr_route['rule'][rule]:
-                    interface = pbr_route['rule'][rule]['inbound_interface']
-                    if interface not in interfaces():
-                        raise ConfigError(f'Interface "{interface}" does not exist')
-
-    return None
-
-def generate(pbr):
-    if not pbr:
-        return None
-
-    return None
-
-def apply(pbr):
-    if not pbr:
-        return None
-
-    # Delete old rule if needed
-    for rule_rm in ['rule_remove', 'rule6_remove']:
-        if rule_rm in pbr:
-            v6 = " -6" if rule_rm == 'rule6_remove' else ""
-
-            for rule, rule_config in pbr[rule_rm].items():
-                source = rule_config.get('source', {}).get('address', [''])
-                source_port = rule_config.get('source', {}).get('port', [''])
-                destination = rule_config.get('destination', {}).get('address', [''])
-                destination_port = rule_config.get('destination', {}).get('port', [''])
-                fwmark = rule_config.get('fwmark', [''])
-                inbound_interface = rule_config.get('inbound_interface', [''])
-                protocol = rule_config.get('protocol', [''])
-                table = rule_config.get('table', [''])
-
-                for src, dst, src_port, dst_port, fwmk, iif, proto, table in product(
-                        source, destination, source_port, destination_port,
-                        fwmark, inbound_interface, protocol, table):
-                    f_src = '' if src == '' else f' from {src} '
-                    f_src_port = '' if src_port == '' else f' sport {src_port} '
-                    f_dst = '' if dst == '' else f' to {dst} '
-                    f_dst_port = '' if dst_port == '' else f' dport {dst_port} '
-                    f_fwmk = '' if fwmk == '' else f' fwmark {fwmk} '
-                    f_iif = '' if iif == '' else f' iif {iif} '
-                    f_proto = '' if proto == '' else f' ipproto {proto} '
-                    f_table = '' if table == '' else f' lookup {table} '
-
-                    call(f'ip{v6} rule del prio {rule} {f_src}{f_dst}{f_proto}{f_src_port}{f_dst_port}{f_fwmk}{f_iif}{f_table}')
-
-    # Generate new config
-    for route in ['local_route', 'local_route6']:
-        if not route in pbr:
-            continue
-
-        v6 = " -6" if route == 'local_route6' else ""
-        pbr_route = pbr[route]
-
-        if 'rule' in pbr_route:
-            for rule, rule_config in pbr_route['rule'].items():
-                table = rule_config['set'].get('table', '')
-                source = rule_config.get('source', {}).get('address', ['all'])
-                source_port = rule_config.get('source', {}).get('port', '')
-                destination = rule_config.get('destination', {}).get('address', ['all'])
-                destination_port = rule_config.get('destination', {}).get('port', '')
-                fwmark = rule_config.get('fwmark', '')
-                inbound_interface = rule_config.get('inbound_interface', '')
-                protocol = rule_config.get('protocol', '')
-
-                for src in source:
-                    f_src = f' from {src} ' if src else ''
-                    for dst in destination:
-                        f_dst = f' to {dst} ' if dst else ''
-                        f_src_port = f' sport {source_port} ' if source_port else ''
-                        f_dst_port = f' dport {destination_port} ' if destination_port else ''
-                        f_fwmk = f' fwmark {fwmark} ' if fwmark else ''
-                        f_iif = f' iif {inbound_interface} ' if inbound_interface else ''
-                        f_proto = f' ipproto {protocol} ' if protocol else ''
-
-                        call(f'ip{v6} rule add prio {rule}{f_src}{f_dst}{f_proto}{f_src_port}{f_dst_port}{f_fwmk}{f_iif} lookup {table}')
-
-    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/policy-route.py b/src/conf_mode/policy-route.py
deleted file mode 100755
index adad012de..000000000
--- a/src/conf_mode/policy-route.py
+++ /dev/null
@@ -1,195 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2021-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 json import loads
-from sys import exit
-
-from vyos.base import Warning
-from vyos.config import Config
-from vyos.template import render
-from vyos.utils.dict import dict_search_args
-from vyos.utils.process import cmd
-from vyos.utils.process import run
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-mark_offset = 0x7FFFFFFF
-nftables_conf = '/run/nftables_policy.conf'
-
-valid_groups = [
-    'address_group',
-    'domain_group',
-    'network_group',
-    'port_group',
-    'interface_group'
-]
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['policy']
-
-    policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
-                                    no_tag_node_value_mangle=True)
-
-    policy['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True,
-                                    no_tag_node_value_mangle=True)
-
-    return policy
-
-def verify_rule(policy, name, rule_conf, ipv6, rule_id):
-    icmp = 'icmp' if not ipv6 else 'icmpv6'
-    if icmp in rule_conf:
-        icmp_defined = False
-        if 'type_name' in rule_conf[icmp]:
-            icmp_defined = True
-            if 'code' in rule_conf[icmp] or 'type' in rule_conf[icmp]:
-                raise ConfigError(f'{name} rule {rule_id}: Cannot use ICMP type/code with ICMP type-name')
-        if 'code' in rule_conf[icmp]:
-            icmp_defined = True
-            if 'type' not in rule_conf[icmp]:
-                raise ConfigError(f'{name} rule {rule_id}: ICMP code can only be defined if ICMP type is defined')
-        if 'type' in rule_conf[icmp]:
-            icmp_defined = True
-
-        if icmp_defined and 'protocol' not in rule_conf or rule_conf['protocol'] != icmp:
-            raise ConfigError(f'{name} rule {rule_id}: ICMP type/code or type-name can only be defined if protocol is ICMP')
-
-    if 'set' in rule_conf:
-        if 'tcp_mss' in rule_conf['set']:
-            tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
-            if not tcp_flags or 'syn' not in tcp_flags:
-                raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS')
-
-    tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
-    if tcp_flags:
-        if dict_search_args(rule_conf, 'protocol') != 'tcp':
-            raise ConfigError('Protocol must be tcp when specifying tcp flags')
-
-        not_flags = dict_search_args(rule_conf, 'tcp', 'flags', 'not')
-        if not_flags:
-            duplicates = [flag for flag in tcp_flags if flag in not_flags]
-            if duplicates:
-                raise ConfigError(f'Cannot match a tcp flag as set and not set')
-
-    for side in ['destination', 'source']:
-        if side in rule_conf:
-            side_conf = rule_conf[side]
-
-            if 'group' in side_conf:
-                if len({'address_group', 'domain_group', 'network_group'} & set(side_conf['group'])) > 1:
-                    raise ConfigError('Only one address-group, domain-group or network-group can be specified')
-
-                for group in valid_groups:
-                    if group in side_conf['group']:
-                        group_name = side_conf['group'][group]
-
-                        if group_name.startswith('!'):
-                            group_name = group_name[1:]
-
-                        fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group
-                        error_group = fw_group.replace("_", "-")
-                        group_obj = dict_search_args(policy['firewall_group'], fw_group, group_name)
-
-                        if group_obj is None:
-                            raise ConfigError(f'Invalid {error_group} "{group_name}" on policy route rule')
-
-                        if not group_obj:
-                            Warning(f'{error_group} "{group_name}" has no members')
-
-            if 'port' in side_conf or dict_search_args(side_conf, 'group', 'port_group'):
-                if 'protocol' not in rule_conf:
-                    raise ConfigError('Protocol must be defined if specifying a port or port-group')
-
-                if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
-                    raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group')
-
-def verify(policy):
-    for route in ['route', 'route6']:
-        ipv6 = route == 'route6'
-        if route in policy:
-            for name, pol_conf in policy[route].items():
-                if 'rule' in pol_conf:
-                    for rule_id, rule_conf in pol_conf['rule'].items():
-                        verify_rule(policy, name, rule_conf, ipv6, rule_id)
-
-    return None
-
-def generate(policy):
-    if not os.path.exists(nftables_conf):
-        policy['first_install'] = True
-
-    render(nftables_conf, 'firewall/nftables-policy.j2', policy)
-    return None
-
-def apply_table_marks(policy):
-    for route in ['route', 'route6']:
-        if route in policy:
-            cmd_str = 'ip' if route == 'route' else 'ip -6'
-            tables = []
-            for name, pol_conf in policy[route].items():
-                if 'rule' in pol_conf:
-                    for rule_id, rule_conf in pol_conf['rule'].items():
-                        set_table = dict_search_args(rule_conf, 'set', 'table')
-                        if set_table:
-                            if set_table == 'main':
-                                set_table = '254'
-                            if set_table in tables:
-                                continue
-                            tables.append(set_table)
-                            table_mark = mark_offset - int(set_table)
-                            cmd(f'{cmd_str} rule add pref {set_table} fwmark {table_mark} table {set_table}')
-
-def cleanup_table_marks():
-    for cmd_str in ['ip', 'ip -6']:
-        json_rules = cmd(f'{cmd_str} -j -N rule list')
-        rules = loads(json_rules)
-        for rule in rules:
-            if 'fwmark' not in rule or 'table' not in rule:
-                continue
-            fwmark = rule['fwmark']
-            table = int(rule['table'])
-            if fwmark[:2] == '0x':
-                fwmark = int(fwmark, 16)
-            if (int(fwmark) == (mark_offset - table)):
-                cmd(f'{cmd_str} rule del fwmark {fwmark} table {table}')
-
-def apply(policy):
-    install_result = run(f'nft -f {nftables_conf}')
-    if install_result == 1:
-        raise ConfigError('Failed to apply policy based routing')
-
-    if 'first_install' not in policy:
-        cleanup_table_marks()
-
-    apply_table_marks(policy)
-
-    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/policy_local-route.py b/src/conf_mode/policy_local-route.py
new file mode 100755
index 000000000..91e4fce2c
--- /dev/null
+++ b/src/conf_mode/policy_local-route.py
@@ -0,0 +1,315 @@
+#!/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 itertools import product
+from sys import exit
+
+from netifaces import interfaces
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configdict import node_changed
+from vyos.configdict import leaf_node_changed
+from vyos.template import render
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+
+def get_config(config=None):
+
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['policy']
+
+    pbr = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+    for route in ['local_route', 'local_route6']:
+        dict_id = 'rule_remove' if route == 'local_route' else 'rule6_remove'
+        route_key = 'local-route' if route == 'local_route' else 'local-route6'
+        base_rule = base + [route_key, 'rule']
+
+        # delete policy local-route
+        dict = {}
+        tmp = node_changed(conf, base_rule, key_mangling=('-', '_'))
+        if tmp:
+            for rule in (tmp or []):
+                src = leaf_node_changed(conf, base_rule + [rule, 'source', 'address'])
+                src_port = leaf_node_changed(conf, base_rule + [rule, 'source', 'port'])
+                fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark'])
+                iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface'])
+                dst = leaf_node_changed(conf, base_rule + [rule, 'destination', 'address'])
+                dst_port = leaf_node_changed(conf, base_rule + [rule, 'destination', 'port'])
+                table = leaf_node_changed(conf, base_rule + [rule, 'set', 'table'])
+                proto = leaf_node_changed(conf, base_rule + [rule, 'protocol'])
+                rule_def = {}
+                if src:
+                    rule_def = dict_merge({'source': {'address': src}}, rule_def)
+                if src_port:
+                    rule_def = dict_merge({'source': {'port': src_port}}, rule_def)
+                if fwmk:
+                    rule_def = dict_merge({'fwmark' : fwmk}, rule_def)
+                if iif:
+                    rule_def = dict_merge({'inbound_interface' : iif}, rule_def)
+                if dst:
+                    rule_def = dict_merge({'destination': {'address': dst}}, rule_def)
+                if dst_port:
+                    rule_def = dict_merge({'destination': {'port': dst_port}}, rule_def)
+                if table:
+                    rule_def = dict_merge({'table' : table}, rule_def)
+                if proto:
+                    rule_def = dict_merge({'protocol' : proto}, rule_def)
+                dict = dict_merge({dict_id : {rule : rule_def}}, dict)
+                pbr.update(dict)
+
+        if not route in pbr:
+            continue
+
+        # delete policy local-route rule x source x.x.x.x
+        # delete policy local-route rule x fwmark x
+        # delete policy local-route rule x destination x.x.x.x
+        if 'rule' in pbr[route]:
+            for rule, rule_config in pbr[route]['rule'].items():
+                src = leaf_node_changed(conf, base_rule + [rule, 'source', 'address'])
+                src_port = leaf_node_changed(conf, base_rule + [rule, 'source', 'port'])
+                fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark'])
+                iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface'])
+                dst = leaf_node_changed(conf, base_rule + [rule, 'destination', 'address'])
+                dst_port = leaf_node_changed(conf, base_rule + [rule, 'destination', 'port'])
+                table = leaf_node_changed(conf, base_rule + [rule, 'set', 'table'])
+                proto = leaf_node_changed(conf, base_rule + [rule, 'protocol'])
+                # keep track of changes in configuration
+                # otherwise we might remove an existing node although nothing else has changed
+                changed = False
+
+                rule_def = {}
+                # src is None if there are no changes to src
+                if src is None:
+                    # if src hasn't changed, include it in the removal selector
+                    # if a new selector is added, we have to remove all previous rules without this selector
+                    # to make sure we remove all previous rules with this source(s), it will be included
+                    if 'source' in rule_config:
+                        if 'address' in rule_config['source']:
+                            rule_def = dict_merge({'source': {'address': rule_config['source']['address']}}, rule_def)
+                else:
+                    # if src is not None, it's previous content will be returned
+                    # this can be an empty array if it's just being set, or the previous value
+                    # either way, something has to be changed and we only want to remove previous values
+                    changed = True
+                    # set the old value for removal if it's not empty
+                    if len(src) > 0:
+                        rule_def = dict_merge({'source': {'address': src}}, rule_def)
+
+                # source port
+                if src_port is None:
+                    if 'source' in rule_config:
+                        if 'port' in rule_config['source']:
+                            tmp = rule_config['source']['port']
+                            if isinstance(tmp, str):
+                                tmp = [tmp]
+                            rule_def = dict_merge({'source': {'port': tmp}}, rule_def)
+                else:
+                    changed = True
+                    if len(src_port) > 0:
+                        rule_def = dict_merge({'source': {'port': src_port}}, rule_def)
+
+                # fwmark
+                if fwmk is None:
+                    if 'fwmark' in rule_config:
+                        tmp = rule_config['fwmark']
+                        if isinstance(tmp, str):
+                            tmp = [tmp]
+                        rule_def = dict_merge({'fwmark': tmp}, rule_def)
+                else:
+                    changed = True
+                    if len(fwmk) > 0:
+                        rule_def = dict_merge({'fwmark' : fwmk}, rule_def)
+
+                # inbound-interface
+                if iif is None:
+                    if 'inbound_interface' in rule_config:
+                        rule_def = dict_merge({'inbound_interface': rule_config['inbound_interface']}, rule_def)
+                else:
+                    changed = True
+                    if len(iif) > 0:
+                        rule_def = dict_merge({'inbound_interface' : iif}, rule_def)
+
+                # destination address
+                if dst is None:
+                    if 'destination' in rule_config:
+                        if 'address' in rule_config['destination']:
+                            rule_def = dict_merge({'destination': {'address': rule_config['destination']['address']}}, rule_def)
+                else:
+                    changed = True
+                    if len(dst) > 0:
+                        rule_def = dict_merge({'destination': {'address': dst}}, rule_def)
+
+                # destination port
+                if dst_port is None:
+                    if 'destination' in rule_config:
+                        if 'port' in rule_config['destination']:
+                            tmp = rule_config['destination']['port']
+                            if isinstance(tmp, str):
+                                tmp = [tmp]
+                            rule_def = dict_merge({'destination': {'port': tmp}}, rule_def)
+                else:
+                    changed = True
+                    if len(dst_port) > 0:
+                        rule_def = dict_merge({'destination': {'port': dst_port}}, rule_def)
+
+                # table
+                if table is None:
+                    if 'set' in rule_config and 'table' in rule_config['set']:
+                        rule_def = dict_merge({'table': [rule_config['set']['table']]}, rule_def)
+                else:
+                    changed = True
+                    if len(table) > 0:
+                        rule_def = dict_merge({'table' : table}, rule_def)
+
+                # protocol
+                if proto is None:
+                    if 'protocol' in rule_config:
+                        tmp = rule_config['protocol']
+                        if isinstance(tmp, str):
+                            tmp = [tmp]
+                        rule_def = dict_merge({'protocol': tmp}, rule_def)
+                else:
+                    changed = True
+                    if len(proto) > 0:
+                        rule_def = dict_merge({'protocol' : proto}, rule_def)
+
+                if changed:
+                    dict = dict_merge({dict_id : {rule : rule_def}}, dict)
+                    pbr.update(dict)
+
+    return pbr
+
+def verify(pbr):
+    # bail out early - looks like removal from running config
+    if not pbr:
+        return None
+
+    for route in ['local_route', 'local_route6']:
+        if not route in pbr:
+            continue
+
+        pbr_route = pbr[route]
+        if 'rule' in pbr_route:
+            for rule in pbr_route['rule']:
+                if (
+                    'source' not in pbr_route['rule'][rule] and
+                    'destination' not in pbr_route['rule'][rule] and
+                    'fwmark' not in pbr_route['rule'][rule] and
+                    'inbound_interface' not in pbr_route['rule'][rule] and
+                    'protocol' not in pbr_route['rule'][rule]
+                ):
+                    raise ConfigError('Source or destination address or fwmark or inbound-interface or protocol is required!')
+
+                if 'set' not in pbr_route['rule'][rule] or 'table' not in pbr_route['rule'][rule]['set']:
+                    raise ConfigError('Table set is required!')
+
+                if 'inbound_interface' in pbr_route['rule'][rule]:
+                    interface = pbr_route['rule'][rule]['inbound_interface']
+                    if interface not in interfaces():
+                        raise ConfigError(f'Interface "{interface}" does not exist')
+
+    return None
+
+def generate(pbr):
+    if not pbr:
+        return None
+
+    return None
+
+def apply(pbr):
+    if not pbr:
+        return None
+
+    # Delete old rule if needed
+    for rule_rm in ['rule_remove', 'rule6_remove']:
+        if rule_rm in pbr:
+            v6 = " -6" if rule_rm == 'rule6_remove' else ""
+
+            for rule, rule_config in pbr[rule_rm].items():
+                source = rule_config.get('source', {}).get('address', [''])
+                source_port = rule_config.get('source', {}).get('port', [''])
+                destination = rule_config.get('destination', {}).get('address', [''])
+                destination_port = rule_config.get('destination', {}).get('port', [''])
+                fwmark = rule_config.get('fwmark', [''])
+                inbound_interface = rule_config.get('inbound_interface', [''])
+                protocol = rule_config.get('protocol', [''])
+                table = rule_config.get('table', [''])
+
+                for src, dst, src_port, dst_port, fwmk, iif, proto, table in product(
+                        source, destination, source_port, destination_port,
+                        fwmark, inbound_interface, protocol, table):
+                    f_src = '' if src == '' else f' from {src} '
+                    f_src_port = '' if src_port == '' else f' sport {src_port} '
+                    f_dst = '' if dst == '' else f' to {dst} '
+                    f_dst_port = '' if dst_port == '' else f' dport {dst_port} '
+                    f_fwmk = '' if fwmk == '' else f' fwmark {fwmk} '
+                    f_iif = '' if iif == '' else f' iif {iif} '
+                    f_proto = '' if proto == '' else f' ipproto {proto} '
+                    f_table = '' if table == '' else f' lookup {table} '
+
+                    call(f'ip{v6} rule del prio {rule} {f_src}{f_dst}{f_proto}{f_src_port}{f_dst_port}{f_fwmk}{f_iif}{f_table}')
+
+    # Generate new config
+    for route in ['local_route', 'local_route6']:
+        if not route in pbr:
+            continue
+
+        v6 = " -6" if route == 'local_route6' else ""
+        pbr_route = pbr[route]
+
+        if 'rule' in pbr_route:
+            for rule, rule_config in pbr_route['rule'].items():
+                table = rule_config['set'].get('table', '')
+                source = rule_config.get('source', {}).get('address', ['all'])
+                source_port = rule_config.get('source', {}).get('port', '')
+                destination = rule_config.get('destination', {}).get('address', ['all'])
+                destination_port = rule_config.get('destination', {}).get('port', '')
+                fwmark = rule_config.get('fwmark', '')
+                inbound_interface = rule_config.get('inbound_interface', '')
+                protocol = rule_config.get('protocol', '')
+
+                for src in source:
+                    f_src = f' from {src} ' if src else ''
+                    for dst in destination:
+                        f_dst = f' to {dst} ' if dst else ''
+                        f_src_port = f' sport {source_port} ' if source_port else ''
+                        f_dst_port = f' dport {destination_port} ' if destination_port else ''
+                        f_fwmk = f' fwmark {fwmark} ' if fwmark else ''
+                        f_iif = f' iif {inbound_interface} ' if inbound_interface else ''
+                        f_proto = f' ipproto {protocol} ' if protocol else ''
+
+                        call(f'ip{v6} rule add prio {rule}{f_src}{f_dst}{f_proto}{f_src_port}{f_dst_port}{f_fwmk}{f_iif} lookup {table}')
+
+    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/policy_route.py b/src/conf_mode/policy_route.py
new file mode 100755
index 000000000..adad012de
--- /dev/null
+++ b/src/conf_mode/policy_route.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-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 json import loads
+from sys import exit
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.template import render
+from vyos.utils.dict import dict_search_args
+from vyos.utils.process import cmd
+from vyos.utils.process import run
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+mark_offset = 0x7FFFFFFF
+nftables_conf = '/run/nftables_policy.conf'
+
+valid_groups = [
+    'address_group',
+    'domain_group',
+    'network_group',
+    'port_group',
+    'interface_group'
+]
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['policy']
+
+    policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
+                                    no_tag_node_value_mangle=True)
+
+    policy['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True,
+                                    no_tag_node_value_mangle=True)
+
+    return policy
+
+def verify_rule(policy, name, rule_conf, ipv6, rule_id):
+    icmp = 'icmp' if not ipv6 else 'icmpv6'
+    if icmp in rule_conf:
+        icmp_defined = False
+        if 'type_name' in rule_conf[icmp]:
+            icmp_defined = True
+            if 'code' in rule_conf[icmp] or 'type' in rule_conf[icmp]:
+                raise ConfigError(f'{name} rule {rule_id}: Cannot use ICMP type/code with ICMP type-name')
+        if 'code' in rule_conf[icmp]:
+            icmp_defined = True
+            if 'type' not in rule_conf[icmp]:
+                raise ConfigError(f'{name} rule {rule_id}: ICMP code can only be defined if ICMP type is defined')
+        if 'type' in rule_conf[icmp]:
+            icmp_defined = True
+
+        if icmp_defined and 'protocol' not in rule_conf or rule_conf['protocol'] != icmp:
+            raise ConfigError(f'{name} rule {rule_id}: ICMP type/code or type-name can only be defined if protocol is ICMP')
+
+    if 'set' in rule_conf:
+        if 'tcp_mss' in rule_conf['set']:
+            tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
+            if not tcp_flags or 'syn' not in tcp_flags:
+                raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS')
+
+    tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
+    if tcp_flags:
+        if dict_search_args(rule_conf, 'protocol') != 'tcp':
+            raise ConfigError('Protocol must be tcp when specifying tcp flags')
+
+        not_flags = dict_search_args(rule_conf, 'tcp', 'flags', 'not')
+        if not_flags:
+            duplicates = [flag for flag in tcp_flags if flag in not_flags]
+            if duplicates:
+                raise ConfigError(f'Cannot match a tcp flag as set and not set')
+
+    for side in ['destination', 'source']:
+        if side in rule_conf:
+            side_conf = rule_conf[side]
+
+            if 'group' in side_conf:
+                if len({'address_group', 'domain_group', 'network_group'} & set(side_conf['group'])) > 1:
+                    raise ConfigError('Only one address-group, domain-group or network-group can be specified')
+
+                for group in valid_groups:
+                    if group in side_conf['group']:
+                        group_name = side_conf['group'][group]
+
+                        if group_name.startswith('!'):
+                            group_name = group_name[1:]
+
+                        fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group
+                        error_group = fw_group.replace("_", "-")
+                        group_obj = dict_search_args(policy['firewall_group'], fw_group, group_name)
+
+                        if group_obj is None:
+                            raise ConfigError(f'Invalid {error_group} "{group_name}" on policy route rule')
+
+                        if not group_obj:
+                            Warning(f'{error_group} "{group_name}" has no members')
+
+            if 'port' in side_conf or dict_search_args(side_conf, 'group', 'port_group'):
+                if 'protocol' not in rule_conf:
+                    raise ConfigError('Protocol must be defined if specifying a port or port-group')
+
+                if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
+                    raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group')
+
+def verify(policy):
+    for route in ['route', 'route6']:
+        ipv6 = route == 'route6'
+        if route in policy:
+            for name, pol_conf in policy[route].items():
+                if 'rule' in pol_conf:
+                    for rule_id, rule_conf in pol_conf['rule'].items():
+                        verify_rule(policy, name, rule_conf, ipv6, rule_id)
+
+    return None
+
+def generate(policy):
+    if not os.path.exists(nftables_conf):
+        policy['first_install'] = True
+
+    render(nftables_conf, 'firewall/nftables-policy.j2', policy)
+    return None
+
+def apply_table_marks(policy):
+    for route in ['route', 'route6']:
+        if route in policy:
+            cmd_str = 'ip' if route == 'route' else 'ip -6'
+            tables = []
+            for name, pol_conf in policy[route].items():
+                if 'rule' in pol_conf:
+                    for rule_id, rule_conf in pol_conf['rule'].items():
+                        set_table = dict_search_args(rule_conf, 'set', 'table')
+                        if set_table:
+                            if set_table == 'main':
+                                set_table = '254'
+                            if set_table in tables:
+                                continue
+                            tables.append(set_table)
+                            table_mark = mark_offset - int(set_table)
+                            cmd(f'{cmd_str} rule add pref {set_table} fwmark {table_mark} table {set_table}')
+
+def cleanup_table_marks():
+    for cmd_str in ['ip', 'ip -6']:
+        json_rules = cmd(f'{cmd_str} -j -N rule list')
+        rules = loads(json_rules)
+        for rule in rules:
+            if 'fwmark' not in rule or 'table' not in rule:
+                continue
+            fwmark = rule['fwmark']
+            table = int(rule['table'])
+            if fwmark[:2] == '0x':
+                fwmark = int(fwmark, 16)
+            if (int(fwmark) == (mark_offset - table)):
+                cmd(f'{cmd_str} rule del fwmark {fwmark} table {table}')
+
+def apply(policy):
+    install_result = run(f'nft -f {nftables_conf}')
+    if install_result == 1:
+        raise ConfigError('Failed to apply policy based routing')
+
+    if 'first_install' not in policy:
+        cleanup_table_marks()
+
+    apply_table_marks(policy)
+
+    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/protocols_igmp-proxy.py b/src/conf_mode/protocols_igmp-proxy.py
new file mode 100755
index 000000000..40db417dd
--- /dev/null
+++ b/src/conf_mode/protocols_igmp-proxy.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 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 sys import exit
+from netifaces import interfaces
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.template import render
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/etc/igmpproxy.conf'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base = ['protocols', 'igmp-proxy']
+    igmp_proxy = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                      get_first_key=True,
+                                      with_defaults=True)
+
+    if conf.exists(['protocols', 'igmp']):
+        igmp_proxy.update({'igmp_configured': ''})
+
+    if conf.exists(['protocols', 'pim']):
+        igmp_proxy.update({'pim_configured': ''})
+
+    return igmp_proxy
+
+def verify(igmp_proxy):
+    # bail out early - looks like removal from running config
+    if not igmp_proxy or 'disable' in igmp_proxy:
+        return None
+
+    if 'igmp_configured' in igmp_proxy or 'pim_configured' in igmp_proxy:
+        raise ConfigError('Can not configure both IGMP proxy and PIM '\
+                          'at the same time')
+
+    # at least two interfaces are required, one upstream and one downstream
+    if 'interface' not in igmp_proxy or len(igmp_proxy['interface']) < 2:
+        raise ConfigError('Must define exactly one upstream and at least one ' \
+                          'downstream interface!')
+
+    upstream = 0
+    for interface, config in igmp_proxy['interface'].items():
+        if interface not in interfaces():
+            raise ConfigError(f'Interface "{interface}" does not exist')
+        if dict_search('role', config) == 'upstream':
+            upstream += 1
+
+    if upstream == 0:
+        raise ConfigError('At least 1 upstream interface is required!')
+    elif upstream > 1:
+        raise ConfigError('Only 1 upstream interface allowed!')
+
+    return None
+
+def generate(igmp_proxy):
+    # bail out early - looks like removal from running config
+    if not igmp_proxy:
+        return None
+
+    # bail out early - service is disabled, but inform user
+    if 'disable' in igmp_proxy:
+        Warning('IGMP Proxy will be deactivated because it is disabled')
+        return None
+
+    render(config_file, 'igmp-proxy/igmpproxy.conf.j2', igmp_proxy)
+
+    return None
+
+def apply(igmp_proxy):
+    if not igmp_proxy or 'disable' in igmp_proxy:
+         # IGMP Proxy support is removed in the commit
+         call('systemctl stop igmpproxy.service')
+         if os.path.exists(config_file):
+             os.unlink(config_file)
+    else:
+        call('systemctl restart igmpproxy.service')
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/protocols_segment-routing.py b/src/conf_mode/protocols_segment-routing.py
new file mode 100755
index 000000000..d865c2ac0
--- /dev/null
+++ b/src/conf_mode/protocols_segment-routing.py
@@ -0,0 +1,118 @@
+#!/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/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import node_changed
+from vyos.template import render_to_string
+from vyos.utils.dict import dict_search
+from vyos.utils.system import sysctl_write
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base = ['protocols', 'segment-routing']
+    sr = conf.get_config_dict(base, key_mangling=('-', '_'),
+                              get_first_key=True,
+                              no_tag_node_value_mangle=True,
+                              with_recursive_defaults=True)
+
+    # FRR has VRF support for different routing daemons. As interfaces belong
+    # to VRFs - or the global VRF, we need to check for changed interfaces so
+    # that they will be properly rendered for the FRR config. Also this eases
+    # removal of interfaces from the running configuration.
+    interfaces_removed = node_changed(conf, base + ['interface'])
+    if interfaces_removed:
+        sr['interface_removed'] = list(interfaces_removed)
+
+    import pprint
+    pprint.pprint(sr)
+    return sr
+
+def verify(sr):
+    if 'srv6' in sr:
+        srv6_enable = False
+        if 'interface' in sr:
+            for interface, interface_config in sr['interface'].items():
+                if 'srv6' in interface_config:
+                    srv6_enable = True
+                    break
+        if not srv6_enable:
+            raise ConfigError('SRv6 should be enabled on at least one interface!')
+    return None
+
+def generate(sr):
+    if not sr:
+        return None
+
+    sr['new_frr_config'] = render_to_string('frr/zebra.segment_routing.frr.j2', sr)
+    return None
+
+def apply(sr):
+    zebra_daemon = 'zebra'
+
+    if 'interface_removed' in sr:
+        for interface in sr['interface_removed']:
+            # Disable processing of IPv6-SR packets
+            sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0')
+
+    if 'interface' in sr:
+        for interface, interface_config in sr['interface'].items():
+            # Accept or drop SR-enabled IPv6 packets on this interface
+            if 'srv6' in interface_config:
+                sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '1')
+                # Define HMAC policy for ingress SR-enabled packets on this interface
+                # It's a redundant check as HMAC has a default value - but better safe
+                # then sorry
+                tmp = dict_search('srv6.hmac', interface_config)
+                if tmp == 'accept':
+                    sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '0')
+                elif tmp == 'drop':
+                    sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '1')
+                elif tmp == 'ignore':
+                    sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '-1')
+            else:
+                sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0')
+
+    # Save original configuration prior to starting any commit actions
+    frr_cfg = frr.FRRConfig()
+    frr_cfg.load_configuration(zebra_daemon)
+    frr_cfg.modify_section(r'^segment-routing')
+    if 'new_frr_config' in sr:
+        frr_cfg.add_before(frr.default_add_before, sr['new_frr_config'])
+    frr_cfg.commit_configuration(zebra_daemon)
+
+    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/protocols_segment_routing.py b/src/conf_mode/protocols_segment_routing.py
deleted file mode 100755
index d865c2ac0..000000000
--- a/src/conf_mode/protocols_segment_routing.py
+++ /dev/null
@@ -1,118 +0,0 @@
-#!/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/>.
-
-import os
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import node_changed
-from vyos.template import render_to_string
-from vyos.utils.dict import dict_search
-from vyos.utils.system import sysctl_write
-from vyos import ConfigError
-from vyos import frr
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base = ['protocols', 'segment-routing']
-    sr = conf.get_config_dict(base, key_mangling=('-', '_'),
-                              get_first_key=True,
-                              no_tag_node_value_mangle=True,
-                              with_recursive_defaults=True)
-
-    # FRR has VRF support for different routing daemons. As interfaces belong
-    # to VRFs - or the global VRF, we need to check for changed interfaces so
-    # that they will be properly rendered for the FRR config. Also this eases
-    # removal of interfaces from the running configuration.
-    interfaces_removed = node_changed(conf, base + ['interface'])
-    if interfaces_removed:
-        sr['interface_removed'] = list(interfaces_removed)
-
-    import pprint
-    pprint.pprint(sr)
-    return sr
-
-def verify(sr):
-    if 'srv6' in sr:
-        srv6_enable = False
-        if 'interface' in sr:
-            for interface, interface_config in sr['interface'].items():
-                if 'srv6' in interface_config:
-                    srv6_enable = True
-                    break
-        if not srv6_enable:
-            raise ConfigError('SRv6 should be enabled on at least one interface!')
-    return None
-
-def generate(sr):
-    if not sr:
-        return None
-
-    sr['new_frr_config'] = render_to_string('frr/zebra.segment_routing.frr.j2', sr)
-    return None
-
-def apply(sr):
-    zebra_daemon = 'zebra'
-
-    if 'interface_removed' in sr:
-        for interface in sr['interface_removed']:
-            # Disable processing of IPv6-SR packets
-            sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0')
-
-    if 'interface' in sr:
-        for interface, interface_config in sr['interface'].items():
-            # Accept or drop SR-enabled IPv6 packets on this interface
-            if 'srv6' in interface_config:
-                sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '1')
-                # Define HMAC policy for ingress SR-enabled packets on this interface
-                # It's a redundant check as HMAC has a default value - but better safe
-                # then sorry
-                tmp = dict_search('srv6.hmac', interface_config)
-                if tmp == 'accept':
-                    sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '0')
-                elif tmp == 'drop':
-                    sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '1')
-                elif tmp == 'ignore':
-                    sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '-1')
-            else:
-                sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0')
-
-    # Save original configuration prior to starting any commit actions
-    frr_cfg = frr.FRRConfig()
-    frr_cfg.load_configuration(zebra_daemon)
-    frr_cfg.modify_section(r'^segment-routing')
-    if 'new_frr_config' in sr:
-        frr_cfg.add_before(frr.default_add_before, sr['new_frr_config'])
-    frr_cfg.commit_configuration(zebra_daemon)
-
-    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/protocols_static_arp.py b/src/conf_mode/protocols_static_arp.py
new file mode 100755
index 000000000..b141f1141
--- /dev/null
+++ b/src/conf_mode/protocols_static_arp.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2022 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/>.
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import node_changed
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base = ['protocols', 'static', 'arp']
+    arp = conf.get_config_dict(base, get_first_key=True)
+
+    if 'interface' in arp:
+        for interface in arp['interface']:
+            tmp = node_changed(conf, base + ['interface', interface, 'address'], recursive=True)
+            if tmp: arp['interface'][interface].update({'address_old' : tmp})
+
+    return arp
+
+def verify(arp):
+    pass
+
+def generate(arp):
+    pass
+
+def apply(arp):
+    if not arp:
+        return None
+
+    if 'interface' in arp:
+        for interface, interface_config in arp['interface'].items():
+            # Delete old static ARP assignments first
+            if 'address_old' in interface_config:
+                for address in interface_config['address_old']:
+                    call(f'ip neigh del {address} dev {interface}')
+
+            # Add new static ARP entries to interface
+            if 'address' not in interface_config:
+                continue
+            for address, address_config in interface_config['address'].items():
+                mac = address_config['mac']
+                call(f'ip neigh replace {address} lladdr {mac} dev {interface}')
+
+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/salt-minion.py b/src/conf_mode/salt-minion.py
deleted file mode 100755
index a8fce8e01..000000000
--- a/src/conf_mode/salt-minion.py
+++ /dev/null
@@ -1,118 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2022 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 socket import gethostname
-from sys import exit
-from urllib3 import PoolManager
-
-from vyos.base import Warning
-from vyos.config import Config
-from vyos.configverify import verify_interface_exists
-from vyos.template import render
-from vyos.utils.process import call
-from vyos.utils.permission import chown
-from vyos import ConfigError
-
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/etc/salt/minion'
-master_keyfile = r'/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'
-
-user='minion'
-group='vyattacfg'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['service', 'salt-minion']
-
-    if not conf.exists(base):
-        return None
-
-    salt = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
-    # ID default is dynamic thus we can not use defaults()
-    if 'id' not in salt:
-        salt['id'] = gethostname()
-    # We have gathered the dict representation of the CLI, but there are default
-    # options which we need to update into the dictionary retrived.
-    salt = conf.merge_defaults(salt, recursive=True)
-
-    if not conf.exists(base):
-        return None
-    else:
-        conf.set_level(base)
-
-    return salt
-
-def verify(salt):
-    if not salt:
-        return None
-
-    if 'hash' in salt and salt['hash'] == 'sha1':
-        Warning('Do not use sha1 hashing algorithm, upgrade to sha256 or later!')
-
-    if 'source_interface' in salt:
-        verify_interface_exists(salt['source_interface'])
-
-    return None
-
-def generate(salt):
-    if not salt:
-        return None
-
-    render(config_file, 'salt-minion/minion.j2', salt, user=user, group=group)
-
-    if not os.path.exists(master_keyfile):
-        if 'master_key' in salt:
-            req = PoolManager().request('GET', salt['master_key'], preload_content=False)
-            with open(master_keyfile, 'wb') as f:
-                while True:
-                    data = req.read(1024)
-                    if not data:
-                        break
-                    f.write(data)
-
-            req.release_conn()
-            chown(master_keyfile, user, group)
-
-    return None
-
-def apply(salt):
-    service_name = 'salt-minion.service'
-    if not salt:
-        # Salt removed from running config
-        call(f'systemctl stop {service_name}')
-        if os.path.exists(config_file):
-            os.unlink(config_file)
-    else:
-        call(f'systemctl restart {service_name}')
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/service_broadcast-relay.py b/src/conf_mode/service_broadcast-relay.py
new file mode 100755
index 000000000..31c552f5a
--- /dev/null
+++ b/src/conf_mode/service_broadcast-relay.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2017-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 glob import glob
+from netifaces import AF_INET
+from sys import exit
+
+from vyos.config import Config
+from vyos.configverify import verify_interface_exists
+from vyos.template import render
+from vyos.utils.process import call
+from vyos.utils.network import is_afi_configured
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file_base = r'/etc/default/udp-broadcast-relay'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['service', 'broadcast-relay']
+
+    relay = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+    return relay
+
+def verify(relay):
+    if not relay or 'disabled' in relay:
+        return None
+
+    for instance, config in relay.get('id', {}).items():
+        # we don't have to check this instance when it's disabled
+        if 'disabled' in config:
+            continue
+
+        # we certainly require a UDP port to listen to
+        if 'port' not in config:
+            raise ConfigError(f'Port number is mandatory for UDP broadcast relay "{instance}"')
+
+        # Relaying data without two interface is kinda senseless ...
+        if len(config.get('interface', [])) < 2:
+            raise ConfigError('At least two interfaces are required for UDP broadcast relay "{instance}"')
+
+        for interface in config.get('interface', []):
+            verify_interface_exists(interface)
+            if not is_afi_configured(interface, AF_INET):
+                raise ConfigError(f'Interface "{interface}" has no IPv4 address configured!')
+
+    return None
+
+def generate(relay):
+    if not relay or 'disabled' in relay:
+        return None
+
+    for config in glob(config_file_base + '*'):
+        os.remove(config)
+
+    for instance, config in relay.get('id').items():
+        # we don't have to check this instance when it's disabled
+        if 'disabled' in config:
+            continue
+
+        config['instance'] = instance
+        render(config_file_base + instance, 'bcast-relay/udp-broadcast-relay.j2',
+               config)
+
+    return None
+
+def apply(relay):
+    # first stop all running services
+    call('systemctl stop udp-broadcast-relay@*.service')
+
+    if not relay or 'disable' in relay:
+        return None
+
+    # start only required service instances
+    for instance, config in relay.get('id').items():
+        # we don't have to check this instance when it's disabled
+        if 'disabled' in config:
+            continue
+
+        call(f'systemctl start udp-broadcast-relay@{instance}.service')
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_config-sync.py b/src/conf_mode/service_config-sync.py
new file mode 100755
index 000000000..4b8a7f6ee
--- /dev/null
+++ b/src/conf_mode/service_config-sync.py
@@ -0,0 +1,105 @@
+#!/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/>.
+
+import os
+import json
+from pathlib import Path
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos import airbag
+
+airbag.enable()
+
+
+service_conf = Path(f'/run/config_sync_conf.conf')
+post_commit_dir = '/run/scripts/commit/post-hooks.d'
+post_commit_file_src = '/usr/libexec/vyos/vyos_config_sync.py'
+post_commit_file = f'{post_commit_dir}/vyos_config_sync'
+
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base = ['service', 'config-sync']
+    if not conf.exists(base):
+        return None
+    config = conf.get_config_dict(base, get_first_key=True,
+                                  with_recursive_defaults=True)
+
+    return config
+
+
+def verify(config):
+    # bail out early - looks like removal from running config
+    if not config:
+        return None
+
+    if 'mode' not in config:
+        raise ConfigError(f'config-sync mode is mandatory!')
+
+    for option in ['secondary', 'section']:
+        if option not in config:
+            raise ConfigError(f"config-sync '{option}' is not configured!")
+
+    if 'address' not in config['secondary']:
+        raise ConfigError(f'secondary address is mandatory!')
+    if 'key' not in config['secondary']:
+        raise ConfigError(f'secondary key is mandatory!')
+
+
+def generate(config):
+    if not config:
+
+        if os.path.exists(post_commit_file):
+            os.unlink(post_commit_file)
+
+        if service_conf.exists():
+            service_conf.unlink()
+
+        return None
+
+    # Write configuration file
+    conf_json = json.dumps(config, indent=4)
+    service_conf.write_text(conf_json)
+
+    # Create post commit dir
+    if not os.path.isdir(post_commit_dir):
+        os.makedirs(post_commit_dir)
+
+    # Symlink from helpers to post-commit
+    if not os.path.exists(post_commit_file):
+        os.symlink(post_commit_file_src, post_commit_file)
+
+    return None
+
+
+def apply(config):
+    return None
+
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_config_sync.py b/src/conf_mode/service_config_sync.py
deleted file mode 100755
index 4b8a7f6ee..000000000
--- a/src/conf_mode/service_config_sync.py
+++ /dev/null
@@ -1,105 +0,0 @@
-#!/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/>.
-
-import os
-import json
-from pathlib import Path
-
-from vyos.config import Config
-from vyos import ConfigError
-from vyos import airbag
-
-airbag.enable()
-
-
-service_conf = Path(f'/run/config_sync_conf.conf')
-post_commit_dir = '/run/scripts/commit/post-hooks.d'
-post_commit_file_src = '/usr/libexec/vyos/vyos_config_sync.py'
-post_commit_file = f'{post_commit_dir}/vyos_config_sync'
-
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base = ['service', 'config-sync']
-    if not conf.exists(base):
-        return None
-    config = conf.get_config_dict(base, get_first_key=True,
-                                  with_recursive_defaults=True)
-
-    return config
-
-
-def verify(config):
-    # bail out early - looks like removal from running config
-    if not config:
-        return None
-
-    if 'mode' not in config:
-        raise ConfigError(f'config-sync mode is mandatory!')
-
-    for option in ['secondary', 'section']:
-        if option not in config:
-            raise ConfigError(f"config-sync '{option}' is not configured!")
-
-    if 'address' not in config['secondary']:
-        raise ConfigError(f'secondary address is mandatory!')
-    if 'key' not in config['secondary']:
-        raise ConfigError(f'secondary key is mandatory!')
-
-
-def generate(config):
-    if not config:
-
-        if os.path.exists(post_commit_file):
-            os.unlink(post_commit_file)
-
-        if service_conf.exists():
-            service_conf.unlink()
-
-        return None
-
-    # Write configuration file
-    conf_json = json.dumps(config, indent=4)
-    service_conf.write_text(conf_json)
-
-    # Create post commit dir
-    if not os.path.isdir(post_commit_dir):
-        os.makedirs(post_commit_dir)
-
-    # Symlink from helpers to post-commit
-    if not os.path.exists(post_commit_file):
-        os.symlink(post_commit_file_src, post_commit_file)
-
-    return None
-
-
-def apply(config):
-    return None
-
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/service_conntrack-sync.py b/src/conf_mode/service_conntrack-sync.py
new file mode 100755
index 000000000..4fb2ce27f
--- /dev/null
+++ b/src/conf_mode/service_conntrack-sync.py
@@ -0,0 +1,141 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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
+
+from sys import exit
+from vyos.config import Config
+from vyos.configverify import verify_interface_exists
+from vyos.utils.dict import dict_search
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+from vyos.utils.process import call
+from vyos.utils.process import run
+from vyos.template import render
+from vyos.template import get_ipv4
+from vyos.utils.network import is_addr_assigned
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = '/run/conntrackd/conntrackd.conf'
+
+def resync_vrrp():
+    tmp = run('/usr/libexec/vyos/conf_mode/high-availability.py')
+    if tmp > 0:
+        print('ERROR: error restarting VRRP daemon!')
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['service', 'conntrack-sync']
+    if not conf.exists(base):
+        return None
+
+    conntrack = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                     get_first_key=True, with_defaults=True)
+
+    conntrack['hash_size'] = read_file('/sys/module/nf_conntrack/parameters/hashsize')
+    conntrack['table_size'] = read_file('/proc/sys/net/netfilter/nf_conntrack_max')
+
+    conntrack['vrrp'] = conf.get_config_dict(['high-availability', 'vrrp', 'sync-group'],
+                                     get_first_key=True)
+
+    return conntrack
+
+def verify(conntrack):
+    if not conntrack:
+        return None
+
+    if 'interface' not in conntrack:
+        raise ConfigError('Interface not defined!')
+
+    has_peer = False
+    for interface, interface_config in conntrack['interface'].items():
+        verify_interface_exists(interface)
+        # Interface must not only exist, it must also carry an IP address
+        if len(get_ipv4(interface)) < 1:
+            raise ConfigError(f'Interface {interface} requires an IP address!')
+        if 'peer' in interface_config:
+            has_peer = True
+
+    # If one interface runs in unicast mode instead of multicast, so must all the
+    # others, else conntrackd will error out with: "cannot use UDP with other
+    # dedicated link protocols"
+    if has_peer:
+        for interface, interface_config in conntrack['interface'].items():
+            if 'peer' not in interface_config:
+                raise ConfigError('Can not mix unicast and multicast mode!')
+
+    if 'expect_sync' in conntrack:
+        if len(conntrack['expect_sync']) > 1 and 'all' in conntrack['expect_sync']:
+            raise ConfigError('Can not configure expect-sync "all" with other protocols!')
+
+    if 'listen_address' in conntrack:
+        for address in conntrack['listen_address']:
+            if not is_addr_assigned(address):
+                raise ConfigError(f'Specified listen-address {address} not assigned to any interface!')
+
+    vrrp_group = dict_search('failover_mechanism.vrrp.sync_group', conntrack)
+    if vrrp_group == None:
+        raise ConfigError(f'No VRRP sync-group defined!')
+    if vrrp_group not in conntrack['vrrp']:
+        raise ConfigError(f'VRRP sync-group {vrrp_group} not configured!')
+
+    return None
+
+def generate(conntrack):
+    if not conntrack:
+        if os.path.isfile(config_file):
+            os.unlink(config_file)
+        return None
+
+    render(config_file, 'conntrackd/conntrackd.conf.j2', conntrack)
+
+    return None
+
+def apply(conntrack):
+    systemd_service = 'conntrackd.service'
+    if not conntrack:
+        # Failover mechanism daemon should be indicated that it no longer needs
+        # to execute conntrackd actions on transition. This is only required
+        # once when conntrackd is stopped and taken out of service!
+        if process_named_running('conntrackd'):
+            resync_vrrp()
+
+        call(f'systemctl stop {systemd_service}')
+        return None
+
+    # Failover mechanism daemon should be indicated that it needs to execute
+    # conntrackd actions on transition. This is only required once when conntrackd
+    # is started the first time!
+    if not process_named_running('conntrackd'):
+        resync_vrrp()
+
+    call(f'systemctl reload-or-restart {systemd_service}')
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_dhcp-relay.py b/src/conf_mode/service_dhcp-relay.py
new file mode 100755
index 000000000..37d708847
--- /dev/null
+++ b/src/conf_mode/service_dhcp-relay.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 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 sys import exit
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.template import render
+from vyos.base import Warning
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/dhcp-relay/dhcrelay.conf'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['service', 'dhcp-relay']
+    if not conf.exists(base):
+        return None
+
+    relay = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                 get_first_key=True,
+                                 with_recursive_defaults=True)
+
+    return relay
+
+def verify(relay):
+    # bail out early - looks like removal from running config
+    if not relay or 'disable' in relay:
+        return None
+
+    if 'lo' in (dict_search('interface', relay) or []):
+        raise ConfigError('DHCP relay does not support the loopback interface.')
+
+    if 'server' not in relay :
+        raise ConfigError('No DHCP relay server(s) configured.\n' \
+                          'At least one DHCP relay server required.')
+
+    if 'interface' in relay:
+        Warning('DHCP relay interface is DEPRECATED - please use upstream-interface and listen-interface instead!')
+        if 'upstream_interface' in relay or 'listen_interface' in relay:
+            raise ConfigError('<interface> configuration is not compatible with upstream/listen interface')
+        else:
+            Warning('<interface> is going to be deprecated.\n'  \
+                    'Please use <listen-interface> and <upstream-interface>')
+
+    if 'upstream_interface' in relay and 'listen_interface' not in relay:
+        raise ConfigError('No listen-interface configured')
+    if 'listen_interface' in relay and 'upstream_interface' not in relay:
+        raise ConfigError('No upstream-interface configured')
+
+    return None
+
+def generate(relay):
+    # bail out early - looks like removal from running config
+    if not relay or 'disable' in relay:
+        return None
+
+    render(config_file, 'dhcp-relay/dhcrelay.conf.j2', relay)
+    return None
+
+def apply(relay):
+    # bail out early - looks like removal from running config
+    service_name = 'isc-dhcp-relay.service'
+    if not relay or 'disable' in relay:
+        call(f'systemctl stop {service_name}')
+        if os.path.exists(config_file):
+            os.unlink(config_file)
+        return None
+
+    call(f'systemctl restart {service_name}')
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py
new file mode 100755
index 000000000..7ebc560ba
--- /dev/null
+++ b/src/conf_mode/service_dhcp-server.py
@@ -0,0 +1,385 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2023 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from ipaddress import ip_address
+from ipaddress import ip_network
+from netaddr import IPRange
+from sys import exit
+
+from vyos.config import Config
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_private_key
+from vyos.template import render
+from vyos.utils.dict import dict_search
+from vyos.utils.dict import dict_search_args
+from vyos.utils.file import chmod_775
+from vyos.utils.file import makedir
+from vyos.utils.file import write_file
+from vyos.utils.process import call
+from vyos.utils.network import is_subnet_connected
+from vyos.utils.network import is_addr_assigned
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+ctrl_config_file = '/run/kea/kea-ctrl-agent.conf'
+ctrl_socket = '/run/kea/dhcp4-ctrl-socket'
+config_file = '/run/kea/kea-dhcp4.conf'
+lease_file = '/config/dhcp/dhcp4-leases.csv'
+systemd_override = r'/run/systemd/system/kea-ctrl-agent.service.d/10-override.conf'
+user_group = '_kea'
+
+ca_cert_file = '/run/kea/kea-failover-ca.pem'
+cert_file = '/run/kea/kea-failover.pem'
+cert_key_file = '/run/kea/kea-failover-key.pem'
+
+def dhcp_slice_range(exclude_list, range_dict):
+    """
+    This function is intended to slice a DHCP range. What does it mean?
+
+    Lets assume we have a DHCP range from '192.0.2.1' to '192.0.2.100'
+    but want to exclude address '192.0.2.74' and '192.0.2.75'. We will
+    pass an input 'range_dict' in the format:
+      {'start' : '192.0.2.1', 'stop' : '192.0.2.100' }
+    and we will receive an output list of:
+      [{'start' : '192.0.2.1' , 'stop' : '192.0.2.73'  },
+       {'start' : '192.0.2.76', 'stop' : '192.0.2.100' }]
+    The resulting list can then be used in turn to build the proper dhcpd
+    configuration file.
+    """
+    output = []
+    # exclude list must be sorted for this to work
+    exclude_list = sorted(exclude_list)
+    range_start = range_dict['start']
+    range_stop = range_dict['stop']
+    range_last_exclude = ''
+
+    for e in exclude_list:
+        if (ip_address(e) >= ip_address(range_start)) and \
+           (ip_address(e) <= ip_address(range_stop)):
+            range_last_exclude = e
+
+    for e in exclude_list:
+        if (ip_address(e) >= ip_address(range_start)) and \
+           (ip_address(e) <= ip_address(range_stop)):
+
+            # Build new address range ending one address before exclude address
+            r = {
+                'start' : range_start,
+                'stop' : str(ip_address(e) -1)
+            }
+            # On the next run our address range will start one address after
+            # the exclude address
+            range_start = str(ip_address(e) + 1)
+
+            # on subsequent exclude addresses we can not
+            # append them to our output
+            if not (ip_address(r['start']) > ip_address(r['stop'])):
+                # Everything is fine, add range to result
+                output.append(r)
+
+            # Take care of last IP address range spanning from the last exclude
+            # address (+1) to the end of the initial configured range
+            if ip_address(e) == ip_address(range_last_exclude):
+                r = {
+                  'start': str(ip_address(e) + 1),
+                  'stop': str(range_stop)
+                }
+                if not (ip_address(r['start']) > ip_address(r['stop'])):
+                    output.append(r)
+        else:
+          # if the excluded address was not part of the range, we simply return
+          # the entire ranga again
+          if not range_last_exclude:
+              if range_dict not in output:
+                  output.append(range_dict)
+
+    return output
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['service', 'dhcp-server']
+    if not conf.exists(base):
+        return None
+
+    dhcp = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                no_tag_node_value_mangle=True,
+                                get_first_key=True,
+                                with_recursive_defaults=True)
+
+    if 'shared_network_name' in dhcp:
+        for network, network_config in dhcp['shared_network_name'].items():
+            if 'subnet' in network_config:
+                for subnet, subnet_config in network_config['subnet'].items():
+                    # If exclude IP addresses are defined we need to slice them out of
+                    # the defined ranges
+                    if {'exclude', 'range'} <= set(subnet_config):
+                        new_range_id = 0
+                        new_range_dict = {}
+                        for r, r_config in subnet_config['range'].items():
+                            for slice in dhcp_slice_range(subnet_config['exclude'], r_config):
+                                new_range_dict.update({new_range_id : slice})
+                                new_range_id +=1
+
+                        dhcp['shared_network_name'][network]['subnet'][subnet].update(
+                                {'range' : new_range_dict})
+
+    if dict_search('failover.certificate', dhcp):
+        dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
+
+    return dhcp
+
+def verify(dhcp):
+    # bail out early - looks like removal from running config
+    if not dhcp or 'disable' in dhcp:
+        return None
+
+    # If DHCP is enabled we need one share-network
+    if 'shared_network_name' not in dhcp:
+        raise ConfigError('No DHCP shared networks configured.\n' \
+                          'At least one DHCP shared network must be configured.')
+
+    # Inspect shared-network/subnet
+    listen_ok = False
+    subnets = []
+    failover_ok = False
+    shared_networks =  len(dhcp['shared_network_name'])
+    disabled_shared_networks = 0
+
+
+    # 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!')
+
+            # 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' not in mapping_config and 'duid' not in mapping_config) or \
+                            ('mac' in mapping_config and 'duid' in mapping_config):
+                            raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for '
+                                              f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!')
+
+            # There must be one subnet connected to a listen interface.
+            # This only counts if the network itself is not disabled!
+            if 'disable' not in network_config:
+                if is_subnet_connected(subnet, primary=False):
+                    listen_ok = True
+
+            # Subnets must be non overlapping
+            if subnet in subnets:
+                raise ConfigError(f'Configured subnets must be unique! Subnet "{subnet}"\n'
+                                   'defined multiple times!')
+            subnets.append(subnet)
+
+            # Check for overlapping subnets
+            net = ip_network(subnet)
+            for n in subnets:
+                net2 = ip_network(n)
+                if (net != net2):
+                    if net.overlaps(net2):
+                        raise ConfigError(f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
+
+    # Prevent 'disable' for shared-network if only one network is configured
+    if (shared_networks - disabled_shared_networks) < 1:
+        raise ConfigError(f'At least one shared network must be active!')
+
+    if 'failover' in dhcp:
+        for key in ['name', 'remote', 'source_address', 'status']:
+            if key not in dhcp['failover']:
+                tmp = key.replace('_', '-')
+                raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!')
+
+        if len({'certificate', 'ca_certificate'} & set(dhcp['failover'])) == 1:
+            raise ConfigError(f'DHCP secured failover requires both certificate and CA certificate')
+
+        if 'certificate' in dhcp['failover']:
+            cert_name = dhcp['failover']['certificate']
+
+            if cert_name not in dhcp['pki']['certificate']:
+                raise ConfigError(f'Invalid certificate specified for DHCP failover')
+
+            if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'certificate'):
+                raise ConfigError(f'Invalid certificate specified for DHCP failover')
+
+            if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'private', 'key'):
+                raise ConfigError(f'Missing private key on certificate specified for DHCP failover')
+
+        if 'ca_certificate' in dhcp['failover']:
+            ca_cert_name = dhcp['failover']['ca_certificate']
+            if ca_cert_name not in dhcp['pki']['ca']:
+                raise ConfigError(f'Invalid CA certificate specified for DHCP failover')
+
+            if not dict_search_args(dhcp['pki']['ca'], ca_cert_name, 'certificate'):
+                raise ConfigError(f'Invalid CA certificate specified for DHCP failover')
+
+    for address in (dict_search('listen_address', dhcp) or []):
+        if is_addr_assigned(address):
+            listen_ok = True
+            # no need to probe further networks, we have one that is valid
+            continue
+        else:
+            raise ConfigError(f'listen-address "{address}" not configured on any interface')
+
+
+    if not listen_ok:
+        raise ConfigError('None of the configured subnets have an appropriate primary IP address on any\n'
+                          'broadcast interface configured, nor was there an explicit listen-address\n'
+                          'configured for serving DHCP relay packets!')
+
+    return None
+
+def generate(dhcp):
+    # bail out early - looks like removal from running config
+    if not dhcp or 'disable' in dhcp:
+        return None
+
+    dhcp['lease_file'] = lease_file
+    dhcp['machine'] = os.uname().machine
+
+    # Create directory for lease file if necessary
+    lease_dir = os.path.dirname(lease_file)
+    if not os.path.isdir(lease_dir):
+        makedir(lease_dir, group='vyattacfg')
+        chmod_775(lease_dir)
+
+    # Create lease file if necessary and let kea own it - 'kea-lfc' expects it that way
+    if not os.path.exists(lease_file):
+        write_file(lease_file, '', user=user_group, group=user_group, mode=0o644)
+
+    for f in [cert_file, cert_key_file, ca_cert_file]:
+        if os.path.exists(f):
+            os.unlink(f)
+
+    if 'failover' in dhcp:
+        if 'certificate' in dhcp['failover']:
+            cert_name = dhcp['failover']['certificate']
+            cert_data = dhcp['pki']['certificate'][cert_name]['certificate']
+            key_data = dhcp['pki']['certificate'][cert_name]['private']['key']
+            write_file(cert_file, wrap_certificate(cert_data), user=user_group, mode=0o600)
+            write_file(cert_key_file, wrap_private_key(key_data), user=user_group, mode=0o600)
+
+            dhcp['failover']['cert_file'] = cert_file
+            dhcp['failover']['cert_key_file'] = cert_key_file
+
+        if 'ca_certificate' in dhcp['failover']:
+            ca_cert_name = dhcp['failover']['ca_certificate']
+            ca_cert_data = dhcp['pki']['ca'][ca_cert_name]['certificate']
+            write_file(ca_cert_file, wrap_certificate(ca_cert_data), user=user_group, mode=0o600)
+
+            dhcp['failover']['ca_cert_file'] = ca_cert_file
+
+        render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp)
+
+    render(ctrl_config_file, 'dhcp-server/kea-ctrl-agent.conf.j2', dhcp, user=user_group, group=user_group)
+    render(config_file, 'dhcp-server/kea-dhcp4.conf.j2', dhcp, user=user_group, group=user_group)
+
+    return None
+
+def apply(dhcp):
+    services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server']
+
+    if not dhcp or 'disable' in dhcp:
+        for service in services:
+            call(f'systemctl stop {service}.service')
+
+        if os.path.exists(config_file):
+            os.unlink(config_file)
+
+        return None
+
+    for service in services:
+        action = 'restart'
+
+        if service == 'kea-dhcp-ddns-server' and 'dynamic_dns_update' not in dhcp:
+            action = 'stop'
+
+        if service == 'kea-ctrl-agent' and 'failover' not in dhcp:
+            action = 'stop'
+
+        call(f'systemctl {action} {service}.service')
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_dhcpv6-relay.py b/src/conf_mode/service_dhcpv6-relay.py
new file mode 100755
index 000000000..6537ca3c2
--- /dev/null
+++ b/src/conf_mode/service_dhcpv6-relay.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 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 sys import exit
+
+from vyos.config import Config
+from vyos.ifconfig import Interface
+from vyos.template import render
+from vyos.template import is_ipv6
+from vyos.utils.process import call
+from vyos.utils.network import is_ipv6_link_local
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = '/run/dhcp-relay/dhcrelay6.conf'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['service', 'dhcpv6-relay']
+    if not conf.exists(base):
+        return None
+
+    relay = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                 get_first_key=True,
+                                 with_recursive_defaults=True)
+
+    return relay
+
+def verify(relay):
+    # bail out early - looks like removal from running config
+    if not relay or 'disable' in relay:
+        return None
+
+    if 'upstream_interface' not in relay:
+        raise ConfigError('At least one upstream interface required!')
+    for interface, config in relay['upstream_interface'].items():
+        if 'address' not in config:
+            raise ConfigError('DHCPv6 server required for upstream ' \
+                              f'interface {interface}!')
+
+    if 'listen_interface' not in relay:
+        raise ConfigError('At least one listen interface required!')
+
+    # DHCPv6 relay requires at least one global unicat address assigned to the
+    # interface
+    for interface in relay['listen_interface']:
+        has_global = False
+        for addr in Interface(interface).get_addr():
+            if is_ipv6(addr) and not is_ipv6_link_local(addr):
+                has_global = True
+        if not has_global:
+            raise ConfigError(f'Interface {interface} does not have global '\
+                              'IPv6 address assigned!')
+
+    return None
+
+def generate(relay):
+    # bail out early - looks like removal from running config
+    if not relay or 'disable' in relay:
+        return None
+
+    render(config_file, 'dhcp-relay/dhcrelay6.conf.j2', relay)
+    return None
+
+def apply(relay):
+    # bail out early - looks like removal from running config
+    service_name = 'isc-dhcp-relay6.service'
+    if not relay or 'disable' in relay:
+        # DHCPv6 relay support is removed in the commit
+        call(f'systemctl stop {service_name}')
+        if os.path.exists(config_file):
+            os.unlink(config_file)
+        return None
+
+    call(f'systemctl restart {service_name}')
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_dhcpv6-server.py b/src/conf_mode/service_dhcpv6-server.py
new file mode 100755
index 000000000..9cc57dbcf
--- /dev/null
+++ b/src/conf_mode/service_dhcpv6-server.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2023 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from ipaddress import ip_address
+from ipaddress import ip_network
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.utils.process import call
+from vyos.utils.file import chmod_775
+from vyos.utils.file import makedir
+from vyos.utils.file import write_file
+from vyos.utils.dict import dict_search
+from vyos.utils.network import is_subnet_connected
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = '/run/kea/kea-dhcp6.conf'
+ctrl_socket = '/run/kea/dhcp6-ctrl-socket'
+lease_file = '/config/dhcp/dhcp6-leases.csv'
+user_group = '_kea'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['service', 'dhcpv6-server']
+    if not conf.exists(base):
+        return None
+
+    dhcpv6 = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                  get_first_key=True,
+                                  no_tag_node_value_mangle=True)
+    return dhcpv6
+
+def verify(dhcpv6):
+    # bail out early - looks like removal from running config
+    if not dhcpv6 or 'disable' in dhcpv6:
+        return None
+
+    # If DHCP is enabled we need one share-network
+    if 'shared_network_name' not in dhcpv6:
+        raise ConfigError('No DHCPv6 shared networks configured. At least '\
+                          'one DHCPv6 shared network must be configured.')
+
+    # Inspect shared-network/subnet
+    subnets = []
+    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 'prefix' not in subnet_config['prefix_delegation']:
+                    raise ConfigError('prefix-delegation prefix not defined!')
+
+                for prefix, prefix_config in subnet_config['prefix_delegation']['prefix'].items():
+                    if 'delegated_length' not in prefix_config:
+                        raise ConfigError(f'Delegated IPv6 prefix length for "{prefix}" '\
+                                          f'must be configured')
+
+                    if 'prefix_length' not in prefix_config:
+                        raise ConfigError('Length of delegated IPv6 prefix must be configured')
+
+                    if prefix_config['prefix_length'] > prefix_config['delegated_length']:
+                        raise ConfigError('Length of delegated IPv6 prefix must be within parent prefix')
+
+            # Static mappings don't require anything (but check if IP is in subnet if it's set)
+            if 'static_mapping' in subnet_config:
+                for mapping, mapping_config in subnet_config['static_mapping'].items():
+                    if 'ipv6_address' in mapping_config:
+                        # Static address must be in subnet
+                        if ip_address(mapping_config['ipv6_address']) not in ip_network(subnet):
+                            raise ConfigError(f'static-mapping address for mapping "{mapping}" is not in subnet "{subnet}"!')
+
+                        if ('mac' not in mapping_config and 'duid' not in mapping_config) or \
+                            ('mac' in mapping_config and 'duid' in mapping_config):
+                            raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for '
+                                              f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!')
+
+            if 'vendor_option' in subnet_config:
+                if len(dict_search('vendor_option.cisco.tftp_server', subnet_config)) > 2:
+                    raise ConfigError(f'No more then two Cisco tftp-servers should be defined for subnet "{subnet}"!')
+
+            # Subnets must be unique
+            if subnet in subnets:
+                raise ConfigError(f'DHCPv6 subnets must be unique! Subnet {subnet} defined multiple times!')
+            subnets.append(subnet)
+
+        # DHCPv6 requires at least one configured address range or one static mapping
+        # (FIXME: is not actually checked right now?)
+
+        # There must be one subnet connected to a listen interface if network is not disabled.
+        if 'disable' not in network_config:
+            if is_subnet_connected(subnet):
+                listen_ok = True
+
+            # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping
+            # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32"
+            net = ip_network(subnet)
+            for n in subnets:
+                net2 = ip_network(n)
+                if (net != net2):
+                    if net.overlaps(net2):
+                        raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2))
+
+    if not listen_ok:
+        raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on '\
+                          'this machine. At least one subnet6 must be connected such that '\
+                          'DHCPv6 listens on an interface!')
+
+
+    return None
+
+def generate(dhcpv6):
+    # bail out early - looks like removal from running config
+    if not dhcpv6 or 'disable' in dhcpv6:
+        return None
+
+    dhcpv6['lease_file'] = lease_file
+    dhcpv6['machine'] = os.uname().machine
+
+    # Create directory for lease file if necessary
+    lease_dir = os.path.dirname(lease_file)
+    if not os.path.isdir(lease_dir):
+        makedir(lease_dir, group='vyattacfg')
+        chmod_775(lease_dir)
+
+    # Create lease file if necessary and let kea own it - 'kea-lfc' expects it that way
+    if not os.path.exists(lease_file):
+        write_file(lease_file, '', user=user_group, group=user_group, mode=0o644)
+
+    render(config_file, 'dhcp-server/kea-dhcp6.conf.j2', dhcpv6, user=user_group, group=user_group)
+    return None
+
+def apply(dhcpv6):
+    # bail out early - looks like removal from running config
+    service_name = 'kea-dhcp6-server.service'
+    if not dhcpv6 or 'disable' in dhcpv6:
+        # DHCP server is removed in the commit
+        call(f'systemctl stop {service_name}')
+        if os.path.exists(config_file):
+            os.unlink(config_file)
+        return None
+
+    call(f'systemctl restart {service_name}')
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_dns_dynamic.py b/src/conf_mode/service_dns_dynamic.py
new file mode 100755
index 000000000..99fa8feee
--- /dev/null
+++ b/src/conf_mode/service_dns_dynamic.py
@@ -0,0 +1,187 @@
+#!/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
+import re
+from sys import exit
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.configverify import verify_interface_exists
+from vyos.template import render
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/ddclient/ddclient.conf'
+systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf'
+
+# Dynamic interfaces that might not exist when the configuration is loaded
+dynamic_interfaces = ('pppoe', 'sstpc')
+
+# Protocols that require zone
+zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi',
+                  'nfsn', 'nsupdate']
+zone_supported = zone_necessary + ['dnsexit2', 'zoneedit1']
+
+# Protocols that do not require username
+username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'digitalocean', 'dnsexit2',
+                        'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla',
+                        'nsupdate', 'regfishde']
+
+# Protocols that support TTL
+ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn',
+                 'nsupdate']
+
+# Protocols that support both IPv4 and IPv6
+dualstack_supported = ['cloudflare', 'digitalocean', 'dnsexit2', 'duckdns',
+                       'dyndns2', 'easydns', 'freedns', 'hetzner', 'infomaniak',
+                       'njalla']
+
+# dyndns2 protocol in ddclient honors dual stack for selective servers
+# because of the way it is implemented in ddclient
+dyndns_dualstack_servers = ['members.dyndns.org', 'dynv6.com']
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base = ['service', 'dns', 'dynamic']
+    if not conf.exists(base):
+        return None
+
+    dyndns = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                  no_tag_node_value_mangle=True,
+                                  get_first_key=True,
+                                  with_recursive_defaults=True)
+
+    dyndns['config_file'] = config_file
+    return dyndns
+
+def verify(dyndns):
+    # bail out early - looks like removal from running config
+    if not dyndns or 'name' not in dyndns:
+        return None
+
+    # Dynamic DNS service provider - configuration validation
+    for service, config in dyndns['name'].items():
+
+        error_msg_req = f'is required for Dynamic DNS service "{service}"'
+        error_msg_uns = f'is not supported for Dynamic DNS service "{service}"'
+
+        for field in ['protocol', 'address', 'host_name']:
+            if field not in config:
+                raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}')
+
+        # If dyndns address is an interface, ensure
+        # that the interface exists (or just warn if dynamic interface)
+        # and that web-options are not set
+        if config['address'] != 'web':
+            # exclude check interface for dynamic interfaces
+            if config['address'].startswith(dynamic_interfaces):
+                Warning(f'Interface "{config["address"]}" does not exist yet and cannot '
+                        f'be used for Dynamic DNS service "{service}" until it is up!')
+            else:
+                verify_interface_exists(config['address'])
+            if 'web_options' in config:
+                raise ConfigError(f'"web-options" is applicable only when using HTTP(S) '
+                                  f'web request to obtain the IP address')
+
+        # Warn if using checkip.dyndns.org, as it does not support HTTPS
+        # See: https://github.com/ddclient/ddclient/issues/597
+        if 'web_options' in config:
+            if 'url' not in config['web_options']:
+                raise ConfigError(f'"url" in "web-options" {error_msg_req} '
+                                  f'with protocol "{config["protocol"]}"')
+            elif re.search("^(https?://)?checkip\.dyndns\.org", config['web_options']['url']):
+                Warning(f'"checkip.dyndns.org" does not support HTTPS requests for IP address '
+                        f'lookup. Please use a different IP address lookup service.')
+
+        # RFC2136 uses 'key' instead of 'password'
+        if config['protocol'] != 'nsupdate' and 'password' not in config:
+            raise ConfigError(f'"password" {error_msg_req}')
+
+        # Other RFC2136 specific configuration validation
+        if config['protocol'] == 'nsupdate':
+            if 'password' in config:
+                raise ConfigError(f'"password" {error_msg_uns} with protocol "{config["protocol"]}"')
+            for field in ['server', 'key']:
+                if field not in config:
+                    raise ConfigError(f'"{field}" {error_msg_req} with protocol "{config["protocol"]}"')
+
+        if config['protocol'] in zone_necessary and 'zone' not in config:
+            raise ConfigError(f'"zone" {error_msg_req} with protocol "{config["protocol"]}"')
+
+        if config['protocol'] not in zone_supported and 'zone' in config:
+            raise ConfigError(f'"zone" {error_msg_uns} with protocol "{config["protocol"]}"')
+
+        if config['protocol'] not in username_unnecessary and 'username' not in config:
+            raise ConfigError(f'"username" {error_msg_req} with protocol "{config["protocol"]}"')
+
+        if config['protocol'] not in ttl_supported and 'ttl' in config:
+            raise ConfigError(f'"ttl" {error_msg_uns} with protocol "{config["protocol"]}"')
+
+        if config['ip_version'] == 'both':
+            if config['protocol'] not in dualstack_supported:
+                raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} '
+                                  f'with protocol "{config["protocol"]}"')
+            # dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org)
+            if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] not in dyndns_dualstack_servers:
+                raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} '
+                                  f'for "{config["server"]}" with protocol "{config["protocol"]}"')
+
+        if {'wait_time', 'expiry_time'} <= config.keys() and int(config['expiry_time']) < int(config['wait_time']):
+                raise ConfigError(f'"expiry-time" must be greater than "wait-time" for '
+                                  f'Dynamic DNS service "{service}"')
+
+    return None
+
+def generate(dyndns):
+    # bail out early - looks like removal from running config
+    if not dyndns or 'name' not in dyndns:
+        return None
+
+    render(config_file, 'dns-dynamic/ddclient.conf.j2', dyndns, permission=0o600)
+    render(systemd_override, 'dns-dynamic/override.conf.j2', dyndns)
+    return None
+
+def apply(dyndns):
+    systemd_service = 'ddclient.service'
+    # Reload systemd manager configuration
+    call('systemctl daemon-reload')
+
+    # bail out early - looks like removal from running config
+    if not dyndns or 'name' not in dyndns:
+        call(f'systemctl stop {systemd_service}')
+        if os.path.exists(config_file):
+            os.unlink(config_file)
+    else:
+        call(f'systemctl reload-or-restart {systemd_service}')
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_dns_forwarding.py b/src/conf_mode/service_dns_forwarding.py
new file mode 100755
index 000000000..c186f47af
--- /dev/null
+++ b/src/conf_mode/service_dns_forwarding.py
@@ -0,0 +1,358 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2022 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 netifaces import interfaces
+from sys import exit
+from glob import glob
+
+from vyos.config import Config
+from vyos.hostsd_client import Client as hostsd_client
+from vyos.template import render
+from vyos.template import bracketize_ipv6
+from vyos.utils.process import call
+from vyos.utils.permission import chown
+from vyos.utils.dict import dict_search
+
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+pdns_rec_user = pdns_rec_group = 'pdns'
+pdns_rec_run_dir = '/run/powerdns'
+pdns_rec_lua_conf_file = f'{pdns_rec_run_dir}/recursor.conf.lua'
+pdns_rec_hostsd_lua_conf_file = f'{pdns_rec_run_dir}/recursor.vyos-hostsd.conf.lua'
+pdns_rec_hostsd_zones_file = f'{pdns_rec_run_dir}/recursor.forward-zones.conf'
+pdns_rec_config_file = f'{pdns_rec_run_dir}/recursor.conf'
+
+hostsd_tag = 'static'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['service', 'dns', 'forwarding']
+    if not conf.exists(base):
+        return None
+
+    dns = conf.get_config_dict(base, key_mangling=('-', '_'),
+                               no_tag_node_value_mangle=True,
+                               get_first_key=True,
+                               with_recursive_defaults=True)
+
+    # some additions to the default dictionary
+    if 'system' in dns:
+        base_nameservers = ['system', 'name-server']
+        if conf.exists(base_nameservers):
+            dns.update({'system_name_server': conf.return_values(base_nameservers)})
+
+    if 'authoritative_domain' in dns:
+        dns['authoritative_zones'] = []
+        dns['authoritative_zone_errors'] = []
+        for node in dns['authoritative_domain']:
+            zonedata = dns['authoritative_domain'][node]
+            if ('disable' in zonedata) or (not 'records' in zonedata):
+                continue
+            zone = {
+                'name': node,
+                'file': "{}/zone.{}.conf".format(pdns_rec_run_dir, node),
+                'records': [],
+            }
+
+            recorddata = zonedata['records']
+
+            for rtype in [ 'a', 'aaaa', 'cname', 'mx', 'ns', 'ptr', 'txt', 'spf', 'srv', 'naptr' ]:
+                if rtype not in recorddata:
+                    continue
+                for subnode in recorddata[rtype]:
+                    if 'disable' in recorddata[rtype][subnode]:
+                        continue
+
+                    rdata = recorddata[rtype][subnode]
+
+                    if rtype in [ 'a', 'aaaa' ]:
+                        if not 'address' in rdata:
+                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one address is required')
+                            continue
+
+                        if subnode == 'any':
+                            subnode = '*'
+
+                        for address in rdata['address']:
+                            zone['records'].append({
+                                'name': subnode,
+                                'type': rtype.upper(),
+                                'ttl': rdata['ttl'],
+                                'value': address
+                            })
+                    elif rtype in ['cname', 'ptr', 'ns']:
+                        if not 'target' in rdata:
+                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: target is required')
+                            continue
+
+                        zone['records'].append({
+                            'name': subnode,
+                            'type': rtype.upper(),
+                            'ttl': rdata['ttl'],
+                            'value': '{}.'.format(rdata['target'])
+                        })
+                    elif rtype == 'mx':
+                        if not 'server' in rdata:
+                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one server is required')
+                            continue
+
+                        for servername in rdata['server']:
+                            serverdata = rdata['server'][servername]
+                            zone['records'].append({
+                                'name': subnode,
+                                'type': rtype.upper(),
+                                'ttl': rdata['ttl'],
+                                'value': '{} {}.'.format(serverdata['priority'], servername)
+                            })
+                    elif rtype == 'txt':
+                        if not 'value' in rdata:
+                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one value is required')
+                            continue
+
+                        for value in rdata['value']:
+                            zone['records'].append({
+                                'name': subnode,
+                                'type': rtype.upper(),
+                                'ttl': rdata['ttl'],
+                                'value': "\"{}\"".format(value.replace("\"", "\\\""))
+                            })
+                    elif rtype == 'spf':
+                        if not 'value' in rdata:
+                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: value is required')
+                            continue
+
+                        zone['records'].append({
+                            'name': subnode,
+                            'type': rtype.upper(),
+                            'ttl': rdata['ttl'],
+                            'value': '"{}"'.format(rdata['value'].replace("\"", "\\\""))
+                        })
+                    elif rtype == 'srv':
+                        if not 'entry' in rdata:
+                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one entry is required')
+                            continue
+
+                        for entryno in rdata['entry']:
+                            entrydata = rdata['entry'][entryno]
+                            if not 'hostname' in entrydata:
+                                dns['authoritative_zone_errors'].append(f'{subnode}.{node}: hostname is required for entry {entryno}')
+                                continue
+
+                            if not 'port' in entrydata:
+                                dns['authoritative_zone_errors'].append(f'{subnode}.{node}: port is required for entry {entryno}')
+                                continue
+
+                            zone['records'].append({
+                                'name': subnode,
+                                'type': rtype.upper(),
+                                'ttl': rdata['ttl'],
+                                'value': '{} {} {} {}.'.format(entrydata['priority'], entrydata['weight'], entrydata['port'], entrydata['hostname'])
+                            })
+                    elif rtype == 'naptr':
+                        if not 'rule' in rdata:
+                            dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one rule is required')
+                            continue
+
+                        for ruleno in rdata['rule']:
+                            ruledata = rdata['rule'][ruleno]
+                            flags = ""
+                            if 'lookup-srv' in ruledata:
+                                flags += "S"
+                            if 'lookup-a' in ruledata:
+                                flags += "A"
+                            if 'resolve-uri' in ruledata:
+                                flags += "U"
+                            if 'protocol-specific' in ruledata:
+                                flags += "P"
+
+                            if 'order' in ruledata:
+                                order = ruledata['order']
+                            else:
+                                order = ruleno
+
+                            if 'regexp' in ruledata:
+                                regexp= ruledata['regexp'].replace("\"", "\\\"")
+                            else:
+                                regexp = ''
+
+                            if ruledata['replacement']:
+                                replacement = '{}.'.format(ruledata['replacement'])
+                            else:
+                                replacement = ''
+
+                            zone['records'].append({
+                                'name': subnode,
+                                'type': rtype.upper(),
+                                'ttl': rdata['ttl'],
+                                'value': '{} {} "{}" "{}" "{}" {}'.format(order, ruledata['preference'], flags, ruledata['service'], regexp, replacement)
+                            })
+
+            dns['authoritative_zones'].append(zone)
+
+    return dns
+
+def verify(dns):
+    # bail out early - looks like removal from running config
+    if not dns:
+        return None
+
+    if 'listen_address' not in dns:
+        raise ConfigError('DNS forwarding requires a listen-address')
+
+    if 'allow_from' not in dns:
+        raise ConfigError('DNS forwarding requires an allow-from network')
+
+    # we can not use dict_search() when testing for domain servers
+    # as a domain will contains dot's which is out dictionary delimiter.
+    if 'domain' in dns:
+        for domain in dns['domain']:
+            if 'name_server' not in dns['domain'][domain]:
+                raise ConfigError(f'No server configured for domain {domain}!')
+
+    if 'dns64_prefix' in dns:
+        dns_prefix = dns['dns64_prefix'].split('/')[1]
+        # RFC 6147 requires prefix /96
+        if int(dns_prefix) != 96:
+            raise ConfigError('DNS 6to4 prefix must be of length /96')
+
+    if ('authoritative_zone_errors' in dns) and dns['authoritative_zone_errors']:
+        for error in dns['authoritative_zone_errors']:
+            print(error)
+        raise ConfigError('Invalid authoritative records have been defined')
+
+    if 'system' in dns:
+        if not 'system_name_server' in dns:
+            print('Warning: No "system name-server" configured')
+
+    return None
+
+def generate(dns):
+    # bail out early - looks like removal from running config
+    if not dns:
+        return None
+
+    render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.j2',
+            dns, user=pdns_rec_user, group=pdns_rec_group)
+
+    render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.j2',
+            dns, user=pdns_rec_user, group=pdns_rec_group)
+
+    for zone_filename in glob(f'{pdns_rec_run_dir}/zone.*.conf'):
+        os.unlink(zone_filename)
+
+    if 'authoritative_zones' in dns:
+        for zone in dns['authoritative_zones']:
+            render(zone['file'], 'dns-forwarding/recursor.zone.conf.j2',
+                    zone, user=pdns_rec_user, group=pdns_rec_group)
+
+
+    # if vyos-hostsd didn't create its files yet, create them (empty)
+    for file in [pdns_rec_hostsd_lua_conf_file, pdns_rec_hostsd_zones_file]:
+        with open(file, 'a'):
+            pass
+        chown(file, user=pdns_rec_user, group=pdns_rec_group)
+
+    return None
+
+def apply(dns):
+    if not dns:
+        # DNS forwarding is removed in the commit
+        call('systemctl stop pdns-recursor.service')
+
+        if os.path.isfile(pdns_rec_config_file):
+            os.unlink(pdns_rec_config_file)
+
+        for zone_filename in glob(f'{pdns_rec_run_dir}/zone.*.conf'):
+            os.unlink(zone_filename)
+    else:
+        ### first apply vyos-hostsd config
+        hc = hostsd_client()
+
+        # add static nameservers to hostsd so they can be joined with other
+        # sources
+        hc.delete_name_servers([hostsd_tag])
+        if 'name_server' in dns:
+            # 'name_server' is of the form
+            # {'192.0.2.1': {'port': 53}, '2001:db8::1': {'port': 853}, ...}
+            # canonicalize them as ['192.0.2.1:53', '[2001:db8::1]:853', ...]
+            nslist = [(lambda h, p: f"{bracketize_ipv6(h)}:{p['port']}")(h, p)
+                      for (h, p) in dns['name_server'].items()]
+            hc.add_name_servers({hostsd_tag: nslist})
+
+        # delete all nameserver tags
+        hc.delete_name_server_tags_recursor(hc.get_name_server_tags_recursor())
+
+        ## add nameserver tags - the order determines the nameserver order!
+        # our own tag (static)
+        hc.add_name_server_tags_recursor([hostsd_tag])
+
+        if 'system' in dns:
+            hc.add_name_server_tags_recursor(['system'])
+        else:
+            hc.delete_name_server_tags_recursor(['system'])
+
+        # add dhcp nameserver tags for configured interfaces
+        if 'system_name_server' in dns:
+            for interface in dns['system_name_server']:
+                # system_name_server key contains both IP addresses and interface
+                # names (DHCP) to use DNS servers. We need to check if the
+                # value is an interface name - only if this is the case, add the
+                # interface based DNS forwarder.
+                if interface in interfaces():
+                    hc.add_name_server_tags_recursor(['dhcp-' + interface,
+                                                      'dhcpv6-' + interface ])
+
+        # hostsd will generate the forward-zones file
+        # the list and keys() are required as get returns a dict, not list
+        hc.delete_forward_zones(list(hc.get_forward_zones().keys()))
+        if 'domain' in dns:
+            zones = dns['domain']
+            for domain in zones.keys():
+                # 'name_server' is of the form
+                # {'192.0.2.1': {'port': 53}, '2001:db8::1': {'port': 853}, ...}
+                # canonicalize them as ['192.0.2.1:53', '[2001:db8::1]:853', ...]
+                zones[domain]['name_server'] = [(lambda h, p: f"{bracketize_ipv6(h)}:{p['port']}")(h, p)
+                                                for (h, p) in zones[domain]['name_server'].items()]
+            hc.add_forward_zones(zones)
+
+        # hostsd generates NTAs for the authoritative zones
+        # the list and keys() are required as get returns a dict, not list
+        hc.delete_authoritative_zones(list(hc.get_authoritative_zones()))
+        if 'authoritative_zones' in dns:
+            hc.add_authoritative_zones(list(map(lambda zone: zone['name'], dns['authoritative_zones'])))
+
+        # call hostsd to generate forward-zones and its lua-config-file
+        hc.apply()
+
+        ### finally (re)start pdns-recursor
+        call('systemctl restart pdns-recursor.service')
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_event-handler.py b/src/conf_mode/service_event-handler.py
new file mode 100755
index 000000000..5028ef52f
--- /dev/null
+++ b/src/conf_mode/service_event-handler.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 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 json
+from pathlib import Path
+
+from vyos.config import Config
+from vyos.utils.dict import dict_search
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+
+airbag.enable()
+
+service_name = 'vyos-event-handler'
+service_conf = Path(f'/run/{service_name}.conf')
+
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base = ['service', 'event-handler', 'event']
+    config = conf.get_config_dict(base,
+                                  get_first_key=True,
+                                  no_tag_node_value_mangle=True)
+
+    return config
+
+
+def verify(config):
+    # bail out early - looks like removal from running config
+    if not config:
+        return None
+
+    for name, event_config in config.items():
+        if not dict_search('filter.pattern', event_config) or not dict_search(
+                'script.path', event_config):
+            raise ConfigError(
+                'Event-handler: both pattern and script path items are mandatory'
+            )
+
+        if dict_search('script.environment.message', event_config):
+            raise ConfigError(
+                'Event-handler: "message" environment variable is reserved for log message text'
+            )
+
+
+def generate(config):
+    if not config:
+        # Remove old config and return
+        service_conf.unlink(missing_ok=True)
+        return None
+
+    # Write configuration file
+    conf_json = json.dumps(config, indent=4)
+    service_conf.write_text(conf_json)
+
+    return None
+
+
+def apply(config):
+    if config:
+        call(f'systemctl restart {service_name}.service')
+    else:
+        call(f'systemctl stop {service_name}.service')
+
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_event_handler.py b/src/conf_mode/service_event_handler.py
deleted file mode 100755
index 5028ef52f..000000000
--- a/src/conf_mode/service_event_handler.py
+++ /dev/null
@@ -1,92 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2022 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 json
-from pathlib import Path
-
-from vyos.config import Config
-from vyos.utils.dict import dict_search
-from vyos.utils.process import call
-from vyos import ConfigError
-from vyos import airbag
-
-airbag.enable()
-
-service_name = 'vyos-event-handler'
-service_conf = Path(f'/run/{service_name}.conf')
-
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base = ['service', 'event-handler', 'event']
-    config = conf.get_config_dict(base,
-                                  get_first_key=True,
-                                  no_tag_node_value_mangle=True)
-
-    return config
-
-
-def verify(config):
-    # bail out early - looks like removal from running config
-    if not config:
-        return None
-
-    for name, event_config in config.items():
-        if not dict_search('filter.pattern', event_config) or not dict_search(
-                'script.path', event_config):
-            raise ConfigError(
-                'Event-handler: both pattern and script path items are mandatory'
-            )
-
-        if dict_search('script.environment.message', event_config):
-            raise ConfigError(
-                'Event-handler: "message" environment variable is reserved for log message text'
-            )
-
-
-def generate(config):
-    if not config:
-        # Remove old config and return
-        service_conf.unlink(missing_ok=True)
-        return None
-
-    # Write configuration file
-    conf_json = json.dumps(config, indent=4)
-    service_conf.write_text(conf_json)
-
-    return None
-
-
-def apply(config):
-    if config:
-        call(f'systemctl restart {service_name}.service')
-    else:
-        call(f'systemctl stop {service_name}.service')
-
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/service_https.py b/src/conf_mode/service_https.py
new file mode 100755
index 000000000..3dc5dfc01
--- /dev/null
+++ b/src/conf_mode/service_https.py
@@ -0,0 +1,335 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-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 json
+
+from copy import deepcopy
+from time import sleep
+
+import vyos.defaults
+import vyos.certbot_util
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.configdiff import get_config_diff
+from vyos.configverify import verify_vrf
+from vyos import ConfigError
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_private_key
+from vyos.template import render
+from vyos.utils.process import call
+from vyos.utils.process import is_systemd_service_running
+from vyos.utils.process import is_systemd_service_active
+from vyos.utils.network import check_port_availability
+from vyos.utils.network import is_listen_port_bind_service
+from vyos.utils.file import write_file
+
+from vyos import airbag
+airbag.enable()
+
+config_file = '/etc/nginx/sites-available/default'
+systemd_override = r'/run/systemd/system/nginx.service.d/override.conf'
+cert_dir = '/etc/ssl/certs'
+key_dir = '/etc/ssl/private'
+certbot_dir = vyos.defaults.directories['certbot']
+
+api_config_state = '/run/http-api-state'
+systemd_service = '/run/systemd/system/vyos-http-api.service'
+
+# https config needs to coordinate several subsystems: api, certbot,
+# self-signed certificate, as well as the virtual hosts defined within the
+# https config definition itself. Consequently, one needs a general dict,
+# encompassing the https and other configs, and a list of such virtual hosts
+# (server blocks in nginx terminology) to pass to the jinja2 template.
+default_server_block = {
+    'id'        : '',
+    'address'   : '*',
+    'port'      : '443',
+    'name'      : ['_'],
+    'api'       : False,
+    'vyos_cert' : {},
+    'certbot'   : False
+}
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base = ['service', 'https']
+    if not conf.exists(base):
+        return None
+
+    diff = get_config_diff(conf)
+
+    https = conf.get_config_dict(base, get_first_key=True)
+
+    if https:
+        https['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+                                            no_tag_node_value_mangle=True,
+                                            get_first_key=True)
+
+    https['children_changed'] = diff.node_changed_children(base)
+    https['api_add_or_delete'] = diff.node_changed_presence(base + ['api'])
+
+    if 'api' not in https:
+        return https
+
+    http_api = conf.get_config_dict(base + ['api'], key_mangling=('-', '_'),
+                                    no_tag_node_value_mangle=True,
+                                    get_first_key=True,
+                                    with_recursive_defaults=True)
+
+    if http_api.from_defaults(['graphql']):
+        del http_api['graphql']
+
+    # Do we run inside a VRF context?
+    vrf_path = ['service', 'https', 'vrf']
+    if conf.exists(vrf_path):
+        http_api['vrf'] = conf.return_value(vrf_path)
+
+    https['api'] = http_api
+
+    return https
+
+def verify(https):
+    from vyos.utils.dict import dict_search
+
+    if https is None:
+        return None
+
+    if 'certificates' in https:
+        certificates = https['certificates']
+
+        if 'certificate' in certificates:
+            if not https['pki']:
+                raise ConfigError("PKI is not configured")
+
+            cert_name = certificates['certificate']
+
+            if cert_name not in https['pki']['certificate']:
+                raise ConfigError("Invalid certificate on https configuration")
+
+            pki_cert = https['pki']['certificate'][cert_name]
+
+            if 'certificate' not in pki_cert:
+                raise ConfigError("Missing certificate on https configuration")
+
+            if 'private' not in pki_cert or 'key' not in pki_cert['private']:
+                raise ConfigError("Missing certificate private key on https configuration")
+
+        if 'certbot' in https['certificates']:
+            vhost_names = []
+            for _, vh_conf in https.get('virtual-host', {}).items():
+                vhost_names += vh_conf.get('server-name', [])
+            domains = https['certificates']['certbot'].get('domain-name', [])
+            domains_found = [domain for domain in domains if domain in vhost_names]
+            if not domains_found:
+                raise ConfigError("At least one 'virtual-host <id> server-name' "
+                              "matching the 'certbot domain-name' is required.")
+
+    server_block_list = []
+
+    # organize by vhosts
+    vhost_dict = https.get('virtual-host', {})
+
+    if not vhost_dict:
+        # no specified virtual hosts (server blocks); use default
+        server_block_list.append(default_server_block)
+    else:
+        for vhost in list(vhost_dict):
+            server_block = deepcopy(default_server_block)
+            data = vhost_dict.get(vhost, {})
+            server_block['address'] = data.get('listen-address', '*')
+            server_block['port'] = data.get('port', '443')
+            server_block_list.append(server_block)
+
+    for entry in server_block_list:
+        _address = entry.get('address')
+        _address = '0.0.0.0' if _address == '*' else _address
+        _port = entry.get('port')
+        proto = 'tcp'
+        if check_port_availability(_address, int(_port), proto) is not True and \
+                not is_listen_port_bind_service(int(_port), 'nginx'):
+            raise ConfigError(f'"{proto}" port "{_port}" is used by another service')
+
+    verify_vrf(https)
+
+    # Verify API server settings, if present
+    if 'api' in https:
+        keys = dict_search('api.keys.id', https)
+        gql_auth_type = dict_search('api.graphql.authentication.type', https)
+
+        # If "api graphql" is not defined and `gql_auth_type` is None,
+        # there's certainly no JWT auth option, and keys are required
+        jwt_auth = (gql_auth_type == "token")
+
+        # Check for incomplete key configurations in every case
+        valid_keys_exist = False
+        if keys:
+            for k in keys:
+                if 'key' not in keys[k]:
+                    raise ConfigError(f'Missing HTTPS API key string for key id "{k}"')
+                else:
+                    valid_keys_exist = True
+
+        # If only key-based methods are enabled,
+        # fail the commit if no valid key configurations are found
+        if (not valid_keys_exist) and (not jwt_auth):
+            raise ConfigError('At least one HTTPS API key is required unless GraphQL token authentication is enabled')
+
+        if (not valid_keys_exist) and jwt_auth:
+            Warning(f'API keys are not configured: the classic (non-GraphQL) API will be unavailable.')
+
+    return None
+
+def generate(https):
+    if https is None:
+        return None
+
+    if 'api' not in https:
+        if os.path.exists(systemd_service):
+            os.unlink(systemd_service)
+    else:
+        render(systemd_service, 'https/vyos-http-api.service.j2', https['api'])
+        with open(api_config_state, 'w') as f:
+            json.dump(https['api'], f, indent=2)
+
+    server_block_list = []
+
+    # organize by vhosts
+
+    vhost_dict = https.get('virtual-host', {})
+
+    if not vhost_dict:
+        # no specified virtual hosts (server blocks); use default
+        server_block_list.append(default_server_block)
+    else:
+        for vhost in list(vhost_dict):
+            server_block = deepcopy(default_server_block)
+            server_block['id'] = vhost
+            data = vhost_dict.get(vhost, {})
+            server_block['address'] = data.get('listen-address', '*')
+            server_block['port'] = data.get('port', '443')
+            name = data.get('server-name', ['_'])
+            server_block['name'] = name
+            allow_client = data.get('allow-client', {})
+            server_block['allow_client'] = allow_client.get('address', [])
+            server_block_list.append(server_block)
+
+    # get certificate data
+
+    cert_dict = https.get('certificates', {})
+
+    if 'certificate' in cert_dict:
+        cert_name = cert_dict['certificate']
+        pki_cert = https['pki']['certificate'][cert_name]
+
+        cert_path = os.path.join(cert_dir, f'{cert_name}.pem')
+        key_path = os.path.join(key_dir, f'{cert_name}.pem')
+
+        server_cert = str(wrap_certificate(pki_cert['certificate']))
+        if 'ca-certificate' in cert_dict:
+            ca_cert = cert_dict['ca-certificate']
+            server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate']))
+
+        write_file(cert_path, server_cert)
+        write_file(key_path, wrap_private_key(pki_cert['private']['key']))
+
+        vyos_cert_data = {
+            'crt': cert_path,
+            'key': key_path
+        }
+
+        for block in server_block_list:
+            block['vyos_cert'] = vyos_cert_data
+
+    # letsencrypt certificate using certbot
+
+    certbot = False
+    cert_domains = cert_dict.get('certbot', {}).get('domain-name', [])
+    if cert_domains:
+        certbot = True
+        for domain in cert_domains:
+            sub_list = vyos.certbot_util.choose_server_block(server_block_list,
+                                                             domain)
+            if sub_list:
+                for sb in sub_list:
+                    sb['certbot'] = True
+                    sb['certbot_dir'] = certbot_dir
+                    # certbot organizes certificates by first domain
+                    sb['certbot_domain_dir'] = cert_domains[0]
+
+    if 'api' in list(https):
+        vhost_list = https.get('api-restrict', {}).get('virtual-host', [])
+        if not vhost_list:
+            for block in server_block_list:
+                block['api'] = True
+        else:
+            for block in server_block_list:
+                if block['id'] in vhost_list:
+                    block['api'] = True
+
+    data = {
+        'server_block_list': server_block_list,
+        'certbot': certbot
+    }
+
+    render(config_file, 'https/nginx.default.j2', data)
+    render(systemd_override, 'https/override.conf.j2', https)
+    return None
+
+def apply(https):
+    # Reload systemd manager configuration
+    call('systemctl daemon-reload')
+    http_api_service_name = 'vyos-http-api.service'
+    https_service_name = 'nginx.service'
+
+    if https is None:
+        if is_systemd_service_active(f'{http_api_service_name}'):
+            call(f'systemctl stop {http_api_service_name}')
+        call(f'systemctl stop {https_service_name}')
+        return
+
+    if 'api' in https['children_changed']:
+        if 'api' in https:
+            if is_systemd_service_running(f'{http_api_service_name}'):
+                call(f'systemctl reload {http_api_service_name}')
+            else:
+                call(f'systemctl restart {http_api_service_name}')
+            # Let uvicorn settle before (possibly) restarting nginx
+            sleep(1)
+        else:
+            if is_systemd_service_active(f'{http_api_service_name}'):
+                call(f'systemctl stop {http_api_service_name}')
+
+    if (not is_systemd_service_running(f'{https_service_name}') or
+        https['api_add_or_delete'] or
+        set(https['children_changed']) - set(['api'])):
+        call(f'systemctl restart {https_service_name}')
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        sys.exit(1)
diff --git a/src/conf_mode/service_https_certificates_certbot.py b/src/conf_mode/service_https_certificates_certbot.py
new file mode 100755
index 000000000..1a6a498de
--- /dev/null
+++ b/src/conf_mode/service_https_certificates_certbot.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2020 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 sys
+import os
+
+import vyos.defaults
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.utils.process import cmd
+from vyos.utils.process import call
+from vyos.utils.process import is_systemd_service_running
+
+from vyos import airbag
+airbag.enable()
+
+vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode']
+vyos_certbot_dir = vyos.defaults.directories['certbot']
+
+dependencies = [
+    'service_https.py',
+]
+
+def request_certbot(cert):
+    email = cert.get('email')
+    if email is not None:
+        email_flag = '-m {0}'.format(email)
+    else:
+        email_flag = ''
+
+    domains = cert.get('domains')
+    if domains is not None:
+        domain_flag = '-d ' + ' -d '.join(domains)
+    else:
+        domain_flag = ''
+
+    certbot_cmd = f'certbot certonly --config-dir {vyos_certbot_dir} -n --nginx --agree-tos --no-eff-email --expand {email_flag} {domain_flag}'
+
+    cmd(certbot_cmd,
+        raising=ConfigError,
+        message="The certbot request failed for the specified domains.")
+
+def get_config():
+    conf = Config()
+    if not conf.exists('service https certificates certbot'):
+        return None
+    else:
+        conf.set_level('service https certificates certbot')
+
+    cert = {}
+
+    if conf.exists('domain-name'):
+        cert['domains'] = conf.return_values('domain-name')
+
+    if conf.exists('email'):
+        cert['email'] = conf.return_value('email')
+
+    return cert
+
+def verify(cert):
+    if cert is None:
+        return None
+
+    if 'domains' not in cert:
+        raise ConfigError("At least one domain name is required to"
+                          " request a letsencrypt certificate.")
+
+    if 'email' not in cert:
+        raise ConfigError("An email address is required to request"
+                          " a letsencrypt certificate.")
+
+def generate(cert):
+    if cert is None:
+        return None
+
+    # certbot will attempt to reload nginx, even with 'certonly';
+    # start nginx if not active
+    if not is_systemd_service_running('nginx.service'):
+        call('systemctl start nginx.service')
+
+    request_certbot(cert)
+
+def apply(cert):
+    if cert is not None:
+        call('systemctl restart certbot.timer')
+    else:
+        call('systemctl stop certbot.timer')
+        return None
+
+    for dep in dependencies:
+        cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError)
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        sys.exit(1)
diff --git a/src/conf_mode/service_ids_ddos-protection.py b/src/conf_mode/service_ids_ddos-protection.py
new file mode 100755
index 000000000..276a71fcb
--- /dev/null
+++ b/src/conf_mode/service_ids_ddos-protection.py
@@ -0,0 +1,104 @@
+#!/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 sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/fastnetmon/fastnetmon.conf'
+networks_list = r'/run/fastnetmon/networks_list'
+excluded_networks_list = r'/run/fastnetmon/excluded_networks_list'
+attack_dir = '/var/log/fastnetmon_attacks'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['service', 'ids', 'ddos-protection']
+    if not conf.exists(base):
+        return None
+
+    fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                      get_first_key=True,
+                                      with_recursive_defaults=True)
+
+    return fastnetmon
+
+def verify(fastnetmon):
+    if not fastnetmon:
+        return None
+
+    if 'mode' not in fastnetmon:
+        raise ConfigError('Specify operating mode!')
+
+    if fastnetmon.get('mode') == 'mirror' and 'listen_interface' not in fastnetmon:
+        raise ConfigError("Incorrect settings for 'mode mirror': must specify interface(s) for traffic mirroring")
+
+    if fastnetmon.get('mode') == 'sflow' and 'listen_address' not in fastnetmon.get('sflow', {}):
+        raise ConfigError("Incorrect settings for 'mode sflow': must specify sFlow 'listen-address'")
+
+    if 'alert_script' in fastnetmon:
+        if os.path.isfile(fastnetmon['alert_script']):
+            # Check script permissions
+            if not os.access(fastnetmon['alert_script'], os.X_OK):
+                raise ConfigError('Script "{alert_script}" is not executable!'.format(fastnetmon['alert_script']))
+        else:
+           raise ConfigError('File "{alert_script}" does not exists!'.format(fastnetmon))
+
+def generate(fastnetmon):
+    if not fastnetmon:
+        for file in [config_file, networks_list]:
+            if os.path.isfile(file):
+                os.unlink(file)
+
+        return None
+
+    # Create dir for log attack details
+    if not os.path.exists(attack_dir):
+        os.mkdir(attack_dir)
+
+    render(config_file, 'ids/fastnetmon.j2', fastnetmon)
+    render(networks_list, 'ids/fastnetmon_networks_list.j2', fastnetmon)
+    render(excluded_networks_list, 'ids/fastnetmon_excluded_networks_list.j2', fastnetmon)
+    return None
+
+def apply(fastnetmon):
+    systemd_service = 'fastnetmon.service'
+    if not fastnetmon:
+        # Stop fastnetmon service if removed
+        call(f'systemctl stop {systemd_service}')
+    else:
+        call(f'systemctl reload-or-restart {systemd_service}')
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py
deleted file mode 100755
index 276a71fcb..000000000
--- a/src/conf_mode/service_ids_fastnetmon.py
+++ /dev/null
@@ -1,104 +0,0 @@
-#!/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 sys import exit
-
-from vyos.config import Config
-from vyos.template import render
-from vyos.utils.process import call
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/run/fastnetmon/fastnetmon.conf'
-networks_list = r'/run/fastnetmon/networks_list'
-excluded_networks_list = r'/run/fastnetmon/excluded_networks_list'
-attack_dir = '/var/log/fastnetmon_attacks'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['service', 'ids', 'ddos-protection']
-    if not conf.exists(base):
-        return None
-
-    fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                      get_first_key=True,
-                                      with_recursive_defaults=True)
-
-    return fastnetmon
-
-def verify(fastnetmon):
-    if not fastnetmon:
-        return None
-
-    if 'mode' not in fastnetmon:
-        raise ConfigError('Specify operating mode!')
-
-    if fastnetmon.get('mode') == 'mirror' and 'listen_interface' not in fastnetmon:
-        raise ConfigError("Incorrect settings for 'mode mirror': must specify interface(s) for traffic mirroring")
-
-    if fastnetmon.get('mode') == 'sflow' and 'listen_address' not in fastnetmon.get('sflow', {}):
-        raise ConfigError("Incorrect settings for 'mode sflow': must specify sFlow 'listen-address'")
-
-    if 'alert_script' in fastnetmon:
-        if os.path.isfile(fastnetmon['alert_script']):
-            # Check script permissions
-            if not os.access(fastnetmon['alert_script'], os.X_OK):
-                raise ConfigError('Script "{alert_script}" is not executable!'.format(fastnetmon['alert_script']))
-        else:
-           raise ConfigError('File "{alert_script}" does not exists!'.format(fastnetmon))
-
-def generate(fastnetmon):
-    if not fastnetmon:
-        for file in [config_file, networks_list]:
-            if os.path.isfile(file):
-                os.unlink(file)
-
-        return None
-
-    # Create dir for log attack details
-    if not os.path.exists(attack_dir):
-        os.mkdir(attack_dir)
-
-    render(config_file, 'ids/fastnetmon.j2', fastnetmon)
-    render(networks_list, 'ids/fastnetmon_networks_list.j2', fastnetmon)
-    render(excluded_networks_list, 'ids/fastnetmon_excluded_networks_list.j2', fastnetmon)
-    return None
-
-def apply(fastnetmon):
-    systemd_service = 'fastnetmon.service'
-    if not fastnetmon:
-        # Stop fastnetmon service if removed
-        call(f'systemctl stop {systemd_service}')
-    else:
-        call(f'systemctl reload-or-restart {systemd_service}')
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/service_lldp.py b/src/conf_mode/service_lldp.py
new file mode 100755
index 000000000..3c647a0e8
--- /dev/null
+++ b/src/conf_mode/service_lldp.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2017-2022 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 sys import exit
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.utils.network import is_addr_assigned
+from vyos.utils.network import is_loopback_addr
+from vyos.version import get_version_data
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
+from vyos.template import render
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = "/etc/default/lldpd"
+vyos_config_file = "/etc/lldpd.d/01-vyos.conf"
+base = ['service', 'lldp']
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    if not conf.exists(base):
+        return {}
+
+    lldp = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                no_tag_node_value_mangle=True,
+                                get_first_key=True,
+                                with_recursive_defaults=True)
+
+    if conf.exists(['service', 'snmp']):
+        lldp['system_snmp_enabled'] = ''
+
+    version_data = get_version_data()
+    lldp['version'] = version_data['version']
+
+    # prune location information if not set by user
+    for interface in lldp.get('interface', []):
+        if lldp.from_defaults(['interface', interface, 'location']):
+            del lldp['interface'][interface]['location']
+        elif lldp.from_defaults(['interface', interface, 'location','coordinate_based']):
+            del lldp['interface'][interface]['location']['coordinate_based']
+
+    return lldp
+
+def verify(lldp):
+    # bail out early - looks like removal from running config
+    if lldp is None:
+        return
+
+    if 'management_address' in lldp:
+        for address in lldp['management_address']:
+            message = f'LLDP management address "{address}" is invalid'
+            if is_loopback_addr(address):
+                Warning(f'{message} - loopback address')
+            elif not is_addr_assigned(address):
+                Warning(f'{message} - not assigned to any interface')
+
+    if 'interface' in lldp:
+        for interface, interface_config in lldp['interface'].items():
+            # bail out early if no location info present in interface config
+            if 'location' not in interface_config:
+                continue
+            if 'coordinate_based' in interface_config['location']:
+                if not {'latitude', 'latitude'} <= set(interface_config['location']['coordinate_based']):
+                    raise ConfigError(f'Must define both longitude and latitude for "{interface}" location!')
+
+    # check options
+    if 'snmp' in lldp:
+        if 'system_snmp_enabled' not in lldp:
+            raise ConfigError('SNMP must be configured to enable LLDP SNMP!')
+
+
+def generate(lldp):
+    # bail out early - looks like removal from running config
+    if lldp is None:
+        return
+
+    render(config_file, 'lldp/lldpd.j2', lldp)
+    render(vyos_config_file, 'lldp/vyos.conf.j2', lldp)
+
+def apply(lldp):
+    systemd_service = 'lldpd.service'
+    if lldp:
+        # start/restart lldp service
+        call(f'systemctl restart {systemd_service}')
+    else:
+        # LLDP service has been terminated
+        call(f'systemctl stop {systemd_service}')
+        if os.path.isfile(config_file):
+            os.unlink(config_file)
+        if os.path.isfile(vyos_config_file):
+            os.unlink(vyos_config_file)
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_mdns-repeater.py b/src/conf_mode/service_mdns-repeater.py
deleted file mode 100755
index 6526c23d1..000000000
--- a/src/conf_mode/service_mdns-repeater.py
+++ /dev/null
@@ -1,146 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2017-2022 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 json import loads
-from sys import exit
-from netifaces import ifaddresses, interfaces, AF_INET, AF_INET6
-
-from vyos.config import Config
-from vyos.ifconfig.vrrp import VRRP
-from vyos.template import render
-from vyos.utils.process import call
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = '/run/avahi-daemon/avahi-daemon.conf'
-systemd_override = r'/run/systemd/system/avahi-daemon.service.d/override.conf'
-vrrp_running_file = '/run/mdns_vrrp_active'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base = ['service', 'mdns', 'repeater']
-    if not conf.exists(base):
-        return None
-
-    mdns = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                no_tag_node_value_mangle=True,
-                                get_first_key=True,
-                                with_recursive_defaults=True)
-
-    if mdns:
-        mdns['vrrp_exists'] = conf.exists('high-availability vrrp')
-        mdns['config_file'] = config_file
-
-    return mdns
-
-def verify(mdns):
-    if not mdns or 'disable' in mdns:
-        return None
-
-    # We need at least two interfaces to repeat mDNS advertisments
-    if 'interface' not in mdns or len(mdns['interface']) < 2:
-        raise ConfigError('mDNS repeater requires at least 2 configured interfaces!')
-
-    # For mdns-repeater to work it is essential that the interfaces has
-    # an IPv4 address assigned
-    for interface in mdns['interface']:
-        if interface not in interfaces():
-            raise ConfigError(f'Interface "{interface}" does not exist!')
-
-        if mdns['ip_version'] in ['ipv4', 'both'] and AF_INET not in ifaddresses(interface):
-            raise ConfigError('mDNS repeater requires an IPv4 address to be '
-                                  f'configured on interface "{interface}"')
-
-        if mdns['ip_version'] in ['ipv6', 'both'] and AF_INET6 not in ifaddresses(interface):
-            raise ConfigError('mDNS repeater requires an IPv6 address to be '
-                                  f'configured on interface "{interface}"')
-
-    return None
-
-# Get VRRP states from interfaces, returns only interfaces where state is MASTER
-def get_vrrp_master(interfaces):
-    json_data = loads(VRRP.collect('json'))
-    for group in json_data:
-        if 'data' in group:
-            if 'ifp_ifname' in group['data']:
-                iface = group['data']['ifp_ifname']
-                state = group['data']['state'] # 2 = Master
-                if iface in interfaces and state != 2:
-                    interfaces.remove(iface)
-    return interfaces
-
-def generate(mdns):
-    if not mdns:
-        return None
-
-    if 'disable' in mdns:
-        print('Warning: mDNS repeater will be deactivated because it is disabled')
-        return None
-
-    if mdns['vrrp_exists'] and 'vrrp_disable' in mdns:
-        mdns['interface'] = get_vrrp_master(mdns['interface'])
-
-        if len(mdns['interface']) < 2:
-            return None
-
-    render(config_file, 'mdns-repeater/avahi-daemon.conf.j2', mdns)
-    render(systemd_override, 'mdns-repeater/override.conf.j2', mdns)
-    return None
-
-def apply(mdns):
-    systemd_service = 'avahi-daemon.service'
-    # Reload systemd manager configuration
-    call('systemctl daemon-reload')
-
-    if not mdns or 'disable' in mdns:
-        call(f'systemctl stop {systemd_service}')
-        if os.path.exists(config_file):
-            os.unlink(config_file)
-
-        if os.path.exists(vrrp_running_file):
-            os.unlink(vrrp_running_file)
-    else:
-        if 'vrrp_disable' not in mdns and os.path.exists(vrrp_running_file):
-            os.unlink(vrrp_running_file)
-
-        if mdns['vrrp_exists'] and 'vrrp_disable' in mdns:
-            if not os.path.exists(vrrp_running_file):
-                os.mknod(vrrp_running_file) # vrrp script looks for this file to update mdns repeater
-
-            if len(mdns['interface']) < 2:
-                call(f'systemctl stop {systemd_service}')
-                return None
-
-        call(f'systemctl restart {systemd_service}')
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/service_mdns_repeater.py b/src/conf_mode/service_mdns_repeater.py
new file mode 100755
index 000000000..6526c23d1
--- /dev/null
+++ b/src/conf_mode/service_mdns_repeater.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2017-2022 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 json import loads
+from sys import exit
+from netifaces import ifaddresses, interfaces, AF_INET, AF_INET6
+
+from vyos.config import Config
+from vyos.ifconfig.vrrp import VRRP
+from vyos.template import render
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = '/run/avahi-daemon/avahi-daemon.conf'
+systemd_override = r'/run/systemd/system/avahi-daemon.service.d/override.conf'
+vrrp_running_file = '/run/mdns_vrrp_active'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base = ['service', 'mdns', 'repeater']
+    if not conf.exists(base):
+        return None
+
+    mdns = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                no_tag_node_value_mangle=True,
+                                get_first_key=True,
+                                with_recursive_defaults=True)
+
+    if mdns:
+        mdns['vrrp_exists'] = conf.exists('high-availability vrrp')
+        mdns['config_file'] = config_file
+
+    return mdns
+
+def verify(mdns):
+    if not mdns or 'disable' in mdns:
+        return None
+
+    # We need at least two interfaces to repeat mDNS advertisments
+    if 'interface' not in mdns or len(mdns['interface']) < 2:
+        raise ConfigError('mDNS repeater requires at least 2 configured interfaces!')
+
+    # For mdns-repeater to work it is essential that the interfaces has
+    # an IPv4 address assigned
+    for interface in mdns['interface']:
+        if interface not in interfaces():
+            raise ConfigError(f'Interface "{interface}" does not exist!')
+
+        if mdns['ip_version'] in ['ipv4', 'both'] and AF_INET not in ifaddresses(interface):
+            raise ConfigError('mDNS repeater requires an IPv4 address to be '
+                                  f'configured on interface "{interface}"')
+
+        if mdns['ip_version'] in ['ipv6', 'both'] and AF_INET6 not in ifaddresses(interface):
+            raise ConfigError('mDNS repeater requires an IPv6 address to be '
+                                  f'configured on interface "{interface}"')
+
+    return None
+
+# Get VRRP states from interfaces, returns only interfaces where state is MASTER
+def get_vrrp_master(interfaces):
+    json_data = loads(VRRP.collect('json'))
+    for group in json_data:
+        if 'data' in group:
+            if 'ifp_ifname' in group['data']:
+                iface = group['data']['ifp_ifname']
+                state = group['data']['state'] # 2 = Master
+                if iface in interfaces and state != 2:
+                    interfaces.remove(iface)
+    return interfaces
+
+def generate(mdns):
+    if not mdns:
+        return None
+
+    if 'disable' in mdns:
+        print('Warning: mDNS repeater will be deactivated because it is disabled')
+        return None
+
+    if mdns['vrrp_exists'] and 'vrrp_disable' in mdns:
+        mdns['interface'] = get_vrrp_master(mdns['interface'])
+
+        if len(mdns['interface']) < 2:
+            return None
+
+    render(config_file, 'mdns-repeater/avahi-daemon.conf.j2', mdns)
+    render(systemd_override, 'mdns-repeater/override.conf.j2', mdns)
+    return None
+
+def apply(mdns):
+    systemd_service = 'avahi-daemon.service'
+    # Reload systemd manager configuration
+    call('systemctl daemon-reload')
+
+    if not mdns or 'disable' in mdns:
+        call(f'systemctl stop {systemd_service}')
+        if os.path.exists(config_file):
+            os.unlink(config_file)
+
+        if os.path.exists(vrrp_running_file):
+            os.unlink(vrrp_running_file)
+    else:
+        if 'vrrp_disable' not in mdns and os.path.exists(vrrp_running_file):
+            os.unlink(vrrp_running_file)
+
+        if mdns['vrrp_exists'] and 'vrrp_disable' in mdns:
+            if not os.path.exists(vrrp_running_file):
+                os.mknod(vrrp_running_file) # vrrp script looks for this file to update mdns repeater
+
+            if len(mdns['interface']) < 2:
+                call(f'systemctl stop {systemd_service}')
+                return None
+
+        call(f'systemctl restart {systemd_service}')
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_ntp.py b/src/conf_mode/service_ntp.py
new file mode 100755
index 000000000..1cc23a7df
--- /dev/null
+++ b/src/conf_mode/service_ntp.py
@@ -0,0 +1,136 @@
+#!/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 vyos.config import Config
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_interface_exists
+from vyos.utils.process import call
+from vyos.utils.permission import chmod_750
+from vyos.utils.network import get_interface_config
+from vyos.template import render
+from vyos.template import is_ipv4
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/chrony/chrony.conf'
+systemd_override = r'/run/systemd/system/chrony.service.d/override.conf'
+user_group = '_chrony'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['service', 'ntp']
+    if not conf.exists(base):
+        return None
+
+    ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+    ntp['config_file'] = config_file
+    ntp['user'] = user_group
+
+    tmp = is_node_changed(conf, base + ['vrf'])
+    if tmp: ntp.update({'restart_required': {}})
+
+    return ntp
+
+def verify(ntp):
+    # bail out early - looks like removal from running config
+    if not ntp:
+        return None
+
+    if 'server' not in ntp:
+        raise ConfigError('NTP server not configured')
+
+    verify_vrf(ntp)
+
+    if 'interface' in ntp:
+        # If ntpd should listen on a given interface, ensure it exists
+        interface = ntp['interface']
+        verify_interface_exists(interface)
+
+        # If we run in a VRF, our interface must belong to this VRF, too
+        if 'vrf' in ntp:
+            tmp = get_interface_config(interface)
+            vrf_name = ntp['vrf']
+            if 'master' not in tmp or tmp['master'] != vrf_name:
+                raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{interface}" '\
+                                  f'does not belong to this VRF!')
+
+    if 'listen_address' in ntp:
+        ipv4_addresses = 0
+        ipv6_addresses = 0
+        for address in ntp['listen_address']:
+            if is_ipv4(address):
+                ipv4_addresses += 1
+            else:
+                ipv6_addresses += 1
+        if ipv4_addresses > 1:
+            raise ConfigError(f'NTP Only admits one ipv4 value for listen-address parameter ')
+        if ipv6_addresses > 1:
+            raise ConfigError(f'NTP Only admits one ipv6 value for listen-address parameter ')
+
+    return None
+
+def generate(ntp):
+    # bail out early - looks like removal from running config
+    if not ntp:
+        return None
+
+    render(config_file, 'chrony/chrony.conf.j2', ntp, user=user_group, group=user_group)
+    render(systemd_override, 'chrony/override.conf.j2', ntp, user=user_group, group=user_group)
+
+    # Ensure proper permission for chrony command socket
+    config_dir = os.path.dirname(config_file)
+    chmod_750(config_dir)
+
+    return None
+
+def apply(ntp):
+    systemd_service = 'chrony.service'
+    # Reload systemd manager configuration
+    call('systemctl daemon-reload')
+
+    if not ntp:
+        # NTP support is removed in the commit
+        call(f'systemctl stop {systemd_service}')
+        if os.path.exists(config_file):
+            os.unlink(config_file)
+        if os.path.isfile(systemd_override):
+            os.unlink(systemd_override)
+        return
+
+    # we need to restart the service if e.g. the VRF name changed
+    systemd_action = 'reload-or-restart'
+    if 'restart_required' in ntp:
+        systemd_action = 'restart'
+
+    call(f'systemctl {systemd_action} {systemd_service}')
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_salt-minion.py b/src/conf_mode/service_salt-minion.py
new file mode 100755
index 000000000..a8fce8e01
--- /dev/null
+++ b/src/conf_mode/service_salt-minion.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2022 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 socket import gethostname
+from sys import exit
+from urllib3 import PoolManager
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.configverify import verify_interface_exists
+from vyos.template import render
+from vyos.utils.process import call
+from vyos.utils.permission import chown
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/etc/salt/minion'
+master_keyfile = r'/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'
+
+user='minion'
+group='vyattacfg'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['service', 'salt-minion']
+
+    if not conf.exists(base):
+        return None
+
+    salt = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+    # ID default is dynamic thus we can not use defaults()
+    if 'id' not in salt:
+        salt['id'] = gethostname()
+    # We have gathered the dict representation of the CLI, but there are default
+    # options which we need to update into the dictionary retrived.
+    salt = conf.merge_defaults(salt, recursive=True)
+
+    if not conf.exists(base):
+        return None
+    else:
+        conf.set_level(base)
+
+    return salt
+
+def verify(salt):
+    if not salt:
+        return None
+
+    if 'hash' in salt and salt['hash'] == 'sha1':
+        Warning('Do not use sha1 hashing algorithm, upgrade to sha256 or later!')
+
+    if 'source_interface' in salt:
+        verify_interface_exists(salt['source_interface'])
+
+    return None
+
+def generate(salt):
+    if not salt:
+        return None
+
+    render(config_file, 'salt-minion/minion.j2', salt, user=user, group=group)
+
+    if not os.path.exists(master_keyfile):
+        if 'master_key' in salt:
+            req = PoolManager().request('GET', salt['master_key'], preload_content=False)
+            with open(master_keyfile, 'wb') as f:
+                while True:
+                    data = req.read(1024)
+                    if not data:
+                        break
+                    f.write(data)
+
+            req.release_conn()
+            chown(master_keyfile, user, group)
+
+    return None
+
+def apply(salt):
+    service_name = 'salt-minion.service'
+    if not salt:
+        # Salt removed from running config
+        call(f'systemctl stop {service_name}')
+        if os.path.exists(config_file):
+            os.unlink(config_file)
+    else:
+        call(f'systemctl restart {service_name}')
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_snmp.py b/src/conf_mode/service_snmp.py
new file mode 100755
index 000000000..6565ffd60
--- /dev/null
+++ b/src/conf_mode/service_snmp.py
@@ -0,0 +1,269 @@
+#!/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 sys import exit
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configverify import verify_vrf
+from vyos.snmpv3_hashgen import plaintext_to_md5
+from vyos.snmpv3_hashgen import plaintext_to_sha1
+from vyos.snmpv3_hashgen import random
+from vyos.template import render
+from vyos.utils.process import call
+from vyos.utils.permission import chmod_755
+from vyos.utils.dict import dict_search
+from vyos.utils.network import is_addr_assigned
+from vyos.version import get_version_data
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file_client  = r'/etc/snmp/snmp.conf'
+config_file_daemon  = r'/etc/snmp/snmpd.conf'
+config_file_access  = r'/usr/share/snmp/snmpd.conf'
+config_file_user    = r'/var/lib/snmp/snmpd.conf'
+systemd_override    = r'/run/systemd/system/snmpd.service.d/override.conf'
+systemd_service     = 'snmpd.service'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['service', 'snmp']
+
+    snmp = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                get_first_key=True, no_tag_node_value_mangle=True)
+    if not conf.exists(base):
+        snmp.update({'deleted' : ''})
+
+    if conf.exists(['service', 'lldp', 'snmp']):
+        snmp.update({'lldp_snmp' : ''})
+
+    if 'deleted' in snmp:
+        return snmp
+
+    version_data = get_version_data()
+    snmp['version'] = version_data['version']
+
+    # create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx'
+    snmp['vyos_user'] = 'vyos' + random(8)
+    snmp['vyos_user_pass'] = random(16)
+
+    # We have gathered the dict representation of the CLI, but there are default
+    # options which we need to update into the dictionary retrived.
+    snmp = conf.merge_defaults(snmp, recursive=True)
+
+    if 'listen_address' in snmp:
+        # Always listen on localhost if an explicit address has been configured
+        # This is a safety measure to not end up with invalid listen addresses
+        # that are not configured on this system. See https://vyos.dev/T850
+        if '127.0.0.1' not in snmp['listen_address']:
+            tmp = {'127.0.0.1': {'port': '161'}}
+            snmp['listen_address'] = dict_merge(tmp, snmp['listen_address'])
+
+        if '::1' not in snmp['listen_address']:
+            tmp = {'::1': {'port': '161'}}
+            snmp['listen_address'] = dict_merge(tmp, snmp['listen_address'])
+
+    return snmp
+
+def verify(snmp):
+    if 'deleted' in snmp:
+        return None
+
+    if {'deleted', 'lldp_snmp'} <= set(snmp):
+        raise ConfigError('Can not delete SNMP service, as LLDP still uses SNMP!')
+
+    ### check if the configured script actually exist
+    if 'script_extensions' in snmp and 'extension_name' in snmp['script_extensions']:
+        for extension, extension_opt in snmp['script_extensions']['extension_name'].items():
+            if 'script' not in extension_opt:
+                raise ConfigError(f'Script extension "{extension}" requires an actual script to be configured!')
+
+            tmp = extension_opt['script']
+            if not os.path.isfile(tmp):
+                Warning(f'script "{tmp}" does not exist!')
+            else:
+                chmod_755(extension_opt['script'])
+
+    if 'listen_address' in snmp:
+        for address in snmp['listen_address']:
+            # We only wan't to configure addresses that exist on the system.
+            # Hint the user if they don't exist
+            if 'vrf' in snmp:
+                vrf_name = snmp['vrf']
+                if not is_addr_assigned(address, vrf_name) and address not in ['::1','127.0.0.1']:
+                    raise ConfigError(f'SNMP listen address "{address}" not configured in vrf "{vrf_name}"!')
+            elif not is_addr_assigned(address):
+                raise ConfigError(f'SNMP listen address "{address}" not configured in default vrf!')
+
+    if 'trap_target' in snmp:
+        for trap, trap_config in snmp['trap_target'].items():
+            if 'community' not in trap_config:
+                raise ConfigError(f'Trap target "{trap}" requires a community to be set!')
+
+    if 'oid_enable' in snmp:
+        Warning(f'Custom OIDs are enabled and may lead to system instability and high resource consumption')
+
+
+    verify_vrf(snmp)
+
+    # bail out early if SNMP v3 is not configured
+    if 'v3' not in snmp:
+        return None
+
+    if 'user' in snmp['v3']:
+        for user, user_config in snmp['v3']['user'].items():
+            if 'group' not in user_config:
+                raise ConfigError(f'Group membership required for user "{user}"!')
+
+            if 'plaintext_password' not in user_config['auth'] and 'encrypted_password' not in user_config['auth']:
+                raise ConfigError(f'Must specify authentication encrypted-password or plaintext-password for user "{user}"!')
+
+            if 'plaintext_password' not in user_config['privacy'] and 'encrypted_password' not in user_config['privacy']:
+                raise ConfigError(f'Must specify privacy encrypted-password or plaintext-password for user "{user}"!')
+
+    if 'group' in snmp['v3']:
+        for group, group_config in snmp['v3']['group'].items():
+            if 'seclevel' not in group_config:
+                raise ConfigError(f'Must configure "seclevel" for group "{group}"!')
+            if 'view' not in group_config:
+                raise ConfigError(f'Must configure "view" for group "{group}"!')
+
+            # Check if 'view' exists
+            view = group_config['view']
+            if 'view' not in snmp['v3'] or view not in snmp['v3']['view']:
+                raise ConfigError(f'You must create view "{view}" first!')
+
+    if 'view' in snmp['v3']:
+        for view, view_config in snmp['v3']['view'].items():
+            if 'oid' not in view_config:
+                raise ConfigError(f'Must configure an "oid" for view "{view}"!')
+
+    if 'trap_target' in snmp['v3']:
+        for trap, trap_config in snmp['v3']['trap_target'].items():
+            if 'plaintext_password' not in trap_config['auth'] and 'encrypted_password' not in trap_config['auth']:
+                raise ConfigError(f'Must specify one of authentication encrypted-password or plaintext-password for trap "{trap}"!')
+
+            if {'plaintext_password', 'encrypted_password'} <= set(trap_config['auth']):
+                raise ConfigError(f'Can not specify both authentication encrypted-password and plaintext-password for trap "{trap}"!')
+
+            if 'plaintext_password' not in trap_config['privacy'] and 'encrypted_password' not in trap_config['privacy']:
+                raise ConfigError(f'Must specify one of privacy encrypted-password or plaintext-password for trap "{trap}"!')
+
+            if {'plaintext_password', 'encrypted_password'} <= set(trap_config['privacy']):
+                raise ConfigError(f'Can not specify both privacy encrypted-password and plaintext-password for trap "{trap}"!')
+
+            if 'type' not in trap_config:
+                raise ConfigError('SNMP v3 trap "type" must be specified!')
+
+    return None
+
+def generate(snmp):
+    # As we are manipulating the snmpd user database we have to stop it first!
+    # This is even save if service is going to be removed
+    call(f'systemctl stop {systemd_service}')
+    # Clean config files
+    config_files = [config_file_client, config_file_daemon,
+                    config_file_access, config_file_user, systemd_override]
+    for file in config_files:
+        if os.path.isfile(file):
+            os.unlink(file)
+
+    if 'deleted' in snmp:
+        return None
+
+    if 'v3' in snmp:
+        # net-snmp is now regenerating the configuration file in the background
+        # thus we need to re-open and re-read the file as the content changed.
+        # After that we can no read the encrypted password from the config and
+        # replace the CLI plaintext password with its encrypted version.
+        os.environ['vyos_libexec_dir'] = '/usr/libexec/vyos'
+
+        if 'user' in snmp['v3']:
+            for user, user_config in snmp['v3']['user'].items():
+                if dict_search('auth.type', user_config)  == 'sha':
+                    hash = plaintext_to_sha1
+                else:
+                    hash = plaintext_to_md5
+
+                if dict_search('auth.plaintext_password', user_config) is not None:
+                    tmp = hash(dict_search('auth.plaintext_password', user_config),
+                        dict_search('v3.engineid', snmp))
+
+                    snmp['v3']['user'][user]['auth']['encrypted_password'] = tmp
+                    del snmp['v3']['user'][user]['auth']['plaintext_password']
+
+                    call(f'/opt/vyatta/sbin/my_set service snmp v3 user "{user}" auth encrypted-password "{tmp}" > /dev/null')
+                    call(f'/opt/vyatta/sbin/my_delete service snmp v3 user "{user}" auth plaintext-password > /dev/null')
+
+                if dict_search('privacy.plaintext_password', user_config) is not None:
+                    tmp = hash(dict_search('privacy.plaintext_password', user_config),
+                        dict_search('v3.engineid', snmp))
+
+                    snmp['v3']['user'][user]['privacy']['encrypted_password'] = tmp
+                    del snmp['v3']['user'][user]['privacy']['plaintext_password']
+
+                    call(f'/opt/vyatta/sbin/my_set service snmp v3 user "{user}" privacy encrypted-password "{tmp}" > /dev/null')
+                    call(f'/opt/vyatta/sbin/my_delete service snmp v3 user "{user}" privacy plaintext-password > /dev/null')
+
+    # Write client config file
+    render(config_file_client, 'snmp/etc.snmp.conf.j2', snmp)
+    # Write server config file
+    render(config_file_daemon, 'snmp/etc.snmpd.conf.j2', snmp)
+    # Write access rights config file
+    render(config_file_access, 'snmp/usr.snmpd.conf.j2', snmp)
+    # Write access rights config file
+    render(config_file_user, 'snmp/var.snmpd.conf.j2', snmp)
+    # Write daemon configuration file
+    render(systemd_override, 'snmp/override.conf.j2', snmp)
+
+    return None
+
+def apply(snmp):
+    # Always reload systemd manager configuration
+    call('systemctl daemon-reload')
+
+    if 'deleted' in snmp:
+        return None
+
+    # start SNMP daemon
+    call(f'systemctl restart {systemd_service}')
+
+    # Enable AgentX in FRR
+    # This should be done for each daemon individually because common command
+    # works only if all the daemons started with SNMP support
+    # Following daemons from FRR 9.0/stable have SNMP module compiled in VyOS
+    frr_daemons_list = ['zebra', 'bgpd', 'ospf6d', 'ospfd', 'ripd', 'isisd', 'ldpd']
+    for frr_daemon in frr_daemons_list:
+        call(f'vtysh -c "configure terminal" -d {frr_daemon} -c "agentx" >/dev/null')
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_ssh.py b/src/conf_mode/service_ssh.py
new file mode 100755
index 000000000..ee5e1eca2
--- /dev/null
+++ b/src/conf_mode/service_ssh.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2022 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 sys import exit
+from syslog import syslog
+from syslog import LOG_INFO
+
+from vyos.config import Config
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_vrf
+from vyos.utils.process import call
+from vyos.template import render
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/sshd/sshd_config'
+systemd_override = r'/run/systemd/system/ssh.service.d/override.conf'
+
+sshguard_config_file = '/etc/sshguard/sshguard.conf'
+sshguard_whitelist = '/etc/sshguard/whitelist'
+
+key_rsa = '/etc/ssh/ssh_host_rsa_key'
+key_dsa = '/etc/ssh/ssh_host_dsa_key'
+key_ed25519 = '/etc/ssh/ssh_host_ed25519_key'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['service', 'ssh']
+    if not conf.exists(base):
+        return None
+
+    ssh = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+    tmp = is_node_changed(conf, base + ['vrf'])
+    if tmp: ssh.update({'restart_required': {}})
+
+    # We have gathered the dict representation of the CLI, but there are default
+    # options which we need to update into the dictionary retrived.
+    ssh = conf.merge_defaults(ssh, recursive=True)
+
+    # pass config file path - used in override template
+    ssh['config_file'] = config_file
+
+    # Ignore default XML values if config doesn't exists
+    # Delete key from dict
+    if not conf.exists(base + ['dynamic-protection']):
+         del ssh['dynamic_protection']
+
+    return ssh
+
+def verify(ssh):
+    if not ssh:
+        return None
+
+    if 'rekey' in ssh and 'data' not in ssh['rekey']:
+        raise ConfigError(f'Rekey data is required!')
+
+    verify_vrf(ssh)
+    return None
+
+def generate(ssh):
+    if not ssh:
+        if os.path.isfile(config_file):
+            os.unlink(config_file)
+        if os.path.isfile(systemd_override):
+            os.unlink(systemd_override)
+
+        return None
+
+    # This usually happens only once on a fresh system, SSH keys need to be
+    # freshly generted, one per every system!
+    if not os.path.isfile(key_rsa):
+        syslog(LOG_INFO, 'SSH RSA host key not found, generating new key!')
+        call(f'ssh-keygen -q -N "" -t rsa -f {key_rsa}')
+    if not os.path.isfile(key_dsa):
+        syslog(LOG_INFO, 'SSH DSA host key not found, generating new key!')
+        call(f'ssh-keygen -q -N "" -t dsa -f {key_dsa}')
+    if not os.path.isfile(key_ed25519):
+        syslog(LOG_INFO, 'SSH ed25519 host key not found, generating new key!')
+        call(f'ssh-keygen -q -N "" -t ed25519 -f {key_ed25519}')
+
+    render(config_file, 'ssh/sshd_config.j2', ssh)
+    render(systemd_override, 'ssh/override.conf.j2', ssh)
+
+    if 'dynamic_protection' in ssh:
+        render(sshguard_config_file, 'ssh/sshguard_config.j2', ssh)
+        render(sshguard_whitelist, 'ssh/sshguard_whitelist.j2', ssh)
+    # Reload systemd manager configuration
+    call('systemctl daemon-reload')
+
+    return None
+
+def apply(ssh):
+    systemd_service_ssh = 'ssh.service'
+    systemd_service_sshguard = 'sshguard.service'
+    if not ssh:
+        # SSH access is removed in the commit
+        call(f'systemctl stop {systemd_service_ssh}')
+        call(f'systemctl stop {systemd_service_sshguard}')
+        return None
+
+    if 'dynamic_protection' not in ssh:
+        call(f'systemctl stop {systemd_service_sshguard}')
+    else:
+        call(f'systemctl reload-or-restart {systemd_service_sshguard}')
+
+    # we need to restart the service if e.g. the VRF name changed
+    systemd_action = 'reload-or-restart'
+    if 'restart_required' in ssh:
+        systemd_action = 'restart'
+
+    call(f'systemctl {systemd_action} {systemd_service_ssh}')
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/service_tftp-server.py b/src/conf_mode/service_tftp-server.py
new file mode 100755
index 000000000..3ad346e2e
--- /dev/null
+++ b/src/conf_mode/service_tftp-server.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 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 stat
+import pwd
+
+from copy import deepcopy
+from glob import glob
+from sys import exit
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.configverify import verify_vrf
+from vyos.template import render
+from vyos.template import is_ipv4
+from vyos.utils.process import call
+from vyos.utils.permission import chmod_755
+from vyos.utils.network import is_addr_assigned
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/etc/default/tftpd'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base = ['service', 'tftp-server']
+    if not conf.exists(base):
+        return None
+
+    tftpd = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                 get_first_key=True,
+                                 with_recursive_defaults=True)
+    return tftpd
+
+def verify(tftpd):
+    # bail out early - looks like removal from running config
+    if not tftpd:
+        return None
+
+    # Configuring allowed clients without a server makes no sense
+    if 'directory' not in tftpd:
+        raise ConfigError('TFTP root directory must be configured!')
+
+    if 'listen_address' not in tftpd:
+        raise ConfigError('TFTP server listen address must be configured!')
+
+    for address, address_config in tftpd['listen_address'].items():
+        if not is_addr_assigned(address):
+            Warning(f'TFTP server listen address "{address}" not ' \
+                     'assigned to any interface!')
+        verify_vrf(address_config)
+
+    return None
+
+def generate(tftpd):
+    # cleanup any available configuration file
+    # files will be recreated on demand
+    for i in glob(config_file + '*'):
+        os.unlink(i)
+
+    # bail out early - looks like removal from running config
+    if tftpd is None:
+        return None
+
+    idx = 0
+    for address, address_config in tftpd['listen_address'].items():
+        config = deepcopy(tftpd)
+        port = tftpd['port']
+        if is_ipv4(address):
+            config['listen_address'] = f'{address}:{port} -4'
+        else:
+            config['listen_address'] = f'[{address}]:{port} -6'
+
+        if 'vrf' in address_config:
+            config['vrf'] = address_config['vrf']
+
+        file = config_file + str(idx)
+        render(file, 'tftp-server/default.j2', config)
+        idx = idx + 1
+
+    return None
+
+def apply(tftpd):
+    # stop all services first - then we will decide
+    call('systemctl stop tftpd@*.service')
+
+    # bail out early - e.g. service deletion
+    if tftpd is None:
+        return None
+
+    tftp_root = tftpd['directory']
+    if not os.path.exists(tftp_root):
+        os.makedirs(tftp_root)
+        chmod_755(tftp_root)
+
+    # get UNIX uid for user 'tftp'
+    tftp_uid = pwd.getpwnam('tftp').pw_uid
+    tftp_gid = pwd.getpwnam('tftp').pw_gid
+
+    # get UNIX uid for tftproot directory
+    dir_uid = os.stat(tftp_root).st_uid
+    dir_gid = os.stat(tftp_root).st_gid
+
+    # adjust uid/gid of tftproot directory if files don't belong to user tftp
+    if (tftp_uid != dir_uid) or (tftp_gid != dir_gid):
+        os.chown(tftp_root, tftp_uid, tftp_gid)
+
+    idx = 0
+    for address in tftpd['listen_address']:
+        call(f'systemctl restart tftpd@{idx}.service')
+        idx = idx + 1
+
+    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/snmp.py b/src/conf_mode/snmp.py
deleted file mode 100755
index 6565ffd60..000000000
--- a/src/conf_mode/snmp.py
+++ /dev/null
@@ -1,269 +0,0 @@
-#!/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 sys import exit
-
-from vyos.base import Warning
-from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.configverify import verify_vrf
-from vyos.snmpv3_hashgen import plaintext_to_md5
-from vyos.snmpv3_hashgen import plaintext_to_sha1
-from vyos.snmpv3_hashgen import random
-from vyos.template import render
-from vyos.utils.process import call
-from vyos.utils.permission import chmod_755
-from vyos.utils.dict import dict_search
-from vyos.utils.network import is_addr_assigned
-from vyos.version import get_version_data
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file_client  = r'/etc/snmp/snmp.conf'
-config_file_daemon  = r'/etc/snmp/snmpd.conf'
-config_file_access  = r'/usr/share/snmp/snmpd.conf'
-config_file_user    = r'/var/lib/snmp/snmpd.conf'
-systemd_override    = r'/run/systemd/system/snmpd.service.d/override.conf'
-systemd_service     = 'snmpd.service'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['service', 'snmp']
-
-    snmp = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                get_first_key=True, no_tag_node_value_mangle=True)
-    if not conf.exists(base):
-        snmp.update({'deleted' : ''})
-
-    if conf.exists(['service', 'lldp', 'snmp']):
-        snmp.update({'lldp_snmp' : ''})
-
-    if 'deleted' in snmp:
-        return snmp
-
-    version_data = get_version_data()
-    snmp['version'] = version_data['version']
-
-    # create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx'
-    snmp['vyos_user'] = 'vyos' + random(8)
-    snmp['vyos_user_pass'] = random(16)
-
-    # We have gathered the dict representation of the CLI, but there are default
-    # options which we need to update into the dictionary retrived.
-    snmp = conf.merge_defaults(snmp, recursive=True)
-
-    if 'listen_address' in snmp:
-        # Always listen on localhost if an explicit address has been configured
-        # This is a safety measure to not end up with invalid listen addresses
-        # that are not configured on this system. See https://vyos.dev/T850
-        if '127.0.0.1' not in snmp['listen_address']:
-            tmp = {'127.0.0.1': {'port': '161'}}
-            snmp['listen_address'] = dict_merge(tmp, snmp['listen_address'])
-
-        if '::1' not in snmp['listen_address']:
-            tmp = {'::1': {'port': '161'}}
-            snmp['listen_address'] = dict_merge(tmp, snmp['listen_address'])
-
-    return snmp
-
-def verify(snmp):
-    if 'deleted' in snmp:
-        return None
-
-    if {'deleted', 'lldp_snmp'} <= set(snmp):
-        raise ConfigError('Can not delete SNMP service, as LLDP still uses SNMP!')
-
-    ### check if the configured script actually exist
-    if 'script_extensions' in snmp and 'extension_name' in snmp['script_extensions']:
-        for extension, extension_opt in snmp['script_extensions']['extension_name'].items():
-            if 'script' not in extension_opt:
-                raise ConfigError(f'Script extension "{extension}" requires an actual script to be configured!')
-
-            tmp = extension_opt['script']
-            if not os.path.isfile(tmp):
-                Warning(f'script "{tmp}" does not exist!')
-            else:
-                chmod_755(extension_opt['script'])
-
-    if 'listen_address' in snmp:
-        for address in snmp['listen_address']:
-            # We only wan't to configure addresses that exist on the system.
-            # Hint the user if they don't exist
-            if 'vrf' in snmp:
-                vrf_name = snmp['vrf']
-                if not is_addr_assigned(address, vrf_name) and address not in ['::1','127.0.0.1']:
-                    raise ConfigError(f'SNMP listen address "{address}" not configured in vrf "{vrf_name}"!')
-            elif not is_addr_assigned(address):
-                raise ConfigError(f'SNMP listen address "{address}" not configured in default vrf!')
-
-    if 'trap_target' in snmp:
-        for trap, trap_config in snmp['trap_target'].items():
-            if 'community' not in trap_config:
-                raise ConfigError(f'Trap target "{trap}" requires a community to be set!')
-
-    if 'oid_enable' in snmp:
-        Warning(f'Custom OIDs are enabled and may lead to system instability and high resource consumption')
-
-
-    verify_vrf(snmp)
-
-    # bail out early if SNMP v3 is not configured
-    if 'v3' not in snmp:
-        return None
-
-    if 'user' in snmp['v3']:
-        for user, user_config in snmp['v3']['user'].items():
-            if 'group' not in user_config:
-                raise ConfigError(f'Group membership required for user "{user}"!')
-
-            if 'plaintext_password' not in user_config['auth'] and 'encrypted_password' not in user_config['auth']:
-                raise ConfigError(f'Must specify authentication encrypted-password or plaintext-password for user "{user}"!')
-
-            if 'plaintext_password' not in user_config['privacy'] and 'encrypted_password' not in user_config['privacy']:
-                raise ConfigError(f'Must specify privacy encrypted-password or plaintext-password for user "{user}"!')
-
-    if 'group' in snmp['v3']:
-        for group, group_config in snmp['v3']['group'].items():
-            if 'seclevel' not in group_config:
-                raise ConfigError(f'Must configure "seclevel" for group "{group}"!')
-            if 'view' not in group_config:
-                raise ConfigError(f'Must configure "view" for group "{group}"!')
-
-            # Check if 'view' exists
-            view = group_config['view']
-            if 'view' not in snmp['v3'] or view not in snmp['v3']['view']:
-                raise ConfigError(f'You must create view "{view}" first!')
-
-    if 'view' in snmp['v3']:
-        for view, view_config in snmp['v3']['view'].items():
-            if 'oid' not in view_config:
-                raise ConfigError(f'Must configure an "oid" for view "{view}"!')
-
-    if 'trap_target' in snmp['v3']:
-        for trap, trap_config in snmp['v3']['trap_target'].items():
-            if 'plaintext_password' not in trap_config['auth'] and 'encrypted_password' not in trap_config['auth']:
-                raise ConfigError(f'Must specify one of authentication encrypted-password or plaintext-password for trap "{trap}"!')
-
-            if {'plaintext_password', 'encrypted_password'} <= set(trap_config['auth']):
-                raise ConfigError(f'Can not specify both authentication encrypted-password and plaintext-password for trap "{trap}"!')
-
-            if 'plaintext_password' not in trap_config['privacy'] and 'encrypted_password' not in trap_config['privacy']:
-                raise ConfigError(f'Must specify one of privacy encrypted-password or plaintext-password for trap "{trap}"!')
-
-            if {'plaintext_password', 'encrypted_password'} <= set(trap_config['privacy']):
-                raise ConfigError(f'Can not specify both privacy encrypted-password and plaintext-password for trap "{trap}"!')
-
-            if 'type' not in trap_config:
-                raise ConfigError('SNMP v3 trap "type" must be specified!')
-
-    return None
-
-def generate(snmp):
-    # As we are manipulating the snmpd user database we have to stop it first!
-    # This is even save if service is going to be removed
-    call(f'systemctl stop {systemd_service}')
-    # Clean config files
-    config_files = [config_file_client, config_file_daemon,
-                    config_file_access, config_file_user, systemd_override]
-    for file in config_files:
-        if os.path.isfile(file):
-            os.unlink(file)
-
-    if 'deleted' in snmp:
-        return None
-
-    if 'v3' in snmp:
-        # net-snmp is now regenerating the configuration file in the background
-        # thus we need to re-open and re-read the file as the content changed.
-        # After that we can no read the encrypted password from the config and
-        # replace the CLI plaintext password with its encrypted version.
-        os.environ['vyos_libexec_dir'] = '/usr/libexec/vyos'
-
-        if 'user' in snmp['v3']:
-            for user, user_config in snmp['v3']['user'].items():
-                if dict_search('auth.type', user_config)  == 'sha':
-                    hash = plaintext_to_sha1
-                else:
-                    hash = plaintext_to_md5
-
-                if dict_search('auth.plaintext_password', user_config) is not None:
-                    tmp = hash(dict_search('auth.plaintext_password', user_config),
-                        dict_search('v3.engineid', snmp))
-
-                    snmp['v3']['user'][user]['auth']['encrypted_password'] = tmp
-                    del snmp['v3']['user'][user]['auth']['plaintext_password']
-
-                    call(f'/opt/vyatta/sbin/my_set service snmp v3 user "{user}" auth encrypted-password "{tmp}" > /dev/null')
-                    call(f'/opt/vyatta/sbin/my_delete service snmp v3 user "{user}" auth plaintext-password > /dev/null')
-
-                if dict_search('privacy.plaintext_password', user_config) is not None:
-                    tmp = hash(dict_search('privacy.plaintext_password', user_config),
-                        dict_search('v3.engineid', snmp))
-
-                    snmp['v3']['user'][user]['privacy']['encrypted_password'] = tmp
-                    del snmp['v3']['user'][user]['privacy']['plaintext_password']
-
-                    call(f'/opt/vyatta/sbin/my_set service snmp v3 user "{user}" privacy encrypted-password "{tmp}" > /dev/null')
-                    call(f'/opt/vyatta/sbin/my_delete service snmp v3 user "{user}" privacy plaintext-password > /dev/null')
-
-    # Write client config file
-    render(config_file_client, 'snmp/etc.snmp.conf.j2', snmp)
-    # Write server config file
-    render(config_file_daemon, 'snmp/etc.snmpd.conf.j2', snmp)
-    # Write access rights config file
-    render(config_file_access, 'snmp/usr.snmpd.conf.j2', snmp)
-    # Write access rights config file
-    render(config_file_user, 'snmp/var.snmpd.conf.j2', snmp)
-    # Write daemon configuration file
-    render(systemd_override, 'snmp/override.conf.j2', snmp)
-
-    return None
-
-def apply(snmp):
-    # Always reload systemd manager configuration
-    call('systemctl daemon-reload')
-
-    if 'deleted' in snmp:
-        return None
-
-    # start SNMP daemon
-    call(f'systemctl restart {systemd_service}')
-
-    # Enable AgentX in FRR
-    # This should be done for each daemon individually because common command
-    # works only if all the daemons started with SNMP support
-    # Following daemons from FRR 9.0/stable have SNMP module compiled in VyOS
-    frr_daemons_list = ['zebra', 'bgpd', 'ospf6d', 'ospfd', 'ripd', 'isisd', 'ldpd']
-    for frr_daemon in frr_daemons_list:
-        call(f'vtysh -c "configure terminal" -d {frr_daemon} -c "agentx" >/dev/null')
-
-    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/ssh.py b/src/conf_mode/ssh.py
deleted file mode 100755
index ee5e1eca2..000000000
--- a/src/conf_mode/ssh.py
+++ /dev/null
@@ -1,142 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2022 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 sys import exit
-from syslog import syslog
-from syslog import LOG_INFO
-
-from vyos.config import Config
-from vyos.configdict import is_node_changed
-from vyos.configverify import verify_vrf
-from vyos.utils.process import call
-from vyos.template import render
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/run/sshd/sshd_config'
-systemd_override = r'/run/systemd/system/ssh.service.d/override.conf'
-
-sshguard_config_file = '/etc/sshguard/sshguard.conf'
-sshguard_whitelist = '/etc/sshguard/whitelist'
-
-key_rsa = '/etc/ssh/ssh_host_rsa_key'
-key_dsa = '/etc/ssh/ssh_host_dsa_key'
-key_ed25519 = '/etc/ssh/ssh_host_ed25519_key'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['service', 'ssh']
-    if not conf.exists(base):
-        return None
-
-    ssh = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
-
-    tmp = is_node_changed(conf, base + ['vrf'])
-    if tmp: ssh.update({'restart_required': {}})
-
-    # We have gathered the dict representation of the CLI, but there are default
-    # options which we need to update into the dictionary retrived.
-    ssh = conf.merge_defaults(ssh, recursive=True)
-
-    # pass config file path - used in override template
-    ssh['config_file'] = config_file
-
-    # Ignore default XML values if config doesn't exists
-    # Delete key from dict
-    if not conf.exists(base + ['dynamic-protection']):
-         del ssh['dynamic_protection']
-
-    return ssh
-
-def verify(ssh):
-    if not ssh:
-        return None
-
-    if 'rekey' in ssh and 'data' not in ssh['rekey']:
-        raise ConfigError(f'Rekey data is required!')
-
-    verify_vrf(ssh)
-    return None
-
-def generate(ssh):
-    if not ssh:
-        if os.path.isfile(config_file):
-            os.unlink(config_file)
-        if os.path.isfile(systemd_override):
-            os.unlink(systemd_override)
-
-        return None
-
-    # This usually happens only once on a fresh system, SSH keys need to be
-    # freshly generted, one per every system!
-    if not os.path.isfile(key_rsa):
-        syslog(LOG_INFO, 'SSH RSA host key not found, generating new key!')
-        call(f'ssh-keygen -q -N "" -t rsa -f {key_rsa}')
-    if not os.path.isfile(key_dsa):
-        syslog(LOG_INFO, 'SSH DSA host key not found, generating new key!')
-        call(f'ssh-keygen -q -N "" -t dsa -f {key_dsa}')
-    if not os.path.isfile(key_ed25519):
-        syslog(LOG_INFO, 'SSH ed25519 host key not found, generating new key!')
-        call(f'ssh-keygen -q -N "" -t ed25519 -f {key_ed25519}')
-
-    render(config_file, 'ssh/sshd_config.j2', ssh)
-    render(systemd_override, 'ssh/override.conf.j2', ssh)
-
-    if 'dynamic_protection' in ssh:
-        render(sshguard_config_file, 'ssh/sshguard_config.j2', ssh)
-        render(sshguard_whitelist, 'ssh/sshguard_whitelist.j2', ssh)
-    # Reload systemd manager configuration
-    call('systemctl daemon-reload')
-
-    return None
-
-def apply(ssh):
-    systemd_service_ssh = 'ssh.service'
-    systemd_service_sshguard = 'sshguard.service'
-    if not ssh:
-        # SSH access is removed in the commit
-        call(f'systemctl stop {systemd_service_ssh}')
-        call(f'systemctl stop {systemd_service_sshguard}')
-        return None
-
-    if 'dynamic_protection' not in ssh:
-        call(f'systemctl stop {systemd_service_sshguard}')
-    else:
-        call(f'systemctl reload-or-restart {systemd_service_sshguard}')
-
-    # we need to restart the service if e.g. the VRF name changed
-    systemd_action = 'reload-or-restart'
-    if 'restart_required' in ssh:
-        systemd_action = 'restart'
-
-    call(f'systemctl {systemd_action} {systemd_service_ssh}')
-    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-ip.py b/src/conf_mode/system-ip.py
deleted file mode 100755
index 7612e2c0d..000000000
--- a/src/conf_mode/system-ip.py
+++ /dev/null
@@ -1,143 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-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/>.
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.configverify import verify_route_map
-from vyos.template import render_to_string
-from vyos.utils.dict import dict_search
-from vyos.utils.file import write_file
-from vyos.utils.process import call
-from vyos.utils.process import is_systemd_service_active
-from vyos.utils.system import sysctl_write
-
-from vyos import ConfigError
-from vyos import frr
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['system', 'ip']
-
-    opt = conf.get_config_dict(base, key_mangling=('-', '_'),
-                               get_first_key=True,
-                               with_recursive_defaults=True)
-
-    # When working with FRR we need to know the corresponding address-family
-    opt['afi'] = 'ip'
-
-    # We also need the route-map information from the config
-    #
-    # XXX: one MUST always call this without the key_mangling() option! See
-    # vyos.configverify.verify_common_route_maps() for more information.
-    tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'],
-                                                          get_first_key=True)}}
-    # Merge policy dict into "regular" config dict
-    opt = dict_merge(tmp, opt)
-    return opt
-
-def verify(opt):
-    if 'protocol' in opt:
-        for protocol, protocol_options in opt['protocol'].items():
-            if 'route_map' in protocol_options:
-                verify_route_map(protocol_options['route_map'], opt)
-    return
-
-def generate(opt):
-    opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt)
-    return
-
-def apply(opt):
-    # Apply ARP threshold values
-    # table_size has a default value - thus the key always exists
-    size = int(dict_search('arp.table_size', opt))
-    # Amount upon reaching which the records begin to be cleared immediately
-    sysctl_write('net.ipv4.neigh.default.gc_thresh3', size)
-    # Amount after which the records begin to be cleaned after 5 seconds
-    sysctl_write('net.ipv4.neigh.default.gc_thresh2', size // 2)
-    # Minimum number of stored records is indicated which is not cleared
-    sysctl_write('net.ipv4.neigh.default.gc_thresh1', size // 8)
-
-    # enable/disable IPv4 forwarding
-    tmp = dict_search('disable_forwarding', opt)
-    value = '0' if (tmp != None) else '1'
-    write_file('/proc/sys/net/ipv4/conf/all/forwarding', value)
-
-    # enable/disable IPv4 directed broadcast forwarding
-    tmp = dict_search('disable_directed_broadcast', opt)
-    value = '0' if (tmp != None) else '1'
-    write_file('/proc/sys/net/ipv4/conf/all/bc_forwarding', value)
-
-    # configure multipath
-    tmp = dict_search('multipath.ignore_unreachable_nexthops', opt)
-    value = '1' if (tmp != None) else '0'
-    sysctl_write('net.ipv4.fib_multipath_use_neigh', value)
-
-    tmp = dict_search('multipath.layer4_hashing', opt)
-    value = '1' if (tmp != None) else '0'
-    sysctl_write('net.ipv4.fib_multipath_hash_policy', value)
-
-    # configure TCP options (defaults as of Linux 6.4)
-    tmp = dict_search('tcp.mss.probing', opt)
-    if tmp is None:
-        value = 0
-    elif tmp == 'on-icmp-black-hole':
-        value = 1
-    elif tmp == 'force':
-        value = 2
-    else:
-        # Shouldn't happen
-        raise ValueError("TCP MSS probing is neither 'on-icmp-black-hole' nor 'force'!")
-    sysctl_write('net.ipv4.tcp_mtu_probing', value)
-
-    tmp = dict_search('tcp.mss.base', opt)
-    value = '1024' if (tmp is None) else tmp
-    sysctl_write('net.ipv4.tcp_base_mss', value)
-
-    tmp = dict_search('tcp.mss.floor', opt)
-    value = '48' if (tmp is None) else tmp
-    sysctl_write('net.ipv4.tcp_mtu_probe_floor', value)
-
-    # During startup of vyos-router that brings up FRR, the service is not yet
-    # running when this script is called first. Skip this part and wait for initial
-    # commit of the configuration to trigger this statement
-    if is_systemd_service_active('frr.service'):
-        zebra_daemon = 'zebra'
-        # Save original configuration prior to starting any commit actions
-        frr_cfg = frr.FRRConfig()
-
-        # The route-map used for the FIB (zebra) is part of the zebra daemon
-        frr_cfg.load_configuration(zebra_daemon)
-        frr_cfg.modify_section(r'ip protocol \w+ route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
-        if 'frr_zebra_config' in opt:
-            frr_cfg.add_before(frr.default_add_before, opt['frr_zebra_config'])
-        frr_cfg.commit_configuration(zebra_daemon)
-
-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-ipv6.py b/src/conf_mode/system-ipv6.py
deleted file mode 100755
index 90a1a8087..000000000
--- a/src/conf_mode/system-ipv6.py
+++ /dev/null
@@ -1,120 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-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 sys import exit
-from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.configverify import verify_route_map
-from vyos.template import render_to_string
-from vyos.utils.dict import dict_search
-from vyos.utils.file import write_file
-from vyos.utils.process import is_systemd_service_active
-from vyos.utils.system import sysctl_write
-from vyos import ConfigError
-from vyos import frr
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['system', 'ipv6']
-
-    opt = conf.get_config_dict(base, key_mangling=('-', '_'),
-                               get_first_key=True,
-                               with_recursive_defaults=True)
-
-    # When working with FRR we need to know the corresponding address-family
-    opt['afi'] = 'ipv6'
-
-    # We also need the route-map information from the config
-    #
-    # XXX: one MUST always call this without the key_mangling() option! See
-    # vyos.configverify.verify_common_route_maps() for more information.
-    tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'],
-                                                          get_first_key=True)}}
-    # Merge policy dict into "regular" config dict
-    opt = dict_merge(tmp, opt)
-    return opt
-
-def verify(opt):
-    if 'protocol' in opt:
-        for protocol, protocol_options in opt['protocol'].items():
-            if 'route_map' in protocol_options:
-                verify_route_map(protocol_options['route_map'], opt)
-    return
-
-def generate(opt):
-    opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt)
-    return
-
-def apply(opt):
-    # configure multipath
-    tmp = dict_search('multipath.layer4_hashing', opt)
-    value = '1' if (tmp != None) else '0'
-    sysctl_write('net.ipv6.fib_multipath_hash_policy', value)
-
-    # Apply ND threshold values
-    # table_size has a default value - thus the key always exists
-    size = int(dict_search('neighbor.table_size', opt))
-    # Amount upon reaching which the records begin to be cleared immediately
-    sysctl_write('net.ipv6.neigh.default.gc_thresh3', size)
-    # Amount after which the records begin to be cleaned after 5 seconds
-    sysctl_write('net.ipv6.neigh.default.gc_thresh2', size // 2)
-    # Minimum number of stored records is indicated which is not cleared
-    sysctl_write('net.ipv6.neigh.default.gc_thresh1', size // 8)
-
-    # enable/disable IPv6 forwarding
-    tmp = dict_search('disable_forwarding', opt)
-    value = '0' if (tmp != None) else '1'
-    write_file('/proc/sys/net/ipv6/conf/all/forwarding', value)
-
-    # configure IPv6 strict-dad
-    tmp = dict_search('strict_dad', opt)
-    value = '2' if (tmp != None) else '1'
-    for root, dirs, files in os.walk('/proc/sys/net/ipv6/conf'):
-        for name in files:
-            if name == 'accept_dad':
-                write_file(os.path.join(root, name), value)
-
-    # During startup of vyos-router that brings up FRR, the service is not yet
-    # running when this script is called first. Skip this part and wait for initial
-    # commit of the configuration to trigger this statement
-    if is_systemd_service_active('frr.service'):
-        zebra_daemon = 'zebra'
-        # Save original configuration prior to starting any commit actions
-        frr_cfg = frr.FRRConfig()
-
-        # The route-map used for the FIB (zebra) is part of the zebra daemon
-        frr_cfg.load_configuration(zebra_daemon)
-        frr_cfg.modify_section(r'ipv6 protocol \w+ route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
-        if 'frr_zebra_config' in opt:
-            frr_cfg.add_before(frr.default_add_before, opt['frr_zebra_config'])
-        frr_cfg.commit_configuration(zebra_daemon)
-
-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-banner.py b/src/conf_mode/system-login-banner.py
deleted file mode 100755
index 65fa04417..000000000
--- a/src/conf_mode/system-login-banner.py
+++ /dev/null
@@ -1,107 +0,0 @@
-#!/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/>.
-
-from sys import exit
-from copy import deepcopy
-
-from vyos.config import Config
-from vyos.utils.file import write_file
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-try:
-    with open('/usr/share/vyos/default_motd') as f:
-        motd = f.read()
-except:
-    # Use an empty banner if the default banner file cannot be read
-    motd = "\n"
-
-PRELOGIN_FILE = r'/etc/issue'
-PRELOGIN_NET_FILE = r'/etc/issue.net'
-POSTLOGIN_FILE = r'/etc/motd'
-
-default_config_data = {
-    'issue': 'Welcome to VyOS - \\n \\l\n\n',
-    'issue_net': '',
-    'motd': motd
-}
-
-def get_config(config=None):
-    banner = deepcopy(default_config_data)
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base_level = ['system', 'login', 'banner']
-
-    if not conf.exists(base_level):
-        return banner
-    else:
-        conf.set_level(base_level)
-
-    # Post-Login banner
-    if conf.exists(['post-login']):
-        tmp = conf.return_value(['post-login'])
-        # post-login banner can be empty as well
-        if tmp:
-            tmp = tmp.replace('\\n','\n')
-            tmp = tmp.replace('\\t','\t')
-            # always add newline character
-            tmp += '\n'
-        else:
-            tmp = ''
-
-        banner['motd'] = tmp
-
-    # Pre-Login banner
-    if conf.exists(['pre-login']):
-        tmp = conf.return_value(['pre-login'])
-        # pre-login banner can be empty as well
-        if tmp:
-            tmp = tmp.replace('\\n','\n')
-            tmp = tmp.replace('\\t','\t')
-            # always add newline character
-            tmp += '\n'
-        else:
-            tmp = ''
-
-        banner['issue'] = banner['issue_net'] = tmp
-
-    return banner
-
-def verify(banner):
-    pass
-
-def generate(banner):
-    pass
-
-def apply(banner):
-    write_file(PRELOGIN_FILE, banner['issue'])
-    write_file(PRELOGIN_NET_FILE, banner['issue_net'])
-    write_file(POSTLOGIN_FILE, banner['motd'])
-
-    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
deleted file mode 100755
index f34575aff..000000000
--- a/src/conf_mode/system-login.py
+++ /dev/null
@@ -1,423 +0,0 @@
-#!/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.file import chown
-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,_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
-                # T5875: ensure UID is properly set on home directory if user is re-added
-                if os.path.exists(home_dir):
-                    chown(home_dir, user=user, recursive=True)
-
-                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/conf_mode/system-logs.py b/src/conf_mode/system-logs.py
deleted file mode 100755
index 8ad4875d4..000000000
--- a/src/conf_mode/system-logs.py
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 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/>.
-
-from sys import exit
-
-from vyos import ConfigError
-from vyos import airbag
-from vyos.config import Config
-from vyos.logger import syslog
-from vyos.template import render
-from vyos.utils.dict import dict_search
-airbag.enable()
-
-# path to logrotate configs
-logrotate_atop_file = '/etc/logrotate.d/vyos-atop'
-logrotate_rsyslog_file = '/etc/logrotate.d/vyos-rsyslog'
-
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base = ['system', 'logs']
-    logs_config = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                       get_first_key=True,
-                                       with_recursive_defaults=True)
-
-    return logs_config
-
-
-def verify(logs_config):
-    # Nothing to verify here
-    pass
-
-
-def generate(logs_config):
-    # get configuration for logrotate atop
-    logrotate_atop = dict_search('logrotate.atop', logs_config)
-    # generate new config file for atop
-    syslog.debug('Adding logrotate config for atop')
-    render(logrotate_atop_file, 'logs/logrotate/vyos-atop.j2', logrotate_atop)
-
-    # get configuration for logrotate rsyslog
-    logrotate_rsyslog = dict_search('logrotate.messages', logs_config)
-    # generate new config file for rsyslog
-    syslog.debug('Adding logrotate config for rsyslog')
-    render(logrotate_rsyslog_file, 'logs/logrotate/vyos-rsyslog.j2',
-           logrotate_rsyslog)
-
-
-def apply(logs_config):
-    # No further actions needed
-    pass
-
-
-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-option.py b/src/conf_mode/system-option.py
deleted file mode 100755
index d92121b3d..000000000
--- a/src/conf_mode/system-option.py
+++ /dev/null
@@ -1,159 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019-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 netifaces import interfaces
-from sys import exit
-from time import sleep
-
-from vyos.config import Config
-from vyos.configverify import verify_source_interface
-from vyos.template import render
-from vyos.utils.process import cmd
-from vyos.utils.process import is_systemd_service_running
-from vyos.utils.network import is_addr_assigned
-from vyos.utils.network import is_intf_addr_assigned
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-curlrc_config = r'/etc/curlrc'
-ssh_config = r'/etc/ssh/ssh_config.d/91-vyos-ssh-client-options.conf'
-systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target'
-time_format_to_locale = {
-    '12-hour': 'en_US.UTF-8',
-    '24-hour': 'en_GB.UTF-8'
-}
-
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['system', 'option']
-    options = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                   get_first_key=True,
-                                   with_recursive_defaults=True)
-
-    return options
-
-def verify(options):
-    if 'http_client' in options:
-        config = options['http_client']
-        if 'source_interface' in config:
-            if not config['source_interface'] in interfaces():
-                raise ConfigError(f'Source interface {source_interface} does not '
-                                  f'exist'.format(**config))
-
-        if {'source_address', 'source_interface'} <= set(config):
-            raise ConfigError('Can not define both HTTP source-interface and source-address')
-
-        if 'source_address' in config:
-            if not is_addr_assigned(config['source_address']):
-                raise ConfigError('No interface with give address specified!')
-
-    if 'ssh_client' in options:
-        config = options['ssh_client']
-        if 'source_address' in config:
-            address = config['source_address']
-            if not is_addr_assigned(config['source_address']):
-                raise ConfigError('No interface with address "{address}" configured!')
-
-        if 'source_interface' in config:
-            verify_source_interface(config)
-            if 'source_address' in config:
-                address = config['source_address']
-                interface = config['source_interface']
-                if not is_intf_addr_assigned(interface, address):
-                    raise ConfigError(f'Address "{address}" not assigned on interface "{interface}"!')
-
-    return None
-
-def generate(options):
-    render(curlrc_config, 'system/curlrc.j2', options)
-    render(ssh_config, 'system/ssh_config.j2', options)
-    return None
-
-def apply(options):
-    # System bootup beep
-    if 'startup_beep' in options:
-        cmd('systemctl enable vyos-beep.service')
-    else:
-        cmd('systemctl disable vyos-beep.service')
-
-    # Ctrl-Alt-Delete action
-    if os.path.exists(systemd_action_file):
-        os.unlink(systemd_action_file)
-    if 'ctrl_alt_delete' in options:
-        if options['ctrl_alt_delete'] == 'reboot':
-            os.symlink('/lib/systemd/system/reboot.target', systemd_action_file)
-        elif options['ctrl_alt_delete'] == 'poweroff':
-            os.symlink('/lib/systemd/system/poweroff.target', systemd_action_file)
-
-    # Configure HTTP client
-    if 'http_client' not in options:
-        if os.path.exists(curlrc_config):
-            os.unlink(curlrc_config)
-
-    # Configure SSH client
-    if 'ssh_client' not in options:
-        if os.path.exists(ssh_config):
-            os.unlink(ssh_config)
-
-    # Reboot system on kernel panic
-    timeout = '0'
-    if 'reboot_on_panic' in options:
-        timeout = '60'
-    with open('/proc/sys/kernel/panic', 'w') as f:
-        f.write(timeout)
-
-    # tuned - performance tuning
-    if 'performance' in options:
-        cmd('systemctl restart tuned.service')
-        # wait until daemon has started before sending configuration
-        while (not is_systemd_service_running('tuned.service')):
-            sleep(0.250)
-        cmd('tuned-adm profile network-{performance}'.format(**options))
-    else:
-        cmd('systemctl stop tuned.service')
-
-    # Keyboard layout - there will be always the default key inside the dict
-    # but we check for key existence anyway
-    if 'keyboard_layout' in options:
-        cmd('loadkeys {keyboard_layout}'.format(**options))
-
-    # Enable/diable root-partition-auto-resize SystemD service
-    if 'root_partition_auto_resize' in options:
-      cmd('systemctl enable root-partition-auto-resize.service')
-    else:
-      cmd('systemctl disable root-partition-auto-resize.service')
-
-    # Time format 12|24-hour
-    if 'time_format' in options:
-        time_format = time_format_to_locale.get(options['time_format'])
-        cmd(f'localectl set-locale LC_TIME={time_format}')
-
-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-proxy.py b/src/conf_mode/system-proxy.py
deleted file mode 100755
index 079c43e7e..000000000
--- a/src/conf_mode/system-proxy.py
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2022 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 sys import exit
-
-from vyos.config import Config
-from vyos.template import render
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-proxy_def = r'/etc/profile.d/vyos-system-proxy.sh'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['system', 'proxy']
-    if not conf.exists(base):
-        return None
-
-    proxy = conf.get_config_dict(base, get_first_key=True)
-    return proxy
-
-def verify(proxy):
-    if not proxy:
-        return
-
-    if 'url' not in proxy or 'port' not in proxy:
-        raise ConfigError('Proxy URL and port require a value')
-
-    if ('username' in proxy and 'password' not in proxy) or \
-       ('username' not in proxy and 'password' in proxy):
-       raise ConfigError('Both username and password need to be defined!')
-
-def generate(proxy):
-    if not proxy:
-        if os.path.isfile(proxy_def):
-            os.unlink(proxy_def)
-        return
-
-    render(proxy_def, 'system/proxy.j2', proxy, permission=0o755)
-
-def apply(proxy):
-    pass
-
-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-syslog.py b/src/conf_mode/system-syslog.py
deleted file mode 100755
index 07fbb0734..000000000
--- a/src/conf_mode/system-syslog.py
+++ /dev/null
@@ -1,103 +0,0 @@
-#!/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 sys import exit
-
-from vyos.config import Config
-from vyos.configdict import is_node_changed
-from vyos.configverify import verify_vrf
-from vyos.utils.process import call
-from vyos.template import render
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-rsyslog_conf = '/etc/rsyslog.d/00-vyos.conf'
-logrotate_conf = '/etc/logrotate.d/vyos-rsyslog'
-systemd_override = r'/run/systemd/system/rsyslog.service.d/override.conf'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    base = ['system', 'syslog']
-    if not conf.exists(base):
-        return None
-
-    syslog = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                  get_first_key=True, no_tag_node_value_mangle=True)
-
-    syslog.update({ 'logrotate' : logrotate_conf })
-
-    tmp = is_node_changed(conf, base + ['vrf'])
-    if tmp: syslog.update({'restart_required': {}})
-
-    syslog = conf.merge_defaults(syslog, recursive=True)
-    if syslog.from_defaults(['global']):
-        del syslog['global']
-
-    return syslog
-
-def verify(syslog):
-    if not syslog:
-        return None
-
-    verify_vrf(syslog)
-
-def generate(syslog):
-    if not syslog:
-        if os.path.exists(rsyslog_conf):
-            os.unlink(rsyslog_conf)
-        if os.path.exists(logrotate_conf):
-            os.unlink(logrotate_conf)
-
-        return None
-
-    render(rsyslog_conf, 'rsyslog/rsyslog.conf.j2', syslog)
-    render(systemd_override, 'rsyslog/override.conf.j2', syslog)
-    render(logrotate_conf, 'rsyslog/logrotate.j2', syslog)
-
-    # Reload systemd manager configuration
-    call('systemctl daemon-reload')
-    return None
-
-def apply(syslog):
-    systemd_socket = 'syslog.socket'
-    systemd_service = 'syslog.service'
-    if not syslog:
-        call(f'systemctl stop {systemd_service} {systemd_socket}')
-        return None
-
-    # we need to restart the service if e.g. the VRF name changed
-    systemd_action = 'reload-or-restart'
-    if 'restart_required' in syslog:
-        systemd_action = 'restart'
-
-    call(f'systemctl {systemd_action} {systemd_service}')
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/system-timezone.py b/src/conf_mode/system-timezone.py
deleted file mode 100755
index cd3d4b229..000000000
--- a/src/conf_mode/system-timezone.py
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019 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 sys
-import os
-
-from copy import deepcopy
-from vyos.config import Config
-from vyos import ConfigError
-from vyos.utils.process import call
-
-from vyos import airbag
-airbag.enable()
-
-default_config_data = {
-    'name': 'UTC'
-}
-
-def get_config(config=None):
-    tz = deepcopy(default_config_data)
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    if conf.exists('system time-zone'):
-        tz['name'] = conf.return_value('system time-zone')
-
-    return tz
-
-def verify(tz):
-    pass
-
-def generate(tz):
-    pass
-
-def apply(tz):
-    call('/usr/bin/timedatectl set-timezone {}'.format(tz['name']))
-    call('systemctl restart rsyslog')
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        sys.exit(1)
diff --git a/src/conf_mode/system_acceleration.py b/src/conf_mode/system_acceleration.py
new file mode 100755
index 000000000..e4b248675
--- /dev/null
+++ b/src/conf_mode/system_acceleration.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-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 re
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.utils.process import popen
+from vyos.utils.process import run
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+qat_init_script = '/etc/init.d/qat_service'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    data = {}
+
+    if conf.exists(['system', 'acceleration', 'qat']):
+        data.update({'qat_enable' : ''})
+
+    if conf.exists(['vpn', 'ipsec']):
+        data.update({'ipsec' : ''})
+
+    if conf.exists(['interfaces', 'openvpn']):
+        data.update({'openvpn' : ''})
+
+    return data
+
+
+def vpn_control(action, force_ipsec=False):
+    # XXX: Should these commands report failure?
+    if action == 'restore' and force_ipsec:
+        return run('ipsec start')
+
+    return run(f'ipsec {action}')
+
+
+def verify(qat):
+    if 'qat_enable' not in qat:
+        return
+
+    # Check if QAT service installed
+    if not os.path.exists(qat_init_script):
+        raise ConfigError('QAT init script not found')
+
+    # Check if QAT device exist
+    output, err = popen('lspci -nn', decode='utf-8')
+    if not err:
+        # PCI id | Chipset
+        # 19e2 -> C3xx
+        # 37c8 -> C62x
+        # 0435 -> DH895
+        # 6f54 -> D15xx
+        # 18ee -> QAT_200XX
+        data = re.findall(
+            '(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)|(8086:18ee)', output)
+        # If QAT devices found
+        if not data:
+            raise ConfigError('No QAT acceleration device found')
+
+def apply(qat):
+    # Shutdown VPN service which can use QAT
+    if 'ipsec' in qat:
+        vpn_control('stop')
+
+    # Enable/Disable QAT service
+    if 'qat_enable' in qat:
+        run(f'{qat_init_script} start')
+    else:
+        run(f'{qat_init_script} stop')
+
+    # Recover VPN service
+    if 'ipsec' in qat:
+        vpn_control('start')
+
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        vpn_control('restore', force_ipsec=('ipsec' in c))
+        exit(1)
diff --git a/src/conf_mode/system_config-management.py b/src/conf_mode/system_config-management.py
new file mode 100755
index 000000000..c681a8405
--- /dev/null
+++ b/src/conf_mode/system_config-management.py
@@ -0,0 +1,96 @@
+#!/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/>.
+
+import os
+import sys
+
+from vyos import ConfigError
+from vyos.config import Config
+from vyos.config_mgmt import ConfigMgmt
+from vyos.config_mgmt import commit_post_hook_dir, commit_hooks
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base = ['system', 'config-management']
+    if not conf.exists(base):
+        return None
+
+    mgmt = ConfigMgmt(config=conf)
+
+    return mgmt
+
+def verify(_mgmt):
+    return
+
+def generate(mgmt):
+    if mgmt is None:
+        return
+
+    mgmt.initialize_revision()
+
+def apply(mgmt):
+    if mgmt is None:
+        return
+
+    locations = mgmt.locations
+    archive_target = os.path.join(commit_post_hook_dir,
+                               commit_hooks['commit_archive'])
+    if locations:
+        try:
+            os.symlink('/usr/bin/config-mgmt', archive_target)
+        except FileExistsError:
+            pass
+        except OSError as exc:
+            raise ConfigError from exc
+    else:
+        try:
+            os.unlink(archive_target)
+        except FileNotFoundError:
+            pass
+        except OSError as exc:
+            raise ConfigError from exc
+
+    revisions = mgmt.max_revisions
+    revision_target = os.path.join(commit_post_hook_dir,
+                               commit_hooks['commit_revision'])
+    if revisions > 0:
+        try:
+            os.symlink('/usr/bin/config-mgmt', revision_target)
+        except FileExistsError:
+            pass
+        except OSError as exc:
+            raise ConfigError from exc
+    else:
+        try:
+            os.unlink(revision_target)
+        except FileNotFoundError:
+            pass
+        except OSError as exc:
+            raise ConfigError from exc
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        sys.exit(1)
diff --git a/src/conf_mode/system_conntrack.py b/src/conf_mode/system_conntrack.py
new file mode 100755
index 000000000..7f6c71440
--- /dev/null
+++ b/src/conf_mode/system_conntrack.py
@@ -0,0 +1,243 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-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 re
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdep import set_dependents, call_dependents
+from vyos.utils.process import process_named_running
+from vyos.utils.dict import dict_search
+from vyos.utils.dict import dict_search_args
+from vyos.utils.dict import dict_search_recursive
+from vyos.utils.process import cmd
+from vyos.utils.process import rc_cmd
+from vyos.utils.process import run
+from vyos.template import render
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+conntrack_config = r'/etc/modprobe.d/vyatta_nf_conntrack.conf'
+sysctl_file = r'/run/sysctl/10-vyos-conntrack.conf'
+nftables_ct_file = r'/run/nftables-ct.conf'
+
+# Every ALG (Application Layer Gateway) consists of either a Kernel Object
+# also called a Kernel Module/Driver or some rules present in iptables
+module_map = {
+    'ftp': {
+        'ko': ['nf_nat_ftp', 'nf_conntrack_ftp'],
+        'nftables': ['ct helper set "ftp_tcp" tcp dport {21} return']
+    },
+    'h323': {
+        'ko': ['nf_nat_h323', 'nf_conntrack_h323'],
+        'nftables': ['ct helper set "ras_udp" udp dport {1719} return',
+                     'ct helper set "q931_tcp" tcp dport {1720} return']
+    },
+    'nfs': {
+        'nftables': ['ct helper set "rpc_tcp" tcp dport {111} return',
+                     'ct helper set "rpc_udp" udp dport {111} return']
+    },
+    'pptp': {
+        'ko': ['nf_nat_pptp', 'nf_conntrack_pptp'],
+        'nftables': ['ct helper set "pptp_tcp" tcp dport {1723} return'],
+        'ipv4': True
+     },
+    'sip': {
+        'ko': ['nf_nat_sip', 'nf_conntrack_sip'],
+        'nftables': ['ct helper set "sip_tcp" tcp dport {5060,5061} return',
+                     'ct helper set "sip_udp" udp dport {5060,5061} return']
+     },
+    'sqlnet': {
+        'nftables': ['ct helper set "tns_tcp" tcp dport {1521,1525,1536} return']
+    },
+    'tftp': {
+        'ko': ['nf_nat_tftp', 'nf_conntrack_tftp'],
+        'nftables': ['ct helper set "tftp_udp" udp dport {69} return']
+     },
+}
+
+valid_groups = [
+    'address_group',
+    'domain_group',
+    'network_group',
+    'port_group'
+]
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['system', 'conntrack']
+
+    conntrack = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                     get_first_key=True,
+                                     with_recursive_defaults=True)
+
+    conntrack['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'),
+                                                 get_first_key=True,
+                                                 no_tag_node_value_mangle=True)
+
+    conntrack['ipv4_nat_action'] = 'accept' if conf.exists(['nat']) else 'return'
+    conntrack['ipv6_nat_action'] = 'accept' if conf.exists(['nat66']) else 'return'
+    conntrack['wlb_action'] = 'accept' if conf.exists(['load-balancing', 'wan']) else 'return'
+    conntrack['wlb_local_action'] = conf.exists(['load-balancing', 'wan', 'enable-local-traffic'])
+
+    conntrack['module_map'] = module_map
+
+    if conf.exists(['service', 'conntrack-sync']):
+        set_dependents('conntrack_sync', conf)
+
+    return conntrack
+
+def verify(conntrack):
+    for inet in ['ipv4', 'ipv6']:
+        if dict_search_args(conntrack, 'ignore', inet, 'rule') != None:
+            for rule, rule_config in conntrack['ignore'][inet]['rule'].items():
+                if dict_search('destination.port', rule_config) or \
+                   dict_search('destination.group.port_group', rule_config) or \
+                   dict_search('source.port', rule_config) or \
+                   dict_search('source.group.port_group', rule_config):
+                   if 'protocol' not in rule_config or rule_config['protocol'] not in ['tcp', 'udp']:
+                       raise ConfigError(f'Port requires tcp or udp as protocol in rule {rule}')
+
+                tcp_flags = dict_search_args(rule_config, 'tcp', 'flags')
+                if tcp_flags:
+                    if dict_search_args(rule_config, 'protocol') != 'tcp':
+                        raise ConfigError('Protocol must be tcp when specifying tcp flags')
+
+                    not_flags = dict_search_args(rule_config, 'tcp', 'flags', 'not')
+                    if not_flags:
+                        duplicates = [flag for flag in tcp_flags if flag in not_flags]
+                        if duplicates:
+                            raise ConfigError(f'Cannot match a tcp flag as set and not set')
+
+                for side in ['destination', 'source']:
+                    if side in rule_config:
+                        side_conf = rule_config[side]
+
+                        if 'group' in side_conf:
+                            if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1:
+                                raise ConfigError('Only one address-group, network-group or domain-group can be specified')
+
+                            for group in valid_groups:
+                                if group in side_conf['group']:
+                                    group_name = side_conf['group'][group]
+                                    error_group = group.replace("_", "-")
+
+                                    if group in ['address_group', 'network_group', 'domain_group']:
+                                        if 'address' in side_conf:
+                                            raise ConfigError(f'{error_group} and address cannot both be defined')
+
+                                    if group_name and group_name[0] == '!':
+                                        group_name = group_name[1:]
+
+                                    if inet == 'ipv6':
+                                        group = f'ipv6_{group}'
+
+                                    group_obj = dict_search_args(conntrack['firewall'], 'group', group, group_name)
+
+                                    if group_obj is None:
+                                        raise ConfigError(f'Invalid {error_group} "{group_name}" on ignore rule')
+
+                                    if not group_obj:
+                                        Warning(f'{error_group} "{group_name}" has no members!')
+
+        if dict_search_args(conntrack, 'timeout', 'custom', inet, 'rule') != None:
+            for rule, rule_config in conntrack['timeout']['custom'][inet]['rule'].items():
+                if 'protocol' not in rule_config:
+                    raise ConfigError(f'Conntrack custom timeout rule {rule} requires protocol tcp or udp')
+                else:
+                    if 'tcp' in rule_config['protocol'] and 'udp' in rule_config['protocol']:
+                        raise ConfigError(f'conntrack custom timeout rule {rule} - Cant use both tcp and udp protocol')
+    return None
+
+def generate(conntrack):
+    if not os.path.exists(nftables_ct_file):
+        conntrack['first_install'] = True
+
+    # Determine if conntrack is needed
+    conntrack['ipv4_firewall_action'] = 'return'
+    conntrack['ipv6_firewall_action'] = 'return'
+
+    for rules, path in dict_search_recursive(conntrack['firewall'], 'rule'):
+        if any(('state' in rule_conf or 'connection_status' in rule_conf or 'offload_target' in rule_conf) for rule_conf in rules.values()):
+            if path[0] == 'ipv4':
+                conntrack['ipv4_firewall_action'] = 'accept'
+            elif path[0] == 'ipv6':
+                conntrack['ipv6_firewall_action'] = 'accept'
+
+    render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.j2', conntrack)
+    render(sysctl_file, 'conntrack/sysctl.conf.j2', conntrack)
+    render(nftables_ct_file, 'conntrack/nftables-ct.j2', conntrack)
+    return None
+
+def apply(conntrack):
+    # Depending on the enable/disable state of the ALG (Application Layer Gateway)
+    # modules we need to either insmod or rmmod the helpers.
+    
+    add_modules = []
+    rm_modules = []
+
+    for module, module_config in module_map.items():
+        if dict_search_args(conntrack, 'modules', module) is None:
+            if 'ko' in module_config:
+                unloaded = [mod for mod in module_config['ko'] if os.path.exists(f'/sys/module/{mod}')]
+                rm_modules.extend(unloaded)
+        else:
+            if 'ko' in module_config:
+                add_modules.extend(module_config['ko'])
+
+    # Add modules before nftables uses them
+    if add_modules:
+        module_str = ' '.join(add_modules)
+        cmd(f'modprobe -a {module_str}')
+
+    # Load new nftables ruleset
+    install_result, output = rc_cmd(f'nft -f {nftables_ct_file}')
+    if install_result == 1:
+        raise ConfigError(f'Failed to apply configuration: {output}')
+
+    # Remove modules after nftables stops using them
+    if rm_modules:
+        module_str = ' '.join(rm_modules)
+        cmd(f'rmmod {module_str}')
+
+    try:
+        call_dependents()
+    except ConfigError:
+        # Ignore config errors on dependent due to being called too early. Example:
+        # ConfigError("ConfigError('Interface ethN requires an IP address!')")
+        pass
+
+    # We silently ignore all errors
+    # See: https://bugzilla.redhat.com/show_bug.cgi?id=1264080
+    cmd(f'sysctl -f {sysctl_file}')
+
+    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_flow-accounting.py b/src/conf_mode/system_flow-accounting.py
new file mode 100755
index 000000000..206f513c8
--- /dev/null
+++ b/src/conf_mode/system_flow-accounting.py
@@ -0,0 +1,320 @@
+#!/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
+import re
+
+from sys import exit
+from ipaddress import ip_address
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.config import config_dict_merge
+from vyos.configverify import verify_vrf
+from vyos.ifconfig import Section
+from vyos.template import render
+from vyos.utils.process import call
+from vyos.utils.process import cmd
+from vyos.utils.process import run
+from vyos.utils.network import is_addr_assigned
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+uacctd_conf_path = '/run/pmacct/uacctd.conf'
+systemd_service = 'uacctd.service'
+systemd_override = f'/run/systemd/system/{systemd_service}.d/override.conf'
+nftables_nflog_table = 'raw'
+nftables_nflog_chain = 'VYOS_PREROUTING_HOOK'
+egress_nftables_nflog_table = 'inet mangle'
+egress_nftables_nflog_chain = 'FORWARD'
+
+# get nftables rule dict for chain in table
+def _nftables_get_nflog(chain, table):
+    # define list with rules
+    rules = []
+
+    # prepare regex for parsing rules
+    rule_pattern = '[io]ifname "(?P<interface>[\w\.\*\-]+)".*handle (?P<handle>[\d]+)'
+    rule_re = re.compile(rule_pattern)
+
+    # run nftables, save output and split it by lines
+    nftables_command = f'nft -a list chain {table} {chain}'
+    tmp = cmd(nftables_command, message='Failed to get flows list')
+    # parse each line and add information to list
+    for current_rule in tmp.splitlines():
+        if 'FLOW_ACCOUNTING_RULE' not in current_rule:
+            continue
+        current_rule_parsed = rule_re.search(current_rule)
+        if current_rule_parsed:
+            groups = current_rule_parsed.groupdict()
+            rules.append({ 'interface': groups["interface"], 'table': table, 'handle': groups["handle"] })
+
+    # return list with rules
+    return rules
+
+def _nftables_config(configured_ifaces, direction, length=None):
+    # define list of nftables commands to modify settings
+    nftable_commands = []
+    nftables_chain = nftables_nflog_chain
+    nftables_table = nftables_nflog_table
+
+    if direction == "egress":
+        nftables_chain = egress_nftables_nflog_chain
+        nftables_table = egress_nftables_nflog_table
+
+    # prepare extended list with configured interfaces
+    configured_ifaces_extended = []
+    for iface in configured_ifaces:
+        configured_ifaces_extended.append({ 'iface': iface })
+
+    # get currently configured interfaces with nftables rules
+    active_nflog_rules = _nftables_get_nflog(nftables_chain, nftables_table)
+
+    # compare current active list with configured one and delete excessive interfaces, add missed
+    active_nflog_ifaces = []
+    for rule in active_nflog_rules:
+        interface = rule['interface']
+        if interface not in configured_ifaces:
+            table = rule['table']
+            handle = rule['handle']
+            nftable_commands.append(f'nft delete rule {table} {nftables_chain} handle {handle}')
+        else:
+            active_nflog_ifaces.append({
+                'iface': interface,
+            })
+
+    # do not create new rules for already configured interfaces
+    for iface in active_nflog_ifaces:
+        if iface in active_nflog_ifaces and iface in configured_ifaces_extended:
+            configured_ifaces_extended.remove(iface)
+
+    # create missed rules
+    for iface_extended in configured_ifaces_extended:
+        iface = iface_extended['iface']
+        iface_prefix = "o" if direction == "egress" else "i"
+        rule_definition = f'{iface_prefix}ifname "{iface}" counter log group 2 snaplen {length} queue-threshold 100 comment "FLOW_ACCOUNTING_RULE"'
+        nftable_commands.append(f'nft insert rule {nftables_table} {nftables_chain} {rule_definition}')
+        # Also add IPv6 ingres logging
+        if nftables_table == nftables_nflog_table:
+            nftable_commands.append(f'nft insert rule ip6 {nftables_table} {nftables_chain} {rule_definition}')
+
+    # change nftables
+    for command in nftable_commands:
+        cmd(command, raising=ConfigError)
+
+
+def _nftables_trigger_setup(operation: str) -> None:
+    """Add a dummy rule to unlock the main pmacct loop with a packet-trigger
+
+    Args:
+        operation (str): 'add' or 'delete' a trigger
+    """
+    # check if a chain exists
+    table_exists = False
+    if run('nft -snj list table ip pmacct') == 0:
+        table_exists = True
+
+    if operation == 'delete' and table_exists:
+        nft_cmd: str = 'nft delete table ip pmacct'
+        cmd(nft_cmd, raising=ConfigError)
+    if operation == 'add' and not table_exists:
+        nft_cmds: list[str] = [
+            'nft add table ip pmacct',
+            'nft add chain ip pmacct pmacct_out { type filter hook output priority raw - 50 \\; policy accept \\; }',
+            'nft add rule ip pmacct pmacct_out oif lo ip daddr 127.0.254.0 counter log group 2 snaplen 1 queue-threshold 0 comment NFLOG_TRIGGER'
+        ]
+        for nft_cmd in nft_cmds:
+            cmd(nft_cmd, raising=ConfigError)
+
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['system', 'flow-accounting']
+    if not conf.exists(base):
+        return None
+
+    flow_accounting = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+    # We have gathered the dict representation of the CLI, but there are
+    # default values which we need to conditionally update into the
+    # dictionary retrieved.
+    default_values = conf.get_config_defaults(**flow_accounting.kwargs,
+                                              recursive=True)
+
+    # delete individual flow type defaults - should only be added if user
+    # sets this feature
+    for flow_type in ['sflow', 'netflow']:
+        if flow_type not in flow_accounting and flow_type in default_values:
+            del default_values[flow_type]
+
+    flow_accounting = config_dict_merge(default_values, flow_accounting)
+
+    return flow_accounting
+
+def verify(flow_config):
+    if not flow_config:
+        return None
+
+    # check if at least one collector is enabled
+    if 'sflow' not in flow_config and 'netflow' not in flow_config and 'disable_imt' in flow_config:
+        raise ConfigError('You need to configure at least sFlow or NetFlow, ' \
+                          'or not set "disable-imt" for flow-accounting!')
+
+    # Check if at least one interface is configured
+    if 'interface' not in flow_config:
+        raise ConfigError('Flow accounting requires at least one interface to ' \
+                          'be configured!')
+
+    # check that all configured interfaces exists in the system
+    for interface in flow_config['interface']:
+        if interface not in Section.interfaces():
+            # Changed from error to warning to allow adding dynamic interfaces
+            # and interface templates
+            Warning(f'Interface "{interface}" is not presented in the system')
+
+    # check sFlow configuration
+    if 'sflow' in flow_config:
+        # check if at least one sFlow collector is configured
+        if 'server' not in flow_config['sflow']:
+            raise ConfigError('You need to configure at least one sFlow server!')
+
+        # check that all sFlow collectors use the same IP protocol version
+        sflow_collector_ipver = None
+        for server in flow_config['sflow']['server']:
+            if sflow_collector_ipver:
+                if sflow_collector_ipver != ip_address(server).version:
+                    raise ConfigError("All sFlow servers must use the same IP protocol")
+            else:
+                sflow_collector_ipver = ip_address(server).version
+
+        # check if vrf is defined for Sflow
+        verify_vrf(flow_config)
+        sflow_vrf = None
+        if 'vrf' in flow_config:
+            sflow_vrf = flow_config['vrf']
+
+        # check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa
+        for server in flow_config['sflow']['server']:
+            if 'agent_address' in flow_config['sflow']:
+                if ip_address(server).version != ip_address(flow_config['sflow']['agent_address']).version:
+                    raise ConfigError('IPv4 and IPv6 addresses can not be mixed in "sflow agent-address" and "sflow '\
+                                      'server". You need to set the same IP version for both "agent-address" and '\
+                                      'all sFlow servers')
+
+        if 'agent_address' in flow_config['sflow']:
+            tmp = flow_config['sflow']['agent_address']
+            if not is_addr_assigned(tmp, sflow_vrf):
+                raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!')
+
+        # Check if configured sflow source-address exist in the system
+        if 'source_address' in flow_config['sflow']:
+            if not is_addr_assigned(flow_config['sflow']['source_address'], sflow_vrf):
+                tmp = flow_config['sflow']['source_address']
+                raise ConfigError(f'Configured "sflow source-address {tmp}" does not exist on the system!')
+
+    # check NetFlow configuration
+    if 'netflow' in flow_config:
+        # check if vrf is defined for netflow
+        netflow_vrf = None
+        if 'vrf' in flow_config:
+            netflow_vrf = flow_config['vrf']
+
+        # check if at least one NetFlow collector is configured if NetFlow configuration is presented
+        if 'server' not in flow_config['netflow']:
+            raise ConfigError('You need to configure at least one NetFlow server!')
+
+        # Check if configured netflow source-address exist in the system
+        if 'source_address' in flow_config['netflow']:
+            if not is_addr_assigned(flow_config['netflow']['source_address'], netflow_vrf):
+                tmp = flow_config['netflow']['source_address']
+                raise ConfigError(f'Configured "netflow source-address {tmp}" does not exist on the system!')
+
+        # Check if engine-id compatible with selected protocol version
+        if 'engine_id' in flow_config['netflow']:
+            v5_filter = '^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]):(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$'
+            v9v10_filter = '^(\d|[1-9]\d{1,8}|[1-3]\d{9}|4[01]\d{8}|42[0-8]\d{7}|429[0-3]\d{6}|4294[0-8]\d{5}|42949[0-5]\d{4}|429496[0-6]\d{3}|4294967[01]\d{2}|42949672[0-8]\d|429496729[0-5])$'
+            engine_id = flow_config['netflow']['engine_id']
+            version = flow_config['netflow']['version']
+
+            if flow_config['netflow']['version'] == '5':
+                regex_filter = re.compile(v5_filter)
+                if not regex_filter.search(engine_id):
+                    raise ConfigError(f'You cannot use NetFlow engine-id "{engine_id}" '\
+                                      f'together with NetFlow protocol version "{version}"!')
+            else:
+                regex_filter = re.compile(v9v10_filter)
+                if not regex_filter.search(flow_config['netflow']['engine_id']):
+                    raise ConfigError(f'Can not use NetFlow engine-id "{engine_id}" together '\
+                                      f'with NetFlow protocol version "{version}"!')
+
+    # return True if all checks were passed
+    return True
+
+def generate(flow_config):
+    if not flow_config:
+        return None
+
+    render(uacctd_conf_path, 'pmacct/uacctd.conf.j2', flow_config)
+    render(systemd_override, 'pmacct/override.conf.j2', flow_config)
+    # Reload systemd manager configuration
+    call('systemctl daemon-reload')
+
+def apply(flow_config):
+    # Check if flow-accounting was removed and define command
+    if not flow_config:
+        _nftables_config([], 'ingress')
+        _nftables_config([], 'egress')
+
+        # Stop flow-accounting daemon and remove configuration file
+        call(f'systemctl stop {systemd_service}')
+        if os.path.exists(uacctd_conf_path):
+            os.unlink(uacctd_conf_path)
+
+        # must be done after systemctl
+        _nftables_trigger_setup('delete')
+
+        return
+
+    # Start/reload flow-accounting daemon
+    call(f'systemctl restart {systemd_service}')
+
+    # configure nftables rules for defined interfaces
+    if 'interface' in flow_config:
+        _nftables_config(flow_config['interface'], 'ingress', flow_config['packet_length'])
+
+        # configure egress the same way if configured otherwise remove it
+        if 'enable_egress' in flow_config:
+            _nftables_config(flow_config['interface'], 'egress', flow_config['packet_length'])
+        else:
+            _nftables_config([], 'egress')
+
+    # add a trigger for signal processing
+    _nftables_trigger_setup('add')
+
+
+if __name__ == '__main__':
+    try:
+        config = get_config()
+        verify(config)
+        generate(config)
+        apply(config)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/system_host-name.py b/src/conf_mode/system_host-name.py
new file mode 100755
index 000000000..6204cf247
--- /dev/null
+++ b/src/conf_mode/system_host-name.py
@@ -0,0 +1,188 @@
+#!/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 re
+import sys
+import copy
+
+import vyos.hostsd_client
+
+from vyos.base import Warning
+from vyos.config import Config
+from vyos.ifconfig import Section
+from vyos.template import is_ip
+from vyos.utils.process import cmd
+from vyos.utils.process import call
+from vyos.utils.process import process_named_running
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+default_config_data = {
+    'hostname': 'vyos',
+    'domain_name': '',
+    'domain_search': [],
+    'nameserver': [],
+    'nameservers_dhcp_interfaces': {},
+    'static_host_mapping': {}
+}
+
+hostsd_tag = 'system'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    hosts = copy.deepcopy(default_config_data)
+
+    hosts['hostname'] = conf.return_value(['system', 'host-name'])
+
+    # This may happen if the config is not loaded yet,
+    # e.g. if run by cloud-init
+    if not hosts['hostname']:
+        hosts['hostname'] = default_config_data['hostname']
+
+    if conf.exists(['system', 'domain-name']):
+        hosts['domain_name'] = conf.return_value(['system', 'domain-name'])
+        hosts['domain_search'].append(hosts['domain_name'])
+
+    if conf.exists(['system', 'domain-search']):
+        for search in conf.return_values(['system', 'domain-search']):
+            hosts['domain_search'].append(search)
+
+    if conf.exists(['system', 'name-server']):
+        for ns in conf.return_values(['system', 'name-server']):
+            if is_ip(ns):
+                hosts['nameserver'].append(ns)
+            else:
+                tmp = ''
+                if_type = Section.section(ns)
+                if conf.exists(['interfaces', if_type, ns, 'address']):
+                    tmp = conf.return_values(['interfaces', if_type, ns, 'address'])
+
+                hosts['nameservers_dhcp_interfaces'].update({ ns : tmp })
+
+    # system static-host-mapping
+    for hn in conf.list_nodes(['system', 'static-host-mapping', 'host-name']):
+        hosts['static_host_mapping'][hn] = {}
+        hosts['static_host_mapping'][hn]['address'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'inet'])
+        hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'alias'])
+
+    return hosts
+
+
+def verify(hosts):
+    if hosts is None:
+        return None
+
+    # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)"
+    hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$")
+    if not hostname_regex.match(hosts['hostname']):
+        raise ConfigError('Invalid host name ' + hosts["hostname"])
+
+    # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length"
+    length = len(hosts['hostname'])
+    if length < 1 or length > 63:
+        raise ConfigError(
+            'Invalid host-name length, must be less than 63 characters')
+
+    all_static_host_mapping_addresses = []
+    # static mappings alias hostname
+    for host, hostprops in hosts['static_host_mapping'].items():
+        if not hostprops['address']:
+            raise ConfigError(f'IP address required for static-host-mapping "{host}"')
+        all_static_host_mapping_addresses.append(hostprops['address'])
+        for a in hostprops['aliases']:
+            if not hostname_regex.match(a) and len(a) != 0:
+                raise ConfigError(f'Invalid alias "{a}" in static-host-mapping "{host}"')
+
+    for interface, interface_config in hosts['nameservers_dhcp_interfaces'].items():
+        # Warnin user if interface does not have DHCP or DHCPv6 configured
+        if not set(interface_config).intersection(['dhcp', 'dhcpv6']):
+            Warning(f'"{interface}" is not a DHCP interface but uses DHCP name-server option!')
+
+    return None
+
+
+def generate(config):
+    pass
+
+def apply(config):
+    if config is None:
+        return None
+
+    ## Send the updated data to vyos-hostsd
+    try:
+        hc = vyos.hostsd_client.Client()
+
+        hc.set_host_name(config['hostname'], config['domain_name'])
+
+        hc.delete_search_domains([hostsd_tag])
+        if config['domain_search']:
+            hc.add_search_domains({hostsd_tag: config['domain_search']})
+
+        hc.delete_name_servers([hostsd_tag])
+        if config['nameserver']:
+            hc.add_name_servers({hostsd_tag: config['nameserver']})
+
+        # add our own tag's (system) nameservers and search to resolv.conf
+        hc.delete_name_server_tags_system(hc.get_name_server_tags_system())
+        hc.add_name_server_tags_system([hostsd_tag])
+
+        # this will add the dhcp client nameservers to resolv.conf
+        for intf in config['nameservers_dhcp_interfaces']:
+            hc.add_name_server_tags_system([f'dhcp-{intf}', f'dhcpv6-{intf}'])
+
+        hc.delete_hosts([hostsd_tag])
+        if config['static_host_mapping']:
+            hc.add_hosts({hostsd_tag: config['static_host_mapping']})
+
+        hc.apply()
+    except vyos.hostsd_client.VyOSHostsdError as e:
+        raise ConfigError(str(e))
+
+    ## Actually update the hostname -- vyos-hostsd doesn't do that
+
+    # No domain name -- the Debian way.
+    hostname_new = config['hostname']
+
+    # rsyslog runs into a race condition at boot time with systemd
+    # restart rsyslog only if the hostname changed.
+    hostname_old = cmd('hostnamectl --static')
+    call(f'hostnamectl set-hostname --static {hostname_new}')
+
+    # Restart services that use the hostname
+    if hostname_new != hostname_old:
+        call("systemctl restart rsyslog.service")
+
+    # If SNMP is running, restart it too
+    if process_named_running('snmpd'):
+        call('systemctl restart snmpd.service')
+
+    return None
+
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        sys.exit(1)
diff --git a/src/conf_mode/system_ip.py b/src/conf_mode/system_ip.py
new file mode 100755
index 000000000..7612e2c0d
--- /dev/null
+++ b/src/conf_mode/system_ip.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-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/>.
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configverify import verify_route_map
+from vyos.template import render_to_string
+from vyos.utils.dict import dict_search
+from vyos.utils.file import write_file
+from vyos.utils.process import call
+from vyos.utils.process import is_systemd_service_active
+from vyos.utils.system import sysctl_write
+
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['system', 'ip']
+
+    opt = conf.get_config_dict(base, key_mangling=('-', '_'),
+                               get_first_key=True,
+                               with_recursive_defaults=True)
+
+    # When working with FRR we need to know the corresponding address-family
+    opt['afi'] = 'ip'
+
+    # We also need the route-map information from the config
+    #
+    # XXX: one MUST always call this without the key_mangling() option! See
+    # vyos.configverify.verify_common_route_maps() for more information.
+    tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'],
+                                                          get_first_key=True)}}
+    # Merge policy dict into "regular" config dict
+    opt = dict_merge(tmp, opt)
+    return opt
+
+def verify(opt):
+    if 'protocol' in opt:
+        for protocol, protocol_options in opt['protocol'].items():
+            if 'route_map' in protocol_options:
+                verify_route_map(protocol_options['route_map'], opt)
+    return
+
+def generate(opt):
+    opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt)
+    return
+
+def apply(opt):
+    # Apply ARP threshold values
+    # table_size has a default value - thus the key always exists
+    size = int(dict_search('arp.table_size', opt))
+    # Amount upon reaching which the records begin to be cleared immediately
+    sysctl_write('net.ipv4.neigh.default.gc_thresh3', size)
+    # Amount after which the records begin to be cleaned after 5 seconds
+    sysctl_write('net.ipv4.neigh.default.gc_thresh2', size // 2)
+    # Minimum number of stored records is indicated which is not cleared
+    sysctl_write('net.ipv4.neigh.default.gc_thresh1', size // 8)
+
+    # enable/disable IPv4 forwarding
+    tmp = dict_search('disable_forwarding', opt)
+    value = '0' if (tmp != None) else '1'
+    write_file('/proc/sys/net/ipv4/conf/all/forwarding', value)
+
+    # enable/disable IPv4 directed broadcast forwarding
+    tmp = dict_search('disable_directed_broadcast', opt)
+    value = '0' if (tmp != None) else '1'
+    write_file('/proc/sys/net/ipv4/conf/all/bc_forwarding', value)
+
+    # configure multipath
+    tmp = dict_search('multipath.ignore_unreachable_nexthops', opt)
+    value = '1' if (tmp != None) else '0'
+    sysctl_write('net.ipv4.fib_multipath_use_neigh', value)
+
+    tmp = dict_search('multipath.layer4_hashing', opt)
+    value = '1' if (tmp != None) else '0'
+    sysctl_write('net.ipv4.fib_multipath_hash_policy', value)
+
+    # configure TCP options (defaults as of Linux 6.4)
+    tmp = dict_search('tcp.mss.probing', opt)
+    if tmp is None:
+        value = 0
+    elif tmp == 'on-icmp-black-hole':
+        value = 1
+    elif tmp == 'force':
+        value = 2
+    else:
+        # Shouldn't happen
+        raise ValueError("TCP MSS probing is neither 'on-icmp-black-hole' nor 'force'!")
+    sysctl_write('net.ipv4.tcp_mtu_probing', value)
+
+    tmp = dict_search('tcp.mss.base', opt)
+    value = '1024' if (tmp is None) else tmp
+    sysctl_write('net.ipv4.tcp_base_mss', value)
+
+    tmp = dict_search('tcp.mss.floor', opt)
+    value = '48' if (tmp is None) else tmp
+    sysctl_write('net.ipv4.tcp_mtu_probe_floor', value)
+
+    # During startup of vyos-router that brings up FRR, the service is not yet
+    # running when this script is called first. Skip this part and wait for initial
+    # commit of the configuration to trigger this statement
+    if is_systemd_service_active('frr.service'):
+        zebra_daemon = 'zebra'
+        # Save original configuration prior to starting any commit actions
+        frr_cfg = frr.FRRConfig()
+
+        # The route-map used for the FIB (zebra) is part of the zebra daemon
+        frr_cfg.load_configuration(zebra_daemon)
+        frr_cfg.modify_section(r'ip protocol \w+ route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+        if 'frr_zebra_config' in opt:
+            frr_cfg.add_before(frr.default_add_before, opt['frr_zebra_config'])
+        frr_cfg.commit_configuration(zebra_daemon)
+
+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_ipv6.py b/src/conf_mode/system_ipv6.py
new file mode 100755
index 000000000..90a1a8087
--- /dev/null
+++ b/src/conf_mode/system_ipv6.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-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 sys import exit
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configverify import verify_route_map
+from vyos.template import render_to_string
+from vyos.utils.dict import dict_search
+from vyos.utils.file import write_file
+from vyos.utils.process import is_systemd_service_active
+from vyos.utils.system import sysctl_write
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['system', 'ipv6']
+
+    opt = conf.get_config_dict(base, key_mangling=('-', '_'),
+                               get_first_key=True,
+                               with_recursive_defaults=True)
+
+    # When working with FRR we need to know the corresponding address-family
+    opt['afi'] = 'ipv6'
+
+    # We also need the route-map information from the config
+    #
+    # XXX: one MUST always call this without the key_mangling() option! See
+    # vyos.configverify.verify_common_route_maps() for more information.
+    tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'],
+                                                          get_first_key=True)}}
+    # Merge policy dict into "regular" config dict
+    opt = dict_merge(tmp, opt)
+    return opt
+
+def verify(opt):
+    if 'protocol' in opt:
+        for protocol, protocol_options in opt['protocol'].items():
+            if 'route_map' in protocol_options:
+                verify_route_map(protocol_options['route_map'], opt)
+    return
+
+def generate(opt):
+    opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt)
+    return
+
+def apply(opt):
+    # configure multipath
+    tmp = dict_search('multipath.layer4_hashing', opt)
+    value = '1' if (tmp != None) else '0'
+    sysctl_write('net.ipv6.fib_multipath_hash_policy', value)
+
+    # Apply ND threshold values
+    # table_size has a default value - thus the key always exists
+    size = int(dict_search('neighbor.table_size', opt))
+    # Amount upon reaching which the records begin to be cleared immediately
+    sysctl_write('net.ipv6.neigh.default.gc_thresh3', size)
+    # Amount after which the records begin to be cleaned after 5 seconds
+    sysctl_write('net.ipv6.neigh.default.gc_thresh2', size // 2)
+    # Minimum number of stored records is indicated which is not cleared
+    sysctl_write('net.ipv6.neigh.default.gc_thresh1', size // 8)
+
+    # enable/disable IPv6 forwarding
+    tmp = dict_search('disable_forwarding', opt)
+    value = '0' if (tmp != None) else '1'
+    write_file('/proc/sys/net/ipv6/conf/all/forwarding', value)
+
+    # configure IPv6 strict-dad
+    tmp = dict_search('strict_dad', opt)
+    value = '2' if (tmp != None) else '1'
+    for root, dirs, files in os.walk('/proc/sys/net/ipv6/conf'):
+        for name in files:
+            if name == 'accept_dad':
+                write_file(os.path.join(root, name), value)
+
+    # During startup of vyos-router that brings up FRR, the service is not yet
+    # running when this script is called first. Skip this part and wait for initial
+    # commit of the configuration to trigger this statement
+    if is_systemd_service_active('frr.service'):
+        zebra_daemon = 'zebra'
+        # Save original configuration prior to starting any commit actions
+        frr_cfg = frr.FRRConfig()
+
+        # The route-map used for the FIB (zebra) is part of the zebra daemon
+        frr_cfg.load_configuration(zebra_daemon)
+        frr_cfg.modify_section(r'ipv6 protocol \w+ route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+        if 'frr_zebra_config' in opt:
+            frr_cfg.add_before(frr.default_add_before, opt['frr_zebra_config'])
+        frr_cfg.commit_configuration(zebra_daemon)
+
+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
new file mode 100755
index 000000000..f34575aff
--- /dev/null
+++ b/src/conf_mode/system_login.py
@@ -0,0 +1,423 @@
+#!/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.file import chown
+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,_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
+                # T5875: ensure UID is properly set on home directory if user is re-added
+                if os.path.exists(home_dir):
+                    chown(home_dir, user=user, recursive=True)
+
+                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/conf_mode/system_login_banner.py b/src/conf_mode/system_login_banner.py
new file mode 100755
index 000000000..65fa04417
--- /dev/null
+++ b/src/conf_mode/system_login_banner.py
@@ -0,0 +1,107 @@
+#!/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/>.
+
+from sys import exit
+from copy import deepcopy
+
+from vyos.config import Config
+from vyos.utils.file import write_file
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+try:
+    with open('/usr/share/vyos/default_motd') as f:
+        motd = f.read()
+except:
+    # Use an empty banner if the default banner file cannot be read
+    motd = "\n"
+
+PRELOGIN_FILE = r'/etc/issue'
+PRELOGIN_NET_FILE = r'/etc/issue.net'
+POSTLOGIN_FILE = r'/etc/motd'
+
+default_config_data = {
+    'issue': 'Welcome to VyOS - \\n \\l\n\n',
+    'issue_net': '',
+    'motd': motd
+}
+
+def get_config(config=None):
+    banner = deepcopy(default_config_data)
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base_level = ['system', 'login', 'banner']
+
+    if not conf.exists(base_level):
+        return banner
+    else:
+        conf.set_level(base_level)
+
+    # Post-Login banner
+    if conf.exists(['post-login']):
+        tmp = conf.return_value(['post-login'])
+        # post-login banner can be empty as well
+        if tmp:
+            tmp = tmp.replace('\\n','\n')
+            tmp = tmp.replace('\\t','\t')
+            # always add newline character
+            tmp += '\n'
+        else:
+            tmp = ''
+
+        banner['motd'] = tmp
+
+    # Pre-Login banner
+    if conf.exists(['pre-login']):
+        tmp = conf.return_value(['pre-login'])
+        # pre-login banner can be empty as well
+        if tmp:
+            tmp = tmp.replace('\\n','\n')
+            tmp = tmp.replace('\\t','\t')
+            # always add newline character
+            tmp += '\n'
+        else:
+            tmp = ''
+
+        banner['issue'] = banner['issue_net'] = tmp
+
+    return banner
+
+def verify(banner):
+    pass
+
+def generate(banner):
+    pass
+
+def apply(banner):
+    write_file(PRELOGIN_FILE, banner['issue'])
+    write_file(PRELOGIN_NET_FILE, banner['issue_net'])
+    write_file(POSTLOGIN_FILE, banner['motd'])
+
+    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_logs.py b/src/conf_mode/system_logs.py
new file mode 100755
index 000000000..8ad4875d4
--- /dev/null
+++ b/src/conf_mode/system_logs.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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/>.
+
+from sys import exit
+
+from vyos import ConfigError
+from vyos import airbag
+from vyos.config import Config
+from vyos.logger import syslog
+from vyos.template import render
+from vyos.utils.dict import dict_search
+airbag.enable()
+
+# path to logrotate configs
+logrotate_atop_file = '/etc/logrotate.d/vyos-atop'
+logrotate_rsyslog_file = '/etc/logrotate.d/vyos-rsyslog'
+
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base = ['system', 'logs']
+    logs_config = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                       get_first_key=True,
+                                       with_recursive_defaults=True)
+
+    return logs_config
+
+
+def verify(logs_config):
+    # Nothing to verify here
+    pass
+
+
+def generate(logs_config):
+    # get configuration for logrotate atop
+    logrotate_atop = dict_search('logrotate.atop', logs_config)
+    # generate new config file for atop
+    syslog.debug('Adding logrotate config for atop')
+    render(logrotate_atop_file, 'logs/logrotate/vyos-atop.j2', logrotate_atop)
+
+    # get configuration for logrotate rsyslog
+    logrotate_rsyslog = dict_search('logrotate.messages', logs_config)
+    # generate new config file for rsyslog
+    syslog.debug('Adding logrotate config for rsyslog')
+    render(logrotate_rsyslog_file, 'logs/logrotate/vyos-rsyslog.j2',
+           logrotate_rsyslog)
+
+
+def apply(logs_config):
+    # No further actions needed
+    pass
+
+
+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_option.py b/src/conf_mode/system_option.py
new file mode 100755
index 000000000..d92121b3d
--- /dev/null
+++ b/src/conf_mode/system_option.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-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 netifaces import interfaces
+from sys import exit
+from time import sleep
+
+from vyos.config import Config
+from vyos.configverify import verify_source_interface
+from vyos.template import render
+from vyos.utils.process import cmd
+from vyos.utils.process import is_systemd_service_running
+from vyos.utils.network import is_addr_assigned
+from vyos.utils.network import is_intf_addr_assigned
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+curlrc_config = r'/etc/curlrc'
+ssh_config = r'/etc/ssh/ssh_config.d/91-vyos-ssh-client-options.conf'
+systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target'
+time_format_to_locale = {
+    '12-hour': 'en_US.UTF-8',
+    '24-hour': 'en_GB.UTF-8'
+}
+
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['system', 'option']
+    options = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                   get_first_key=True,
+                                   with_recursive_defaults=True)
+
+    return options
+
+def verify(options):
+    if 'http_client' in options:
+        config = options['http_client']
+        if 'source_interface' in config:
+            if not config['source_interface'] in interfaces():
+                raise ConfigError(f'Source interface {source_interface} does not '
+                                  f'exist'.format(**config))
+
+        if {'source_address', 'source_interface'} <= set(config):
+            raise ConfigError('Can not define both HTTP source-interface and source-address')
+
+        if 'source_address' in config:
+            if not is_addr_assigned(config['source_address']):
+                raise ConfigError('No interface with give address specified!')
+
+    if 'ssh_client' in options:
+        config = options['ssh_client']
+        if 'source_address' in config:
+            address = config['source_address']
+            if not is_addr_assigned(config['source_address']):
+                raise ConfigError('No interface with address "{address}" configured!')
+
+        if 'source_interface' in config:
+            verify_source_interface(config)
+            if 'source_address' in config:
+                address = config['source_address']
+                interface = config['source_interface']
+                if not is_intf_addr_assigned(interface, address):
+                    raise ConfigError(f'Address "{address}" not assigned on interface "{interface}"!')
+
+    return None
+
+def generate(options):
+    render(curlrc_config, 'system/curlrc.j2', options)
+    render(ssh_config, 'system/ssh_config.j2', options)
+    return None
+
+def apply(options):
+    # System bootup beep
+    if 'startup_beep' in options:
+        cmd('systemctl enable vyos-beep.service')
+    else:
+        cmd('systemctl disable vyos-beep.service')
+
+    # Ctrl-Alt-Delete action
+    if os.path.exists(systemd_action_file):
+        os.unlink(systemd_action_file)
+    if 'ctrl_alt_delete' in options:
+        if options['ctrl_alt_delete'] == 'reboot':
+            os.symlink('/lib/systemd/system/reboot.target', systemd_action_file)
+        elif options['ctrl_alt_delete'] == 'poweroff':
+            os.symlink('/lib/systemd/system/poweroff.target', systemd_action_file)
+
+    # Configure HTTP client
+    if 'http_client' not in options:
+        if os.path.exists(curlrc_config):
+            os.unlink(curlrc_config)
+
+    # Configure SSH client
+    if 'ssh_client' not in options:
+        if os.path.exists(ssh_config):
+            os.unlink(ssh_config)
+
+    # Reboot system on kernel panic
+    timeout = '0'
+    if 'reboot_on_panic' in options:
+        timeout = '60'
+    with open('/proc/sys/kernel/panic', 'w') as f:
+        f.write(timeout)
+
+    # tuned - performance tuning
+    if 'performance' in options:
+        cmd('systemctl restart tuned.service')
+        # wait until daemon has started before sending configuration
+        while (not is_systemd_service_running('tuned.service')):
+            sleep(0.250)
+        cmd('tuned-adm profile network-{performance}'.format(**options))
+    else:
+        cmd('systemctl stop tuned.service')
+
+    # Keyboard layout - there will be always the default key inside the dict
+    # but we check for key existence anyway
+    if 'keyboard_layout' in options:
+        cmd('loadkeys {keyboard_layout}'.format(**options))
+
+    # Enable/diable root-partition-auto-resize SystemD service
+    if 'root_partition_auto_resize' in options:
+      cmd('systemctl enable root-partition-auto-resize.service')
+    else:
+      cmd('systemctl disable root-partition-auto-resize.service')
+
+    # Time format 12|24-hour
+    if 'time_format' in options:
+        time_format = time_format_to_locale.get(options['time_format'])
+        cmd(f'localectl set-locale LC_TIME={time_format}')
+
+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_proxy.py b/src/conf_mode/system_proxy.py
new file mode 100755
index 000000000..079c43e7e
--- /dev/null
+++ b/src/conf_mode/system_proxy.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2022 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 sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+proxy_def = r'/etc/profile.d/vyos-system-proxy.sh'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['system', 'proxy']
+    if not conf.exists(base):
+        return None
+
+    proxy = conf.get_config_dict(base, get_first_key=True)
+    return proxy
+
+def verify(proxy):
+    if not proxy:
+        return
+
+    if 'url' not in proxy or 'port' not in proxy:
+        raise ConfigError('Proxy URL and port require a value')
+
+    if ('username' in proxy and 'password' not in proxy) or \
+       ('username' not in proxy and 'password' in proxy):
+       raise ConfigError('Both username and password need to be defined!')
+
+def generate(proxy):
+    if not proxy:
+        if os.path.isfile(proxy_def):
+            os.unlink(proxy_def)
+        return
+
+    render(proxy_def, 'system/proxy.j2', proxy, permission=0o755)
+
+def apply(proxy):
+    pass
+
+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_syslog.py b/src/conf_mode/system_syslog.py
new file mode 100755
index 000000000..07fbb0734
--- /dev/null
+++ b/src/conf_mode/system_syslog.py
@@ -0,0 +1,103 @@
+#!/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 sys import exit
+
+from vyos.config import Config
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_vrf
+from vyos.utils.process import call
+from vyos.template import render
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+rsyslog_conf = '/etc/rsyslog.d/00-vyos.conf'
+logrotate_conf = '/etc/logrotate.d/vyos-rsyslog'
+systemd_override = r'/run/systemd/system/rsyslog.service.d/override.conf'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['system', 'syslog']
+    if not conf.exists(base):
+        return None
+
+    syslog = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                  get_first_key=True, no_tag_node_value_mangle=True)
+
+    syslog.update({ 'logrotate' : logrotate_conf })
+
+    tmp = is_node_changed(conf, base + ['vrf'])
+    if tmp: syslog.update({'restart_required': {}})
+
+    syslog = conf.merge_defaults(syslog, recursive=True)
+    if syslog.from_defaults(['global']):
+        del syslog['global']
+
+    return syslog
+
+def verify(syslog):
+    if not syslog:
+        return None
+
+    verify_vrf(syslog)
+
+def generate(syslog):
+    if not syslog:
+        if os.path.exists(rsyslog_conf):
+            os.unlink(rsyslog_conf)
+        if os.path.exists(logrotate_conf):
+            os.unlink(logrotate_conf)
+
+        return None
+
+    render(rsyslog_conf, 'rsyslog/rsyslog.conf.j2', syslog)
+    render(systemd_override, 'rsyslog/override.conf.j2', syslog)
+    render(logrotate_conf, 'rsyslog/logrotate.j2', syslog)
+
+    # Reload systemd manager configuration
+    call('systemctl daemon-reload')
+    return None
+
+def apply(syslog):
+    systemd_socket = 'syslog.socket'
+    systemd_service = 'syslog.service'
+    if not syslog:
+        call(f'systemctl stop {systemd_service} {systemd_socket}')
+        return None
+
+    # we need to restart the service if e.g. the VRF name changed
+    systemd_action = 'reload-or-restart'
+    if 'restart_required' in syslog:
+        systemd_action = 'restart'
+
+    call(f'systemctl {systemd_action} {systemd_service}')
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/system_task-scheduler.py b/src/conf_mode/system_task-scheduler.py
new file mode 100755
index 000000000..129be5d3c
--- /dev/null
+++ b/src/conf_mode/system_task-scheduler.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2017 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 re
+import sys
+
+from vyos.config import Config
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+crontab_file = "/etc/cron.d/vyos-crontab"
+
+
+def format_task(minute="*", hour="*", day="*", dayofweek="*", month="*", user="root", rawspec=None, command=""):
+    fmt_full = "{minute} {hour} {day} {month} {dayofweek} {user} {command}\n"
+    fmt_raw = "{spec} {user} {command}\n"
+
+    if rawspec is None:
+        s = fmt_full.format(minute=minute, hour=hour, day=day,
+                            dayofweek=dayofweek, month=month, command=command, user=user)
+    else:
+        s = fmt_raw.format(spec=rawspec, user=user, command=command)
+
+    return s
+
+def split_interval(s):
+    result = re.search(r"(\d+)([mdh]?)", s)
+    value = int(result.group(1))
+    suffix = result.group(2)
+    return( (value, suffix) )
+
+def make_command(executable, arguments):
+    if arguments:
+        return("sg vyattacfg \"{0} {1}\"".format(executable, arguments))
+    else:
+        return("sg vyattacfg \"{0}\"".format(executable))
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    conf.set_level("system task-scheduler task")
+    task_names = conf.list_nodes("")
+    tasks = []
+
+    for name in task_names:
+        interval = conf.return_value("{0} interval".format(name))
+        spec = conf.return_value("{0} crontab-spec".format(name))
+        executable = conf.return_value("{0} executable path".format(name))
+        args = conf.return_value("{0} executable arguments".format(name))
+        task = {
+                "name": name,
+                "interval": interval,
+                "spec": spec,
+                "executable": executable,
+                "args": args
+              }
+        tasks.append(task)
+
+    return tasks
+
+def verify(tasks):
+    for task in tasks:
+        if not task["interval"] and not task["spec"]:
+            raise ConfigError("Invalid task {0}: must define either interval or crontab-spec".format(task["name"]))
+
+        if task["interval"]:
+            if task["spec"]:
+                raise ConfigError("Invalid task {0}: cannot use interval and crontab-spec at the same time".format(task["name"]))
+ 
+            if not re.match(r"^\d+[mdh]?$", task["interval"]):
+                raise(ConfigError("Invalid interval {0} in task {1}: interval should be a number optionally followed by m, h, or d".format(task["name"], task["interval"])))
+            else:
+                # Check if values are within allowed range
+                value, suffix = split_interval(task["interval"])
+
+                if not suffix or suffix == "m":
+                    if value > 60:
+                        raise ConfigError("Invalid task {0}: interval in minutes must not exceed 60".format(task["name"]))
+                elif suffix == "h":
+                    if value > 24:
+                        raise ConfigError("Invalid task {0}: interval in hours must not exceed 24".format(task["name"]))
+                elif suffix == "d":
+                    if value > 31:
+                        raise ConfigError("Invalid task {0}: interval in days must not exceed 31".format(task["name"]))
+
+        if not task["executable"]:
+            raise ConfigError("Invalid task {0}: executable is not defined".format(task["name"]))
+        else:
+            # Check if executable exists and is executable
+            if not (os.path.isfile(task["executable"]) and os.access(task["executable"], os.X_OK)):
+                raise ConfigError("Invalid task {0}: file {1} does not exist or is not executable".format(task["name"], task["executable"]))
+
+def generate(tasks):
+    crontab_header = "### Generated by vyos-update-crontab.py ###\n"
+    if len(tasks) == 0:
+        if os.path.exists(crontab_file):
+            os.remove(crontab_file)
+        else:
+            pass
+    else:
+        crontab_lines = []
+        for task in tasks:
+            command = make_command(task["executable"], task["args"])
+            if task["spec"]:
+                line = format_task(command=command, rawspec=task["spec"])
+            else:
+                value, suffix = split_interval(task["interval"])
+                if not suffix or suffix == "m":
+                    line = format_task(command=command, minute="*/{0}".format(value))
+                elif suffix == "h":
+                    line = format_task(command=command, minute="0", hour="*/{0}".format(value))
+                elif suffix == "d":
+                    line = format_task(command=command, minute="0", hour="0", day="*/{0}".format(value))
+            crontab_lines.append(line)
+
+        with open(crontab_file, 'w') as f:
+            f.write(crontab_header)
+            f.writelines(crontab_lines)
+
+def apply(config):
+    # No daemon restarts etc. needed for cron
+    pass
+
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        sys.exit(1)
diff --git a/src/conf_mode/system_timezone.py b/src/conf_mode/system_timezone.py
new file mode 100755
index 000000000..cd3d4b229
--- /dev/null
+++ b/src/conf_mode/system_timezone.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 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 sys
+import os
+
+from copy import deepcopy
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.utils.process import call
+
+from vyos import airbag
+airbag.enable()
+
+default_config_data = {
+    'name': 'UTC'
+}
+
+def get_config(config=None):
+    tz = deepcopy(default_config_data)
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    if conf.exists('system time-zone'):
+        tz['name'] = conf.return_value('system time-zone')
+
+    return tz
+
+def verify(tz):
+    pass
+
+def generate(tz):
+    pass
+
+def apply(tz):
+    call('/usr/bin/timedatectl set-timezone {}'.format(tz['name']))
+    call('systemctl restart rsyslog')
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        sys.exit(1)
diff --git a/src/conf_mode/system_update-check.py b/src/conf_mode/system_update-check.py
new file mode 100755
index 000000000..8d641a97d
--- /dev/null
+++ b/src/conf_mode/system_update-check.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 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 json
+import jmespath
+
+from pathlib import Path
+from sys import exit
+
+from vyos.config import Config
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+
+base = ['system', 'update-check']
+service_name = 'vyos-system-update'
+service_conf = Path(f'/run/{service_name}.conf')
+motd_file = Path('/run/motd.d/10-vyos-update')
+
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    if not conf.exists(base):
+        return None
+
+    config = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                  get_first_key=True, no_tag_node_value_mangle=True)
+
+    return config
+
+
+def verify(config):
+    # bail out early - looks like removal from running config
+    if config is None:
+        return
+
+    if 'url' not in config:
+        raise ConfigError('URL is required!')
+
+
+def generate(config):
+    # bail out early - looks like removal from running config
+    if config is None:
+        # Remove old config and return
+        service_conf.unlink(missing_ok=True)
+        # MOTD used in /run/motd.d/10-update
+        motd_file.unlink(missing_ok=True)
+        return None
+
+    # Write configuration file
+    conf_json = json.dumps(config, indent=4)
+    service_conf.write_text(conf_json)
+
+    return None
+
+
+def apply(config):
+    if config:
+        if 'auto_check' in config:
+            call(f'systemctl restart {service_name}.service')
+    else:
+        call(f'systemctl stop {service_name}.service')
+
+
+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_update_check.py b/src/conf_mode/system_update_check.py
deleted file mode 100755
index 8d641a97d..000000000
--- a/src/conf_mode/system_update_check.py
+++ /dev/null
@@ -1,93 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2022 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 json
-import jmespath
-
-from pathlib import Path
-from sys import exit
-
-from vyos.config import Config
-from vyos.utils.process import call
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-
-base = ['system', 'update-check']
-service_name = 'vyos-system-update'
-service_conf = Path(f'/run/{service_name}.conf')
-motd_file = Path('/run/motd.d/10-vyos-update')
-
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    if not conf.exists(base):
-        return None
-
-    config = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                  get_first_key=True, no_tag_node_value_mangle=True)
-
-    return config
-
-
-def verify(config):
-    # bail out early - looks like removal from running config
-    if config is None:
-        return
-
-    if 'url' not in config:
-        raise ConfigError('URL is required!')
-
-
-def generate(config):
-    # bail out early - looks like removal from running config
-    if config is None:
-        # Remove old config and return
-        service_conf.unlink(missing_ok=True)
-        # MOTD used in /run/motd.d/10-update
-        motd_file.unlink(missing_ok=True)
-        return None
-
-    # Write configuration file
-    conf_json = json.dumps(config, indent=4)
-    service_conf.write_text(conf_json)
-
-    return None
-
-
-def apply(config):
-    if config:
-        if 'auto_check' in config:
-            call(f'systemctl restart {service_name}.service')
-    else:
-        call(f'systemctl stop {service_name}.service')
-
-
-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/task_scheduler.py b/src/conf_mode/task_scheduler.py
deleted file mode 100755
index 129be5d3c..000000000
--- a/src/conf_mode/task_scheduler.py
+++ /dev/null
@@ -1,153 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2017 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 re
-import sys
-
-from vyos.config import Config
-from vyos import ConfigError
-
-from vyos import airbag
-airbag.enable()
-
-crontab_file = "/etc/cron.d/vyos-crontab"
-
-
-def format_task(minute="*", hour="*", day="*", dayofweek="*", month="*", user="root", rawspec=None, command=""):
-    fmt_full = "{minute} {hour} {day} {month} {dayofweek} {user} {command}\n"
-    fmt_raw = "{spec} {user} {command}\n"
-
-    if rawspec is None:
-        s = fmt_full.format(minute=minute, hour=hour, day=day,
-                            dayofweek=dayofweek, month=month, command=command, user=user)
-    else:
-        s = fmt_raw.format(spec=rawspec, user=user, command=command)
-
-    return s
-
-def split_interval(s):
-    result = re.search(r"(\d+)([mdh]?)", s)
-    value = int(result.group(1))
-    suffix = result.group(2)
-    return( (value, suffix) )
-
-def make_command(executable, arguments):
-    if arguments:
-        return("sg vyattacfg \"{0} {1}\"".format(executable, arguments))
-    else:
-        return("sg vyattacfg \"{0}\"".format(executable))
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-    conf.set_level("system task-scheduler task")
-    task_names = conf.list_nodes("")
-    tasks = []
-
-    for name in task_names:
-        interval = conf.return_value("{0} interval".format(name))
-        spec = conf.return_value("{0} crontab-spec".format(name))
-        executable = conf.return_value("{0} executable path".format(name))
-        args = conf.return_value("{0} executable arguments".format(name))
-        task = {
-                "name": name,
-                "interval": interval,
-                "spec": spec,
-                "executable": executable,
-                "args": args
-              }
-        tasks.append(task)
-
-    return tasks
-
-def verify(tasks):
-    for task in tasks:
-        if not task["interval"] and not task["spec"]:
-            raise ConfigError("Invalid task {0}: must define either interval or crontab-spec".format(task["name"]))
-
-        if task["interval"]:
-            if task["spec"]:
-                raise ConfigError("Invalid task {0}: cannot use interval and crontab-spec at the same time".format(task["name"]))
- 
-            if not re.match(r"^\d+[mdh]?$", task["interval"]):
-                raise(ConfigError("Invalid interval {0} in task {1}: interval should be a number optionally followed by m, h, or d".format(task["name"], task["interval"])))
-            else:
-                # Check if values are within allowed range
-                value, suffix = split_interval(task["interval"])
-
-                if not suffix or suffix == "m":
-                    if value > 60:
-                        raise ConfigError("Invalid task {0}: interval in minutes must not exceed 60".format(task["name"]))
-                elif suffix == "h":
-                    if value > 24:
-                        raise ConfigError("Invalid task {0}: interval in hours must not exceed 24".format(task["name"]))
-                elif suffix == "d":
-                    if value > 31:
-                        raise ConfigError("Invalid task {0}: interval in days must not exceed 31".format(task["name"]))
-
-        if not task["executable"]:
-            raise ConfigError("Invalid task {0}: executable is not defined".format(task["name"]))
-        else:
-            # Check if executable exists and is executable
-            if not (os.path.isfile(task["executable"]) and os.access(task["executable"], os.X_OK)):
-                raise ConfigError("Invalid task {0}: file {1} does not exist or is not executable".format(task["name"], task["executable"]))
-
-def generate(tasks):
-    crontab_header = "### Generated by vyos-update-crontab.py ###\n"
-    if len(tasks) == 0:
-        if os.path.exists(crontab_file):
-            os.remove(crontab_file)
-        else:
-            pass
-    else:
-        crontab_lines = []
-        for task in tasks:
-            command = make_command(task["executable"], task["args"])
-            if task["spec"]:
-                line = format_task(command=command, rawspec=task["spec"])
-            else:
-                value, suffix = split_interval(task["interval"])
-                if not suffix or suffix == "m":
-                    line = format_task(command=command, minute="*/{0}".format(value))
-                elif suffix == "h":
-                    line = format_task(command=command, minute="0", hour="*/{0}".format(value))
-                elif suffix == "d":
-                    line = format_task(command=command, minute="0", hour="0", day="*/{0}".format(value))
-            crontab_lines.append(line)
-
-        with open(crontab_file, 'w') as f:
-            f.write(crontab_header)
-            f.writelines(crontab_lines)
-
-def apply(config):
-    # No daemon restarts etc. needed for cron
-    pass
-
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        sys.exit(1)
diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py
deleted file mode 100755
index 3ad346e2e..000000000
--- a/src/conf_mode/tftp_server.py
+++ /dev/null
@@ -1,142 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 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 stat
-import pwd
-
-from copy import deepcopy
-from glob import glob
-from sys import exit
-
-from vyos.base import Warning
-from vyos.config import Config
-from vyos.configverify import verify_vrf
-from vyos.template import render
-from vyos.template import is_ipv4
-from vyos.utils.process import call
-from vyos.utils.permission import chmod_755
-from vyos.utils.network import is_addr_assigned
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/etc/default/tftpd'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base = ['service', 'tftp-server']
-    if not conf.exists(base):
-        return None
-
-    tftpd = conf.get_config_dict(base, key_mangling=('-', '_'),
-                                 get_first_key=True,
-                                 with_recursive_defaults=True)
-    return tftpd
-
-def verify(tftpd):
-    # bail out early - looks like removal from running config
-    if not tftpd:
-        return None
-
-    # Configuring allowed clients without a server makes no sense
-    if 'directory' not in tftpd:
-        raise ConfigError('TFTP root directory must be configured!')
-
-    if 'listen_address' not in tftpd:
-        raise ConfigError('TFTP server listen address must be configured!')
-
-    for address, address_config in tftpd['listen_address'].items():
-        if not is_addr_assigned(address):
-            Warning(f'TFTP server listen address "{address}" not ' \
-                     'assigned to any interface!')
-        verify_vrf(address_config)
-
-    return None
-
-def generate(tftpd):
-    # cleanup any available configuration file
-    # files will be recreated on demand
-    for i in glob(config_file + '*'):
-        os.unlink(i)
-
-    # bail out early - looks like removal from running config
-    if tftpd is None:
-        return None
-
-    idx = 0
-    for address, address_config in tftpd['listen_address'].items():
-        config = deepcopy(tftpd)
-        port = tftpd['port']
-        if is_ipv4(address):
-            config['listen_address'] = f'{address}:{port} -4'
-        else:
-            config['listen_address'] = f'[{address}]:{port} -6'
-
-        if 'vrf' in address_config:
-            config['vrf'] = address_config['vrf']
-
-        file = config_file + str(idx)
-        render(file, 'tftp-server/default.j2', config)
-        idx = idx + 1
-
-    return None
-
-def apply(tftpd):
-    # stop all services first - then we will decide
-    call('systemctl stop tftpd@*.service')
-
-    # bail out early - e.g. service deletion
-    if tftpd is None:
-        return None
-
-    tftp_root = tftpd['directory']
-    if not os.path.exists(tftp_root):
-        os.makedirs(tftp_root)
-        chmod_755(tftp_root)
-
-    # get UNIX uid for user 'tftp'
-    tftp_uid = pwd.getpwnam('tftp').pw_uid
-    tftp_gid = pwd.getpwnam('tftp').pw_gid
-
-    # get UNIX uid for tftproot directory
-    dir_uid = os.stat(tftp_root).st_uid
-    dir_gid = os.stat(tftp_root).st_gid
-
-    # adjust uid/gid of tftproot directory if files don't belong to user tftp
-    if (tftp_uid != dir_uid) or (tftp_gid != dir_gid):
-        os.chown(tftp_root, tftp_uid, tftp_gid)
-
-    idx = 0
-    for address in tftpd['listen_address']:
-        call(f'systemctl restart tftpd@{idx}.service')
-        idx = idx + 1
-
-    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/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers b/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers
index 222c75f21..5157469f4 100755
--- a/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers
+++ b/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers
@@ -1,5 +1,4 @@
 #!/bin/bash
-### Autogenerated by interfaces-pppoe.py ###
 
 interface=$6
 if [ -z "$interface" ]; then
diff --git a/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers b/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers
index 0fcedbedc..4affaeb5c 100755
--- a/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers
+++ b/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers
@@ -1,5 +1,4 @@
 #!/bin/bash
-### Autogenerated by interfaces-pppoe.py ###
 
 interface=$6
 if [ -z "$interface" ]; then
diff --git a/src/init/vyos-router b/src/init/vyos-router
index 711681a8e..aaecbf2a1 100755
--- a/src/init/vyos-router
+++ b/src/init/vyos-router
@@ -372,11 +372,11 @@ start ()
     # As VyOS does not execute commands that are not present in the CLI we call
     # the script by hand to have a single source for the login banner and MOTD
     ${vyos_conf_scripts_dir}/system_console.py || log_failure_msg "could not reset serial console"
-    ${vyos_conf_scripts_dir}/system-login-banner.py || log_failure_msg "could not reset motd and issue files"
-    ${vyos_conf_scripts_dir}/system-option.py || log_failure_msg "could not reset system option files"
-    ${vyos_conf_scripts_dir}/system-ip.py || log_failure_msg "could not reset system IPv4 options"
-    ${vyos_conf_scripts_dir}/system-ipv6.py || log_failure_msg "could not reset system IPv6 options"
-    ${vyos_conf_scripts_dir}/conntrack.py || log_failure_msg "could not reset conntrack subsystem"
+    ${vyos_conf_scripts_dir}/system_login_banner.py || log_failure_msg "could not reset motd and issue files"
+    ${vyos_conf_scripts_dir}/system_option.py || log_failure_msg "could not reset system option files"
+    ${vyos_conf_scripts_dir}/system_ip.py || log_failure_msg "could not reset system IPv4 options"
+    ${vyos_conf_scripts_dir}/system_ipv6.py || log_failure_msg "could not reset system IPv6 options"
+    ${vyos_conf_scripts_dir}/system_conntrack.py || log_failure_msg "could not reset conntrack subsystem"
     ${vyos_conf_scripts_dir}/container.py || log_failure_msg "could not reset container subsystem"
 
     clear_or_override_config_files || log_failure_msg "could not reset config files"
diff --git a/src/migration-scripts/https/1-to-2 b/src/migration-scripts/https/1-to-2
index b1cf37ea6..1a2cdc1e7 100755
--- a/src/migration-scripts/https/1-to-2
+++ b/src/migration-scripts/https/1-to-2
@@ -15,7 +15,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 # * Move 'api virtual-host' list to 'api-restrict virtual-host' so it
-#   is owned by https.py instead of http-api.py
+#   is owned by service_https.py
 
 import sys
 
diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py
index 89f929be7..10034e499 100755
--- a/src/op_mode/connect_disconnect.py
+++ b/src/op_mode/connect_disconnect.py
@@ -55,7 +55,7 @@ def connect(interface):
         if is_wwan_connected(interface):
             print(f'Interface {interface}: already connected!')
         else:
-            call(f'VYOS_TAGNODE_VALUE={interface} /usr/libexec/vyos/conf_mode/interfaces-wwan.py')
+            call(f'VYOS_TAGNODE_VALUE={interface} /usr/libexec/vyos/conf_mode/interfaces_wwan.py')
     else:
         print(f'Unknown interface {interface}, can not connect. Aborting!')
 
diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py
index 5e19bdbad..6d33e372d 100755
--- a/src/system/keepalived-fifo.py
+++ b/src/system/keepalived-fifo.py
@@ -41,7 +41,7 @@ logger.addHandler(logs_handler_syslog)
 logger.setLevel(logging.DEBUG)
 
 mdns_running_file = '/run/mdns_vrrp_active'
-mdns_update_command = 'sudo /usr/libexec/vyos/conf_mode/service_mdns-repeater.py'
+mdns_update_command = 'sudo /usr/libexec/vyos/conf_mode/service_mdns_repeater.py'
 
 # class for all operations
 class KeepalivedFifo:
diff --git a/src/tests/test_task_scheduler.py b/src/tests/test_task_scheduler.py
index f15fcde88..130f825e6 100644
--- a/src/tests/test_task_scheduler.py
+++ b/src/tests/test_task_scheduler.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2018-2020 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
@@ -17,16 +17,16 @@
 import os
 import tempfile
 import unittest
+import importlib
 
 from vyos import ConfigError
 
 try:
-    from src.conf_mode import task_scheduler
+    task_scheduler = importlib.import_module("src.conf_mode.system_task-scheduler")
 except ModuleNotFoundError:  # for unittest.main()
     import sys
     sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
-    from src.conf_mode import task_scheduler
-
+    task_scheduler = importlib.import_module("src.conf_mode.system_task-scheduler")
 
 class TestUpdateCrontab(unittest.TestCase):
 
-- 
cgit v1.2.3