diff --git a/data/templates/frr/staticd.frr.j2 b/data/templates/frr/staticd.frr.j2
index cf8448f7f..589f03c2c 100644
--- a/data/templates/frr/staticd.frr.j2
+++ b/data/templates/frr/staticd.frr.j2
@@ -1,63 +1,64 @@
 {% from 'frr/static_routes_macro.j2' import static_routes %}
 !
 {% set ip_prefix = 'ip' %}
 {% set ipv6_prefix = 'ipv6' %}
 {% if vrf is vyos_defined %}
 {#     We need to add an additional whitespace in front of the prefix #}
 {#     when VRFs are in use, thus we use a variable for prefix handling #}
 {%     set ip_prefix = ' ip' %}
 {%     set ipv6_prefix = ' ipv6' %}
 vrf {{ vrf }}
 {% endif %}
 {# IPv4 routing #}
 {% if route is vyos_defined %}
 {%     for prefix, prefix_config in route.items() %}
 {{ static_routes(ip_prefix, prefix, prefix_config) }}
 {%     endfor %}
 {% endif %}
 {# IPv4 default routes from DHCP interfaces #}
 {% if dhcp is vyos_defined %}
 {%     for interface, interface_config in dhcp.items() %}
-{#         PPPoE routes behave a bit different ... #}
-{%         if interface.startswith('pppoe') and interface_config.default_route is vyos_defined and interface_config.default_route is not vyos_defined('none') %}
-{{ ip_prefix }} route 0.0.0.0/0 {{ interface }} tag 210
-{%         else %}
-{%             set next_hop = interface | get_dhcp_router %}
-{%             if next_hop is vyos_defined %}
-{{ ip_prefix }} route 0.0.0.0/0 {{ next_hop }} {{ interface }} tag 210 {{ interface_config.distance if interface_config.distance is vyos_defined }}
-{%             endif %}
+{%         set next_hop = interface | get_dhcp_router %}
+{%         if next_hop is vyos_defined %}
+{{ ip_prefix }} route 0.0.0.0/0 {{ next_hop }} {{ interface }} tag 210 {{ interface_config.dhcp_options.default_route_distance if interface_config.dhcp_options.default_route_distance is vyos_defined }}
 {%         endif %}
 {%     endfor %}
 {% endif %}
+{# IPv4 default routes from PPPoE interfaces #}
+{% if pppoe is vyos_defined %}
+{%     for interface, interface_config in pppoe.items() %}
+{{ ip_prefix }} route 0.0.0.0/0 {{ interface }} tag 210 {{ interface_config.default_route_distance if interface_config.default_route_distance is vyos_defined }}
+{%     endfor %}
+{% endif %}
 {# IPv6 routing #}
 {% if route6 is vyos_defined %}
 {%     for prefix, prefix_config in route6.items() %}
 {{ static_routes(ipv6_prefix, prefix, prefix_config) }}
 {%     endfor %}
 {% endif %}
 {% if vrf is vyos_defined %}
  exit-vrf
 {% endif %}
 !
 {# Policy route tables #}
 {% if table is vyos_defined %}
 {%     for table_id, table_config in table.items() %}
 {%         if table_config.route is vyos_defined %}
 {%             for prefix, prefix_config in table_config.route.items() %}
 {{ static_routes('ip', prefix, prefix_config, table_id) }}
 {%             endfor %}
 {%         endif %}
 !
 {%         if table_config.route6 is vyos_defined %}
 {%             for prefix, prefix_config in table_config.route6.items() %}
 {{ static_routes('ipv6', prefix, prefix_config, table_id) }}
 {%             endfor %}
 {%         endif %}
 !
 {%     endfor %}
 {% endif %}
 !
 {% if route_map is vyos_defined %}
 ip protocol static route-map {{ route_map }}
 !
 {% endif %}
diff --git a/interface-definitions/include/version/interfaces-version.xml.i b/interface-definitions/include/version/interfaces-version.xml.i
index b97971531..0a209bc3a 100644
--- a/interface-definitions/include/version/interfaces-version.xml.i
+++ b/interface-definitions/include/version/interfaces-version.xml.i
@@ -1,3 +1,3 @@
 <!-- include start from include/version/interfaces-version.xml.i -->
-<syntaxVersion component='interfaces' version='25'></syntaxVersion>
+<syntaxVersion component='interfaces' version='26'></syntaxVersion>
 <!-- include end -->
diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in
index 3a0b7a40c..8cd8f8c86 100644
--- a/interface-definitions/interfaces-pppoe.xml.in
+++ b/interface-definitions/interfaces-pppoe.xml.in
@@ -1,143 +1,120 @@
 <?xml version="1.0"?>
 <interfaceDefinition>
   <node name="interfaces">
     <children>
       <tagNode name="pppoe" owner="${vyos_conf_scripts_dir}/interfaces-pppoe.py">
         <properties>
           <help>Point-to-Point Protocol over Ethernet (PPPoE)</help>
           <priority>322</priority>
           <constraint>
             <regex>^pppoe[0-9]+$</regex>
           </constraint>
           <constraintErrorMessage>PPPoE interface must be named pppoeN</constraintErrorMessage>
           <valueHelp>
             <format>pppoeN</format>
             <description>PPPoE dialer interface name</description>
           </valueHelp>
         </properties>
         <children>
           #include <include/pppoe-access-concentrator.xml.i>
           #include <include/interface/authentication.xml.i>
           #include <include/interface/dial-on-demand.xml.i>
           #include <include/interface/interface-firewall.xml.i>
           #include <include/interface/interface-policy.xml.i>
-          <leafNode name="default-route">
-            <properties>
-              <help>Default route insertion behaviour</help>
-              <completionHelp>
-                <list>auto none force</list>
-              </completionHelp>
-              <constraint>
-                <regex>^(auto|none|force)$</regex>
-              </constraint>
-              <constraintErrorMessage>PPPoE default-route option must be 'auto', 'none', or 'force'</constraintErrorMessage>
-              <valueHelp>
-                <format>auto</format>
-                <description>Automatically install a default route</description>
-              </valueHelp>
-              <valueHelp>
-                <format>none</format>
-                <description>Do not install a default route</description>
-              </valueHelp>
-              <valueHelp>
-                <format>force</format>
-                <description>Replace existing default route</description>
-              </valueHelp>
-            </properties>
-            <defaultValue>auto</defaultValue>
-          </leafNode>
+          #include <include/interface/no-default-route.xml.i>
+          #include <include/interface/default-route-distance.xml.i>
           #include <include/interface/dhcpv6-options.xml.i>
           #include <include/interface/description.xml.i>
           #include <include/interface/disable.xml.i>
           <leafNode name="idle-timeout">
             <properties>
               <help>Delay before disconnecting idle session (in seconds)</help>
               <valueHelp>
                 <format>u32:0-86400</format>
                 <description>Idle timeout in seconds</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 0-86400"/>
               </constraint>
               <constraintErrorMessage>Timeout must be in range 0 to 86400</constraintErrorMessage>
             </properties>
           </leafNode>
           <node name="ip">
             <properties>
               <help>IPv4 routing parameters</help>
             </properties>
             <children>
               #include <include/interface/adjust-mss.xml.i>
               #include <include/interface/disable-forwarding.xml.i>
               #include <include/interface/source-validation.xml.i>
             </children>
           </node>
           <node name="ipv6">
             <properties>
               <help>IPv6 routing parameters</help>
             </properties>
             <children>
               <node name="address">
                 <properties>
                   <help>IPv6 address configuration modes</help>
                 </properties>
                 <children>
                   #include <include/interface/ipv6-address-autoconf.xml.i>
                 </children>
               </node>
               #include <include/interface/adjust-mss.xml.i>
               #include <include/interface/disable-forwarding.xml.i>
             </children>
           </node>
           #include <include/source-interface.xml.i>
           <leafNode name="local-address">
             <properties>
               <help>IPv4 address of local end of the PPPoE link</help>
               <valueHelp>
                 <format>ipv4</format>
                 <description>Address of local end of the PPPoE link</description>
               </valueHelp>
               <constraint>
                 <validator name="ipv4-address"/>
               </constraint>
             </properties>
           </leafNode>
           #include <include/interface/mirror.xml.i>
           #include <include/interface/mtu-68-1500.xml.i>
           <leafNode name="mtu">
             <defaultValue>1492</defaultValue>
           </leafNode>
           <leafNode name="no-peer-dns">
             <properties>
               <help>Do not use DNS servers provided by the peer</help>
               <valueless/>
             </properties>
           </leafNode>
           <leafNode name="remote-address">
             <properties>
               <help>IPv4 address of remote end of the PPPoE link</help>
               <valueHelp>
                 <format>ipv4</format>
                 <description>Address of remote end of the PPPoE link</description>
               </valueHelp>
               <constraint>
                 <validator name="ipv4-address"/>
               </constraint>
             </properties>
           </leafNode>
           <leafNode name="service-name">
             <properties>
               <help>Service name, only connect to access concentrators advertising this</help>
               <constraint>
                 <regex>[a-zA-Z0-9]+$</regex>
               </constraint>
               <constraintErrorMessage>Service name must be alphanumeric only</constraintErrorMessage>
             </properties>
           </leafNode>
           #include <include/interface/redirect.xml.i>
           #include <include/interface/vrf.xml.i>
         </children>
       </tagNode>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index df4c80f23..399ac6feb 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -1,707 +1,741 @@
 # Copyright 2019 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.util import dict_search
 from vyos.xml import defaults
 from vyos.util 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):
    from vyos.configdiff import get_config_diff
    D = get_config_diff(conf, key_mangling=('-', '_'))
    D.set_level(conf.get_level())
    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=('-', '_'))
     D.set_level(conf.get_level())
     (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):
     """
     Check if a leaf node was altered. If it has been altered - values has been
     changed, or it was added/removed, we will return the old value. If nothing
     has been changed, None is returned
     """
     from vyos.configdiff import get_config_diff, Diff
     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, expand_nodes=Diff.DELETE, recursive=recursive)['delete'].keys()
     return list(keys)
 
 def get_removed_vlans(conf, 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(['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(['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(['vif-s', vif, 'vif-c'], expand_nodes=Diff.DELETE)['delete'].keys()
         if keys: dict['vif_s'][vif]['vif_c_remove'] = [*keys]
 
     return dict
 
 def T2665_set_dhcpv6pd_defaults(config_dict):
     """ Properly configure DHCPv6 default options in the dictionary. If there is
     no DHCPv6 configured at all, it is safe to remove the entire configuration.
     """
     # As this is the same for every interface type it is safe to assume this
     # for ethernet
     pd_defaults = defaults(['interfaces', 'ethernet', 'dhcpv6-options', 'pd'])
 
     # Implant default dictionary for DHCPv6-PD instances
     if dict_search('dhcpv6_options.pd.length', config_dict):
         del config_dict['dhcpv6_options']['pd']['length']
 
     for pd in (dict_search('dhcpv6_options.pd', config_dict) or []):
         config_dict['dhcpv6_options']['pd'][pd] = dict_merge(pd_defaults,
             config_dict['dhcpv6_options']['pd'][pd])
 
     return config_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:
     None -> Interface is not a member
     interface name -> Interface is a member of this interface
     False -> interface type cannot have members
     """
     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]
 
     # set config level to root
     old_level = conf.get_level()
     conf.set_level([])
 
     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})
 
     old_level = conf.set_level(old_level)
     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
     old_level = conf.get_level()
     conf.set_level([])
     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}
 
     old_level = conf.set_level(old_level)
     return ret_val
 
 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
 
     old_level = conf.get_level()
     conf.set_level([])
 
     intfpath = ['interfaces', Section.section(intf), intf]
     if ( conf.exists(intfpath + ['vif']) or
             conf.exists(intfpath + ['vif-s'])):
         ret = True
 
     conf.set_level(old_level)
     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 intftype not in intftypes + [None]:
         raise ValueError(f'unknown interface type "{intftype}" or it can not '
             'have a source-interface')
 
     intftype = intftypes if intftype == None else [intftype]
 
     # set config level to root
     old_level = conf.get_level()
     conf.set_level([])
 
     for it in intftype:
         base = ['interfaces', it]
         for intf in conf.list_nodes(base):
             lower_intf = base + [intf, 'source-interface']
             if conf.exists(lower_intf) and interface in conf.return_values(lower_intf):
                 ret_val = intf
                 break
 
     old_level = conf.set_level(old_level)
     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. """
+    # Cache and reset config level
+    old_level = conf.get_level()
+    conf.set_level([])
+
     dhcp_interfaces = {}
     dict = conf.get_config_dict(['interfaces'], get_first_key=True)
     if not dict:
         return dhcp_interfaces
 
-    def check_dhcp(config, ifname):
+    def check_dhcp(config):
+        ifname = config['ifname']
         tmp = {}
-        if dict_search('address', config) == 'dhcp' or dict_search('default_route', config) != None:
+        if 'address' in config and 'dhcp' in config['address']:
             options = {}
             if dict_search('dhcp_options.default_route_distance', config) != None:
-                options.update({'distance' : config['dhcp_options']['default_route_distance']})
-            if dict_search('default_route', config) != None:
-                options.update({'distance' : config['default_route']})
+                options.update({'dhcp_options' : config['dhcp_options']})
             if 'vrf' in config:
                 if vrf is config['vrf']: tmp.update({ifname : options})
             else: tmp.update({ifname : options})
+
         return tmp
 
     for section, interface in dict.items():
         for ifname in interface:
-            # always reset config level
+            # 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, 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, f'{ifname}.{vif}')
+                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, f'{ifname}.{vif_s}')
+            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, f'{ifname}.{vif_s}.{vif_c}')
+                for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items():
+                    tmp = check_dhcp(vif_c_config)
                     dhcp_interfaces.update(tmp)
 
+    # reset old config level
     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. """
+    # Cache and reset config level
+    old_level = conf.get_level()
+    conf.set_level([])
+
+    pppoe_interfaces = {}
+    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 is ifconfig['vrf']: pppoe_interfaces.update({ifname : options})
+        else: pppoe_interfaces.update({ifname : options})
+
+    # reset old config level
+    conf.set_level(old_level)
+    return pppoe_interfaces
+
 def get_interface_dict(config, base, ifname=''):
     """
     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']
 
     # retrieve interface default values
     default_values = defaults(base)
 
     # We take care about VLAN (vif, vif-s, vif-c) default values later on when
     # parsing vlans in default dict and merge the "proper" values in correctly,
     # see T2665.
     for vif in ['vif', 'vif_s']:
         if vif in default_values: del default_values[vif]
 
     # setup config level which is extracted in get_removed_vlans()
     config.set_level(base + [ifname])
     dict = config.get_config_dict([], key_mangling=('-', '_'), get_first_key=True,
                                   no_tag_node_value_mangle=True)
 
     # 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([]):
         dict.update({'deleted' : ''})
 
     # Add interface instance name into dictionary
     dict.update({'ifname': ifname})
 
     # XXX: T2665: When there is no DHCPv6-PD configuration given, we can safely
     # remove the default values from the dict.
     if 'dhcpv6_options' not in dict:
         if 'dhcpv6_options' in default_values:
             del default_values['dhcpv6_options']
 
     # We have gathered the dict representation of the CLI, but there are
     # default options which we need to update into the dictionary retrived.
     # But we should only add them when interface is not deleted - as this might
     # confuse parsers
     if 'deleted' not in dict:
         dict = dict_merge(default_values, dict)
 
         # 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']
 
     # XXX: T2665: blend in proper DHCPv6-PD default values
     dict = T2665_set_dhcpv6pd_defaults(dict)
 
     address = leaf_node_changed(config, ['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 = node_changed(config, ['dhcp-options'], recursive=True)
     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, ['mac'])
     if mac: dict.update({'mac_old' : mac})
 
     eui64 = leaf_node_changed(config, ['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})
 
     # Implant default dictionary in vif/vif-s VLAN interfaces. Values are
     # identical for all types of VLAN interfaces as they all include the same
     # XML definitions which hold the defaults.
     for vif, vif_config in dict.get('vif', {}).items():
         # Add subinterface name to dictionary
         dict['vif'][vif].update({'ifname' : f'{ifname}.{vif}'})
 
         default_vif_values = defaults(base + ['vif'])
         # XXX: T2665: When there is no DHCPv6-PD configuration given, we can safely
         # remove the default values from the dict.
         if not 'dhcpv6_options' in vif_config:
             del default_vif_values['dhcpv6_options']
 
         # Only add defaults if interface is not about to be deleted - this is
         # to keep a cleaner config dict.
         if 'deleted' not in dict:
             address = leaf_node_changed(config, ['vif', vif, 'address'])
             if address: dict['vif'][vif].update({'address_old' : address})
 
             dict['vif'][vif] = dict_merge(default_vif_values, dict['vif'][vif])
             # XXX: T2665: blend in proper DHCPv6-PD default values
             dict['vif'][vif] = T2665_set_dhcpv6pd_defaults(dict['vif'][vif])
 
             # 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 = node_changed(config, ['vif', vif, 'dhcp-options'], recursive=True)
         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}'})
 
         default_vif_s_values = defaults(base + ['vif-s'])
         # XXX: T2665: we only wan't the vif-s defaults - do not care about vif-c
         if 'vif_c' in default_vif_s_values: del default_vif_s_values['vif_c']
 
         # XXX: T2665: When there is no DHCPv6-PD configuration given, we can safely
         # remove the default values from the dict.
         if not 'dhcpv6_options' in vif_s_config:
             del default_vif_s_values['dhcpv6_options']
 
         # Only add defaults if interface is not about to be deleted - this is
         # to keep a cleaner config dict.
         if 'deleted' not in dict:
             address = leaf_node_changed(config, ['vif-s', vif_s, 'address'])
             if address: dict['vif_s'][vif_s].update({'address_old' : address})
 
             dict['vif_s'][vif_s] = dict_merge(default_vif_s_values,
                     dict['vif_s'][vif_s])
             # XXX: T2665: blend in proper DHCPv6-PD default values
             dict['vif_s'][vif_s] = T2665_set_dhcpv6pd_defaults(dict['vif_s'][vif_s])
 
             # 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 = node_changed(config, ['vif-s', vif_s, 'dhcp-options'], recursive=True)
         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}'})
 
             default_vif_c_values = defaults(base + ['vif-s', 'vif-c'])
 
             # XXX: T2665: When there is no DHCPv6-PD configuration given, we can safely
             # remove the default values from the dict.
             if not 'dhcpv6_options' in vif_c_config:
                 del default_vif_c_values['dhcpv6_options']
 
             # Only add defaults if interface is not about to be deleted - this is
             # to keep a cleaner config dict.
             if 'deleted' not in dict:
                 address = leaf_node_changed(config, ['vif-s', vif_s, 'vif-c', vif_c, 'address'])
                 if address: dict['vif_s'][vif_s]['vif_c'][vif_c].update(
                         {'address_old' : address})
 
                 dict['vif_s'][vif_s]['vif_c'][vif_c] = dict_merge(
                     default_vif_c_values, dict['vif_s'][vif_s]['vif_c'][vif_c])
                 # XXX: T2665: blend in proper DHCPv6-PD default values
                 dict['vif_s'][vif_s]['vif_c'][vif_c] = T2665_set_dhcpv6pd_defaults(
                     dict['vif_s'][vif_s]['vif_c'][vif_c])
 
                 # 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 = node_changed(config, ['vif-s', vif_s, 'vif-c', vif_c, 'dhcp-options'], recursive=True)
             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, dict)
     return 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):
     """
     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.util import get_half_cpus
     from vyos.template import is_ipv4
 
     dict = config.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
 
     # 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 = defaults(base)
 
     # T2665: defaults include RADIUS server specifics per TAG node which need to
     # be added to individual RADIUS servers instead - so we can simply delete them
     if dict_search('authentication.radius.server', default_values):
         del default_values['authentication']['radius']['server']
 
     # T2665: defaults include static-ip address per TAG node which need to be
     # added to individual local users instead - so we can simply delete them
     if dict_search('authentication.local_users.username', default_values):
         del default_values['authentication']['local_users']['username']
 
     # T2665: defaults include IPv6 client-pool mask per TAG node which need to be
     # added to individual local users instead - so we can simply delete them
     if dict_search('client_ipv6_pool.prefix.mask', default_values):
         del default_values['client_ipv6_pool']['prefix']['mask']
 
     dict = dict_merge(default_values, dict)
 
     # set CPUs cores to process requests
     dict.update({'thread_count' : get_half_cpus()})
     # 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']
 
     # Add individual RADIUS server default values
     if dict_search('authentication.radius.server', dict):
         # T2665
         default_values = defaults(base + ['authentication', 'radius', 'server'])
 
         for server in dict_search('authentication.radius.server', dict):
             dict['authentication']['radius']['server'][server] = dict_merge(
                 default_values, dict['authentication']['radius']['server'][server])
 
             # Check option "disable-accounting" per server and replace default value from '1813' to '0'
             # set vpn sstp authentication radius server x.x.x.x disable-accounting
             if 'disable_accounting' in dict['authentication']['radius']['server'][server]:
                 dict['authentication']['radius']['server'][server]['acct_port'] = '0'
 
     # Add individual local-user default values
     if dict_search('authentication.local_users.username', dict):
         # T2665
         default_values = defaults(base + ['authentication', 'local-users', 'username'])
 
         for username in dict_search('authentication.local_users.username', dict):
             dict['authentication']['local_users']['username'][username] = dict_merge(
                 default_values, dict['authentication']['local_users']['username'][username])
 
     # Add individual IPv6 client-pool default mask if required
     if dict_search('client_ipv6_pool.prefix', dict):
         # T2665
         default_values = defaults(base + ['client-ipv6-pool', 'prefix'])
 
         for prefix in dict_search('client_ipv6_pool.prefix', dict):
             dict['client_ipv6_pool']['prefix'][prefix] = dict_merge(
                 default_values, dict['client_ipv6_pool']['prefix'][prefix])
 
     return dict
diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py
index 1d13264bf..63ffc8069 100644
--- a/python/vyos/ifconfig/pppoe.py
+++ b/python/vyos/ifconfig/pppoe.py
@@ -1,151 +1,115 @@
 # Copyright 2020-2021 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/>.
 
 from vyos.ifconfig.interface import Interface
 from vyos.util import get_interface_config
 
 @Interface.register
 class PPPoEIf(Interface):
     iftype = 'pppoe'
     definition = {
         **Interface.definition,
         **{
             'section': 'pppoe',
             'prefixes': ['pppoe', ],
         },
     }
 
-    def _remove_routes(self, vrf=''):
+    def _remove_routes(self, vrf=None):
         # Always delete default routes when interface is removed
+        vrf_cmd = ''
         if vrf:
-            vrf = f'-c "vrf {vrf}"'
-        self._cmd(f'vtysh -c "conf t" {vrf} -c "no ip route 0.0.0.0/0 {self.ifname} tag 210"')
-        self._cmd(f'vtysh -c "conf t" {vrf} -c "no ipv6 route ::/0 {self.ifname} tag 210"')
+            vrf_cmd = f'-c "vrf {vrf}"'
+        self._cmd(f'vtysh -c "conf t" {vrf_cmd} -c "no ip route 0.0.0.0/0 {self.ifname} tag 210"')
+        self._cmd(f'vtysh -c "conf t" {vrf_cmd} -c "no ipv6 route ::/0 {self.ifname} tag 210"')
 
     def remove(self):
         """
         Remove interface from operating system. Removing the interface
         deconfigures all assigned IP addresses and clear possible DHCP(v6)
         client processes.
         Example:
         >>> from vyos.ifconfig import Interface
         >>> i = Interface('pppoe0')
         >>> i.remove()
         """
-
+        vrf = None
         tmp = get_interface_config(self.ifname)
-        vrf = ''
         if 'master' in tmp:
-            self._remove_routes(tmp['master'])
+            vrf = tmp['master']
+        self._remove_routes(vrf)
 
         # remove bond master which places members in disabled state
         super().remove()
 
     def _create(self):
         # we can not create this interface as it is managed outside
         pass
 
     def _delete(self):
         # we can not create this interface as it is managed outside
         pass
 
     def del_addr(self, addr):
         # we can not create this interface as it is managed outside
         pass
 
     def get_mac(self):
         """ Get a synthetic MAC address. """
         return self.get_mac_synthetic()
 
     def update(self, config):
         """ General helper function which works on a dictionary retrived by
         get_config_dict(). It's main intention is to consolidate the scattered
         interface setup code and provide a single point of entry when workin
         on any interface. """
 
         # Cache the configuration - it will be reused inside e.g. DHCP handler
         # XXX: maybe pass the option via __init__ in the future and rename this
         # method to apply()?
         #
         # We need to copy this from super().update() as we utilize self.set_dhcpv6()
         # before this is done by the base class.
         self._config = config
 
         # remove old routes from an e.g. old VRF assignment
-        vrf = ''
-        if 'vrf_old' in config:
-            vrf = config['vrf_old']
-        self._remove_routes(vrf)
+        if 'shutdown_required':
+            vrf = None
+            tmp = get_interface_config(self.ifname)
+            if 'master' in tmp:
+                vrf = tmp['master']
+            self._remove_routes(vrf)
 
         # DHCPv6 PD handling is a bit different on PPPoE interfaces, as we do
         # not require an 'address dhcpv6' CLI option as with other interfaces
         if 'dhcpv6_options' in config and 'pd' in config['dhcpv6_options']:
             self.set_dhcpv6(True)
         else:
             self.set_dhcpv6(False)
 
         super().update(config)
 
-        if 'default_route' not in config or config['default_route'] == 'none':
-            return
-
-        #
-        # Set default routes pointing to pppoe interface
-        #
-        vrf = ''
-        sed_opt = '^ip route'
-
-        install_v4 = True
-        install_v6 = True
-
         # generate proper configuration string when VRFs are in use
+        vrf = ''
         if 'vrf' in config:
             tmp = config['vrf']
             vrf = f'-c "vrf {tmp}"'
-            sed_opt = f'vrf {tmp}'
-
-        if config['default_route'] == 'auto':
-            # only add route if there is no default route present
-            tmp = self._cmd(f'vtysh -c "show running-config staticd no-header" | sed -n "/{sed_opt}/,/!/p"')
-            for line in tmp.splitlines():
-                line = line.lstrip()
-                if line.startswith('ip route 0.0.0.0/0'):
-                    install_v4 = False
-                    continue
-
-                if 'ipv6' in config and line.startswith('ipv6 route ::/0'):
-                    install_v6 = False
-                    continue
-
-        elif config['default_route'] == 'force':
-            # Force means that all static routes are replaced with the ones from this interface
-            tmp = self._cmd(f'vtysh -c "show running-config staticd no-header" | sed -n "/{sed_opt}/,/!/p"')
-            for line in tmp.splitlines():
-                if self.ifname in line:
-                    # It makes no sense to remove a route with our interface and the later re-add it.
-                    # This will only make traffic disappear - which is a no-no!
-                    continue
-
-                line = line.lstrip()
-                if line.startswith('ip route 0.0.0.0/0'):
-                    self._cmd(f'vtysh -c "conf t" {vrf} -c "no {line}"')
-
-                if 'ipv6' in config and line.startswith('ipv6 route ::/0'):
-                    self._cmd(f'vtysh -c "conf t" {vrf} -c "no {line}"')
-
-        if install_v4:
-            self._cmd(f'vtysh -c "conf t" {vrf} -c "ip route 0.0.0.0/0 {self.ifname} tag 210"')
-        if install_v6 and 'ipv6' in config:
-            self._cmd(f'vtysh -c "conf t" {vrf} -c "ipv6 route ::/0 {self.ifname} tag 210"')
+
+        if 'no_default_route' not in config:
+            # Set default route(s) pointing to PPPoE interface
+            distance = config['default_route_distance']
+            self._cmd(f'vtysh -c "conf t" {vrf} -c "ip route 0.0.0.0/0 {self.ifname} tag 210 {distance}"')
+            if 'ipv6' in config:
+                self._cmd(f'vtysh -c "conf t" {vrf} -c "ipv6 route ::/0 {self.ifname} tag 210 {distance}"')
diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py
index 4f1e1ee99..16f6e542b 100755
--- a/smoketest/scripts/cli/test_interfaces_pppoe.py
+++ b/smoketest/scripts/cli/test_interfaces_pppoe.py
@@ -1,164 +1,163 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2019-2021 VyOS maintainers and contributors
+# 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/>.
 
 import re
 import unittest
 
 from psutil import process_iter
 from base_vyostest_shim import VyOSUnitTestSHIM
 
 from vyos.configsession import ConfigSessionError
 
 config_file = '/etc/ppp/peers/{}'
 base_path = ['interfaces', 'pppoe']
 
 def get_config_value(interface, key):
     with open(config_file.format(interface), 'r') as f:
         for line in f:
             if line.startswith(key):
                 return list(line.split())
     return []
 
 # add a classmethod to setup a temporaray PPPoE server for "proper" validation
 class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase):
     def setUp(self):
         self._interfaces = ['pppoe10', 'pppoe20', 'pppoe30']
         self._source_interface = 'eth0'
 
     def tearDown(self):
         # Validate PPPoE client process
         for interface in self._interfaces:
             running = False
             for proc in process_iter():
                 if interface in proc.cmdline():
                     running = True
                     break
             self.assertTrue(running)
 
         self.cli_delete(base_path)
         self.cli_commit()
 
     def test_01_pppoe_client(self):
         # Check if PPPoE dialer can be configured and runs
         for interface in self._interfaces:
             user = 'VyOS-user-' + interface
             passwd = 'VyOS-passwd-' + interface
             mtu = '1400'
 
             self.cli_set(base_path + [interface, 'authentication', 'user', user])
             self.cli_set(base_path + [interface, 'authentication', 'password', passwd])
-            self.cli_set(base_path + [interface, 'default-route', 'auto'])
             self.cli_set(base_path + [interface, 'mtu', mtu])
             self.cli_set(base_path + [interface, 'no-peer-dns'])
 
             # check validate() - a source-interface is required
             with self.assertRaises(ConfigSessionError):
                 self.cli_commit()
             self.cli_set(base_path + [interface, 'source-interface', self._source_interface])
 
         # commit changes
         self.cli_commit()
 
         # verify configuration file(s)
         for interface in self._interfaces:
             user = 'VyOS-user-' + interface
             password = 'VyOS-passwd-' + interface
 
             tmp = get_config_value(interface, 'mtu')[1]
             self.assertEqual(tmp, mtu)
             tmp = get_config_value(interface, 'user')[1].replace('"', '')
             self.assertEqual(tmp, user)
             tmp = get_config_value(interface, 'password')[1].replace('"', '')
             self.assertEqual(tmp, password)
             tmp = get_config_value(interface, 'ifname')[1]
             self.assertEqual(tmp, interface)
 
     def test_02_pppoe_client_disabled_interface(self):
         # Check if PPPoE Client can be disabled
         for interface in self._interfaces:
             self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos'])
             self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos'])
             self.cli_set(base_path + [interface, 'source-interface', self._source_interface])
             self.cli_set(base_path + [interface, 'disable'])
 
         self.cli_commit()
 
         # Validate PPPoE client process - must not run as interfaces are disabled
         for interface in self._interfaces:
             running = False
             for proc in process_iter():
                 if interface in proc.cmdline():
                     running = True
                     break
             self.assertFalse(running)
 
         # enable PPPoE interfaces
         for interface in self._interfaces:
             self.cli_delete(base_path + [interface, 'disable'])
 
         self.cli_commit()
 
 
     def test_03_pppoe_authentication(self):
         # When username or password is set - so must be the other
         for interface in self._interfaces:
             self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos'])
             self.cli_set(base_path + [interface, 'source-interface', self._source_interface])
             self.cli_set(base_path + [interface, 'ipv6', 'address', 'autoconf'])
 
             # check validate() - if user is set, so must be the password
             with self.assertRaises(ConfigSessionError):
                 self.cli_commit()
 
             self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos'])
 
         self.cli_commit()
 
     def test_04_pppoe_dhcpv6pd(self):
         # Check if PPPoE dialer can be configured with DHCPv6-PD
         address = '1'
         sla_id = '0'
         sla_len = '8'
 
         for interface in self._interfaces:
             self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos'])
             self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos'])
-            self.cli_set(base_path + [interface, 'default-route', 'none'])
+            self.cli_set(base_path + [interface, 'no-default-route'])
             self.cli_set(base_path + [interface, 'no-peer-dns'])
             self.cli_set(base_path + [interface, 'source-interface', self._source_interface])
             self.cli_set(base_path + [interface, 'ipv6', 'address', 'autoconf'])
 
             # prefix delegation stuff
             dhcpv6_pd_base = base_path + [interface, 'dhcpv6-options', 'pd', '0']
             self.cli_set(dhcpv6_pd_base + ['length', '56'])
             self.cli_set(dhcpv6_pd_base + ['interface', self._source_interface, 'address', address])
             self.cli_set(dhcpv6_pd_base + ['interface', self._source_interface, 'sla-id',  sla_id])
 
             # commit changes
             self.cli_commit()
 
             # verify "normal" PPPoE value - 1492 is default MTU
             tmp = get_config_value(interface, 'mtu')[1]
             self.assertEqual(tmp, '1492')
             tmp = get_config_value(interface, 'user')[1].replace('"', '')
             self.assertEqual(tmp, 'vyos')
             tmp = get_config_value(interface, 'password')[1].replace('"', '')
             self.assertEqual(tmp, 'vyos')
             tmp = get_config_value(interface, '+ipv6 ipv6cp-use-ipaddr')
             self.assertListEqual(tmp, ['+ipv6', 'ipv6cp-use-ipaddr'])
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index bfb1fadd5..279369a32 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -1,148 +1,137 @@
 #!/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 leaf_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.util import call
 from vyos.util 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']
     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!
-    tmp = leaf_node_changed(conf, ['access-concentrator'])
-    if tmp: pppoe.update({'shutdown_required': {}})
-
-    tmp = leaf_node_changed(conf, ['connect-on-demand'])
-    if tmp: pppoe.update({'shutdown_required': {}})
-
-    tmp = leaf_node_changed(conf, ['service-name'])
-    if tmp: pppoe.update({'shutdown_required': {}})
-
-    tmp = leaf_node_changed(conf, ['source-interface'])
-    if tmp: pppoe.update({'shutdown_required': {}})
-
-    tmp = leaf_node_changed(conf, ['vrf'])
-    # leaf_node_changed() returns a list, as VRF is a non-multi node, there
-    # will be only one list element
-    if tmp: pppoe.update({'vrf_old': tmp[0]})
-
-    tmp = leaf_node_changed(conf, ['authentication', 'user'])
-    if tmp: pppoe.update({'shutdown_required': {}})
-
-    tmp = leaf_node_changed(conf, ['authentication', 'password'])
-    if tmp: pppoe.update({'shutdown_required': {}})
+    for options in ['access-concentrator', 'connect-on-demand', 'service-name',
+                    'source-interface', 'vrf', 'no-default-route', 'authentication']:
+        if is_node_changed(conf, options):
+            pppoe.update({'shutdown_required': {}})
+            # bail out early - no need to further process other nodes
+            break
 
     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')
 
     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.tmpl', 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, no-peer-dns, source-interface
+    # 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/protocols_static.py b/src/conf_mode/protocols_static.py
index 87432bc1c..58e202928 100755
--- a/src/conf_mode/protocols_static.py
+++ b/src/conf_mode/protocols_static.py
@@ -1,131 +1,134 @@
 #!/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 sys import argv
 
 from vyos.config import Config
 from vyos.configdict import dict_merge
 from vyos.configdict import get_dhcp_interfaces
+from vyos.configdict import get_pppoe_interfaces
 from vyos.configverify import verify_common_route_maps
 from vyos.configverify import verify_vrf
 from vyos.template import render_to_string
 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()
 
     vrf = None
     if len(argv) > 1:
         vrf = argv[1]
 
     base_path = ['protocols', 'static']
     # eqivalent of the C foo ? 'a' : 'b' statement
     base = vrf and ['vrf', 'name', vrf, 'protocols', 'static'] or base_path
     static = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
 
     # Assign the name of our VRF context
     if vrf: static['vrf'] = vrf
 
     # We also need some additional information from the config, prefix-lists
     # and route-maps for instance. They will be used in verify().
     #
     # XXX: one MUST always call this without the key_mangling() option! See
     # vyos.configverify.verify_common_route_maps() for more information.
     tmp = conf.get_config_dict(['policy'])
     # Merge policy dict into "regular" config dict
     static = dict_merge(tmp, static)
 
     # T3680 - get a list of all interfaces currently configured to use DHCP
     tmp = get_dhcp_interfaces(conf, vrf)
-    if tmp: static['dhcp'] = tmp
+    if tmp: static.update({'dhcp' : tmp})
+    tmp = get_pppoe_interfaces(conf, vrf)
+    if tmp: static.update({'pppoe' : tmp})
 
     return static
 
 def verify(static):
     verify_common_route_maps(static)
 
     for route in ['route', 'route6']:
         # if there is no route(6) key in the dictionary we can immediately
         # bail out early
         if route not in static:
             continue
 
         # When leaking routes to other VRFs we must ensure that the destination
         # VRF exists
         for prefix, prefix_options in static[route].items():
             # both the interface and next-hop CLI node can have a VRF subnode,
             # thus we check this using a for loop
             for type in ['interface', 'next_hop']:
                 if type in prefix_options:
                     for interface, interface_config in prefix_options[type].items():
                         verify_vrf(interface_config)
 
             if {'blackhole', 'reject'} <= set(prefix_options):
                 raise ConfigError(f'Can not use both blackhole and reject for '\
                                   'prefix "{prefix}"!')
 
     return None
 
 def generate(static):
     if not static:
         return None
     static['new_frr_config'] = render_to_string('frr/staticd.frr.j2', static)
     return None
 
 def apply(static):
     static_daemon = 'staticd'
     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 static route-map [-a-zA-Z0-9.]+', '')
     frr_cfg.commit_configuration(zebra_daemon)
     frr_cfg.load_configuration(static_daemon)
 
     if 'vrf' in static:
         vrf = static['vrf']
         frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit', remove_stop_mark=True)
     else:
         frr_cfg.modify_section(r'^ip route .*')
         frr_cfg.modify_section(r'^ipv6 route .*')
 
     if 'new_frr_config' in static:
         frr_cfg.add_before(frr.default_add_before, static['new_frr_config'])
     frr_cfg.commit_configuration(static_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/etc/ppp/ip-up.d/99-vyos-pppoe-callback b/src/etc/ppp/ip-up.d/99-vyos-pppoe-callback
index bb918a468..78ca09010 100755
--- a/src/etc/ppp/ip-up.d/99-vyos-pppoe-callback
+++ b/src/etc/ppp/ip-up.d/99-vyos-pppoe-callback
@@ -1,59 +1,59 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-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/>.
 
 # This is a Python hook script which is invoked whenever a PPPoE session goes
 # "ip-up". It will call into our vyos.ifconfig library and will then execute
 # common tasks for the PPPoE interface. The reason we have to "hook" this is
 # that we can not create a pppoeX interface in advance in linux and then connect
 # pppd to this already existing interface.
 
 from sys import argv
 from sys import exit
 
 from syslog import syslog
 from syslog import openlog
 from syslog import LOG_PID
 from syslog import LOG_INFO
 
-from vyos.configquery import ConfigTreeQuery
+from vyos.configquery import Config
+from vyos.configdict import get_interface_dict
 from vyos.ifconfig import PPPoEIf
 from vyos.util import read_file
 
 # When the ppp link comes up, this script is called with the following
 # parameters
 #       $1      the interface name used by pppd (e.g. ppp3)
 #       $2      the tty device name
 #       $3      the tty device speed
 #       $4      the local IP address for the interface
 #       $5      the remote IP address
 #       $6      the parameter specified by the 'ipparam' option to pppd
 
 if (len(argv) < 7):
     exit(1)
 
 interface = argv[6]
 dialer_pid = read_file(f'/var/run/{interface}.pid')
 
 openlog(ident=f'pppd[{dialer_pid}]', facility=LOG_INFO)
 syslog('executing ' + argv[0])
 
-conf = ConfigTreeQuery()
-pppoe = conf.get_config_dict(['interfaces', 'pppoe', argv[6]],
-                             get_first_key=True, key_mangling=('-', '_'))
-pppoe['ifname'] = argv[6]
+conf = Config()
+pppoe = get_interface_dict(conf, ['interfaces', 'pppoe'], argv[6])
 
+# Update the config
 p = PPPoEIf(pppoe['ifname'])
 p.update(pppoe)
diff --git a/src/migration-scripts/interfaces/25-to-26 b/src/migration-scripts/interfaces/25-to-26
new file mode 100755
index 000000000..a8936235e
--- /dev/null
+++ b/src/migration-scripts/interfaces/25-to-26
@@ -0,0 +1,54 @@
+#!/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/>.
+
+# T4384: pppoe: replace default-route CLI option with common CLI nodes already
+#        present for DHCP
+
+from sys import argv
+
+from vyos.ethtool import Ethtool
+from vyos.configtree import ConfigTree
+
+if (len(argv) < 1):
+    print("Must specify file name!")
+    exit(1)
+
+file_name = argv[1]
+with open(file_name, 'r') as f:
+    config_file = f.read()
+
+base = ['interfaces', 'pppoe']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+    exit(0)
+
+for ifname in config.list_nodes(base):
+    tmp_config = base + [ifname, 'default-route']
+    if config.exists(tmp_config):
+        # Retrieve current config value
+        value = config.return_value(tmp_config)
+        # Delete old Config node
+        config.delete(tmp_config)
+        if value == 'none':
+            config.set(base + [ifname, 'no-default-route'])
+
+try:
+    with open(file_name, 'w') as f:
+        f.write(config.to_string())
+except OSError as e:
+    print(f'Failed to save the modified config: {e}')
+    exit(1)