diff --git a/data/templates/frr/zebra.route-map.frr.j2 b/data/templates/frr/zebra.route-map.frr.j2
index 669d58354..70a810f43 100644
--- a/data/templates/frr/zebra.route-map.frr.j2
+++ b/data/templates/frr/zebra.route-map.frr.j2
@@ -1,14 +1,16 @@
 !
+{{ 'no ' if disable_forwarding is vyos_defined }}{{ afi }} forwarding
+!
 {% if nht.no_resolve_via_default is vyos_defined %}
 no {{ afi }} nht resolve-via-default
 {% endif %}
 !
 {% if protocol is vyos_defined %}
 {%     for protocol_name, protocol_config in protocol.items() %}
 {%         if protocol_name is vyos_defined('ospfv3') %}
 {%             set protocol_name = 'ospf6' %}
 {%         endif %}
 {{ afi }} protocol {{ protocol_name }} route-map {{ protocol_config.route_map }}
 {%     endfor %}
 {% endif %}
 !
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index f5e84267e..9522d8fcc 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -1,1147 +1,1156 @@
 # Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
 # License as published by the Free Software Foundation; either
 # version 2.1 of the License, or (at your option) any later version.
 #
 # This library is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public
 # License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
 """
 A library for retrieving value dicts from VyOS configs in a declarative fashion.
 """
 import os
 import json
 
 from vyos.defaults import frr_debug_enable
 from vyos.utils.dict import dict_search
 from vyos.utils.process import cmd
 
 def retrieve_config(path_hash, base_path, config):
     """
     Retrieves a VyOS config as a dict according to a declarative description
 
     The description dict, passed in the first argument, must follow this format:
     ``field_name : <path, type, [inner_options_dict]>``.
 
     Supported types are: ``str`` (for normal nodes),
     ``list`` (returns a list of strings, for multi nodes),
     ``bool`` (returns True if valueless node exists),
     ``dict`` (for tag nodes, returns a dict indexed by node names,
     according to description in the third item of the tuple).
 
     Args:
         path_hash (dict): Declarative description of the config to retrieve
         base_path (list): A base path to prepend to all option paths
         config (vyos.config.Config): A VyOS config object
 
     Returns:
         dict: config dict
     """
     config_hash = {}
 
     for k in path_hash:
 
         if type(path_hash[k]) != tuple:
             raise ValueError("In field {0}: expected a tuple, got a value {1}".format(k, str(path_hash[k])))
         if len(path_hash[k]) < 2:
             raise ValueError("In field {0}: field description must be a tuple of at least two items, path (list) and type".format(k))
 
         path = path_hash[k][0]
         if type(path) != list:
             raise ValueError("In field {0}: path must be a list, not a {1}".format(k, type(path)))
 
         typ = path_hash[k][1]
         if type(typ) != type:
             raise ValueError("In field {0}: type must be a type, not a {1}".format(k, type(typ)))
 
         path = base_path + path
 
         path_str = " ".join(path)
 
         if typ == str:
             config_hash[k] = config.return_value(path_str)
         elif typ == list:
             config_hash[k] = config.return_values(path_str)
         elif typ == bool:
             config_hash[k] = config.exists(path_str)
         elif typ == dict:
             try:
                 inner_hash = path_hash[k][2]
             except IndexError:
                 raise ValueError("The type of the \'{0}\' field is dict, but inner options hash is missing from the tuple".format(k))
             config_hash[k] = {}
             nodes = config.list_nodes(path_str)
             for node in nodes:
                 config_hash[k][node] = retrieve_config(inner_hash, path + [node], config)
 
     return config_hash
 
 
 def dict_merge(source, destination):
     """ Merge two dictionaries. Only keys which are not present in destination
     will be copied from source, anything else will be kept untouched. Function
     will return a new dict which has the merged key/value pairs. """
     from copy import deepcopy
     tmp = deepcopy(destination)
 
     for key, value in source.items():
         if key not in tmp:
             tmp[key] = value
         elif isinstance(source[key], dict):
             tmp[key] = dict_merge(source[key], tmp[key])
 
     return tmp
 
 def list_diff(first, second):
     """ Diff two dictionaries and return only unique items """
     second = set(second)
     return [item for item in first if item not in second]
 
 def is_node_changed(conf, path):
    """
    Check if any key under path has been changed and return True.
    If nothing changed, return false
    """
    from vyos.configdiff import get_config_diff
    D = get_config_diff(conf, key_mangling=('-', '_'))
    return D.is_node_changed(path)
 
 def leaf_node_changed(conf, path):
     """
     Check if a leaf node was altered. If it has been altered - values has been
     changed, or it was added/removed, we will return a list containing the old
     value(s). If nothing has been changed, None is returned.
 
     NOTE: path must use the real CLI node name (e.g. with a hyphen!)
     """
     from vyos.configdiff import get_config_diff
     D = get_config_diff(conf, key_mangling=('-', '_'))
     (new, old) = D.get_value_diff(path)
     if new != old:
         if isinstance(old, dict):
             # valueLess nodes return {} if node is deleted
             return True
         if old is None and isinstance(new, dict):
             # valueLess nodes return {} if node was added
             return True
         if old is None:
             return []
         if isinstance(old, str):
             return [old]
         if isinstance(old, list):
             if isinstance(new, str):
                 new = [new]
             elif isinstance(new, type(None)):
                 new = []
             return list_diff(old, new)
 
     return None
 
 def node_changed(conf, path, key_mangling=None, recursive=False, expand_nodes=None) -> list:
     """
     Check if node under path (or anything under path if recursive=True) was changed. By default
     we only check if a node or subnode (recursive) was deleted from path. If expand_nodes
     is set to Diff.ADD we can also check if something was added to the path.
 
     If nothing changed, an empty list is returned.
     """
     from vyos.configdiff import get_config_diff
     from vyos.configdiff import Diff
     # to prevent circular dependencies we assign the default here
     if not expand_nodes: expand_nodes = Diff.DELETE
     D = get_config_diff(conf, key_mangling)
     # get_child_nodes_diff() will return dict_keys()
     tmp = D.get_child_nodes_diff(path, expand_nodes=expand_nodes, recursive=recursive)
     output = []
     if expand_nodes & Diff.DELETE:
         output.extend(list(tmp['delete'].keys()))
     if expand_nodes & Diff.ADD:
         output.extend(list(tmp['add'].keys()))
 
     # remove duplicate keys from list, this happens when a node (e.g. description) is altered
     output = list(dict.fromkeys(output))
     return output
 
 def get_removed_vlans(conf, path, dict):
     """
     Common function to parse a dictionary retrieved via get_config_dict() and
     determine any added/removed VLAN interfaces - be it 802.1q or Q-in-Q.
     """
     from vyos.configdiff import get_config_diff, Diff
 
     # Check vif, vif-s/vif-c VLAN interfaces for removal
     D = get_config_diff(conf, key_mangling=('-', '_'))
     D.set_level(conf.get_level())
 
     # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
     keys = D.get_child_nodes_diff(path + ['vif'], expand_nodes=Diff.DELETE)['delete'].keys()
     if keys: dict['vif_remove'] = [*keys]
 
     # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
     keys = D.get_child_nodes_diff(path + ['vif-s'], expand_nodes=Diff.DELETE)['delete'].keys()
     if keys: dict['vif_s_remove'] = [*keys]
 
     for vif in dict.get('vif_s', {}).keys():
         keys = D.get_child_nodes_diff(path + ['vif-s', vif, 'vif-c'], expand_nodes=Diff.DELETE)['delete'].keys()
         if keys: dict['vif_s'][vif]['vif_c_remove'] = [*keys]
 
     return dict
 
 def is_member(conf, interface, intftype=None):
     """
     Checks if passed interface is member of other interface of specified type.
     intftype is optional, if not passed it will search all known types
     (currently bridge and bonding)
 
     Returns: dict
     empty -> Interface is not a member
     key -> Interface is a member of this interface
     """
     ret_val = {}
     intftypes = ['bonding', 'bridge']
 
     if intftype not in intftypes + [None]:
         raise ValueError((
             f'unknown interface type "{intftype}" or it cannot '
             f'have member interfaces'))
 
     intftype = intftypes if intftype == None else [intftype]
 
     for iftype in intftype:
         base = ['interfaces', iftype]
         for intf in conf.list_nodes(base):
             member = base + [intf, 'member', 'interface', interface]
             if conf.exists(member):
                 tmp = conf.get_config_dict(member, key_mangling=('-', '_'),
                                            get_first_key=True,
                                            no_tag_node_value_mangle=True)
                 ret_val.update({intf : tmp})
 
     return ret_val
 
 def is_mirror_intf(conf, interface, direction=None):
     """
     Check whether the passed interface is used for port mirroring. Direction
     is optional, if not passed it will search all known direction
     (currently ingress and egress)
 
     Returns:
     None -> Interface is not a monitor interface
     Array() -> This interface is a monitor interface of interfaces
     """
     from vyos.ifconfig import Section
 
     directions = ['ingress', 'egress']
     if direction not in directions + [None]:
         raise ValueError(f'Unknown interface mirror direction "{direction}"')
 
     direction = directions if direction == None else [direction]
 
     ret_val = None
     base = ['interfaces']
 
     for dir in direction:
         for iftype in conf.list_nodes(base):
             iftype_base = base + [iftype]
             for intf in conf.list_nodes(iftype_base):
                 mirror = iftype_base + [intf, 'mirror', dir, interface]
                 if conf.exists(mirror):
                     path = ['interfaces', Section.section(intf), intf]
                     tmp = conf.get_config_dict(path, key_mangling=('-', '_'),
                                                get_first_key=True)
                     ret_val = {intf : tmp}
 
     return ret_val
 
 def has_address_configured(conf, intf):
     """
     Checks if interface has an address configured.
     Checks the following config nodes:
     'address', 'ipv6 address eui64', 'ipv6 address autoconf'
 
     Returns True if interface has address configured, False if it doesn't.
     """
     from vyos.ifconfig import Section
     ret = False
 
     old_level = conf.get_level()
     conf.set_level([])
 
     intfpath = ['interfaces', Section.get_config_path(intf)]
     if (conf.exists([intfpath, 'address']) or
         conf.exists([intfpath, 'ipv6', 'address', 'autoconf']) or
         conf.exists([intfpath, 'ipv6', 'address', 'eui64'])):
         ret = True
 
     conf.set_level(old_level)
     return ret
 
 def has_vrf_configured(conf, intf):
     """
     Checks if interface has a VRF configured.
 
     Returns True if interface has VRF configured, False if it doesn't.
     """
     from vyos.ifconfig import Section
     ret = False
 
     old_level = conf.get_level()
     conf.set_level([])
 
     if conf.exists(['interfaces', Section.get_config_path(intf), 'vrf']):
         ret = True
 
     conf.set_level(old_level)
     return ret
 
 def has_vlan_subinterface_configured(conf, intf):
     """
     Checks if interface has an VLAN subinterface configured.
     Checks the following config nodes:
     'vif', 'vif-s'
 
     Return True if interface has VLAN subinterface configured.
     """
     from vyos.ifconfig import Section
     ret = False
 
     intfpath = ['interfaces', Section.section(intf), intf]
     if (conf.exists(intfpath + ['vif']) or conf.exists(intfpath + ['vif-s'])):
         ret = True
 
     return ret
 
 def is_source_interface(conf, interface, intftype=None):
     """
     Checks if passed interface is configured as source-interface of other
     interfaces of specified type. intftype is optional, if not passed it will
     search all known types (currently pppoe, macsec, pseudo-ethernet, tunnel
     and vxlan)
 
     Returns:
     None -> Interface is not a member
     interface name -> Interface is a member of this interface
     False -> interface type cannot have members
     """
     ret_val = None
     intftypes = ['macsec', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan']
     if not intftype:
         intftype = intftypes
 
     if isinstance(intftype, str):
         intftype = [intftype]
     elif not isinstance(intftype, list):
         raise ValueError(f'Interface type "{type(intftype)}" must be either str or list!')
 
     if not all(x in intftypes for x in intftype):
         raise ValueError(f'unknown interface type "{intftype}" or it can not '
             'have a source-interface')
 
     for it in intftype:
         base = ['interfaces', it]
         for intf in conf.list_nodes(base):
             src_intf = base + [intf, 'source-interface']
             if conf.exists(src_intf) and interface in conf.return_values(src_intf):
                 ret_val = intf
                 break
 
     return ret_val
 
 def get_dhcp_interfaces(conf, vrf=None):
     """ Common helper functions to retrieve all interfaces from current CLI
     sessions that have DHCP configured. """
     dhcp_interfaces = {}
     dict = conf.get_config_dict(['interfaces'], get_first_key=True)
     if not dict:
         return dhcp_interfaces
 
     def check_dhcp(config):
         ifname = config['ifname']
         tmp = {}
         if 'address' in config and 'dhcp' in config['address']:
             options = {}
             if dict_search('dhcp_options.default_route_distance', config) != None:
                 options.update({'dhcp_options' : config['dhcp_options']})
             if 'vrf' in config:
                 if vrf == config['vrf']: tmp.update({ifname : options})
             else:
                 if vrf is None: tmp.update({ifname : options})
 
         return tmp
 
     for section, interface in dict.items():
         for ifname in interface:
             # always reset config level, as get_interface_dict() will alter it
             conf.set_level([])
             # we already have a dict representation of the config from get_config_dict(),
             # but with the extended information from get_interface_dict() we also
             # get the DHCP client default-route-distance default option if not specified.
             _, ifconfig = get_interface_dict(conf, ['interfaces', section], ifname)
 
             tmp = check_dhcp(ifconfig)
             dhcp_interfaces.update(tmp)
             # check per VLAN interfaces
             for vif, vif_config in ifconfig.get('vif', {}).items():
                 tmp = check_dhcp(vif_config)
                 dhcp_interfaces.update(tmp)
             # check QinQ VLAN interfaces
             for vif_s, vif_s_config in ifconfig.get('vif_s', {}).items():
                 tmp = check_dhcp(vif_s_config)
                 dhcp_interfaces.update(tmp)
                 for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items():
                     tmp = check_dhcp(vif_c_config)
                     dhcp_interfaces.update(tmp)
 
     return dhcp_interfaces
 
 def get_pppoe_interfaces(conf, vrf=None):
     """ Common helper functions to retrieve all interfaces from current CLI
     sessions that have DHCP configured. """
     pppoe_interfaces = {}
     conf.set_level([])
     for ifname in conf.list_nodes(['interfaces', 'pppoe']):
         # always reset config level, as get_interface_dict() will alter it
         conf.set_level([])
         # we already have a dict representation of the config from get_config_dict(),
         # but with the extended information from get_interface_dict() we also
         # get the DHCP client default-route-distance default option if not specified.
         _, ifconfig = get_interface_dict(conf, ['interfaces', 'pppoe'], ifname)
 
         options = {}
         if 'default_route_distance' in ifconfig:
             options.update({'default_route_distance' : ifconfig['default_route_distance']})
         if 'no_default_route' in ifconfig:
             options.update({'no_default_route' : {}})
         if 'vrf' in ifconfig:
             if vrf == ifconfig['vrf']: pppoe_interfaces.update({ifname : options})
         else:
             if vrf is None: pppoe_interfaces.update({ifname : options})
 
     return pppoe_interfaces
 
 def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pki=False):
     """
     Common utility function to retrieve and mangle the interfaces configuration
     from the CLI input nodes. All interfaces have a common base where value
     retrival is identical. This function must be used whenever possible when
     working on the interfaces node!
 
     Return a dictionary with the necessary interface config keys.
     """
     if not ifname:
         from vyos import ConfigError
         # determine tagNode instance
         if 'VYOS_TAGNODE_VALUE' not in os.environ:
             raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
         ifname = os.environ['VYOS_TAGNODE_VALUE']
 
     # Check if interface has been removed. We must use exists() as
     # get_config_dict() will always return {} - even when an empty interface
     # node like the following exists.
     # +macsec macsec1 {
     # +}
     if not config.exists(base + [ifname]):
         dict = config.get_config_dict(base + [ifname], key_mangling=('-', '_'),
                                       get_first_key=True,
                                       no_tag_node_value_mangle=True)
         dict.update({'deleted' : {}})
     else:
         # Get config_dict with default values
         dict = config.get_config_dict(base + [ifname], key_mangling=('-', '_'),
                                       get_first_key=True,
                                       no_tag_node_value_mangle=True,
                                       with_defaults=True,
                                       with_recursive_defaults=recursive_defaults,
                                       with_pki=with_pki)
 
         # If interface does not request an IPv4 DHCP address there is no need
         # to keep the dhcp-options key
         if 'address' not in dict or 'dhcp' not in dict['address']:
             if 'dhcp_options' in dict:
                 del dict['dhcp_options']
 
     # Add interface instance name into dictionary
     dict.update({'ifname': ifname})
 
     # Check if QoS policy applied on this interface - See ifconfig.interface.set_mirror_redirect()
     if config.exists(['qos', 'interface', ifname]):
         dict.update({'traffic_policy': {}})
 
     address = leaf_node_changed(config, base + [ifname, 'address'])
     if address: dict.update({'address_old' : address})
 
     # Check if we are a member of a bridge device
     bridge = is_member(config, ifname, 'bridge')
     if bridge: dict.update({'is_bridge_member' : bridge})
 
     # Check if it is a monitor interface
     mirror = is_mirror_intf(config, ifname)
     if mirror: dict.update({'is_mirror_intf' : mirror})
 
     # Check if we are a member of a bond device
     bond = is_member(config, ifname, 'bonding')
     if bond: dict.update({'is_bond_member' : bond})
 
     # Check if any DHCP options changed which require a client restat
     dhcp = is_node_changed(config, base + [ifname, 'dhcp-options'])
     if dhcp: dict.update({'dhcp_options_changed' : {}})
 
     # Changine interface VRF assignemnts require a DHCP restart, too
     dhcp = is_node_changed(config, base + [ifname, 'vrf'])
     if dhcp: dict.update({'dhcp_options_changed' : {}})
 
     # Some interfaces come with a source_interface which must also not be part
     # of any other bond or bridge interface as it is exclusivly assigned as the
     # Kernels "lower" interface to this new "virtual/upper" interface.
     if 'source_interface' in dict:
         # Check if source interface is member of another bridge
         tmp = is_member(config, dict['source_interface'], 'bridge')
         if tmp: dict.update({'source_interface_is_bridge_member' : tmp})
 
         # Check if source interface is member of another bridge
         tmp = is_member(config, dict['source_interface'], 'bonding')
         if tmp: dict.update({'source_interface_is_bond_member' : tmp})
 
     mac = leaf_node_changed(config, base + [ifname, 'mac'])
     if mac: dict.update({'mac_old' : mac})
 
     eui64 = leaf_node_changed(config, base + [ifname, 'ipv6', 'address', 'eui64'])
     if eui64:
         tmp = dict_search('ipv6.address', dict)
         if not tmp:
             dict.update({'ipv6': {'address': {'eui64_old': eui64}}})
         else:
             dict['ipv6']['address'].update({'eui64_old': eui64})
 
     for vif, vif_config in dict.get('vif', {}).items():
         # Add subinterface name to dictionary
         dict['vif'][vif].update({'ifname' : f'{ifname}.{vif}'})
 
         if config.exists(['qos', 'interface', f'{ifname}.{vif}']):
             dict['vif'][vif].update({'traffic_policy': {}})
 
         if 'deleted' not in dict:
             address = leaf_node_changed(config, base + [ifname, 'vif', vif, 'address'])
             if address: dict['vif'][vif].update({'address_old' : address})
 
             # If interface does not request an IPv4 DHCP address there is no need
             # to keep the dhcp-options key
             if 'address' not in dict['vif'][vif] or 'dhcp' not in dict['vif'][vif]['address']:
                 if 'dhcp_options' in dict['vif'][vif]:
                     del dict['vif'][vif]['dhcp_options']
 
         # Check if we are a member of a bridge device
         bridge = is_member(config, f'{ifname}.{vif}', 'bridge')
         if bridge: dict['vif'][vif].update({'is_bridge_member' : bridge})
 
         # Check if any DHCP options changed which require a client restat
         dhcp = is_node_changed(config, base + [ifname, 'vif', vif, 'dhcp-options'])
         if dhcp: dict['vif'][vif].update({'dhcp_options_changed' : {}})
 
     for vif_s, vif_s_config in dict.get('vif_s', {}).items():
         # Add subinterface name to dictionary
         dict['vif_s'][vif_s].update({'ifname' : f'{ifname}.{vif_s}'})
 
         if config.exists(['qos', 'interface', f'{ifname}.{vif_s}']):
             dict['vif_s'][vif_s].update({'traffic_policy': {}})
 
         if 'deleted' not in dict:
             address = leaf_node_changed(config, base + [ifname, 'vif-s', vif_s, 'address'])
             if address: dict['vif_s'][vif_s].update({'address_old' : address})
 
             # If interface does not request an IPv4 DHCP address there is no need
             # to keep the dhcp-options key
             if 'address' not in dict['vif_s'][vif_s] or 'dhcp' not in \
                 dict['vif_s'][vif_s]['address']:
                 if 'dhcp_options' in dict['vif_s'][vif_s]:
                     del dict['vif_s'][vif_s]['dhcp_options']
 
         # Check if we are a member of a bridge device
         bridge = is_member(config, f'{ifname}.{vif_s}', 'bridge')
         if bridge: dict['vif_s'][vif_s].update({'is_bridge_member' : bridge})
 
         # Check if any DHCP options changed which require a client restat
         dhcp = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'dhcp-options'])
         if dhcp: dict['vif_s'][vif_s].update({'dhcp_options_changed' : {}})
 
         for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items():
             # Add subinterface name to dictionary
             dict['vif_s'][vif_s]['vif_c'][vif_c].update({'ifname' : f'{ifname}.{vif_s}.{vif_c}'})
 
             if config.exists(['qos', 'interface', f'{ifname}.{vif_s}.{vif_c}']):
                 dict['vif_s'][vif_s]['vif_c'][vif_c].update({'traffic_policy': {}})
 
             if 'deleted' not in dict:
                 address = leaf_node_changed(config, base + [ifname, 'vif-s', vif_s, 'vif-c', vif_c, 'address'])
                 if address: dict['vif_s'][vif_s]['vif_c'][vif_c].update(
                         {'address_old' : address})
 
                 # If interface does not request an IPv4 DHCP address there is no need
                 # to keep the dhcp-options key
                 if 'address' not in dict['vif_s'][vif_s]['vif_c'][vif_c] or 'dhcp' \
                     not in dict['vif_s'][vif_s]['vif_c'][vif_c]['address']:
                     if 'dhcp_options' in dict['vif_s'][vif_s]['vif_c'][vif_c]:
                         del dict['vif_s'][vif_s]['vif_c'][vif_c]['dhcp_options']
 
             # Check if we are a member of a bridge device
             bridge = is_member(config, f'{ifname}.{vif_s}.{vif_c}', 'bridge')
             if bridge: dict['vif_s'][vif_s]['vif_c'][vif_c].update(
                 {'is_bridge_member' : bridge})
 
             # Check if any DHCP options changed which require a client restat
             dhcp = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'vif-c', vif_c, 'dhcp-options'])
             if dhcp: dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcp_options_changed' : {}})
 
     # Check vif, vif-s/vif-c VLAN interfaces for removal
     dict = get_removed_vlans(config, base + [ifname], dict)
     return ifname, dict
 
 def get_vlan_ids(interface):
     """
     Get the VLAN ID of the interface bound to the bridge
     """
     vlan_ids = set()
 
     bridge_status = cmd('bridge -j vlan show', shell=True)
     vlan_filter_status = json.loads(bridge_status)
 
     if vlan_filter_status is not None:
         for interface_status in vlan_filter_status:
             ifname = interface_status['ifname']
             if interface == ifname:
                 vlans_status = interface_status['vlans']
                 for vlan_status in vlans_status:
                     vlan_id = vlan_status['vlan']
                     vlan_ids.add(vlan_id)
 
     return vlan_ids
 
 def get_accel_dict(config, base, chap_secrets, with_pki=False):
     """
     Common utility function to retrieve and mangle the Accel-PPP configuration
     from different CLI input nodes. All Accel-PPP services have a common base
     where value retrival is identical. This function must be used whenever
     possible when working with Accel-PPP services!
 
     Return a dictionary with the necessary interface config keys.
     """
     from vyos.utils.cpu import get_core_count
     from vyos.template import is_ipv4
 
     dict = config.get_config_dict(base, key_mangling=('-', '_'),
                                   get_first_key=True,
                                   no_tag_node_value_mangle=True,
                                   with_recursive_defaults=True,
                                   with_pki=with_pki)
 
     # set CPUs cores to process requests
     dict.update({'thread_count' : get_core_count()})
     # we need to store the path to the secrets file
     dict.update({'chap_secrets_file' : chap_secrets})
 
     # We can only have two IPv4 and three IPv6 nameservers - also they are
     # configured in a different way in the configuration, this is why we split
     # the configuration
     if 'name_server' in dict:
         ns_v4 = []
         ns_v6 = []
         for ns in dict['name_server']:
             if is_ipv4(ns): ns_v4.append(ns)
             else: ns_v6.append(ns)
 
         dict.update({'name_server_ipv4' : ns_v4, 'name_server_ipv6' : ns_v6})
         del dict['name_server']
 
     # Check option "disable-accounting" per server and replace default value from '1813' to '0'
     for server in (dict_search('authentication.radius.server', dict) or []):
         if 'disable_accounting' in dict['authentication']['radius']['server'][server]:
             dict['authentication']['radius']['server'][server]['acct_port'] = '0'
 
     return dict
 
 def get_frrender_dict(conf, argv=None) -> dict:
     from copy import deepcopy
     from vyos.config import config_dict_merge
     from vyos.frrender import frr_protocols
 
     # Create an empty dictionary which will be filled down the code path and
     # returned to the caller
     dict = {}
 
     if argv and len(argv) > 1:
         dict['vrf_context'] = argv[1]
 
     def dict_helper_ospf_defaults(ospf, path):
         # We have gathered the dict representation of the CLI, but there are default
         # options which we need to update into the dictionary retrived.
         default_values = conf.get_config_defaults(path, key_mangling=('-', '_'),
                                                   get_first_key=True, recursive=True)
 
         # We have to cleanup the default dict, as default values could enable features
         # which are not explicitly enabled on the CLI. Example: default-information
         # originate comes with a default metric-type of 2, which will enable the
         # entire default-information originate tree, even when not set via CLI so we
         # need to check this first and probably drop that key.
         if dict_search('default_information.originate', ospf) is None:
             del default_values['default_information']
         if 'mpls_te' not in ospf:
             del default_values['mpls_te']
         if 'graceful_restart' not in ospf:
             del default_values['graceful_restart']
         for area_num in default_values.get('area', []):
             if dict_search(f'area.{area_num}.area_type.nssa', ospf) is None:
                 del default_values['area'][area_num]['area_type']['nssa']
 
         for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']:
             if dict_search(f'redistribute.{protocol}', ospf) is None:
                 del default_values['redistribute'][protocol]
         if not bool(default_values['redistribute']):
             del default_values['redistribute']
 
         for interface in ospf.get('interface', []):
             # We need to reload the defaults on every pass b/c of
             # hello-multiplier dependency on dead-interval
             # If hello-multiplier is set, we need to remove the default from
             # dead-interval.
             if 'hello_multiplier' in ospf['interface'][interface]:
                 del default_values['interface'][interface]['dead_interval']
 
         ospf = config_dict_merge(default_values, ospf)
         return ospf
 
     def dict_helper_ospfv3_defaults(ospfv3, path):
         # We have gathered the dict representation of the CLI, but there are default
         # options which we need to update into the dictionary retrived.
         default_values = conf.get_config_defaults(path, key_mangling=('-', '_'),
                                                   get_first_key=True, recursive=True)
 
         # We have to cleanup the default dict, as default values could enable features
         # which are not explicitly enabled on the CLI. Example: default-information
         # originate comes with a default metric-type of 2, which will enable the
         # entire default-information originate tree, even when not set via CLI so we
         # need to check this first and probably drop that key.
         if dict_search('default_information.originate', ospfv3) is None:
             del default_values['default_information']
         if 'graceful_restart' not in ospfv3:
             del default_values['graceful_restart']
 
         for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'ripng', 'static']:
             if dict_search(f'redistribute.{protocol}', ospfv3) is None:
                 del default_values['redistribute'][protocol]
         if not bool(default_values['redistribute']):
             del default_values['redistribute']
 
         default_values.pop('interface', {})
 
         # merge in remaining default values
         ospfv3 = config_dict_merge(default_values, ospfv3)
         return ospfv3
 
     def dict_helper_pim_defaults(pim, path):
         # We have gathered the dict representation of the CLI, but there are default
         # options which we need to update into the dictionary retrived.
         default_values = conf.get_config_defaults(path, key_mangling=('-', '_'),
                                                   get_first_key=True, recursive=True)
 
         # We have to cleanup the default dict, as default values could enable features
         # which are not explicitly enabled on the CLI.
         for interface in pim.get('interface', []):
             if 'igmp' not in pim['interface'][interface]:
                 del default_values['interface'][interface]['igmp']
 
         pim = config_dict_merge(default_values, pim)
         return pim
 
     # Ethernet and bonding interfaces can participate in EVPN which is configured via FRR
     tmp = {}
     for if_type in ['ethernet', 'bonding']:
         interface_path = ['interfaces', if_type]
         if not conf.exists(interface_path):
             continue
         for interface in conf.list_nodes(interface_path):
             evpn_path = interface_path + [interface, 'evpn']
             if not conf.exists(evpn_path):
                 continue
 
             evpn = conf.get_config_dict(evpn_path, key_mangling=('-', '_'))
             tmp.update({interface : evpn})
     # At least one participating EVPN interface found, add to result dict
     if tmp: dict['interfaces'] = tmp
 
+    # Zebra prefix exchange for Kernel IP/IPv6 and routing protocols
+    for ip_version in ['ip', 'ipv6']:
+        ip_cli_path = ['system', ip_version]
+        ip_dict = conf.get_config_dict(ip_cli_path, key_mangling=('-', '_'),
+                                        get_first_key=True, with_recursive_defaults=True)
+        if ip_dict:
+            ip_dict['afi'] = ip_version
+            dict.update({ip_version : ip_dict})
+
     # Enable SNMP agentx support
     # SNMP AgentX support cannot be disabled once enabled
     if conf.exists(['service', 'snmp']):
         dict['snmp'] = {}
 
     # We will always need the policy key
     dict['policy'] = conf.get_config_dict(['policy'], key_mangling=('-', '_'),
                                           get_first_key=True,
                                           no_tag_node_value_mangle=True)
 
     # We need to check the CLI if the BABEL node is present and thus load in all the default
     # values present on the CLI - that's why we have if conf.exists()
     babel_cli_path = ['protocols', 'babel']
     if conf.exists(babel_cli_path):
         babel = conf.get_config_dict(babel_cli_path, key_mangling=('-', '_'),
                                      get_first_key=True,
                                      with_recursive_defaults=True)
         dict.update({'babel' : babel})
 
     # We need to check the CLI if the BFD node is present and thus load in all the default
     # values present on the CLI - that's why we have if conf.exists()
     bfd_cli_path = ['protocols', 'bfd']
     if conf.exists(bfd_cli_path):
         bfd = conf.get_config_dict(bfd_cli_path, key_mangling=('-', '_'),
                                    get_first_key=True,
                                    no_tag_node_value_mangle=True,
                                    with_recursive_defaults=True)
         dict.update({'bfd' : bfd})
 
     # We need to check the CLI if the BGP node is present and thus load in all the default
     # values present on the CLI - that's why we have if conf.exists()
     bgp_cli_path = ['protocols', 'bgp']
     if conf.exists(bgp_cli_path):
         bgp = conf.get_config_dict(bgp_cli_path, key_mangling=('-', '_'),
                                    get_first_key=True,
                                    no_tag_node_value_mangle=True,
                                    with_recursive_defaults=True)
         bgp['dependent_vrfs'] = {}
         dict.update({'bgp' : bgp})
     elif conf.exists_effective(bgp_cli_path):
         dict.update({'bgp' : {'deleted' : '', 'dependent_vrfs' : {}}})
 
     # We need to check the CLI if the EIGRP node is present and thus load in all the default
     # values present on the CLI - that's why we have if conf.exists()
     eigrp_cli_path = ['protocols', 'eigrp']
     if conf.exists(eigrp_cli_path):
         isis = conf.get_config_dict(eigrp_cli_path, key_mangling=('-', '_'),
                                     get_first_key=True,
                                     no_tag_node_value_mangle=True,
                                     with_recursive_defaults=True)
         dict.update({'eigrp' : isis})
     elif conf.exists_effective(eigrp_cli_path):
         dict.update({'eigrp' : {'deleted' : ''}})
 
     # We need to check the CLI if the ISIS node is present and thus load in all the default
     # values present on the CLI - that's why we have if conf.exists()
     isis_cli_path = ['protocols', 'isis']
     if conf.exists(isis_cli_path):
         isis = conf.get_config_dict(isis_cli_path, key_mangling=('-', '_'),
                                     get_first_key=True,
                                     no_tag_node_value_mangle=True,
                                     with_recursive_defaults=True)
         dict.update({'isis' : isis})
     elif conf.exists_effective(isis_cli_path):
         dict.update({'isis' : {'deleted' : ''}})
 
     # We need to check the CLI if the MPLS node is present and thus load in all the default
     # values present on the CLI - that's why we have if conf.exists()
     mpls_cli_path = ['protocols', 'mpls']
     if conf.exists(mpls_cli_path):
         mpls = conf.get_config_dict(mpls_cli_path, key_mangling=('-', '_'),
                                     get_first_key=True)
         dict.update({'mpls' : mpls})
     elif conf.exists_effective(mpls_cli_path):
         dict.update({'mpls' : {'deleted' : ''}})
 
     # We need to check the CLI if the OPENFABRIC node is present and thus load in all the default
     # values present on the CLI - that's why we have if conf.exists()
     openfabric_cli_path = ['protocols', 'openfabric']
     if conf.exists(openfabric_cli_path):
         openfabric = conf.get_config_dict(openfabric_cli_path, key_mangling=('-', '_'),
                                           get_first_key=True,
                                           no_tag_node_value_mangle=True)
         dict.update({'openfabric' : openfabric})
     elif conf.exists_effective(openfabric_cli_path):
         dict.update({'openfabric' : {'deleted' : ''}})
 
     # We need to check the CLI if the OSPF node is present and thus load in all the default
     # values present on the CLI - that's why we have if conf.exists()
     ospf_cli_path = ['protocols', 'ospf']
     if conf.exists(ospf_cli_path):
         ospf = conf.get_config_dict(ospf_cli_path, key_mangling=('-', '_'),
                                     get_first_key=True)
         ospf = dict_helper_ospf_defaults(ospf, ospf_cli_path)
         dict.update({'ospf' : ospf})
     elif conf.exists_effective(ospf_cli_path):
         dict.update({'ospf' : {'deleted' : ''}})
 
     # We need to check the CLI if the OSPFv3 node is present and thus load in all the default
     # values present on the CLI - that's why we have if conf.exists()
     ospfv3_cli_path = ['protocols', 'ospfv3']
     if conf.exists(ospfv3_cli_path):
         ospfv3 = conf.get_config_dict(ospfv3_cli_path, key_mangling=('-', '_'),
                                       get_first_key=True)
         ospfv3 = dict_helper_ospfv3_defaults(ospfv3, ospfv3_cli_path)
         dict.update({'ospfv3' : ospfv3})
     elif conf.exists_effective(ospfv3_cli_path):
         dict.update({'ospfv3' : {'deleted' : ''}})
 
     # We need to check the CLI if the PIM node is present and thus load in all the default
     # values present on the CLI - that's why we have if conf.exists()
     pim_cli_path = ['protocols', 'pim']
     if conf.exists(pim_cli_path):
         pim = conf.get_config_dict(pim_cli_path, key_mangling=('-', '_'),
                                    get_first_key=True)
         pim = dict_helper_pim_defaults(pim, pim_cli_path)
         dict.update({'pim' : pim})
     elif conf.exists_effective(pim_cli_path):
         dict.update({'pim' : {'deleted' : ''}})
 
     # We need to check the CLI if the PIM6 node is present and thus load in all the default
     # values present on the CLI - that's why we have if conf.exists()
     pim6_cli_path = ['protocols', 'pim6']
     if conf.exists(pim6_cli_path):
         pim6 = conf.get_config_dict(pim6_cli_path, key_mangling=('-', '_'),
                                     get_first_key=True,
                                     with_recursive_defaults=True)
         dict.update({'pim6' : pim6})
     elif conf.exists_effective(pim6_cli_path):
         dict.update({'pim6' : {'deleted' : ''}})
 
     # We need to check the CLI if the RIP node is present and thus load in all the default
     # values present on the CLI - that's why we have if conf.exists()
     rip_cli_path = ['protocols', 'rip']
     if conf.exists(rip_cli_path):
         rip = conf.get_config_dict(rip_cli_path, key_mangling=('-', '_'),
                                    get_first_key=True,
                                    with_recursive_defaults=True)
         dict.update({'rip' : rip})
     elif conf.exists_effective(rip_cli_path):
         dict.update({'rip' : {'deleted' : ''}})
 
     # We need to check the CLI if the RIPng node is present and thus load in all the default
     # values present on the CLI - that's why we have if conf.exists()
     ripng_cli_path = ['protocols', 'ripng']
     if conf.exists(ripng_cli_path):
         ripng = conf.get_config_dict(ripng_cli_path, key_mangling=('-', '_'),
                                      get_first_key=True,
                                      with_recursive_defaults=True)
         dict.update({'ripng' : ripng})
     elif conf.exists_effective(ripng_cli_path):
         dict.update({'ripng' : {'deleted' : ''}})
 
     # We need to check the CLI if the RPKI node is present and thus load in all the default
     # values present on the CLI - that's why we have if conf.exists()
     rpki_cli_path = ['protocols', 'rpki']
     if conf.exists(rpki_cli_path):
         rpki = conf.get_config_dict(rpki_cli_path, key_mangling=('-', '_'),
                                      get_first_key=True, with_pki=True,
                                      with_recursive_defaults=True)
         dict.update({'rpki' : rpki})
     elif conf.exists_effective(rpki_cli_path):
         dict.update({'rpki' : {'deleted' : ''}})
 
     # We need to check the CLI if the Segment Routing node is present and thus load in
     # all the default values present on the CLI - that's why we have if conf.exists()
     sr_cli_path = ['protocols', 'segment-routing']
     if conf.exists(sr_cli_path):
         sr = conf.get_config_dict(sr_cli_path, key_mangling=('-', '_'),
                                   get_first_key=True,
                                   no_tag_node_value_mangle=True,
                                   with_recursive_defaults=True)
         dict.update({'segment_routing' : sr})
     elif conf.exists_effective(sr_cli_path):
         dict.update({'segment_routing' : {'deleted' : ''}})
 
     # We need to check the CLI if the static node is present and thus load in
     # all the default values present on the CLI - that's why we have if conf.exists()
     static_cli_path = ['protocols', 'static']
     if conf.exists(static_cli_path):
         static = conf.get_config_dict(static_cli_path, key_mangling=('-', '_'),
                                   get_first_key=True,
                                   no_tag_node_value_mangle=True)
 
         # T3680 - get a list of all interfaces currently configured to use DHCP
         tmp = get_dhcp_interfaces(conf)
         if tmp: static.update({'dhcp' : tmp})
         tmp = get_pppoe_interfaces(conf)
         if tmp: static.update({'pppoe' : tmp})
 
         dict.update({'static' : static})
     elif conf.exists_effective(static_cli_path):
         dict.update({'static' : {'deleted' : ''}})
 
     # keep a re-usable list of dependent VRFs
     dependent_vrfs_default = {}
     if 'bgp' in dict:
         dependent_vrfs_default = deepcopy(dict['bgp'])
         # we do not need to nest the 'dependent_vrfs' key - simply remove it
         if 'dependent_vrfs' in dependent_vrfs_default:
             del dependent_vrfs_default['dependent_vrfs']
 
     vrf_cli_path = ['vrf', 'name']
     if conf.exists(vrf_cli_path):
         vrf = conf.get_config_dict(vrf_cli_path, key_mangling=('-', '_'),
                                    get_first_key=False,
                                    no_tag_node_value_mangle=True)
         # We do not have any VRF related default values on the CLI. The defaults will only
         # come into place under the protocols tree, thus we can safely merge them with the
         # appropriate routing protocols
         for vrf_name, vrf_config in vrf['name'].items():
             bgp_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'bgp']
             if 'bgp' in vrf_config.get('protocols', []):
                 # We have gathered the dict representation of the CLI, but there are default
                 # options which we need to update into the dictionary retrived.
                 default_values = conf.get_config_defaults(bgp_vrf_path, key_mangling=('-', '_'),
                                                         get_first_key=True, recursive=True)
 
                 # merge in remaining default values
                 vrf_config['protocols']['bgp'] = config_dict_merge(default_values,
                                                                    vrf_config['protocols']['bgp'])
 
                 # Add this BGP VRF instance as dependency into the default VRF
                 if 'bgp' in dict:
                     dict['bgp']['dependent_vrfs'].update({vrf_name : deepcopy(vrf_config)})
 
                 vrf_config['protocols']['bgp']['dependent_vrfs'] = conf.get_config_dict(
                     vrf_cli_path, key_mangling=('-', '_'), get_first_key=True,
                     no_tag_node_value_mangle=True)
 
                 # We can safely delete ourself from the dependent VRF list
                 if vrf_name in vrf_config['protocols']['bgp']['dependent_vrfs']:
                     del vrf_config['protocols']['bgp']['dependent_vrfs'][vrf_name]
 
                 # Add dependency on possible existing default VRF to this VRF
                 if 'bgp' in dict:
                     vrf_config['protocols']['bgp']['dependent_vrfs'].update({'default': {'protocols': {
                         'bgp': dependent_vrfs_default}}})
             elif conf.exists_effective(bgp_vrf_path):
                 # Add this BGP VRF instance as dependency into the default VRF
                 tmp = {'deleted' : '', 'dependent_vrfs': deepcopy(vrf['name'])}
                 # We can safely delete ourself from the dependent VRF list
                 if vrf_name in tmp['dependent_vrfs']:
                     del tmp['dependent_vrfs'][vrf_name]
 
                 # Add dependency on possible existing default VRF to this VRF
                 if 'bgp' in dict:
                     tmp['dependent_vrfs'].update({'default': {'protocols': {
                         'bgp': dependent_vrfs_default}}})
 
                 if 'bgp' in dict:
                     dict['bgp']['dependent_vrfs'].update({vrf_name : {'protocols': tmp} })
 
                 if 'protocols' not in vrf['name'][vrf_name]:
                     vrf['name'][vrf_name].update({'protocols': {'bgp' : tmp}})
                 else:
                     vrf['name'][vrf_name]['protocols'].update({'bgp' : tmp})
 
             # We need to check the CLI if the EIGRP node is present and thus load in all the default
             # values present on the CLI - that's why we have if conf.exists()
             eigrp_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'eigrp']
             if 'eigrp' in vrf_config.get('protocols', []):
                 eigrp = conf.get_config_dict(eigrp_vrf_path, key_mangling=('-', '_'), get_first_key=True,
                                             no_tag_node_value_mangle=True)
                 vrf['name'][vrf_name]['protocols'].update({'eigrp' : isis})
             elif conf.exists_effective(eigrp_vrf_path):
                 vrf['name'][vrf_name]['protocols'].update({'eigrp' : {'deleted' : ''}})
 
             # We need to check the CLI if the ISIS node is present and thus load in all the default
             # values present on the CLI - that's why we have if conf.exists()
             isis_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'isis']
             if 'isis' in vrf_config.get('protocols', []):
                 isis = conf.get_config_dict(isis_vrf_path, key_mangling=('-', '_'), get_first_key=True,
                                             no_tag_node_value_mangle=True, with_recursive_defaults=True)
                 vrf['name'][vrf_name]['protocols'].update({'isis' : isis})
             elif conf.exists_effective(isis_vrf_path):
                 vrf['name'][vrf_name]['protocols'].update({'isis' : {'deleted' : ''}})
 
             # We need to check the CLI if the OSPF node is present and thus load in all the default
             # values present on the CLI - that's why we have if conf.exists()
             ospf_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'ospf']
             if 'ospf' in vrf_config.get('protocols', []):
                 ospf = conf.get_config_dict(ospf_vrf_path, key_mangling=('-', '_'), get_first_key=True)
                 ospf = dict_helper_ospf_defaults(vrf_config['protocols']['ospf'], ospf_vrf_path)
                 vrf['name'][vrf_name]['protocols'].update({'ospf' : ospf})
             elif conf.exists_effective(ospf_vrf_path):
                 vrf['name'][vrf_name]['protocols'].update({'ospf' : {'deleted' : ''}})
 
             # We need to check the CLI if the OSPFv3 node is present and thus load in all the default
             # values present on the CLI - that's why we have if conf.exists()
             ospfv3_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'ospfv3']
             if 'ospfv3' in vrf_config.get('protocols', []):
                 ospfv3 = conf.get_config_dict(ospfv3_vrf_path, key_mangling=('-', '_'), get_first_key=True)
                 ospfv3 = dict_helper_ospfv3_defaults(vrf_config['protocols']['ospfv3'], ospfv3_vrf_path)
                 vrf['name'][vrf_name]['protocols'].update({'ospfv3' : ospfv3})
             elif conf.exists_effective(ospfv3_vrf_path):
                 vrf['name'][vrf_name]['protocols'].update({'ospfv3' : {'deleted' : ''}})
 
             # We need to check the CLI if the static node is present and thus load in all the default
             # values present on the CLI - that's why we have if conf.exists()
             static_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'static']
             if 'static' in vrf_config.get('protocols', []):
                 static = conf.get_config_dict(static_vrf_path, key_mangling=('-', '_'),
                                               get_first_key=True,
                                               no_tag_node_value_mangle=True)
                 # T3680 - get a list of all interfaces currently configured to use DHCP
                 tmp = get_dhcp_interfaces(conf, vrf_name)
                 if tmp: static.update({'dhcp' : tmp})
                 tmp = get_pppoe_interfaces(conf, vrf_name)
                 if tmp: static.update({'pppoe' : tmp})
 
                 vrf['name'][vrf_name]['protocols'].update({'static': static})
             elif conf.exists_effective(static_vrf_path):
                 vrf['name'][vrf_name]['protocols'].update({'static': {'deleted' : ''}})
 
             vrf_vni_path = ['vrf', 'name', vrf_name, 'vni']
             if conf.exists_effective(vrf_vni_path):
                 vrf_config.update({'vni': conf.return_effective_value(vrf_vni_path)})
 
             dict.update({'vrf' : vrf})
     elif conf.exists_effective(vrf_cli_path):
         effective_vrf = conf.get_config_dict(vrf_cli_path, key_mangling=('-', '_'),
                                              get_first_key=False,
                                              no_tag_node_value_mangle=True,
                                              effective=True)
         vrf = {'name' : {}}
         for vrf_name, vrf_config in effective_vrf.get('name', {}).items():
             vrf['name'].update({vrf_name : {}})
             for protocol in frr_protocols:
                 if protocol in vrf_config.get('protocols', []):
                     # Create initial protocols key if not present
                     if 'protocols' not in vrf['name'][vrf_name]:
                         vrf['name'][vrf_name].update({'protocols' : {}})
                     # All routing protocols are deleted when we pass this point
                     tmp = {'deleted' : ''}
 
                     # Special treatment for BGP routing protocol
                     if protocol == 'bgp':
                         tmp['dependent_vrfs'] = {}
                         if 'name' in vrf:
                             tmp['dependent_vrfs'] = conf.get_config_dict(
                                 vrf_cli_path, key_mangling=('-', '_'),
                                 get_first_key=True, no_tag_node_value_mangle=True,
                                 effective=True)
                         # Add dependency on possible existing default VRF to this VRF
                         if 'bgp' in dict:
                             tmp['dependent_vrfs'].update({'default': {'protocols': {
                                 'bgp': dependent_vrfs_default}}})
                         # We can safely delete ourself from the dependent VRF list
                         if vrf_name in tmp['dependent_vrfs']:
                             del tmp['dependent_vrfs'][vrf_name]
 
                     # Update VRF related dict
                     vrf['name'][vrf_name]['protocols'].update({protocol : tmp})
 
         dict.update({'vrf' : vrf})
 
     if os.path.exists(frr_debug_enable):
         print('======== < BEGIN > ==========')
         import pprint
         pprint.pprint(dict)
         print('========= < END > ===========')
 
     # Use singleton instance of the FRR render class
     if hasattr(conf, 'frrender_cls'):
         frrender = getattr(conf, 'frrender_cls')
         dict.update({'frrender_cls' : frrender})
         frrender.generate(dict)
 
     return dict
diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py
index f1bb39094..7a0b661a3 100644
--- a/python/vyos/frrender.py
+++ b/python/vyos/frrender.py
@@ -1,157 +1,167 @@
 # Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
 # License as published by the Free Software Foundation; either
 # version 2.1 of the License, or (at your option) any later version.
 #
 # This library is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public License
 # along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
 """
 Library used to interface with FRRs mgmtd introduced in version 10.0
 """
 
 import os
 
 from vyos.defaults import frr_debug_enable
 from vyos.utils.file import write_file
 from vyos.utils.process import rc_cmd
 from vyos.template import render_to_string
 from vyos import ConfigError
 
 DEBUG_ON = os.path.exists(frr_debug_enable)
 
 def debug(message):
     if not DEBUG_ON:
         return
     print(message)
 
-pim_daemon = 'pimd'
-
 frr_protocols = ['babel', 'bfd', 'bgp', 'eigrp', 'isis', 'mpls', 'nhrp',
                  'openfabric', 'ospf', 'ospfv3', 'pim', 'pim6', 'rip',
                  'ripng', 'rpki', 'segment_routing', 'static']
 
+bgp_daemon = 'bgpd'
+isis_daemon = 'isisd'
+mgmt_daemon = 'mgmtd'
+pim_daemon = 'pimd'
+zebra_daemon = 'zebra'
+
 class FRRender:
     def __init__(self):
         self._frr_conf = '/run/frr/config/frr.conf'
 
     def generate(self, config):
         if not isinstance(config, dict):
             tmp = type(config)
             raise ValueError(f'Config must be of type "dict" and not "{tmp}"!')
 
         def inline_helper(config_dict) -> str:
             output = '!\n'
             if 'babel' in config_dict and 'deleted' not in config_dict['babel']:
                 output += render_to_string('frr/babeld.frr.j2', config_dict['babel'])
                 output += '\n'
             if 'bfd' in config_dict and 'deleted' not in config_dict['bfd']:
                 output += render_to_string('frr/bfdd.frr.j2', config_dict['bfd'])
                 output += '\n'
             if 'bgp' in config_dict and 'deleted' not in config_dict['bgp']:
                 output += render_to_string('frr/bgpd.frr.j2', config_dict['bgp'])
                 output += '\n'
             if 'eigrp' in config_dict and 'deleted' not in config_dict['eigrp']:
                 output += render_to_string('frr/eigrpd.frr.j2', config_dict['eigrp'])
                 output += '\n'
             if 'isis' in config_dict and 'deleted' not in config_dict['isis']:
                 output += render_to_string('frr/isisd.frr.j2', config_dict['isis'])
                 output += '\n'
             if 'mpls' in config_dict and 'deleted' not in config_dict['mpls']:
                 output += render_to_string('frr/ldpd.frr.j2', config_dict['mpls'])
                 output += '\n'
             if 'openfabric' in config_dict and 'deleted' not in config_dict['openfabric']:
                 output += render_to_string('frr/fabricd.frr.j2', config_dict['openfabric'])
                 output += '\n'
             if 'ospf' in config_dict and 'deleted' not in config_dict['ospf']:
                 output += render_to_string('frr/ospfd.frr.j2', config_dict['ospf'])
                 output += '\n'
             if 'ospfv3' in config_dict and 'deleted' not in config_dict['ospfv3']:
                 output += render_to_string('frr/ospf6d.frr.j2', config_dict['ospfv3'])
                 output += '\n'
             if 'pim' in config_dict and 'deleted' not in config_dict['pim']:
                 output += render_to_string('frr/pimd.frr.j2', config_dict['pim'])
                 output += '\n'
             if 'pim6' in config_dict and 'deleted' not in config_dict['pim6']:
                 output += render_to_string('frr/pim6d.frr.j2', config_dict['pim6'])
                 output += '\n'
             if 'policy' in config_dict and len(config_dict['policy']) > 0:
                 output += render_to_string('frr/policy.frr.j2', config_dict['policy'])
                 output += '\n'
             if 'rip' in config_dict and 'deleted' not in config_dict['rip']:
                 output += render_to_string('frr/ripd.frr.j2', config_dict['rip'])
                 output += '\n'
             if 'ripng' in config_dict and 'deleted' not in config_dict['ripng']:
                 output += render_to_string('frr/ripngd.frr.j2', config_dict['ripng'])
                 output += '\n'
             if 'rpki' in config_dict and 'deleted' not in config_dict['rpki']:
                 output += render_to_string('frr/rpki.frr.j2', config_dict['rpki'])
                 output += '\n'
             if 'segment_routing' in config_dict and 'deleted' not in config_dict['segment_routing']:
                 output += render_to_string('frr/zebra.segment_routing.frr.j2', config_dict['segment_routing'])
                 output += '\n'
             if 'static' in config_dict and 'deleted' not in config_dict['static']:
                 output += render_to_string('frr/staticd.frr.j2', config_dict['static'])
                 output += '\n'
+            if 'ip' in config_dict and 'deleted' not in config_dict['ip']:
+                output += render_to_string('frr/zebra.route-map.frr.j2', config_dict['ip'])
+                output += '\n'
+            if 'ipv6' in config_dict and 'deleted' not in config_dict['ipv6']:
+                output += render_to_string('frr/zebra.route-map.frr.j2', config_dict['ipv6'])
+                output += '\n'
             return output
 
         debug('======< RENDERING CONFIG >======')
         # we can not reload an empty file, thus we always embed the marker
         output = '!\n'
         # Enable SNMP agentx support
         # SNMP AgentX support cannot be disabled once enabled
         if 'snmp' in config:
             output += 'agentx\n'
         # Add routing protocols in global VRF
         output += inline_helper(config)
         # Interface configuration for EVPN is not VRF related
         if 'interfaces' in config:
             output += render_to_string('frr/evpn.mh.frr.j2', {'interfaces' : config['interfaces']})
             output += '\n'
 
         if 'vrf' in config and 'name' in config['vrf']:
             output += render_to_string('frr/zebra.vrf.route-map.frr.j2', config['vrf']) + '\n'
             for vrf, vrf_config in config['vrf']['name'].items():
                 if 'protocols' not in vrf_config:
                     continue
                 for protocol in vrf_config['protocols']:
                     vrf_config['protocols'][protocol]['vrf'] = vrf
 
                 output += inline_helper(vrf_config['protocols'])
 
         debug(output)
         debug('======< RENDERING CONFIG COMPLETE >======')
         write_file(self._frr_conf, output)
         if DEBUG_ON: write_file('/tmp/frr.conf.debug', output)
 
     def apply(self):
         count = 0
         count_max = 5
         emsg = ''
         while count < count_max:
             count += 1
             debug(f'FRR: Reloading configuration - tries: {count} | Python class ID: {id(self)}')
 
             cmdline = '/usr/lib/frr/frr-reload.py --reload'
             if DEBUG_ON:
                 cmdline += ' --debug'
             rc, emsg = rc_cmd(f'{cmdline} {self._frr_conf}')
             if rc != 0:
                 debug('FRR configuration reload failed, retrying')
                 continue
             debug(emsg)
             debug('======< DONE APPLYING CONFIG  >======')
             break
         if count >= count_max:
             raise ConfigError(emsg)
 
     def save_configuration():
         """ T3217: Save FRR configuration to /run/frr/config/frr.conf """
         return cmd('/usr/bin/vtysh -n --writeconfig')
diff --git a/smoketest/scripts/cli/test_system_ip.py b/smoketest/scripts/cli/test_system_ip.py
index 4ab5e8181..7d730f7b2 100755
--- a/smoketest/scripts/cli/test_system_ip.py
+++ b/smoketest/scripts/cli/test_system_ip.py
@@ -1,127 +1,134 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2020-2024 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import unittest
 from base_vyostest_shim import VyOSUnitTestSHIM
 
 from vyos.configsession import ConfigSessionError
-from vyos.utils.file import read_file
-from vyos.frr import mgmt_daemon
+from vyos.utils.system import sysctl_read
+from vyos.xml_ref import default_value
+from vyos.frrender import mgmt_daemon
+from vyos.frrender import zebra_daemon
 
 base_path = ['system', 'ip']
 
 class TestSystemIP(VyOSUnitTestSHIM.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        super(TestSystemIP, cls).setUpClass()
+        # ensure we can also run this test on a live system - so lets clean
+        # out the current configuration :)
+        cls.cli_delete(cls, base_path)
+
     def tearDown(self):
         self.cli_delete(base_path)
         self.cli_commit()
 
     def test_system_ip_forwarding(self):
         # Test if IPv4 forwarding can be disabled globally, default is '1'
         # which means forwarding enabled
-        all_forwarding = '/proc/sys/net/ipv4/conf/all/forwarding'
-        self.assertEqual(read_file(all_forwarding), '1')
+        self.assertEqual(sysctl_read('net.ipv4.conf.all.forwarding'), '1')
 
         self.cli_set(base_path + ['disable-forwarding'])
         self.cli_commit()
+        self.assertEqual(sysctl_read('net.ipv4.conf.all.forwarding'), '0')
+        frrconfig = self.getFRRconfig('', end='', daemon=zebra_daemon)
+        self.assertIn('no ip forwarding', frrconfig)
 
-        self.assertEqual(read_file(all_forwarding), '0')
+        self.cli_delete(base_path + ['disable-forwarding'])
+        self.cli_commit()
+        self.assertEqual(sysctl_read('net.ipv4.conf.all.forwarding'), '1')
+        frrconfig = self.getFRRconfig('', end='', daemon=zebra_daemon)
+        self.assertNotIn('no ip forwarding', frrconfig)
 
     def test_system_ip_multipath(self):
         # Test IPv4 multipathing options, options default to off -> '0'
-        use_neigh = '/proc/sys/net/ipv4/fib_multipath_use_neigh'
-        hash_policy = '/proc/sys/net/ipv4/fib_multipath_hash_policy'
-
-        self.assertEqual(read_file(use_neigh), '0')
-        self.assertEqual(read_file(hash_policy), '0')
+        self.assertEqual(sysctl_read('net.ipv4.fib_multipath_use_neigh'), '0')
+        self.assertEqual(sysctl_read('net.ipv4.fib_multipath_hash_policy'), '0')
 
         self.cli_set(base_path + ['multipath', 'ignore-unreachable-nexthops'])
         self.cli_set(base_path + ['multipath', 'layer4-hashing'])
         self.cli_commit()
 
-        self.assertEqual(read_file(use_neigh), '1')
-        self.assertEqual(read_file(hash_policy), '1')
+        self.assertEqual(sysctl_read('net.ipv4.fib_multipath_use_neigh'), '1')
+        self.assertEqual(sysctl_read('net.ipv4.fib_multipath_hash_policy'), '1')
 
     def test_system_ip_arp_table_size(self):
-        # Maximum number of entries to keep in the ARP cache, the
-        # default is 8k
+        cli_default = int(default_value(base_path + ['arp', 'table-size']))
+        def _verify_gc_thres(table_size):
+            self.assertEqual(sysctl_read('net.ipv4.neigh.default.gc_thresh3'), str(table_size))
+            self.assertEqual(sysctl_read('net.ipv4.neigh.default.gc_thresh2'), str(table_size // 2))
+            self.assertEqual(sysctl_read('net.ipv4.neigh.default.gc_thresh1'), str(table_size // 8))
 
-        gc_thresh3 = '/proc/sys/net/ipv4/neigh/default/gc_thresh3'
-        gc_thresh2 = '/proc/sys/net/ipv4/neigh/default/gc_thresh2'
-        gc_thresh1 = '/proc/sys/net/ipv4/neigh/default/gc_thresh1'
-        self.assertEqual(read_file(gc_thresh3), '8192')
-        self.assertEqual(read_file(gc_thresh2), '4096')
-        self.assertEqual(read_file(gc_thresh1), '1024')
+        _verify_gc_thres(cli_default)
 
         for size in [1024, 2048, 4096, 8192, 16384, 32768]:
             self.cli_set(base_path + ['arp', 'table-size', str(size)])
             self.cli_commit()
-
-            self.assertEqual(read_file(gc_thresh3), str(size))
-            self.assertEqual(read_file(gc_thresh2), str(size // 2))
-            self.assertEqual(read_file(gc_thresh1), str(size // 8))
+            _verify_gc_thres(size)
 
     def test_system_ip_protocol_route_map(self):
         protocols = ['any', 'babel', 'bgp', 'connected', 'eigrp', 'isis',
                      'kernel', 'ospf', 'rip', 'static', 'table']
 
         for protocol in protocols:
             self.cli_set(['policy', 'route-map', f'route-map-{protocol}', 'rule', '10', 'action', 'permit'])
             self.cli_set(base_path + ['protocol', protocol, 'route-map', f'route-map-{protocol}'])
 
         self.cli_commit()
 
         # Verify route-map properly applied to FRR
         frrconfig = self.getFRRconfig('ip protocol', end='', daemon=mgmt_daemon)
         for protocol in protocols:
             self.assertIn(f'ip protocol {protocol} route-map route-map-{protocol}', frrconfig)
 
         # Delete route-maps
         self.cli_delete(['policy', 'route-map'])
         self.cli_delete(base_path + ['protocol'])
 
         self.cli_commit()
 
         # Verify route-map properly applied to FRR
         frrconfig = self.getFRRconfig('ip protocol', end='', daemon=mgmt_daemon)
         self.assertNotIn(f'ip protocol', frrconfig)
 
     def test_system_ip_protocol_non_existing_route_map(self):
         non_existing = 'non-existing'
         self.cli_set(base_path + ['protocol', 'static', 'route-map', non_existing])
 
         # VRF does yet not exist - an error must be thrown
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
         self.cli_set(['policy', 'route-map', non_existing, 'rule', '10', 'action', 'deny'])
 
         # Commit again
         self.cli_commit()
 
     def test_system_ip_nht(self):
         self.cli_set(base_path + ['nht', 'no-resolve-via-default'])
         self.cli_commit()
         # Verify CLI config applied to FRR
         frrconfig = self.getFRRconfig('', end='', daemon=mgmt_daemon)
         self.assertIn(f'no ip nht resolve-via-default', frrconfig)
 
         self.cli_delete(base_path + ['nht', 'no-resolve-via-default'])
         self.cli_commit()
         # Verify CLI config removed to FRR
         frrconfig = self.getFRRconfig('', end='', daemon=mgmt_daemon)
         self.assertNotIn(f'no ip nht resolve-via-default', frrconfig)
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_ipv6.py b/smoketest/scripts/cli/test_system_ipv6.py
index 1c778a11d..be9751c4d 100755
--- a/smoketest/scripts/cli/test_system_ipv6.py
+++ b/smoketest/scripts/cli/test_system_ipv6.py
@@ -1,146 +1,155 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2021-2024 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import unittest
 
 from base_vyostest_shim import VyOSUnitTestSHIM
 
 from vyos.configsession import ConfigSessionError
-from vyos.utils.file import read_file
-from vyos.frr import mgmt_daemon
+from vyos.utils.system import sysctl_read
+from vyos.xml_ref import default_value
+from vyos.frrender import mgmt_daemon
+from vyos.frrender import zebra_daemon
 
 base_path = ['system', 'ipv6']
 
-file_forwarding = '/proc/sys/net/ipv6/conf/all/forwarding'
-file_disable = '/proc/sys/net/ipv6/conf/all/disable_ipv6'
-file_dad = '/proc/sys/net/ipv6/conf/all/accept_dad'
-file_multipath = '/proc/sys/net/ipv6/fib_multipath_hash_policy'
-
 class TestSystemIPv6(VyOSUnitTestSHIM.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        super(TestSystemIPv6, cls).setUpClass()
+        # ensure we can also run this test on a live system - so lets clean
+        # out the current configuration :)
+        cls.cli_delete(cls, base_path)
+
     def tearDown(self):
         self.cli_delete(base_path)
         self.cli_commit()
 
     def test_system_ipv6_forwarding(self):
         # Test if IPv6 forwarding can be disabled globally, default is '1'
         # which means forwearding enabled
-        self.assertEqual(read_file(file_forwarding), '1')
+        self.assertEqual(sysctl_read('net.ipv6.conf.all.forwarding'), '1')
 
         self.cli_set(base_path + ['disable-forwarding'])
         self.cli_commit()
+        self.assertEqual(sysctl_read('net.ipv6.conf.all.forwarding'), '0')
+        frrconfig = self.getFRRconfig('', end='', daemon=zebra_daemon)
+        self.assertIn('no ipv6 forwarding', frrconfig)
 
-        self.assertEqual(read_file(file_forwarding), '0')
+        self.cli_delete(base_path + ['disable-forwarding'])
+        self.cli_commit()
+        self.assertEqual(sysctl_read('net.ipv6.conf.all.forwarding'), '1')
+        frrconfig = self.getFRRconfig('', end='', daemon=zebra_daemon)
+        self.assertNotIn('no ipv6 forwarding', frrconfig)
 
     def test_system_ipv6_strict_dad(self):
         # This defaults to 1
-        self.assertEqual(read_file(file_dad), '1')
+        self.assertEqual(sysctl_read('net.ipv6.conf.all.accept_dad'), '1')
 
         # Do not assign any IPv6 address on interfaces, this requires a reboot
         # which can not be tested, but we can read the config file :)
         self.cli_set(base_path + ['strict-dad'])
         self.cli_commit()
 
         # Verify configuration file
-        self.assertEqual(read_file(file_dad), '2')
+        self.assertEqual(sysctl_read('net.ipv6.conf.all.accept_dad'), '2')
 
     def test_system_ipv6_multipath(self):
         # This defaults to 0
-        self.assertEqual(read_file(file_multipath), '0')
+        self.assertEqual(sysctl_read('net.ipv6.fib_multipath_hash_policy'), '0')
 
         # Do not assign any IPv6 address on interfaces, this requires a reboot
         # which can not be tested, but we can read the config file :)
         self.cli_set(base_path + ['multipath', 'layer4-hashing'])
         self.cli_commit()
 
         # Verify configuration file
-        self.assertEqual(read_file(file_multipath), '1')
+        self.assertEqual(sysctl_read('net.ipv6.fib_multipath_hash_policy'), '1')
 
     def test_system_ipv6_neighbor_table_size(self):
         # Maximum number of entries to keep in the ARP cache, the
         # default is 8192
+        cli_default = int(default_value(base_path + ['neighbor', 'table-size']))
 
-        gc_thresh3 = '/proc/sys/net/ipv6/neigh/default/gc_thresh3'
-        gc_thresh2 = '/proc/sys/net/ipv6/neigh/default/gc_thresh2'
-        gc_thresh1 = '/proc/sys/net/ipv6/neigh/default/gc_thresh1'
-        self.assertEqual(read_file(gc_thresh3), '8192')
-        self.assertEqual(read_file(gc_thresh2), '4096')
-        self.assertEqual(read_file(gc_thresh1), '1024')
+        def _verify_gc_thres(table_size):
+            self.assertEqual(sysctl_read('net.ipv6.neigh.default.gc_thresh3'), str(table_size))
+            self.assertEqual(sysctl_read('net.ipv6.neigh.default.gc_thresh2'), str(table_size // 2))
+            self.assertEqual(sysctl_read('net.ipv6.neigh.default.gc_thresh1'), str(table_size // 8))
+
+        _verify_gc_thres(cli_default)
 
         for size in [1024, 2048, 4096, 8192, 16384, 32768]:
             self.cli_set(base_path + ['neighbor', 'table-size', str(size)])
             self.cli_commit()
-
-            self.assertEqual(read_file(gc_thresh3), str(size))
-            self.assertEqual(read_file(gc_thresh2), str(size // 2))
-            self.assertEqual(read_file(gc_thresh1), str(size // 8))
+            _verify_gc_thres(size)
 
     def test_system_ipv6_protocol_route_map(self):
         protocols = ['any', 'babel', 'bgp', 'connected', 'isis',
                      'kernel', 'ospfv3', 'ripng', 'static', 'table']
 
         for protocol in protocols:
             route_map = 'route-map-' + protocol.replace('ospfv3', 'ospf6')
 
             self.cli_set(['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit'])
             self.cli_set(base_path + ['protocol', protocol, 'route-map', route_map])
 
         self.cli_commit()
 
         # Verify route-map properly applied to FRR
         frrconfig = self.getFRRconfig('ipv6 protocol', end='', daemon=mgmt_daemon)
         for protocol in protocols:
             # VyOS and FRR use a different name for OSPFv3 (IPv6)
             if protocol == 'ospfv3':
                 protocol = 'ospf6'
             self.assertIn(f'ipv6 protocol {protocol} route-map route-map-{protocol}', frrconfig)
 
         # Delete route-maps
         self.cli_delete(['policy', 'route-map'])
         self.cli_delete(base_path + ['protocol'])
 
         self.cli_commit()
 
         # Verify route-map properly applied to FRR
         frrconfig = self.getFRRconfig('ipv6 protocol', end='', daemon=mgmt_daemon)
         self.assertNotIn(f'ipv6 protocol', frrconfig)
 
     def test_system_ipv6_protocol_non_existing_route_map(self):
         non_existing = 'non-existing6'
         self.cli_set(base_path + ['protocol', 'static', 'route-map', non_existing])
 
         # VRF does yet not exist - an error must be thrown
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
         self.cli_set(['policy', 'route-map', non_existing, 'rule', '10', 'action', 'deny'])
 
         # Commit again
         self.cli_commit()
 
     def test_system_ipv6_nht(self):
         self.cli_set(base_path + ['nht', 'no-resolve-via-default'])
         self.cli_commit()
         # Verify CLI config applied to FRR
         frrconfig = self.getFRRconfig('', end='', daemon=mgmt_daemon)
         self.assertIn(f'no ipv6 nht resolve-via-default', frrconfig)
 
         self.cli_delete(base_path + ['nht', 'no-resolve-via-default'])
         self.cli_commit()
         # Verify CLI config removed to FRR
         frrconfig = self.getFRRconfig('', end='', daemon=mgmt_daemon)
         self.assertNotIn(f'no ipv6 nht resolve-via-default', frrconfig)
 
 if __name__ == '__main__':
-    unittest.main(verbosity=2)
+    unittest.main(verbosity=2, failfast=True)
diff --git a/src/conf_mode/system_ip.py b/src/conf_mode/system_ip.py
index 5afb57404..374e6e611 100755
--- a/src/conf_mode/system_ip.py
+++ b/src/conf_mode/system_ip.py
@@ -1,145 +1,126 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2019-2024 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from sys import exit
 
 from vyos.config import Config
-from vyos.configdict import dict_merge
+from vyos.configdep import set_dependents
+from vyos.configdep import call_dependents
+from vyos.configdict import get_frrender_dict
+from vyos.configverify import has_frr_protocol_in_dict
 from vyos.configverify import verify_route_map
-from vyos.template import render_to_string
+from vyos.frrender import FRRender
 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.configdep import set_dependents
-from vyos.configdep import call_dependents
 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)
 
     # If IPv4 ARP table size is set here and also manually in sysctl, the more
     # fine grained value from sysctl must win
     set_dependents('sysctl', conf)
+    return get_frrender_dict(conf)
 
-    return opt
+def verify(config_dict):
+    if not has_frr_protocol_in_dict(config_dict, 'ip'):
+        return None
+
+    opt = config_dict['ip']
+    opt['policy'] = config_dict['policy']
 
-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 generate(config_dict):
+    if config_dict and 'frrender_cls' not in config_dict:
+        FRRender().generate(config_dict)
+    return None
+
+def apply(config_dict):
+    if not has_frr_protocol_in_dict(config_dict, 'ip'):
+
+        return None
+    opt = config_dict['ip']
 
-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)
-
     # 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'):
-        # 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(frr.mgmt_daemon)
-        frr_cfg.modify_section(r'no ip nht resolve-via-default')
-        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()
+        if config_dict and 'frrender_cls' not in config_dict:
+            FRRender().apply()
 
     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/system_ipv6.py b/src/conf_mode/system_ipv6.py
index 90d5100d7..02c9a8201 100755
--- a/src/conf_mode/system_ipv6.py
+++ b/src/conf_mode/system_ipv6.py
@@ -1,127 +1,110 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2019-2024 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
 
 from sys import exit
 from vyos.config import Config
-from vyos.configdict import dict_merge
+from vyos.configdep import set_dependents
+from vyos.configdep import call_dependents
+from vyos.configdict import get_frrender_dict
+from vyos.configverify import has_frr_protocol_in_dict
 from vyos.configverify import verify_route_map
-from vyos.template import render_to_string
+from vyos.frrender import FRRender
 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.configdep import set_dependents
-from vyos.configdep import call_dependents
 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)
 
     # If IPv6 neighbor table size is set here and also manually in sysctl, the more
     # fine grained value from sysctl must win
     set_dependents('sysctl', conf)
+    return get_frrender_dict(conf)
+
+def verify(config_dict):
+    if not has_frr_protocol_in_dict(config_dict, 'ipv6'):
+        return None
 
-    return opt
+    opt = config_dict['ipv6']
+    opt['policy'] = config_dict['policy']
 
-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 generate(config_dict):
+    if config_dict and 'frrender_cls' not in config_dict:
+        FRRender().generate(config_dict)
+    return None
+
+def apply(config_dict):
+    if not has_frr_protocol_in_dict(config_dict, 'ipv6'):
+        return None
+    opt = config_dict['ipv6']
 
-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'):
-        # Save original configuration prior to starting any commit actions
-        frr_cfg = frr.FRRConfig()
-        frr_cfg.load_configuration(frr.mgmt_daemon)
-        frr_cfg.modify_section(r'no ipv6 nht resolve-via-default')
-        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()
+        if config_dict and 'frrender_cls' not in config_dict:
+            FRRender().apply()
 
     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)