From 201257fe60afc40d101d162cc08e2878dfa3467b Mon Sep 17 00:00:00 2001
From: zsdc <taras@vyos.io>
Date: Tue, 4 Jan 2022 15:38:29 +0200
Subject: ipsec: T1925: Fixed `show vpn ipsec sa` output

After the a1aaf4fb9c0e4111670ef3dd491796fa35a2311f commit, only single
(latest) CHILD_SA for each connection can be displayed in the
`show vpn ipsec sa` output. This commit backs the proper behavior for
the command and adds a little optimization to the formatter to make it
easier.
---
 src/op_mode/show_ipsec_sa.py | 186 +++++++++++++++++++++----------------------
 1 file changed, 92 insertions(+), 94 deletions(-)

diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py
index e72f0f965..5b8f00dba 100755
--- a/src/op_mode/show_ipsec_sa.py
+++ b/src/op_mode/show_ipsec_sa.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2019 VyOS maintainers and contributors
+# 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
@@ -14,119 +14,117 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import re
-import sys
+from re import split as re_split
+from sys import exit
 
-import vici
-import tabulate
-import hurry.filesize
+from hurry import filesize
+from tabulate import tabulate
+from vici import Session as vici_session
+
+from vyos.util import seconds_to_human
 
-import vyos.util
 
 def convert(text):
     return int(text) if text.isdigit() else text.lower()
 
+
 def alphanum_key(key):
-    return [convert(c) for c in re.split('([0-9]+)', str(key))]
+    return [convert(c) for c in re_split('([0-9]+)', str(key))]
 
-def format_output(conns, sas):
+
+def format_output(sas):
     sa_data = []
 
-    for peer, parent_conn in conns.items():
-        if peer not in sas:
-            continue
-
-        parent_sa = sas[peer]
-        child_sas = parent_sa['child-sas']
-        installed_sas = {v['name'].decode(): v for k, v in child_sas.items() if v["state"] == b"INSTALLED"}
-
-        # parent_sa["state"] = IKE state, child_sas["state"] = ESP state
-        state = 'down'
-        uptime = 'N/A'
-
-        if parent_sa["state"] == b"ESTABLISHED" and installed_sas:
-            state = "up"
-
-        remote_host = parent_sa["remote-host"].decode()
-        remote_id = parent_sa["remote-id"].decode()
-
-        if remote_host == remote_id:
-            remote_id = "N/A"
-
-        # The counters can only be obtained from the child SAs
-        for child_conn in parent_conn['children']:
-            if child_conn not in installed_sas:
-                data = [child_conn, "down", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"]
-                sa_data.append(data)
-                continue
-
-            isa = installed_sas[child_conn]
-            csa_name = isa['name']
-            csa_name = csa_name.decode()
-
-            bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode()))
-            bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode()))
-            bytes_str = "{0}/{1}".format(bytes_in, bytes_out)
-
-            pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si)
-            pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si)
-            pkts_str = "{0}/{1}".format(pkts_in, pkts_out)
-            # Remove B from <1K values
-            pkts_str = re.sub(r'B', r'', pkts_str)
-
-            uptime = vyos.util.seconds_to_human(isa['install-time'].decode())
-
-            enc = isa["encr-alg"].decode()
-            if "encr-keysize" in isa:
-                key_size = isa["encr-keysize"].decode()
-            else:
-                key_size = ""
-            if "integ-alg" in isa:
-                hash = isa["integ-alg"].decode()
-            else:
-                hash = ""
-            if "dh-group" in isa:
-                dh_group = isa["dh-group"].decode()
-            else:
-                dh_group = ""
-
-            proposal = enc
-            if key_size:
-                proposal = "{0}_{1}".format(proposal, key_size)
-            if hash:
-                proposal = "{0}/{1}".format(proposal, hash)
-            if dh_group:
-                proposal = "{0}/{1}".format(proposal, dh_group)
-
-            data = [csa_name, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal]
-            sa_data.append(data)
+    for sa in sas:
+        for parent_sa in sa.values():
+            # create an item for each child-sa
+            for child_sa in parent_sa.get('child-sas', {}).values():
+                # prepare a list for output data
+                sa_out_name = sa_out_state = sa_out_uptime = sa_out_bytes = sa_out_packets = sa_out_remote_addr = sa_out_remote_id = sa_out_proposal = 'N/A'
+
+                # collect raw data
+                sa_name = child_sa.get('name')
+                sa_state = child_sa.get('state')
+                sa_uptime = child_sa.get('install-time')
+                sa_bytes_in = child_sa.get('bytes-in')
+                sa_bytes_out = child_sa.get('bytes-out')
+                sa_packets_in = child_sa.get('packets-in')
+                sa_packets_out = child_sa.get('packets-out')
+                sa_remote_addr = parent_sa.get('remote-host')
+                sa_remote_id = parent_sa.get('remote-id')
+                sa_proposal_encr_alg = child_sa.get('encr-alg')
+                sa_proposal_integ_alg = child_sa.get('integ-alg')
+                sa_proposal_encr_keysize = child_sa.get('encr-keysize')
+                sa_proposal_dh_group = child_sa.get('dh-group')
+
+                # format data to display
+                if sa_name:
+                    sa_out_name = sa_name.decode()
+                if sa_state:
+                    if sa_state == b'INSTALLED':
+                        sa_out_state = 'up'
+                    else:
+                        sa_out_state = 'down'
+                if sa_uptime:
+                    sa_out_uptime = seconds_to_human(sa_uptime.decode())
+                if sa_bytes_in and sa_bytes_out:
+                    bytes_in = filesize.size(int(sa_bytes_in.decode()))
+                    bytes_out = filesize.size(int(sa_bytes_out.decode()))
+                    sa_out_bytes = f'{bytes_in}/{bytes_out}'
+                if sa_packets_in and sa_packets_out:
+                    packets_in = filesize.size(int(sa_packets_in.decode()),
+                                               system=filesize.si)
+                    packets_out = filesize.size(int(sa_packets_out.decode()),
+                                                system=filesize.si)
+                    sa_out_packets = f'{packets_in}/{packets_out}'
+                if sa_remote_addr:
+                    sa_out_remote_addr = sa_remote_addr.decode()
+                if sa_remote_id:
+                    sa_out_remote_id = sa_remote_id.decode()
+                # format proposal
+                if sa_proposal_encr_alg:
+                    sa_out_proposal = sa_proposal_encr_alg.decode()
+                if sa_proposal_encr_keysize:
+                    sa_proposal_encr_keysize_str = sa_proposal_encr_keysize.decode()
+                    sa_out_proposal = f'{sa_out_proposal}_{sa_proposal_encr_keysize_str}'
+                if sa_proposal_integ_alg:
+                    sa_proposal_integ_alg_str = sa_proposal_integ_alg.decode()
+                    sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_integ_alg_str}'
+                if sa_proposal_dh_group:
+                    sa_proposal_dh_group_str = sa_proposal_dh_group.decode()
+                    sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_dh_group_str}'
+
+                # add a new item to output data
+                sa_data.append([
+                    sa_out_name, sa_out_state, sa_out_uptime, sa_out_bytes,
+                    sa_out_packets, sa_out_remote_addr, sa_out_remote_id,
+                    sa_out_proposal
+                ])
+
+    # return output data
     return sa_data
 
+
 if __name__ == '__main__':
     try:
-        session = vici.Session()
-        conns = {}
-        sas = {}
+        session = vici_session()
+        sas = list(session.list_sas())
 
-        for conn in session.list_conns():
-            for key in conn:
-                conns[key] = conn[key]
-
-        for sa in session.list_sas():
-            for key in sa:
-                sas[key] = sa[key]
-
-        headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"]
-        sa_data = format_output(conns, sas)
+        sa_data = format_output(sas)
         sa_data = sorted(sa_data, key=alphanum_key)
-        output = tabulate.tabulate(sa_data, headers)
+
+        headers = [
+            "Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out",
+            "Remote address", "Remote ID", "Proposal"
+        ]
+        output = tabulate(sa_data, headers)
         print(output)
     except PermissionError:
         print("You do not have a permission to connect to the IPsec daemon")
-        sys.exit(1)
+        exit(1)
     except ConnectionRefusedError:
         print("IPsec is not runing")
-        sys.exit(1)
+        exit(1)
     except Exception as e:
         print("An error occured: {0}".format(e))
-        sys.exit(1)
+        exit(1)
-- 
cgit v1.2.3


From ed67750b94e8bc779ec0e2cf6d568a3f7292de13 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sat, 29 Jan 2022 13:18:28 +0100
Subject: firewall: T4218: Adds a prefix to all user defined chains

---
 data/templates/firewall/nftables.tmpl     |  4 ++--
 data/templates/zone_policy/nftables.tmpl  | 12 ++++++------
 python/vyos/template.py                   |  3 ++-
 smoketest/scripts/cli/test_firewall.py    |  6 +++---
 smoketest/scripts/cli/test_zone_policy.py |  4 ++--
 src/conf_mode/firewall-interface.py       | 11 +++++++----
 src/conf_mode/firewall.py                 |  7 +++++--
 src/op_mode/firewall.py                   |  3 ++-
 8 files changed, 29 insertions(+), 21 deletions(-)

diff --git a/data/templates/firewall/nftables.tmpl b/data/templates/firewall/nftables.tmpl
index 33c821e84..468a5a32f 100644
--- a/data/templates/firewall/nftables.tmpl
+++ b/data/templates/firewall/nftables.tmpl
@@ -32,7 +32,7 @@ table ip filter {
 {% endif %}
 {% if name is defined %}
 {%   for name_text, conf in name.items() %}
-    chain {{ name_text }} {
+    chain NAME_{{ name_text }} {
 {%     if conf.rule is defined %}
 {%       for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not defined %}
         {{ rule_conf | nft_rule(name_text, rule_id) }}
@@ -82,7 +82,7 @@ table ip6 filter {
 {% endif %}
 {% if ipv6_name is defined %}
 {%   for name_text, conf in ipv6_name.items() %}
-    chain {{ name_text }} {
+    chain NAME6_{{ name_text }} {
 {%     if conf.rule is defined %}
 {%       for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not defined %}
         {{ rule_conf | nft_rule(name_text, rule_id, 'ip6') }}
diff --git a/data/templates/zone_policy/nftables.tmpl b/data/templates/zone_policy/nftables.tmpl
index e59208a0d..093da6bd8 100644
--- a/data/templates/zone_policy/nftables.tmpl
+++ b/data/templates/zone_policy/nftables.tmpl
@@ -13,7 +13,7 @@ table ip filter {
     chain VZONE_{{ zone_name }}_IN {
         iifname lo counter return
 {%       for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.name is defined %}
-        iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }}
+        iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME_{{ from_conf.firewall.name }}
         iifname { {{ zone[from_zone].interface | join(",") }} } counter return
 {%       endfor %}
         counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
@@ -21,7 +21,7 @@ table ip filter {
     chain VZONE_{{ zone_name }}_OUT {
         oifname lo counter return
 {%         for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall.name is defined %}
-        oifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }}
+        oifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME_{{ from_conf.firewall.name }}
         oifname { {{ zone[from_zone].interface | join(",") }} } counter return
 {%         endfor %}
         counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
@@ -34,7 +34,7 @@ table ip filter {
 {%       endif %}
 {%       for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.name is defined %}
 {%         if zone[from_zone].local_zone is not defined %}
-        iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }}
+        iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME_{{ from_conf.firewall.name }}
         iifname { {{ zone[from_zone].interface | join(",") }} } counter return
 {%         endif %}
 {%       endfor %}
@@ -50,7 +50,7 @@ table ip6 filter {
     chain VZONE6_{{ zone_name }}_IN {
         iifname lo counter return
 {%       for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.ipv6_name is defined %}
-        iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }}
+        iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME6_{{ from_conf.firewall.ipv6_name }}
         iifname { {{ zone[from_zone].interface | join(",") }} } counter return
 {%       endfor %}
         counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
@@ -58,7 +58,7 @@ table ip6 filter {
     chain VZONE6_{{ zone_name }}_OUT {
         oifname lo counter return
 {%         for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall.ipv6_name is defined %}
-        oifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }}
+        oifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME6_{{ from_conf.firewall.ipv6_name }}
         oifname { {{ zone[from_zone].interface | join(",") }} } counter return
 {%         endfor %}
         counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
@@ -71,7 +71,7 @@ table ip6 filter {
 {%       endif %}
 {%       for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.ipv6_name is defined %}
 {%         if zone[from_zone].local_zone is not defined %}
-        iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }}
+        iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME6_{{ from_conf.firewall.ipv6_name }}
         iifname { {{ zone[from_zone].interface | join(",") }} } counter return
 {%         endif %}
 {%       endfor %}
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 633b28ade..3675aef5d 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -548,6 +548,7 @@ def nft_intra_zone_action(zone_conf, ipv6=False):
     if 'intra_zone_filtering' in zone_conf:
         intra_zone = zone_conf['intra_zone_filtering']
         fw_name = 'ipv6_name' if ipv6 else 'name'
+        name_prefix = 'NAME6_' if ipv6 else 'NAME_'
 
         if 'action' in intra_zone:
             if intra_zone['action'] == 'accept':
@@ -555,5 +556,5 @@ def nft_intra_zone_action(zone_conf, ipv6=False):
             return intra_zone['action']
         elif dict_search_args(intra_zone, 'firewall', fw_name):
             name = dict_search_args(intra_zone, 'firewall', fw_name)
-            return f'jump {name}'
+            return f'jump {name_prefix}{name}'
     return 'return'
diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py
index 6b74e6c92..ecc0c29a0 100755
--- a/smoketest/scripts/cli/test_firewall.py
+++ b/smoketest/scripts/cli/test_firewall.py
@@ -63,7 +63,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
         self.cli_commit()
 
         nftables_search = [
-            ['iifname "eth0"', 'jump smoketest'],
+            ['iifname "eth0"', 'jump NAME_smoketest'],
             ['ip saddr { 172.16.99.0/24 }', 'ip daddr 172.16.10.10', 'th dport { 53, 123 }', 'return'],
             ['ether saddr { 00:01:02:03:04:05 }', 'return']
         ]
@@ -94,7 +94,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
         self.cli_commit()
 
         nftables_search = [
-            ['iifname "eth0"', 'jump smoketest'],
+            ['iifname "eth0"', 'jump NAME_smoketest'],
             ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'return'],
             ['tcp flags & (syn | ack) == syn', 'tcp dport { 8888 }', 'reject'],
             ['smoketest default-action', 'drop']
@@ -124,7 +124,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
         self.cli_commit()
 
         nftables_search = [
-            ['iifname "eth0"', 'jump v6-smoketest'],
+            ['iifname "eth0"', 'jump NAME6_v6-smoketest'],
             ['saddr 2002::1', 'daddr 2002::1:1', 'return'],
             ['meta l4proto { tcp, udp }', 'th dport { 8888 }', 'reject'],
             ['smoketest default-action', 'drop']
diff --git a/smoketest/scripts/cli/test_zone_policy.py b/smoketest/scripts/cli/test_zone_policy.py
index c0af6164b..00dfe0182 100755
--- a/smoketest/scripts/cli/test_zone_policy.py
+++ b/smoketest/scripts/cli/test_zone_policy.py
@@ -44,8 +44,8 @@ class TestZonePolicy(VyOSUnitTestSHIM.TestCase):
             ['oifname { "eth0" }', 'jump VZONE_smoketest-eth0'],
             ['jump VZONE_smoketest-local_IN'],
             ['jump VZONE_smoketest-local_OUT'],
-            ['iifname { "eth0" }', 'jump smoketest'],
-            ['oifname { "eth0" }', 'jump smoketest']
+            ['iifname { "eth0" }', 'jump NAME_smoketest'],
+            ['oifname { "eth0" }', 'jump NAME_smoketest']
         ]
 
         nftables_output = cmd('sudo nft list table ip filter')
diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py
index a7442ecbd..9a5d278e9 100755
--- a/src/conf_mode/firewall-interface.py
+++ b/src/conf_mode/firewall-interface.py
@@ -31,6 +31,9 @@ from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
+NAME_PREFIX = 'NAME_'
+NAME6_PREFIX = 'NAME6_'
+
 NFT_CHAINS = {
     'in': 'VYOS_FW_FORWARD',
     'out': 'VYOS_FW_FORWARD',
@@ -127,7 +130,7 @@ def apply(if_firewall):
 
         name = dict_search_args(if_firewall, direction, 'name')
         if name:
-            rule_exists = cleanup_rule('ip filter', chain, if_prefix, ifname, name)
+            rule_exists = cleanup_rule('ip filter', chain, if_prefix, ifname, f'{NAME_PREFIX}{name}')
 
             if not rule_exists:
                 rule_action = 'insert'
@@ -138,13 +141,13 @@ def apply(if_firewall):
                     rule_action = 'add'
                     rule_prefix = f'position {handle}'
 
-                run(f'nft {rule_action} rule ip filter {chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {name}')
+                run(f'nft {rule_action} rule ip filter {chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {NAME_PREFIX}{name}')
         else:
             cleanup_rule('ip filter', chain, if_prefix, ifname)
 
         ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')
         if ipv6_name:
-            rule_exists = cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname, ipv6_name)
+            rule_exists = cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname, f'{NAME6_PREFIX}{ipv6_name}')
 
             if not rule_exists:
                 rule_action = 'insert'
@@ -155,7 +158,7 @@ def apply(if_firewall):
                     rule_action = 'add'
                     rule_prefix = f'position {handle}'
 
-                run(f'nft {rule_action} rule ip6 filter {ipv6_chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {ipv6_name}')
+                run(f'nft {rule_action} rule ip6 filter {ipv6_chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {NAME6_PREFIX}{ipv6_name}')
         else:
             cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname)
 
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index 358b938e3..5b6c57d04 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -54,6 +54,9 @@ sysfs_config = {
     'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337'}
 }
 
+NAME_PREFIX = 'NAME_'
+NAME6_PREFIX = 'NAME6_'
+
 preserve_chains = [
     'INPUT',
     'FORWARD',
@@ -281,9 +284,9 @@ def cleanup_commands(firewall):
                     else:
                         commands.append(f'flush chain {table} {chain}')
                 elif chain not in preserve_chains and not chain.startswith("VZONE"):
-                    if table == 'ip filter' and dict_search_args(firewall, 'name', chain):
+                    if table == 'ip filter' and dict_search_args(firewall, 'name', chain.replace(NAME_PREFIX, "", 1)):
                         commands.append(f'flush chain {table} {chain}')
-                    elif table == 'ip6 filter' and dict_search_args(firewall, 'ipv6_name', chain):
+                    elif table == 'ip6 filter' and dict_search_args(firewall, 'ipv6_name', chain.replace(NAME6_PREFIX, "", 1)):
                         commands.append(f'flush chain {table} {chain}')
                     else:
                         commands += cleanup_rule(table, chain)
diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py
index b6bb5b802..3146fc357 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -88,7 +88,8 @@ def get_config_firewall(conf, name=None, ipv6=False, interfaces=True):
 
 def get_nftables_details(name, ipv6=False):
     suffix = '6' if ipv6 else ''
-    command = f'sudo nft list chain ip{suffix} filter {name}'
+    name_prefix = 'NAME6_' if ipv6 else 'NAME_'
+    command = f'sudo nft list chain ip{suffix} filter {name_prefix}{name}'
     try:
         results = cmd(command)
     except:
-- 
cgit v1.2.3


From 985a9e8536cb7f049e82dd1c7333ecced34563fa Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sat, 29 Jan 2022 23:34:05 +0100
Subject: firewall: T4216: Add support for negated firewall groups

---
 python/vyos/firewall.py   | 25 +++++++++++++++++++++----
 src/conf_mode/firewall.py |  4 ++++
 2 files changed, 25 insertions(+), 4 deletions(-)

diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index a2e133217..a74fd922a 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -104,13 +104,25 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):
                 group = side_conf['group']
                 if 'address_group' in group:
                     group_name = group['address_group']
-                    output.append(f'{ip_name} {prefix}addr $A{def_suffix}_{group_name}')
+                    operator = ''
+                    if group_name[0] == '!':
+                        operator = '!='
+                        group_name = group_name[1:]
+                    output.append(f'{ip_name} {prefix}addr {operator} $A{def_suffix}_{group_name}')
                 elif 'network_group' in group:
                     group_name = group['network_group']
-                    output.append(f'{ip_name} {prefix}addr $N{def_suffix}_{group_name}')
+                    operator = ''
+                    if group_name[0] == '!':
+                        operator = '!='
+                        group_name = group_name[1:]
+                    output.append(f'{ip_name} {prefix}addr {operator} $N{def_suffix}_{group_name}')
                 if 'mac_group' in group:
                     group_name = group['mac_group']
-                    output.append(f'ether {prefix}addr $M_{group_name}')
+                    operator = ''
+                    if group_name[0] == '!':
+                        operator = '!='
+                        group_name = group_name[1:]
+                    output.append(f'ether {prefix}addr {operator} $M_{group_name}')
                 if 'port_group' in group:
                     proto = rule_conf['protocol']
                     group_name = group['port_group']
@@ -118,7 +130,12 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):
                     if proto == 'tcp_udp':
                         proto = 'th'
 
-                    output.append(f'{proto} {prefix}port $P_{group_name}')
+                    operator = ''
+                    if group_name[0] == '!':
+                        operator = '!='
+                        group_name = group_name[1:]
+
+                    output.append(f'{proto} {prefix}port {operator} $P_{group_name}')
 
     if 'log' in rule_conf and rule_conf['log'] == 'enable':
         action = rule_conf['action'] if 'action' in rule_conf else 'accept'
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index 5b6c57d04..064b2d5a3 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -204,6 +204,10 @@ def verify_rule(firewall, rule_conf, ipv6):
                 for group in valid_groups:
                     if group in side_conf['group']:
                         group_name = side_conf['group'][group]
+
+                        if group_name and group_name[0] == '!':
+                            group_name = group_name[1:]
+
                         fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group
                         error_group = fw_group.replace("_", "-")
                         group_obj = dict_search_args(firewall, 'group', fw_group, group_name)
-- 
cgit v1.2.3


From 8532f2c391e895d7cd4c10b6d83d1e26973202a3 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sun, 30 Jan 2022 00:07:35 +0100
Subject: policy: T4213: Fix duplicate commands from multiple rules with single
 table

---
 src/conf_mode/policy-route.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py
index 7dcab4b58..82f668acf 100755
--- a/src/conf_mode/policy-route.py
+++ b/src/conf_mode/policy-route.py
@@ -206,6 +206,7 @@ def apply_table_marks(policy):
     for route in ['route', 'route6']:
         if route in policy:
             cmd_str = 'ip' if route == 'route' else 'ip -6'
+            tables = []
             for name, pol_conf in policy[route].items():
                 if 'rule' in pol_conf:
                     for rule_id, rule_conf in pol_conf['rule'].items():
@@ -213,6 +214,9 @@ def apply_table_marks(policy):
                         if set_table:
                             if set_table == 'main':
                                 set_table = '254'
+                            if set_table in tables:
+                                continue
+                            tables.append(set_table)
                             table_mark = mark_offset - int(set_table)
                             cmd(f'{cmd_str} rule add pref {set_table} fwmark {table_mark} table {set_table}')
 
-- 
cgit v1.2.3


From c6c562eca6ff469f603697f7f1d9319b2a5504a3 Mon Sep 17 00:00:00 2001
From: Henning Surmeier <me@hensur.de>
Date: Fri, 28 Jan 2022 23:55:06 +0100
Subject: policy: T4219: add local-route(6) incoming-interface

---
 .../include/interface/inbound-interface.xml.i      | 10 ++++
 interface-definitions/policy-local-route.xml.in    |  2 +
 smoketest/scripts/cli/test_policy.py               | 53 +++++++++++++++++++++-
 src/conf_mode/policy-local-route.py                | 34 ++++++++++++--
 4 files changed, 94 insertions(+), 5 deletions(-)
 create mode 100644 interface-definitions/include/interface/inbound-interface.xml.i

diff --git a/interface-definitions/include/interface/inbound-interface.xml.i b/interface-definitions/include/interface/inbound-interface.xml.i
new file mode 100644
index 000000000..5a8d47280
--- /dev/null
+++ b/interface-definitions/include/interface/inbound-interface.xml.i
@@ -0,0 +1,10 @@
+<!-- include start from interface/inbound-interface.xml.i -->
+<leafNode name="inbound-interface">
+  <properties>
+  <help>Inbound Interface</help>
+  <completionHelp>
+    <script>${vyos_completion_dir}/list_interfaces.py</script>
+  </completionHelp>
+  </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/policy-local-route.xml.in b/interface-definitions/policy-local-route.xml.in
index 11b1e04d9..573a7963f 100644
--- a/interface-definitions/policy-local-route.xml.in
+++ b/interface-definitions/policy-local-route.xml.in
@@ -88,6 +88,7 @@
                   <multi/>
                 </properties>
               </leafNode>
+              #include <include/interface/inbound-interface.xml.i>
             </children>
           </tagNode>
         </children>
@@ -177,6 +178,7 @@
                   <multi/>
                 </properties>
               </leafNode>
+              #include <include/interface/inbound-interface.xml.i>
             </children>
           </tagNode>
         </children>
diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py
index 73d93c986..491f1766d 100755
--- a/smoketest/scripts/cli/test_policy.py
+++ b/smoketest/scripts/cli/test_policy.py
@@ -1206,6 +1206,32 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):
 
         self.assertEqual(sort_ip(tmp), sort_ip(original))
 
+    # Test set table for sources with iif
+    def test_iif_sources_table_id(self):
+        path = base_path + ['local-route']
+
+        sources = ['203.0.113.11', '203.0.113.12']
+        iif = 'lo'
+        rule = '100'
+        table = '150'
+
+        self.cli_set(path + ['rule', rule, 'set', 'table', table])
+        self.cli_set(path + ['rule', rule, 'inbound-interface', iif])
+        for src in sources:
+            self.cli_set(path + ['rule', rule, 'source', src])
+
+        self.cli_commit()
+
+        # Check generated configuration
+        # Expected values
+        original = """
+        100:	from 203.0.113.11 iif lo lookup 150
+        100:	from 203.0.113.12 iif lo lookup 150
+        """
+        tmp = cmd('ip rule show prio 100')
+
+        self.assertEqual(sort_ip(tmp), sort_ip(original))
+
     # Test set table for sources and destinations with fwmark
     def test_fwmark_sources_destination_table_id(self):
         path = base_path + ['local-route']
@@ -1318,6 +1344,31 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):
 
         self.assertEqual(sort_ip(tmp), sort_ip(original))
 
+    # Test set table for sources with iif ipv6
+    def test_iif_sources_ipv6_table_id(self):
+        path = base_path + ['local-route6']
+
+        sources = ['2001:db8:1338::/126', '2001:db8:1339::/126']
+        iif = 'lo'
+        rule = '102'
+        table = '150'
+        for src in sources:
+            self.cli_set(path + ['rule', rule, 'set', 'table', table])
+            self.cli_set(path + ['rule', rule, 'source', src])
+            self.cli_set(path + ['rule', rule, 'inbound-interface', iif])
+
+        self.cli_commit()
+
+        # Check generated configuration
+        # Expected values
+        original = """
+        102:	from 2001:db8:1338::/126 iif lo lookup 150
+        102:	from 2001:db8:1339::/126 iif lo lookup 150
+        """
+        tmp = cmd('ip -6 rule show prio 102')
+
+        self.assertEqual(sort_ip(tmp), sort_ip(original))
+
     # Test set table for sources and destinations with fwmark ipv6
     def test_fwmark_sources_destination_ipv6_table_id(self):
         path = base_path + ['local-route6']
@@ -1384,7 +1435,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):
         103:	from 2001:db8:1338::/126 to 2001:db8:16::/48 fwmark 0x17 lookup 150
         103:	from 2001:db8:1339::/56 to 2001:db8:13::/48 fwmark 0x17 lookup 150
         103:	from 2001:db8:1339::/56 to 2001:db8:16::/48 fwmark 0x17 lookup 150
-        103:    from 2001:db8:1338::/126 to 2001:db8:13::/48 fwmark 0x17 lookup 150
+        103:	from 2001:db8:1338::/126 to 2001:db8:13::/48 fwmark 0x17 lookup 150
         """
         tmp = cmd('ip rule show prio 103')
         tmp_v6 = cmd('ip -6 rule show prio 103')
diff --git a/src/conf_mode/policy-local-route.py b/src/conf_mode/policy-local-route.py
index 71183c6ba..0990039c1 100755
--- a/src/conf_mode/policy-local-route.py
+++ b/src/conf_mode/policy-local-route.py
@@ -18,6 +18,7 @@ import os
 
 from sys import exit
 
+from netifaces import interfaces
 from vyos.config import Config
 from vyos.configdict import dict_merge
 from vyos.configdict import node_changed
@@ -51,12 +52,15 @@ def get_config(config=None):
             for rule in (tmp or []):
                 src = leaf_node_changed(conf, base_rule + [rule, 'source'])
                 fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark'])
+                iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface'])
                 dst = leaf_node_changed(conf, base_rule + [rule, 'destination'])
                 rule_def = {}
                 if src:
                     rule_def = dict_merge({'source' : src}, rule_def)
                 if fwmk:
                     rule_def = dict_merge({'fwmark' : fwmk}, rule_def)
+                if iif:
+                    rule_def = dict_merge({'inbound_interface' : iif}, rule_def)
                 if dst:
                     rule_def = dict_merge({'destination' : dst}, rule_def)
                 dict = dict_merge({dict_id : {rule : rule_def}}, dict)
@@ -72,6 +76,7 @@ def get_config(config=None):
             for rule, rule_config in pbr[route]['rule'].items():
                 src = leaf_node_changed(conf, base_rule + [rule, 'source'])
                 fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark'])
+                iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface'])
                 dst = leaf_node_changed(conf, base_rule + [rule, 'destination'])
                 # keep track of changes in configuration
                 # otherwise we might remove an existing node although nothing else has changed
@@ -100,6 +105,13 @@ def get_config(config=None):
                     changed = True
                     if len(fwmk) > 0:
                         rule_def = dict_merge({'fwmark' : fwmk}, rule_def)
+                if iif is None:
+                    if 'inbound_interface' in rule_config:
+                        rule_def = dict_merge({'inbound_interface': rule_config['inbound_interface']}, rule_def)
+                else:
+                    changed = True
+                    if len(iif) > 0:
+                        rule_def = dict_merge({'inbound_interface' : iif}, rule_def)
                 if dst is None:
                     if 'destination' in rule_config:
                         rule_def = dict_merge({'destination': rule_config['destination']}, rule_def)
@@ -125,11 +137,18 @@ def verify(pbr):
         pbr_route = pbr[route]
         if 'rule' in pbr_route:
             for rule in pbr_route['rule']:
-                if 'source' not in pbr_route['rule'][rule] and 'destination' not in pbr_route['rule'][rule] and 'fwmark' not in pbr_route['rule'][rule]:
-                    raise ConfigError('Source or destination address or fwmark is required!')
+                if 'source' not in pbr_route['rule'][rule] \
+                        and 'destination' not in pbr_route['rule'][rule] \
+                        and 'fwmark' not in pbr_route['rule'][rule] \
+                        and 'inbound_interface' not in pbr_route['rule'][rule]:
+                    raise ConfigError('Source or destination address or fwmark or inbound-interface is required!')
                 else:
                     if 'set' not in pbr_route['rule'][rule] or 'table' not in pbr_route['rule'][rule]['set']:
                         raise ConfigError('Table set is required!')
+                    if 'inbound_interface' in pbr_route['rule'][rule]:
+                        interface = pbr_route['rule'][rule]['inbound_interface']
+                        if interface not in interfaces():
+                            raise ConfigError(f'Interface "{interface}" does not exist')
 
     return None
 
@@ -159,7 +178,10 @@ def apply(pbr):
                         rule_config['fwmark'] = rule_config['fwmark'] if 'fwmark' in rule_config else ['']
                         for fwmk in rule_config['fwmark']:
                             f_fwmk = '' if fwmk == '' else f' fwmark {fwmk} '
-                            call(f'ip{v6} rule del prio {rule} {f_src}{f_dst}{f_fwmk}')
+                            rule_config['inbound_interface'] = rule_config['inbound_interface'] if 'inbound_interface' in rule_config else ['']
+                            for iif in rule_config['inbound_interface']:
+                                f_iif = '' if iif == '' else f' iif {iif} '
+                                call(f'ip{v6} rule del prio {rule} {f_src}{f_dst}{f_fwmk}{f_iif}')
 
     # Generate new config
     for route in ['local_route', 'local_route6']:
@@ -183,7 +205,11 @@ def apply(pbr):
                         if 'fwmark' in rule_config:
                             fwmk = rule_config['fwmark']
                             f_fwmk = f' fwmark {fwmk} '
-                        call(f'ip{v6} rule add prio {rule} {f_src}{f_dst}{f_fwmk} lookup {table}')
+                        f_iif = ''
+                        if 'inbound_interface' in rule_config:
+                            iif = rule_config['inbound_interface']
+                            f_iif = f' iif {iif} '
+                        call(f'ip{v6} rule add prio {rule} {f_src}{f_dst}{f_fwmk}{f_iif} lookup {table}')
 
     return None
 
-- 
cgit v1.2.3


From fafd25143d46220c537de8ef514d5954129528eb Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sun, 30 Jan 2022 00:39:12 +0100
Subject: firewall: T2199: Add constraint for tagnode names

---
 interface-definitions/firewall.xml.in     | 24 ++++++++++++++++++++++++
 interface-definitions/policy-route.xml.in |  6 ++++++
 interface-definitions/zone-policy.xml.in  |  3 +++
 3 files changed, 33 insertions(+)

diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in
index f38bcfd9c..f2aca4b3a 100644
--- a/interface-definitions/firewall.xml.in
+++ b/interface-definitions/firewall.xml.in
@@ -74,6 +74,9 @@
           <tagNode name="address-group">
             <properties>
               <help>Firewall address-group</help>
+              <constraint>
+                <regex>^[a-zA-Z0-9][\w\-\.]*$</regex>
+              </constraint>
             </properties>
             <children>
               <leafNode name="address">
@@ -100,6 +103,9 @@
           <tagNode name="ipv6-address-group">
             <properties>
               <help>Firewall ipv6-address-group</help>
+              <constraint>
+                <regex>^[a-zA-Z0-9][\w\-\.]*$</regex>
+              </constraint>
             </properties>
             <children>
               <leafNode name="address">
@@ -126,6 +132,9 @@
           <tagNode name="ipv6-network-group">
             <properties>
               <help>Firewall ipv6-network-group</help>
+              <constraint>
+                <regex>^[a-zA-Z0-9][\w\-\.]*$</regex>
+              </constraint>
             </properties>
             <children>
               #include <include/generic-description.xml.i>
@@ -147,6 +156,9 @@
           <tagNode name="mac-group">
             <properties>
               <help>Firewall mac-group</help>
+              <constraint>
+                <regex>^[a-zA-Z0-9][\w\-\.]*$</regex>
+              </constraint>
             </properties>
             <children>
               #include <include/generic-description.xml.i>
@@ -168,6 +180,9 @@
           <tagNode name="network-group">
             <properties>
               <help>Firewall network-group</help>
+              <constraint>
+                <regex>^[a-zA-Z0-9][\w\-\.]*$</regex>
+              </constraint>
             </properties>
             <children>
               #include <include/generic-description.xml.i>
@@ -189,6 +204,9 @@
           <tagNode name="port-group">
             <properties>
               <help>Firewall port-group</help>
+              <constraint>
+                <regex>^[a-zA-Z0-9][\w\-\.]*$</regex>
+              </constraint>
             </properties>
             <children>
               #include <include/generic-description.xml.i>
@@ -240,6 +258,9 @@
       <tagNode name="ipv6-name">
         <properties>
           <help>IPv6 firewall rule-set name</help>
+          <constraint>
+            <regex>^[a-zA-Z0-9][\w\-\.]*$</regex>
+          </constraint>
         </properties>
         <children>
           #include <include/firewall/name-default-action.xml.i>
@@ -423,6 +444,9 @@
       <tagNode name="name">
         <properties>
           <help>IPv4 firewall rule-set name</help>
+          <constraint>
+            <regex>^[a-zA-Z0-9][\w\-\.]*$</regex>
+          </constraint>
         </properties>
         <children>
           #include <include/firewall/name-default-action.xml.i>
diff --git a/interface-definitions/policy-route.xml.in b/interface-definitions/policy-route.xml.in
index 4ce953b52..a1c3b50de 100644
--- a/interface-definitions/policy-route.xml.in
+++ b/interface-definitions/policy-route.xml.in
@@ -5,6 +5,9 @@
       <tagNode name="route6" owner="${vyos_conf_scripts_dir}/policy-route.py">
         <properties>
           <help>Policy route rule set name for IPv6</help>
+          <constraint>
+            <regex>^[a-zA-Z0-9][\w\-\.]*$</regex>
+          </constraint>
           <priority>201</priority>
         </properties>
         <children>
@@ -51,6 +54,9 @@
       <tagNode name="route" owner="${vyos_conf_scripts_dir}/policy-route.py">
         <properties>
           <help>Policy route rule set name for IPv4</help>
+          <constraint>
+            <regex>^[a-zA-Z0-9][\w\-\.]*$</regex>
+          </constraint>
           <priority>201</priority>
         </properties>
         <children>
diff --git a/interface-definitions/zone-policy.xml.in b/interface-definitions/zone-policy.xml.in
index dd64c7c16..69ee031c7 100644
--- a/interface-definitions/zone-policy.xml.in
+++ b/interface-definitions/zone-policy.xml.in
@@ -13,6 +13,9 @@
             <format>txt</format>
             <description>Zone name</description>
           </valueHelp>
+          <constraint>
+            <regex>^[a-zA-Z0-9][\w\-\.]*$</regex>
+          </constraint>
         </properties>
         <children>
           #include <include/generic-description.xml.i>
-- 
cgit v1.2.3


From ff2cc45f8ba6d7ad1bc75ef384643692a54f31cc Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sun, 30 Jan 2022 22:52:49 +0100
Subject: firewall: T2199: Fix errors when referencing an empty chain

---
 src/conf_mode/firewall.py | 27 ++++++++++++++++-----------
 1 file changed, 16 insertions(+), 11 deletions(-)

diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index 064b2d5a3..9dec2143e 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -73,6 +73,9 @@ preserve_chains = [
     'VYOS_FRAG6_MARK'
 ]
 
+nft_iface_chains = ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL']
+nft6_iface_chains = ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']
+
 valid_groups = [
     'address_group',
     'network_group',
@@ -248,27 +251,29 @@ def verify(firewall):
             name = dict_search_args(if_firewall, direction, 'name')
             ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')
 
-            if name and not dict_search_args(firewall, 'name', name):
+            if name and dict_search_args(firewall, 'name', name) == None:
                 raise ConfigError(f'Firewall name "{name}" is still referenced on interface {ifname}')
 
-            if ipv6_name and not dict_search_args(firewall, 'ipv6_name', ipv6_name):
+            if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None:
                 raise ConfigError(f'Firewall ipv6-name "{ipv6_name}" is still referenced on interface {ifname}')
 
     for fw_name, used_names in firewall['zone_policy'].items():
         for name in used_names:
-            if not dict_search_args(firewall, fw_name, name):
+            if dict_search_args(firewall, fw_name, name) == None:
                 raise ConfigError(f'Firewall {fw_name.replace("_", "-")} "{name}" is still referenced in zone-policy')
 
     return None
 
 def cleanup_rule(table, jump_chain):
     commands = []
-    results = cmd(f'nft -a list table {table}').split("\n")
-    for line in results:
-        if f'jump {jump_chain}' in line:
-            handle_search = re.search('handle (\d+)', line)
-            if handle_search:
-                commands.append(f'delete rule {table} {chain} handle {handle_search[1]}')
+    chains = nft_iface_chains if table == 'ip filter' else nft6_iface_chains
+    for chain in chains:
+        results = cmd(f'nft -a list chain {table} {chain}').split("\n")
+        for line in results:
+            if f'jump {jump_chain}' in line:
+                handle_search = re.search('handle (\d+)', line)
+                if handle_search:
+                    commands.append(f'delete rule {table} {chain} handle {handle_search[1]}')
     return commands
 
 def cleanup_commands(firewall):
@@ -288,9 +293,9 @@ def cleanup_commands(firewall):
                     else:
                         commands.append(f'flush chain {table} {chain}')
                 elif chain not in preserve_chains and not chain.startswith("VZONE"):
-                    if table == 'ip filter' and dict_search_args(firewall, 'name', chain.replace(NAME_PREFIX, "", 1)):
+                    if table == 'ip filter' and dict_search_args(firewall, 'name', chain.replace(NAME_PREFIX, "", 1)) != None:
                         commands.append(f'flush chain {table} {chain}')
-                    elif table == 'ip6 filter' and dict_search_args(firewall, 'ipv6_name', chain.replace(NAME6_PREFIX, "", 1)):
+                    elif table == 'ip6 filter' and dict_search_args(firewall, 'ipv6_name', chain.replace(NAME6_PREFIX, "", 1)) != None:
                         commands.append(f'flush chain {table} {chain}')
                     else:
                         commands += cleanup_rule(table, chain)
-- 
cgit v1.2.3


From 84d790b65e1356f1c82b2a0e498f834abbe08653 Mon Sep 17 00:00:00 2001
From: Daniil Baturin <daniil@vyos.io>
Date: Mon, 31 Jan 2022 07:52:57 -0500
Subject: T4221: add force_to_list Jinja2 filter

---
 python/vyos/template.py | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/python/vyos/template.py b/python/vyos/template.py
index 633b28ade..4d081b4c2 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -127,6 +127,14 @@ def render(
 ##################################
 # Custom template filters follow #
 ##################################
+@register_filter('force_to_list')
+def force_to_list(value):
+    """ Convert scalars to single-item lists and leave lists untouched """
+    if isinstance(value, list):
+        return value
+    else:
+        return [value]
+
 @register_filter('ip_from_cidr')
 def ip_from_cidr(prefix):
     """ Take an IPv4/IPv6 CIDR host and strip cidr mask.
-- 
cgit v1.2.3


From 3dc698f18bc83149ab9044bcf18d60a9332685d2 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Mon, 31 Jan 2022 21:56:46 +0100
Subject: smoketest: upnpd: T3420: refine code and re-use paths

---
 smoketest/scripts/cli/test_service_upnp.py | 68 ++++++++++++++++++++++--------
 1 file changed, 51 insertions(+), 17 deletions(-)

diff --git a/smoketest/scripts/cli/test_service_upnp.py b/smoketest/scripts/cli/test_service_upnp.py
index 9fbbdaff9..c3e9b600f 100755
--- a/smoketest/scripts/cli/test_service_upnp.py
+++ b/smoketest/scripts/cli/test_service_upnp.py
@@ -1,6 +1,6 @@
 #!/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
@@ -20,52 +20,86 @@ import unittest
 from base_vyostest_shim import VyOSUnitTestSHIM
 
 from vyos.configsession import ConfigSession
+from vyos.configsession import ConfigSessionError
+from vyos.template import ip_from_cidr
 from vyos.util import read_file
 from vyos.util import process_named_running
 
 UPNP_CONF = '/run/upnp/miniupnp.conf'
+DAEMON = 'miniupnpd'
 interface = 'eth0'
 base_path = ['service', 'upnp']
 address_base = ['interfaces', 'ethernet', interface, 'address']
 
+ipv4_addr = '100.64.0.1/24'
+ipv6_addr = '2001:db8::1/64'
+
 class TestServiceUPnP(VyOSUnitTestSHIM.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        super(cls, 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)
+
+        cls.cli_set(cls, address_base + [ipv4_addr])
+        cls.cli_set(cls, address_base + [ipv6_addr])
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.cli_delete(cls, address_base)
+        cls._session.commit()
+
+        super(cls, cls).tearDownClass()
+
     def tearDown(self):
-        self.cli_delete(address_base)
+        # Check for running process
+        self.assertTrue(process_named_running(DAEMON))
+
         self.cli_delete(base_path)
         self.cli_commit()
-    
+
+        # Check for running process
+        self.assertFalse(process_named_running(DAEMON))
+
     def test_ipv4_base(self):
-        self.cli_set(address_base + ['100.64.0.1/24'])
         self.cli_set(base_path + ['nat-pmp'])
-        self.cli_set(base_path + ['wan-interface', interface])
         self.cli_set(base_path + ['listen', interface])
+
+        # check validate() - WAN interface is mandatory
+        with self.assertRaises(ConfigSessionError):
+            self.cli_commit()
+        self.cli_set(base_path + ['wan-interface', interface])
+
         self.cli_commit()
-        
+
         config = read_file(UPNP_CONF)
         self.assertIn(f'ext_ifname={interface}', config)
         self.assertIn(f'listening_ip={interface}', config)
         self.assertIn(f'enable_natpmp=yes', config)
         self.assertIn(f'enable_upnp=yes', config)
-        
-        # Check for running process
-        self.assertTrue(process_named_running('miniupnpd'))
-    
+
     def test_ipv6_base(self):
-        self.cli_set(address_base + ['2001:db8::1/64'])
+        v6_addr = ip_from_cidr(ipv6_addr)
+
         self.cli_set(base_path + ['nat-pmp'])
-        self.cli_set(base_path + ['wan-interface', interface])
         self.cli_set(base_path + ['listen', interface])
-        self.cli_set(base_path + ['listen', '2001:db8::1'])
+        self.cli_set(base_path + ['listen', v6_addr])
+
+        # check validate() - WAN interface is mandatory
+        with self.assertRaises(ConfigSessionError):
+            self.cli_commit()
+        self.cli_set(base_path + ['wan-interface', interface])
+
         self.cli_commit()
-        
+
         config = read_file(UPNP_CONF)
         self.assertIn(f'ext_ifname={interface}', config)
         self.assertIn(f'listening_ip={interface}', config)
+        self.assertIn(f'ipv6_listening_ip={v6_addr}', config)
         self.assertIn(f'enable_natpmp=yes', config)
         self.assertIn(f'enable_upnp=yes', config)
-        
-        # Check for running process
-        self.assertTrue(process_named_running('miniupnpd'))
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)
-- 
cgit v1.2.3


From 2ac8376ca1b79ba05bcf83d77c8e9d903e9b50f0 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Mon, 31 Jan 2022 21:57:08 +0100
Subject: upnpd: T3420: use proper include directives

---
 interface-definitions/service_upnp.xml.in | 44 ++++++++-----------------------
 1 file changed, 11 insertions(+), 33 deletions(-)

diff --git a/interface-definitions/service_upnp.xml.in b/interface-definitions/service_upnp.xml.in
index 8d0a14d4e..7cfe1f02e 100644
--- a/interface-definitions/service_upnp.xml.in
+++ b/interface-definitions/service_upnp.xml.in
@@ -19,7 +19,7 @@
           </leafNode>
           <leafNode name="wan-interface">
             <properties>
-              <help>WAN network interface (REQUIRE)</help>
+              <help>WAN network interface</help>
               <completionHelp>
                 <script>${vyos_completion_dir}/list_interfaces.py</script>
               </completionHelp>
@@ -139,49 +139,27 @@
                     <format>txt</format>
                     <description>The STUN server host address</description>
                   </valueHelp>
-                  <valueHelp>
-                    <format>stun.stunprotocol.org</format>
-                    <description>stunprotocol</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>stun.sipgate.net</format>
-                    <description>sipgate</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>stun.xten.com</format>
-                    <description>xten</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>txt</format>
-                    <description>other STUN Server</description>
-                  </valueHelp>
-                </properties>
-              </leafNode>
-              <leafNode name="port">
-                <properties>
-                  <help>The STUN server port</help>
-                  <valueHelp>
-                    <format>txt</format>
-                    <description>The STUN server port</description>
-                  </valueHelp>
+                  <constraint>
+                    <validator name="fqdn"/>
+                  </constraint>
                 </properties>
               </leafNode>
+              #include <include/port-number.xml.i>
             </children>
           </node>
-          <tagNode name="rules">
+          <tagNode name="rule">
             <properties>
               <help>UPnP Rule</help>
+              <valueHelp>
+                <format>u32:0-65535</format>
+                <description>Rule number</description>
+              </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 0-65535"/>
               </constraint>
             </properties>
             <children>
-              <leafNode name="disable">
-                <properties>
-                  <help>Disable Rule</help>
-                  <valueless />
-                </properties>
-              </leafNode>
+              #include <include/generic-disable-node.xml.i>
               <leafNode name="external-port-range">
                 <properties>
                   <help>Port range (REQUIRE)</help>
-- 
cgit v1.2.3


From 494ca8ffa043fd90c83cd052ff7da170646cec05 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Mon, 31 Jan 2022 21:57:24 +0100
Subject: upnpd: T3420: code cleanup

---
 src/conf_mode/service_upnp.py | 44 ++++++++++++++++++++++---------------------
 1 file changed, 23 insertions(+), 21 deletions(-)

diff --git a/src/conf_mode/service_upnp.py b/src/conf_mode/service_upnp.py
index 638296f45..d21b31990 100755
--- a/src/conf_mode/service_upnp.py
+++ b/src/conf_mode/service_upnp.py
@@ -1,6 +1,6 @@
 #!/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
@@ -24,7 +24,6 @@ from ipaddress import IPv6Network
 
 from vyos.config import Config
 from vyos.configdict import dict_merge
-from vyos.configdict import dict_search
 from vyos.configdict import get_interface_dict
 from vyos.configverify import verify_vrf
 from vyos.util import call
@@ -43,17 +42,18 @@ def get_config(config=None):
         conf = config
     else:
         conf = Config()
+
     base = ['service', 'upnp']
     upnpd = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
-    
+
     if not upnpd:
         return None
-    
-    if dict_search('rule', upnpd):
+
+    if 'rule' in upnpd:
         default_member_values = defaults(base + ['rule'])
         for rule,rule_config in upnpd['rule'].items():
             upnpd['rule'][rule] = dict_merge(default_member_values, upnpd['rule'][rule])
-    
+
     uuidgen = uuid.uuid1()
     upnpd.update({'uuid': uuidgen})
 
@@ -62,7 +62,7 @@ def get_config(config=None):
 def get_all_interface_addr(prefix, filter_dev, filter_family):
     list_addr = []
     interfaces = netifaces.interfaces()
-    
+
     for interface in interfaces:
         if filter_dev and interface in filter_dev:
             continue
@@ -87,27 +87,28 @@ def get_all_interface_addr(prefix, filter_dev, filter_family):
                         list_addr.append(addr['addr'] + prefix)
                     else:
                         list_addr.append(addr['addr'])
-    
+
     return list_addr
 
 def verify(upnpd):
     if not upnpd:
         return None
-    
+
     if 'wan_interface' not in upnpd:
         raise ConfigError('To enable UPNP, you must have the "wan-interface" option!')
-    
-    if dict_search('rules', upnpd):
-        for rule,rule_config in upnpd['rule'].items():
+
+    if 'rule' in upnpd:
+        for rule, rule_config in upnpd['rule'].items():
             for option in ['external_port_range', 'internal_port_range', 'ip', 'action']:
                 if option not in rule_config:
-                    raise ConfigError(f'A UPNP rule must have an "{option}" option!')
-    
-    if dict_search('stun', upnpd):
+                    tmp = option.replace('_', '-')
+                    raise ConfigError(f'Every UPNP rule requires "{tmp}" to be set!')
+
+    if 'stun' in upnpd:
         for option in ['host', 'port']:
             if option not in upnpd['stun']:
                 raise ConfigError(f'A UPNP stun support must have an "{option}" option!')
-    
+
     # Check the validity of the IP address
     listen_dev = []
     system_addrs_cidr = get_all_interface_addr(True, [], [netifaces.AF_INET, netifaces.AF_INET6])
@@ -120,7 +121,7 @@ def verify(upnpd):
                 raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed to listen on. It is not an interface address nor a multicast address!')
             if is_ipv6(listen_if_or_addr) and IPv6Network(listen_if_or_addr).is_multicast:
                 raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed to listen on. It is not an interface address nor a multicast address!')
-    
+
     system_listening_dev_addrs_cidr = get_all_interface_addr(True, listen_dev, [netifaces.AF_INET6])
     system_listening_dev_addrs = get_all_interface_addr(False, listen_dev, [netifaces.AF_INET6])
     for listen_if_or_addr in upnpd['listen']:
@@ -130,19 +131,20 @@ def verify(upnpd):
 def generate(upnpd):
     if not upnpd:
         return None
-    
+
     if os.path.isfile(config_file):
         os.unlink(config_file)
-    
+
     render(config_file, 'firewall/upnpd.conf.tmpl', upnpd)
 
 def apply(upnpd):
+    systemd_service_name = 'miniupnpd.service'
     if not upnpd:
         # Stop the UPNP service
-        call('systemctl stop miniupnpd.service')
+        call(f'systemctl stop {systemd_service_name}')
     else:
         # Start the UPNP service
-        call('systemctl restart miniupnpd.service')
+        call(f'systemctl restart {systemd_service_name}')
 
 if __name__ == '__main__':
     try:
-- 
cgit v1.2.3


From bf549b34e7daab7e843176bc1c8a8d03148f3840 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Tue, 1 Feb 2022 08:00:58 +0100
Subject: Revert "dhclient: T3392: remove /usr/sbin prefix from iproute2 ip
 command"

This reverts commit 78b247b724f74bdabab0706aaa7f5b00e5809bc1.
---
 src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper | 16 ++++++++--------
 src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup    |  2 +-
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
index 9d5505758..74a7e83bf 100644
--- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
@@ -4,7 +4,7 @@
 IF_METRIC=${IF_METRIC:-210}
 
 # Check if interface is inside a VRF
-VRF_OPTION=$(ip -j -d link show ${interface} | awk '{if(match($0, /.*"master":"(\w+)".*"info_slave_kind":"vrf"/, IFACE_DETAILS)) printf("vrf %s", IFACE_DETAILS[1])}')
+VRF_OPTION=$(/usr/sbin/ip -j -d link show ${interface} | awk '{if(match($0, /.*"master":"(\w+)".*"info_slave_kind":"vrf"/, IFACE_DETAILS)) printf("vrf %s", IFACE_DETAILS[1])}')
 
 # get status of FRR
 function frr_alive () {
@@ -66,9 +66,9 @@ function iptovtysh () {
 # delete the same route from kernel before adding new one
 function delroute () {
     logmsg info "Checking if the route presented in kernel: $@ $VRF_OPTION"
-    if ip route show $@ $VRF_OPTION | grep -qx "$1 " ; then
-        logmsg info "Deleting IP route: \"ip route del $@ $VRF_OPTION\""
-        ip route del $@ $VRF_OPTION
+    if /usr/sbin/ip route show $@ $VRF_OPTION | grep -qx "$1 " ; then
+        logmsg info "Deleting IP route: \"/usr/sbin/ip route del $@ $VRF_OPTION\""
+        /usr/sbin/ip route del $@ $VRF_OPTION
     fi
 }
 
@@ -76,8 +76,8 @@ function delroute () {
 function ip () {
     # pass comand to system `ip` if this is not related to routes change
     if [ "$2" != "route" ] ; then
-        logmsg info "Passing command to iproute2: \"$@\""
-        ip $@
+        logmsg info "Passing command to /usr/sbin/ip: \"$@\""
+        /usr/sbin/ip $@
     else
         # if we want to work with routes, try to use FRR first
         if frr_alive ; then
@@ -87,8 +87,8 @@ function ip () {
             vtysh -c "conf t" -c "$VTYSH_CMD"
         else
             # add ip route to kernel
-            logmsg info "Modifying routes in kernel: \"ip $@\""
-            ip $@ $VRF_OPTION
+            logmsg info "Modifying routes in kernel: \"/usr/sbin/ip $@\""
+            /usr/sbin/ip $@ $VRF_OPTION
         fi
     fi
 }
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
index a6989441b..ad6a1d5eb 100644
--- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
@@ -1,7 +1,7 @@
 ##
 ## VyOS cleanup
 ##
-# NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via ip or vtysh, according to the system state
+# NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state
 hostsd_client="/usr/bin/vyos-hostsd-client"
 hostsd_changes=
 # check vyos-hostsd status
-- 
cgit v1.2.3


From d331da9949060870f0543ac841e533d73e02c079 Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Wed, 2 Feb 2022 17:58:42 +0000
Subject: monitoring: T3872: Fix template input plugin for running services

Add required capability for input scripts which collect
statistics of running services
---
 data/templates/monitoring/override.conf.tmpl | 2 +-
 data/templates/monitoring/telegraf.tmpl      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/data/templates/monitoring/override.conf.tmpl b/data/templates/monitoring/override.conf.tmpl
index 63f6d7391..f8f150791 100644
--- a/data/templates/monitoring/override.conf.tmpl
+++ b/data/templates/monitoring/override.conf.tmpl
@@ -3,5 +3,5 @@ After=vyos-router.service
 ConditionPathExists=/run/telegraf/vyos-telegraf.conf
 [Service]
 Environment=INFLUX_TOKEN={{ authentication.token }}
-CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN
+CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_ADMIN
 AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN
diff --git a/data/templates/monitoring/telegraf.tmpl b/data/templates/monitoring/telegraf.tmpl
index f05396d91..465b58c80 100644
--- a/data/templates/monitoring/telegraf.tmpl
+++ b/data/templates/monitoring/telegraf.tmpl
@@ -53,7 +53,7 @@
 [[inputs.exec]]
   commands = [
     "{{ custom_scripts_dir }}/show_interfaces_input_filter.py",
-    "cat /tmp/vyos_services_input_filter"
+    "{{ custom_scripts_dir }}/vyos_services_input_filter.py"
   ]
   timeout = "10s"
   data_format = "influx"
-- 
cgit v1.2.3


From 9f7f1ebb15a2dce507693830517bc1c0c2b6815e Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Thu, 3 Feb 2022 00:30:52 +0100
Subject: firewall: T4178: Fix only inverse matching on tcp flags

---
 python/vyos/firewall.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index a74fd922a..c1217b420 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -208,7 +208,7 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):
 def parse_tcp_flags(flags):
     include = [flag for flag in flags if flag != 'not']
     exclude = list(flags['not']) if 'not' in flags else []
-    return f'tcp flags & ({"|".join(include + exclude)}) == {"|".join(include)}'
+    return f'tcp flags & ({"|".join(include + exclude)}) == {"|".join(include) if include else "0x0"}'
 
 def parse_time(time):
     out = []
-- 
cgit v1.2.3


From b10baca3c8663e7e56eb9abfb3c03ce576c34f1f Mon Sep 17 00:00:00 2001
From: srividya0208 <a.srividya@vyos.io>
Date: Thu, 3 Feb 2022 12:05:53 -0500
Subject: T4227:Bridge: Typo in completion help of hello-time option

There is spelling mistake in "advertisement" of hello-time option's
completion help
---
 interface-definitions/interfaces-bridge.xml.in | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in
index 0856615be..89a6d2303 100644
--- a/interface-definitions/interfaces-bridge.xml.in
+++ b/interface-definitions/interfaces-bridge.xml.in
@@ -59,7 +59,7 @@
           </leafNode>
           <leafNode name="hello-time">
             <properties>
-              <help>Hello packet advertisment interval</help>
+              <help>Hello packet advertisement interval</help>
               <valueHelp>
                 <format>u32:1-10</format>
                 <description>Spanning Tree Protocol hello advertisement interval in seconds (default 2)</description>
-- 
cgit v1.2.3


From 22f0794a9f195e69e277d48f031fe934febe9408 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Thu, 27 Jan 2022 16:58:36 +0100
Subject: firewall: T4209: Fix support for rule `recent` matches

---
 data/templates/firewall/nftables.tmpl              | 22 ++++++++++++++++++++++
 .../include/firewall/common-rule.xml.i             | 19 +++++++++++++++----
 python/vyos/firewall.py                            |  4 +---
 src/conf_mode/firewall.py                          |  6 +++++-
 src/migration-scripts/firewall/6-to-7              | 20 ++++++++++++++++++++
 5 files changed, 63 insertions(+), 8 deletions(-)

diff --git a/data/templates/firewall/nftables.tmpl b/data/templates/firewall/nftables.tmpl
index 468a5a32f..0cc977cf9 100644
--- a/data/templates/firewall/nftables.tmpl
+++ b/data/templates/firewall/nftables.tmpl
@@ -31,16 +31,27 @@ table ip filter {
     }
 {% endif %}
 {% if name is defined %}
+{%   set ns = namespace(sets=[]) %}
 {%   for name_text, conf in name.items() %}
     chain NAME_{{ name_text }} {
 {%     if conf.rule is defined %}
 {%       for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not defined %}
         {{ rule_conf | nft_rule(name_text, rule_id) }}
+{%         if rule_conf.recent is defined %}
+{%           set ns.sets = ns.sets + [name_text + '_' + rule_id] %}
+{%         endif %}
 {%       endfor %}
 {%     endif %}
         {{ conf | nft_default_rule(name_text) }}
     }
 {%   endfor %}
+{%   for set_name in ns.sets %}
+    set RECENT_{{ set_name }} {
+        type ipv4_addr
+        size 65535
+        flags dynamic
+    }
+{%   endfor %}
 {% endif %}
 {% if state_policy is defined %}
     chain VYOS_STATE_POLICY {
@@ -81,16 +92,27 @@ table ip6 filter {
     }
 {% endif %}
 {% if ipv6_name is defined %}
+{%   set ns = namespace(sets=[]) %}
 {%   for name_text, conf in ipv6_name.items() %}
     chain NAME6_{{ name_text }} {
 {%     if conf.rule is defined %}
 {%       for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not defined %}
         {{ rule_conf | nft_rule(name_text, rule_id, 'ip6') }}
+{%         if rule_conf.recent is defined %}
+{%           set ns.sets = ns.sets + [name_text + '_' + rule_id] %}
+{%         endif %}
 {%       endfor %}
 {%     endif %}
         {{ conf | nft_default_rule(name_text) }}
     }
 {%   endfor %}
+{%   for set_name in ns.sets %}
+    set RECENT6_{{ set_name }} {
+        type ipv6_addr
+        size 65535
+        flags dynamic
+    }
+{%   endfor %}
 {% endif %}
 {% if state_policy is defined %}
     chain VYOS_STATE_POLICY6 {
diff --git a/interface-definitions/include/firewall/common-rule.xml.i b/interface-definitions/include/firewall/common-rule.xml.i
index 521fe54f2..353804990 100644
--- a/interface-definitions/include/firewall/common-rule.xml.i
+++ b/interface-definitions/include/firewall/common-rule.xml.i
@@ -146,13 +146,24 @@
     </leafNode>
     <leafNode name="time">
       <properties>
-        <help>Source addresses seen in the last N seconds</help>
+        <help>Source addresses seen in the last second/minute/hour</help>
+        <completionHelp>
+          <list>second minute hour</list>
+        </completionHelp>
         <valueHelp>
-          <format>u32:0-4294967295</format>
-          <description>Source addresses seen in the last N seconds</description>
+          <format>second</format>
+          <description>Source addresses seen COUNT times in the last second</description>
+        </valueHelp>
+        <valueHelp>
+          <format>minute</format>
+          <description>Source addresses seen COUNT times in the last minute</description>
+        </valueHelp>
+        <valueHelp>
+          <format>hour</format>
+          <description>Source addresses seen COUNT times in the last hour</description>
         </valueHelp>
         <constraint>
-          <validator name="numeric" argument="--range 0-4294967295"/>
+          <regex>^(second|minute|hour)$</regex>
         </constraint>
       </properties>
     </leafNode>
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index c1217b420..55ce318e7 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -181,9 +181,7 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):
     if 'recent' in rule_conf:
         count = rule_conf['recent']['count']
         time = rule_conf['recent']['time']
-        # output.append(f'meter {fw_name}_{rule_id} {{ ip saddr and 255.255.255.255 limit rate over {count}/{time} burst {count} packets }}')
-        # Waiting on input from nftables developers due to
-        # bug with above line and atomic chain flushing.
+        output.append(f'add @RECENT{def_suffix}_{fw_name}_{rule_id} {{ {ip_name} saddr limit rate over {count}/{time} burst {count} packets }}')
 
     if 'time' in rule_conf:
         output.append(parse_time(rule_conf['time']))
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index 9dec2143e..41df1b84a 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -278,6 +278,7 @@ def cleanup_rule(table, jump_chain):
 
 def cleanup_commands(firewall):
     commands = []
+    commands_end = []
     for table in ['ip filter', 'ip6 filter']:
         state_chain = 'VYOS_STATE_POLICY' if table == 'ip filter' else 'VYOS_STATE_POLICY6'
         json_str = cmd(f'nft -j list table {table}')
@@ -308,7 +309,10 @@ def cleanup_commands(firewall):
                             chain = rule['chain']
                             handle = rule['handle']
                             commands.append(f'delete rule {table} {chain} handle {handle}')
-    return commands
+            elif 'set' in item:
+                set_name = item['set']['name']
+                commands_end.append(f'delete set {table} {set_name}')
+    return commands + commands_end
 
 def generate(firewall):
     if not os.path.exists(nftables_conf):
diff --git a/src/migration-scripts/firewall/6-to-7 b/src/migration-scripts/firewall/6-to-7
index efc901530..5f4cff90d 100755
--- a/src/migration-scripts/firewall/6-to-7
+++ b/src/migration-scripts/firewall/6-to-7
@@ -104,6 +104,7 @@ if config.exists(base + ['name']):
             continue
 
         for rule in config.list_nodes(base + ['name', name, 'rule']):
+            rule_recent = base + ['name', name, 'rule', rule, 'recent']
             rule_time = base + ['name', name, 'rule', rule, 'time']
             rule_tcp_flags = base + ['name', name, 'rule', rule, 'tcp', 'flags']
             rule_icmp = base + ['name', name, 'rule', rule, 'icmp']
@@ -114,6 +115,15 @@ if config.exists(base + ['name']):
             if config.exists(rule_time + ['utc']):
                 config.delete(rule_time + ['utc'])
 
+            if config.exists(rule_recent + ['time']):
+                tmp = int(config.return_value(rule_recent + ['time']))
+                unit = 'minute'
+                if tmp > 600:
+                    unit = 'hour'
+                elif tmp < 10:
+                    unit = 'second'
+                config.set(rule_recent + ['time'], value=unit)
+
             if config.exists(rule_tcp_flags):
                 tmp = config.return_value(rule_tcp_flags)
                 config.delete(rule_tcp_flags)
@@ -148,6 +158,7 @@ if config.exists(base + ['ipv6-name']):
             continue
 
         for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']):
+            rule_recent = base + ['ipv6-name', name, 'rule', rule, 'recent']
             rule_time = base + ['ipv6-name', name, 'rule', rule, 'time']
             rule_tcp_flags = base + ['ipv6-name', name, 'rule', rule, 'tcp', 'flags']
             rule_icmp = base + ['ipv6-name', name, 'rule', rule, 'icmpv6']
@@ -158,6 +169,15 @@ if config.exists(base + ['ipv6-name']):
             if config.exists(rule_time + ['utc']):
                 config.delete(rule_time + ['utc'])
 
+            if config.exists(rule_recent + ['time']):
+                tmp = int(config.return_value(rule_recent + ['time']))
+                unit = 'minute'
+                if tmp > 600:
+                    unit = 'hour'
+                elif tmp < 10:
+                    unit = 'second'
+                config.set(rule_recent + ['time'], value=unit)
+
             if config.exists(rule_tcp_flags):
                 tmp = config.return_value(rule_tcp_flags)
                 config.delete(rule_tcp_flags)
-- 
cgit v1.2.3


From 5444eeda0fab496da5bef7b233c443ba79b100ee Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Fri, 4 Feb 2022 08:45:18 +0000
Subject: policy: T4151: Delete unexpected print added in commit c501ae0f

---
 src/conf_mode/policy-local-route.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/conf_mode/policy-local-route.py b/src/conf_mode/policy-local-route.py
index 0990039c1..3f834f55c 100755
--- a/src/conf_mode/policy-local-route.py
+++ b/src/conf_mode/policy-local-route.py
@@ -162,8 +162,6 @@ def apply(pbr):
     if not pbr:
         return None
 
-    print(pbr)
-
     # Delete old rule if needed
     for rule_rm in ['rule_remove', 'rule6_remove']:
         if rule_rm in pbr:
-- 
cgit v1.2.3


From 11a900e706db59459314622050ced7d4117f090b Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 5 Feb 2022 20:13:04 +0100
Subject: vrrp: T4226: transition-script does not work for groups containing a
 hypen (-)

---
 src/system/keepalived-fifo.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py
index b1fe7e43f..a8df232ae 100755
--- a/src/system/keepalived-fifo.py
+++ b/src/system/keepalived-fifo.py
@@ -71,7 +71,8 @@ class KeepalivedFifo:
 
             # Read VRRP configuration directly from CLI
             self.vrrp_config_dict = conf.get_config_dict(base,
-                                     key_mangling=('-', '_'), get_first_key=True)
+                                     key_mangling=('-', '_'), get_first_key=True,
+                                     no_tag_node_value_mangle=True)
 
             logger.debug(f'Loaded configuration: {self.vrrp_config_dict}')
         except Exception as err:
-- 
cgit v1.2.3


From 5e7e96380b314587bbd8bd584848d39caef86f3f Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 6 Feb 2022 13:54:15 +0100
Subject: config: T4228: is_member() must return all instances not only the
 last one

---
 python/vyos/configdict.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index d974a7565..e7f515ea9 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -196,7 +196,7 @@ def is_member(conf, interface, intftype=None):
     interface name -> Interface is a member of this interface
     False -> interface type cannot have members
     """
-    ret_val = None
+    ret_val = {}
     intftypes = ['bonding', 'bridge']
 
     if intftype not in intftypes + [None]:
@@ -216,8 +216,8 @@ def is_member(conf, interface, intftype=None):
             member = base + [intf, 'member', 'interface', interface]
             if conf.exists(member):
                 tmp = conf.get_config_dict(member, key_mangling=('-', '_'),
-                                           get_first_key=True)
-                ret_val = {intf : tmp}
+                                           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
-- 
cgit v1.2.3


From b4185f8356d69476292906ebe32daf1c4867601a Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 6 Feb 2022 20:56:37 +0100
Subject: smoketest: bond: T4228: verify bond member is only used once

---
 smoketest/scripts/cli/test_interfaces_bonding.py | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py
index 86000553e..1d9a887bd 100755
--- a/smoketest/scripts/cli/test_interfaces_bonding.py
+++ b/smoketest/scripts/cli/test_interfaces_bonding.py
@@ -36,7 +36,6 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
         cls._test_vlan = True
         cls._test_qinq = True
         cls._base_path = ['interfaces', 'bonding']
-        cls._interfaces = ['bond0']
         cls._mirror_interfaces = ['dum21354']
         cls._members = []
 
@@ -52,6 +51,7 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
         cls._options['bond0'] = []
         for member in cls._members:
             cls._options['bond0'].append(f'member interface {member}')
+        cls._interfaces = list(cls._options)
 
         # call base-classes classmethod
         super(cls, cls).setUpClass()
@@ -150,5 +150,19 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
                 defined_policy = read_file(f'/sys/class/net/{interface}/bonding/xmit_hash_policy').split()
                 self.assertEqual(defined_policy[0], hash_policy)
 
+    def test_bonding_multi_use_member(self):
+        # Define available bonding hash policies
+        for interface in ['bond10', 'bond20']:
+            for member in self._members:
+                self.cli_set(self._base_path + [interface, 'member', 'interface', member])
+
+        # check validate() - can not use the same member interfaces multiple times
+        with self.assertRaises(ConfigSessionError):
+            self.cli_commit()
+
+        self.cli_delete(self._base_path + ['bond20'])
+
+        self.cli_commit()
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)
-- 
cgit v1.2.3


From 20090e7df2cc74763e5917c3b97b2262fbd909fc Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Mon, 7 Feb 2022 11:40:22 +0000
Subject: dhcp: T3600: Fix DHCP static table dhcp-interface route

Static table dhcp-interface route required table in template
Without table this route will be placed to table 'main' by default
---
 data/templates/frr/static_routes_macro.j2 | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/data/templates/frr/static_routes_macro.j2 b/data/templates/frr/static_routes_macro.j2
index 3b432b49b..86c7470ca 100644
--- a/data/templates/frr/static_routes_macro.j2
+++ b/data/templates/frr/static_routes_macro.j2
@@ -5,7 +5,7 @@
 {%   if prefix_config.dhcp_interface is defined and prefix_config.dhcp_interface is not none %}
 {%     set next_hop = prefix_config.dhcp_interface | get_dhcp_router %}
 {%     if next_hop is defined and next_hop is not none %}
-{{ ip_ipv6 }} route {{ prefix }} {{ next_hop }} {{ prefix_config.dhcp_interface }}
+{{ ip_ipv6 }} route {{ prefix }} {{ next_hop }} {{ prefix_config.dhcp_interface }} {{ 'table ' + table if table is defined and table is not none }}
 {%     endif %}
 {%   endif %}
 {%   if prefix_config.interface is defined and prefix_config.interface is not none %}
-- 
cgit v1.2.3


From d96bab4e6da517f07133667834cd6f8bcfb5160f Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Mon, 7 Feb 2022 22:27:51 +0100
Subject: xml: ssh: T4233: sync regex for allow/deny usernames to "system
 login"

---
 interface-definitions/include/ssh-user.xml.i | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/interface-definitions/include/ssh-user.xml.i b/interface-definitions/include/ssh-user.xml.i
index 677602dd8..17ba05a90 100644
--- a/interface-definitions/include/ssh-user.xml.i
+++ b/interface-definitions/include/ssh-user.xml.i
@@ -3,9 +3,9 @@
   <properties>
     <help>Allow specific users to login</help>
     <constraint>
-      <regex>[a-z_][a-z0-9_-]{1,31}[$]?</regex>
+      <regex>^[-_a-zA-Z0-9.]{1,100}</regex>
     </constraint>
-    <constraintErrorMessage>illegal characters or more than 32 characters</constraintErrorMessage>
+    <constraintErrorMessage>Illegal characters or more than 100 characters</constraintErrorMessage>
     <multi/>
   </properties>
 </leafNode>
-- 
cgit v1.2.3


From 4ddfe9b7e72e4f1e1fc8e70c5239bf09644e6d9b Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Tue, 8 Feb 2022 08:38:12 +0000
Subject: monitoring: T3872: Add input filter for firewall InfluxDB2

Input filter for firewall allows to get bytes/counters from
nftables in format, required for InfluxDB2
---
 data/templates/monitoring/telegraf.tmpl            |  3 +-
 .../custom_scripts/show_firewall_input_filter.py   | 73 ++++++++++++++++++++++
 2 files changed, 75 insertions(+), 1 deletion(-)
 create mode 100755 src/etc/telegraf/custom_scripts/show_firewall_input_filter.py

diff --git a/data/templates/monitoring/telegraf.tmpl b/data/templates/monitoring/telegraf.tmpl
index 465b58c80..d3145a500 100644
--- a/data/templates/monitoring/telegraf.tmpl
+++ b/data/templates/monitoring/telegraf.tmpl
@@ -17,7 +17,7 @@
 [[outputs.influxdb_v2]]
   urls = ["{{ url }}:{{ port }}"]
   insecure_skip_verify = true
-  token = "{{ authentication.token }}"
+  token = "$INFLUX_TOKEN"
   organization = "{{ authentication.organization }}"
   bucket = "{{ bucket }}"
 [[inputs.cpu]]
@@ -52,6 +52,7 @@
   syslog_standard = "RFC3164"
 [[inputs.exec]]
   commands = [
+    "{{ custom_scripts_dir }}/show_firewall_input_filter.py",
     "{{ custom_scripts_dir }}/show_interfaces_input_filter.py",
     "{{ custom_scripts_dir }}/vyos_services_input_filter.py"
   ]
diff --git a/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py b/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py
new file mode 100755
index 000000000..bf4bfd05d
--- /dev/null
+++ b/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+
+import json
+import re
+import time
+
+from vyos.util import cmd
+
+
+def get_nft_filter_chains():
+    """
+    Get list of nft chains for table filter
+    """
+    nft = cmd('/usr/sbin/nft --json list table ip filter')
+    nft = json.loads(nft)
+    chain_list = []
+
+    for output in nft['nftables']:
+        if 'chain' in output:
+            chain = output['chain']['name']
+            chain_list.append(chain)
+
+    return chain_list
+
+
+def get_nftables_details(name):
+    """
+    Get dict, counters packets and bytes for chain
+    """
+    command = f'/usr/sbin/nft list chain ip filter {name}'
+    try:
+        results = cmd(command)
+    except:
+        return {}
+
+    # Trick to remove 'NAME_' from chain name in the comment
+    # It was added to any chain T4218
+    # counter packets 0 bytes 0 return comment "FOO default-action accept"
+    comment_name = name.replace("NAME_", "")
+    out = {}
+    for line in results.split('\n'):
+        comment_search = re.search(rf'{comment_name}[\- ](\d+|default-action)', line)
+        if not comment_search:
+            continue
+
+        rule = {}
+        rule_id = comment_search[1]
+        counter_search = re.search(r'counter packets (\d+) bytes (\d+)', line)
+        if counter_search:
+            rule['packets'] = counter_search[1]
+            rule['bytes'] = counter_search[2]
+
+        rule['conditions'] = re.sub(r'(\b(counter packets \d+ bytes \d+|drop|reject|return|log)\b|comment "[\w\-]+")', '', line).strip()
+        out[rule_id] = rule
+    return out
+
+
+def get_nft_telegraf(name):
+    """
+    Get data for telegraf in influxDB format
+    """
+    for rule, rule_config in get_nftables_details(name).items():
+        print(f'nftables,table=filter,chain={name},'
+              f'ruleid={rule} '
+              f'pkts={rule_config["packets"]}i,'
+              f'bytes={rule_config["bytes"]}i '
+              f'{str(int(time.time()))}000000000')
+
+
+chains = get_nft_filter_chains()
+
+for chain in chains:
+    get_nft_telegraf(chain)
-- 
cgit v1.2.3


From 2ee94418ce24429dbf6a52c2a327ed08a1935958 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Tue, 8 Feb 2022 21:13:46 -0600
Subject: configtree: T4235: encapsulate config tree diff function

---
 python/vyos/configtree.py | 65 +++++++++++++++++++++++++++++++++++++----------
 1 file changed, 52 insertions(+), 13 deletions(-)

diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index d8ffaca99..866f24e47 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -15,8 +15,9 @@
 import re
 import json
 
-from ctypes import cdll, c_char_p, c_void_p, c_int
+from ctypes import cdll, c_char_p, c_void_p, c_int, POINTER
 
+LIBPATH = '/usr/lib/libvyosconfig.so.0'
 
 def escape_backslash(string: str) -> str:
     """Escape single backslashes in string that are not in escape sequence"""
@@ -42,7 +43,9 @@ class ConfigTreeError(Exception):
 
 
 class ConfigTree(object):
-    def __init__(self, config_string, libpath='/usr/lib/libvyosconfig.so.0'):
+    def __init__(self, config_string=None, address=None, libpath=LIBPATH):
+        if config_string is None and address is None:
+            raise TypeError("ConfigTree() requires one of 'config_string' or 'address'")
         self.__config = None
         self.__lib = cdll.LoadLibrary(libpath)
 
@@ -60,7 +63,7 @@ class ConfigTree(object):
         self.__to_string.restype = c_char_p
 
         self.__to_commands = self.__lib.to_commands
-        self.__to_commands.argtypes = [c_void_p]
+        self.__to_commands.argtypes = [c_void_p, c_char_p]
         self.__to_commands.restype = c_char_p
 
         self.__to_json = self.__lib.to_json
@@ -126,15 +129,19 @@ class ConfigTree(object):
         self.__destroy = self.__lib.destroy
         self.__destroy.argtypes = [c_void_p]
 
-        config_section, version_section = extract_version(config_string)
-        config_section = escape_backslash(config_section)
-        config = self.__from_string(config_section.encode())
-        if config is None:
-            msg = self.__get_error().decode()
-            raise ValueError("Failed to parse config: {0}".format(msg))
+        if address is None:
+            config_section, version_section = extract_version(config_string)
+            config_section = escape_backslash(config_section)
+            config = self.__from_string(config_section.encode())
+            if config is None:
+                msg = self.__get_error().decode()
+                raise ValueError("Failed to parse config: {0}".format(msg))
+            else:
+                self.__config = config
+                self.__version = version_section
         else:
-            self.__config = config
-            self.__version = version_section
+            self.__config = address
+            self.__version = ''
 
     def __del__(self):
         if self.__config is not None:
@@ -143,13 +150,16 @@ class ConfigTree(object):
     def __str__(self):
         return self.to_string()
 
+    def _get_config(self):
+        return self.__config
+
     def to_string(self):
         config_string = self.__to_string(self.__config).decode()
         config_string = "{0}\n{1}".format(config_string, self.__version)
         return config_string
 
-    def to_commands(self):
-        return self.__to_commands(self.__config).decode()
+    def to_commands(self, op="set"):
+        return self.__to_commands(self.__config, op.encode()).decode()
 
     def to_json(self):
         return self.__to_json(self.__config).decode()
@@ -281,3 +291,32 @@ class ConfigTree(object):
         else:
             raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
 
+class Diff:
+    def __init__(self, left, right, path=[], libpath=LIBPATH):
+        if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)):
+            raise TypeError("Arguments must be instances of ConfigTree")
+        if path:
+            if not left.exists(path):
+                raise ConfigTreeError(f"Path {path} doesn't exist in lhs tree")
+            if not right.exists(path):
+                raise ConfigTreeError(f"Path {path} doesn't exist in rhs tree")
+        self.left = left
+        self.right = right
+
+        check_path(path)
+        path_str = " ".join(map(str, path)).encode()
+        df = cdll.LoadLibrary(libpath).diffs
+        df.restype = POINTER(c_void_p * 3)
+        res = list(df(path_str, left._get_config(), right._get_config()).contents)
+        self._diff = {'add': ConfigTree(address=res[0]),
+                      'del': ConfigTree(address=res[1]),
+                      'int': ConfigTree(address=res[2]) }
+
+        self.add = self._diff['add']
+        self.delete = self._diff['del']
+        self.inter = self._diff['int']
+
+    def to_commands(self):
+        add = self.add.to_commands()
+        delete = self.delete.to_commands(op="delete")
+        return delete + "\n" + add
-- 
cgit v1.2.3


From 4ecfd5d87c33aea770878a012f3b4956deafd762 Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Tue, 8 Feb 2022 10:58:20 +0000
Subject: openvpn: T4230: Delete checks if local-host address assigned

OpenVPN can't start if it depends on VRRP virtual-address as
virtual-address is not yet assigned by HA (openvpn and ha
in one commit) as we have checks "if address assigned"
It depends on commit priorities:
 460 interfaces/openvpn
 800 high-availability

Replace check if local-host address assigned from raise ConfigError
to print (just notification)
Allow to bind OpenVPN service to nonlocal address
---
 src/conf_mode/interfaces-openvpn.py | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 3b8fae710..0f6114b4a 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -47,6 +47,7 @@ from vyos.template import is_ipv4
 from vyos.template import is_ipv6
 from vyos.util import call
 from vyos.util import chown
+from vyos.util import cmd
 from vyos.util import dict_search
 from vyos.util import dict_search_args
 from vyos.util import makedir
@@ -423,8 +424,8 @@ def verify(openvpn):
     # verify specified IP address is present on any interface on this system
     if 'local_host' in openvpn:
         if not is_addr_assigned(openvpn['local_host']):
-            raise ConfigError('local-host IP address "{local_host}" not assigned' \
-                              ' to any interface'.format(**openvpn))
+            print('local-host IP address "{local_host}" not assigned' \
+                  ' to any interface'.format(**openvpn))
 
     # TCP active
     if openvpn['protocol'] == 'tcp-active':
@@ -647,6 +648,13 @@ def apply(openvpn):
 
         return None
 
+    # verify specified IP address is present on any interface on this system
+    # Allow to bind service to nonlocal address, if it virtaual-vrrp address
+    # or if address will be assign later
+    if 'local_host' in openvpn:
+        if not is_addr_assigned(openvpn['local_host']):
+            cmd('sysctl -w net.ipv4.ip_nonlocal_bind=1')
+
     # No matching OpenVPN process running - maybe it got killed or none
     # existed - nevertheless, spawn new OpenVPN process
     call(f'systemctl reload-or-restart openvpn@{interface}.service')
-- 
cgit v1.2.3


From 230ac0a202acd7ae9ad9bccb9e777ee5a0e0b7b7 Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Wed, 9 Feb 2022 16:07:55 +0000
Subject: openvpn: T3686: Fix for check local-address in script and tmpl

Local-address should be checked/executed only if it exists in the
openvpn configuration, dictionary, jinja2 template
---
 data/templates/openvpn/server.conf.tmpl | 10 ++++++----
 src/conf_mode/interfaces-openvpn.py     | 13 +++++++------
 2 files changed, 13 insertions(+), 10 deletions(-)

diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl
index 7a0470d0e..fb7ad9e16 100644
--- a/data/templates/openvpn/server.conf.tmpl
+++ b/data/templates/openvpn/server.conf.tmpl
@@ -141,11 +141,13 @@ ping {{ keep_alive.interval }}
 ping-restart {{ keep_alive.failure_count }}
 
 {%   if device_type == 'tap' %}
-{%     for laddr, laddr_conf in local_address.items() if laddr | is_ipv4 %}
-{%       if laddr_conf is defined and laddr_conf.subnet_mask is defined and laddr_conf.subnet_mask is not none %}
+{%     if local_address is defined and local_address is not none  %}
+{%       for laddr, laddr_conf in local_address.items() if laddr | is_ipv4 %}
+{%         if laddr_conf is defined and laddr_conf.subnet_mask is defined and laddr_conf.subnet_mask is not none %}
 ifconfig {{ laddr }} {{ laddr_conf.subnet_mask }}
-{%       endif %}
-{%     endfor %}
+{%         endif %}
+{%       endfor %}
+{%     endif %}
 {%   else %}
 {%     for laddr in local_address if laddr | is_ipv4 %}
 {%       for raddr in remote_address if raddr | is_ipv4 %}
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 3b8fae710..242fae9fb 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -1,6 +1,6 @@
 #!/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
@@ -225,11 +225,12 @@ def verify(openvpn):
         if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn:
             raise ConfigError('Must specify "local-address" or add interface to bridge')
 
-        if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1:
-            raise ConfigError('Only one IPv4 local-address can be specified')
+        if 'local_address' in openvpn:
+            if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1:
+                raise ConfigError('Only one IPv4 local-address can be specified')
 
-        if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1:
-            raise ConfigError('Only one IPv6 local-address can be specified')
+            if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1:
+                raise ConfigError('Only one IPv6 local-address can be specified')
 
         if openvpn['device_type'] == 'tun':
             if 'remote_address' not in openvpn:
@@ -268,7 +269,7 @@ def verify(openvpn):
             if dict_search('remote_host', openvpn) in dict_search('remote_address', openvpn):
                 raise ConfigError('"remote-address" and "remote-host" can not be the same')
 
-        if openvpn['device_type'] == 'tap':
+        if openvpn['device_type'] == 'tap' and 'local_address' in openvpn:
             # we can only have one local_address, this is ensured above
             v4addr = None
             for laddr in openvpn['local_address']:
-- 
cgit v1.2.3


From 19f65290529ac642da419ac77003ddaa70d0cc67 Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Thu, 10 Feb 2022 11:13:33 +0000
Subject: smoketest: T3872: Fix token check for monitoring test

As INFLUX_TOKEN is present in override.conf.tmpl environment we expect
variable "$INFLUX_TOKEN" in the telegraf template and config but not
value of the token
---
 smoketest/scripts/cli/test_service_monitoring_telegraf.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/smoketest/scripts/cli/test_service_monitoring_telegraf.py b/smoketest/scripts/cli/test_service_monitoring_telegraf.py
index b857926e2..09937513e 100755
--- a/smoketest/scripts/cli/test_service_monitoring_telegraf.py
+++ b/smoketest/scripts/cli/test_service_monitoring_telegraf.py
@@ -54,7 +54,7 @@ class TestMonitoringTelegraf(VyOSUnitTestSHIM.TestCase):
 
         # Check telegraf config
         self.assertIn(f'organization = "{org}"', config)
-        self.assertIn(token, config)
+        self.assertIn(f'  token = "$INFLUX_TOKEN"', config)
         self.assertIn(f'urls = ["{url}:{port}"]', config)
         self.assertIn(f'bucket = "{bucket}"', config)
 
-- 
cgit v1.2.3


From 7f7be911b749b6c65ac3c6e57192e9e4ce2dcd24 Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Thu, 10 Feb 2022 22:05:19 +0000
Subject: openvpn: T4236: Add generator for ovpn configurations in op-mode

This generator generates client .ovpn files with required initial
configuration
It gets information from interface vtun, pki ca and certificates
---
 .../generate-openvpn-config-client.xml.in          |  58 +++++++++
 src/op_mode/generate_ovpn_client_file.py           | 145 +++++++++++++++++++++
 2 files changed, 203 insertions(+)
 create mode 100644 op-mode-definitions/generate-openvpn-config-client.xml.in
 create mode 100755 src/op_mode/generate_ovpn_client_file.py

diff --git a/op-mode-definitions/generate-openvpn-config-client.xml.in b/op-mode-definitions/generate-openvpn-config-client.xml.in
new file mode 100644
index 000000000..4f9f31bfe
--- /dev/null
+++ b/op-mode-definitions/generate-openvpn-config-client.xml.in
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="generate">
+    <children>
+      <node name="openvpn">
+        <properties>
+          <help>Generate OpenVPN client configuration ovpn file</help>
+        </properties>
+        <children>
+          <node name="client-config">
+            <properties>
+              <help>Generate Client config</help>
+            </properties>
+            <children>
+              <tagNode name="interface">
+                <properties>
+                  <help>Local interface used for connection</help>
+                  <completionHelp>
+                    <script>${vyos_completion_dir}/list_interfaces.py --type openvpn</script>
+                  </completionHelp>
+                </properties>
+                <children>
+                  <tagNode name="ca">
+                    <properties>
+                      <help>CA certificate</help>
+                      <completionHelp>
+                        <path>pki ca</path>
+                      </completionHelp>
+                    </properties>
+                    <children>
+                      <tagNode name="certificate">
+                        <properties>
+                          <help>Cerificate used by client</help>
+                          <completionHelp>
+                            <path>pki certificate</path>
+                          </completionHelp>
+                        </properties>
+                        <children>
+                          <tagNode name="key">
+                            <properties>
+                              <help>Certificate key used by client</help>
+                            </properties>
+                            <command>sudo ${vyos_op_scripts_dir}/generate_ovpn_client_file.py --interface "$5" --ca "$7" --cert "$9" --key "${11}"</command>
+                          </tagNode>
+                        </children>
+                        <command>sudo ${vyos_op_scripts_dir}/generate_ovpn_client_file.py --interface "$5" --ca "$7" --cert "$9"</command>
+                      </tagNode>
+                    </children>
+                  </tagNode>
+                </children>
+              </tagNode>
+            </children>
+          </node>
+        </children>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/src/op_mode/generate_ovpn_client_file.py b/src/op_mode/generate_ovpn_client_file.py
new file mode 100755
index 000000000..29db41e37
--- /dev/null
+++ b/src/op_mode/generate_ovpn_client_file.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import os
+
+from jinja2 import Template
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.ifconfig import Section
+from vyos.util import cmd
+
+
+client_config = """
+
+client
+nobind
+remote {{ remote_host }} {{ port }}
+remote-cert-tls server
+proto {{ 'tcp-client' if protocol == 'tcp-active' else 'udp' }}
+dev {{ device }}
+dev-type {{ device }}
+persist-key
+persist-tun
+verb 3
+
+# Encryption options
+{% if encryption is defined and encryption is not none %}
+{%   if encryption.cipher is defined and encryption.cipher is not none %}
+cipher {{ encryption.cipher }}
+{%     if encryption.cipher == 'bf128' %}
+keysize 128
+{%     elif encryption.cipher == 'bf256' %}
+keysize 256
+{%     endif %}
+{%   endif %}
+{%   if encryption.ncp_ciphers is defined and encryption.ncp_ciphers is not none %}
+data-ciphers {{ encryption.ncp_ciphers }}
+{%   endif %}
+{% endif %}
+
+{% if hash is defined and hash is not none %}
+auth {{ hash }}
+{% endif %}
+keysize 256
+comp-lzo {{ '' if use_lzo_compression is defined else 'no' }}
+
+<ca>
+-----BEGIN CERTIFICATE-----
+{{ ca }}
+-----END CERTIFICATE-----
+
+</ca>
+
+<cert>
+-----BEGIN CERTIFICATE-----
+{{ cert }}
+-----END CERTIFICATE-----
+
+</cert>
+
+<key>
+-----BEGIN PRIVATE KEY-----
+{{ key }}
+-----END PRIVATE KEY-----
+
+</key>
+
+"""
+
+config = ConfigTreeQuery()
+base = ['interfaces', 'openvpn']
+
+if not config.exists(base):
+    print('OpenVPN not configured')
+    exit(0)
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-i", "--interface", type=str, help='OpenVPN interface the client is connecting to', required=True)
+    parser.add_argument("-a", "--ca", type=str, help='OpenVPN CA cerificate', required=True)
+    parser.add_argument("-c", "--cert", type=str, help='OpenVPN client cerificate', required=True)
+    parser.add_argument("-k", "--key", type=str, help='OpenVPN client cerificate key', action="store")
+    args = parser.parse_args()
+
+    interface = args.interface
+    ca = args.ca
+    cert = args.cert
+    key = args.key
+    if not key:
+        key = args.cert
+
+    if interface not in Section.interfaces('openvpn'):
+        exit(f'OpenVPN interface "{interface}" does not exist!')
+
+    if not config.exists(['pki', 'ca', ca, 'certificate']):
+        exit(f'OpenVPN CA certificate "{ca}" does not exist!')
+
+    if not config.exists(['pki', 'certificate', cert, 'certificate']):
+        exit(f'OpenVPN certificate "{cert}" does not exist!')
+
+    if not config.exists(['pki', 'certificate', cert, 'private', 'key']):
+        exit(f'OpenVPN certificate key "{key}" does not exist!')
+
+    ca = config.value(['pki', 'ca', ca, 'certificate'])
+    cert = config.value(['pki', 'certificate', cert, 'certificate'])
+    key = config.value(['pki', 'certificate', key, 'private', 'key'])
+    remote_host = config.value(base + [interface, 'local-host'])
+
+    ovpn_conf = config.get_config_dict(base + [interface], key_mangling=('-', '_'), get_first_key=True)
+
+    port = '1194' if 'local_port' not in ovpn_conf else ovpn_conf['local_port']
+    proto = 'udp' if 'protocol' not in ovpn_conf else ovpn_conf['protocol']
+    device = 'tun' if 'device_type' not in ovpn_conf else ovpn_conf['device_type']
+
+    config = {
+        'interface'   : interface,
+        'ca'          : ca,
+        'cert'        : cert,
+        'key'         : key,
+        'device'      : device,
+        'port'        : port,
+        'proto'       : proto,
+        'remote_host' : remote_host,
+        'address'     : [],
+    }
+
+# Clear out terminal first
+print('\x1b[2J\x1b[H')
+client = Template(client_config, trim_blocks=True).render(config)
+print(client)
-- 
cgit v1.2.3


From 27daf4a6cd4928be41ed08330ccc1b7f04ad2638 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 12 Feb 2022 08:44:43 +0100
Subject: policy: T2199: bugfix verify_rule() on negated groups

Related to #1215
---
 src/conf_mode/policy-route.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py
index 82f668acf..3d1d7d8c5 100755
--- a/src/conf_mode/policy-route.py
+++ b/src/conf_mode/policy-route.py
@@ -123,6 +123,10 @@ def verify_rule(policy, name, rule_conf, ipv6):
                 for group in valid_groups:
                     if group in side_conf['group']:
                         group_name = side_conf['group'][group]
+
+                        if group_name.startswith('!'):
+                            group_name = group_name[1:]
+
                         fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group
                         error_group = fw_group.replace("_", "-")
                         group_obj = dict_search_args(policy['firewall_group'], fw_group, group_name)
-- 
cgit v1.2.3


From b40315b3c5051888f499961e63410e14c5d1bad7 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 13 Feb 2022 20:04:33 +0100
Subject: vyos.util: T4191: add new sysctl() helper function

---
 python/vyos/util.py | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/python/vyos/util.py b/python/vyos/util.py
index 571d43754..1767ff9d3 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -997,3 +997,12 @@ def boot_configuration_complete() -> bool:
     if os.path.isfile(config_status):
         return True
     return False
+
+def sysctl(name, value):
+    """ Change value via sysctl() - return True if changed, False otherwise """
+    tmp = cmd(f'sysctl {name}')
+    # last list index contains the actual value - only write if value differs
+    if tmp.split()[-1] != str(value):
+        call(f'sysctl -wq {name}={value}')
+        return True
+    return False
-- 
cgit v1.2.3


From 2cec431e5caf9df85640f707cd6dc3077c17c238 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 13 Feb 2022 20:29:25 +0100
Subject: vrf: T4191: bugfix for "ip rule" when VRFs are created

We always mangled and worked on the "ip rule" singleton even when nothing
needed to be changed. This resulted in a VRF hickup when the same VRF was added
and removed multiple times.

set interfaces ethernet eth1 vrf foo
set vrf name foo table '1000'
commit

delete interfaces ethernet eth1 vrf
delete vrf
commit

set interfaces ethernet eth1 vrf foo
set vrf name foo table '1000'
commit

broke reachability on eth1 - a reboot was required.

This change will now only alter the ip rule tables once when VRF instances
are created for the first time and will not touch the Kernel "ip rule"
representation afterwards.
---
 src/conf_mode/vrf.py | 108 ++++++++++++++++++++++++++-------------------------
 1 file changed, 56 insertions(+), 52 deletions(-)

diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 38c0c4463..cfe0f4d8e 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -29,6 +29,7 @@ from vyos.util import dict_search
 from vyos.util import get_interface_config
 from vyos.util import popen
 from vyos.util import run
+from vyos.util import sysctl
 from vyos import ConfigError
 from vyos import frr
 from vyos import airbag
@@ -37,10 +38,16 @@ airbag.enable()
 config_file = '/etc/iproute2/rt_tables.d/vyos-vrf.conf'
 nft_vrf_config = '/tmp/nftables-vrf-zones'
 
-def list_rules():
-    command = 'ip -j -4 rule show'
-    answer = loads(cmd(command))
-    return [_ for _ in answer if _]
+def has_rule(af : str, priority : int, table : str):
+    """ Check if a given ip rule exists """
+    if af not in ['-4', '-6']:
+        raise ValueError()
+    command = f'ip -j {af} rule show'
+    for tmp in loads(cmd(command)):
+        if {'priority', 'table'} <= set(tmp):
+            if tmp['priority'] == priority and tmp['table'] == table:
+                return True
+    return False
 
 def vrf_interfaces(c, match):
     matched = []
@@ -69,7 +76,6 @@ def vrf_routing(c, match):
     c.set_level(old_level)
     return matched
 
-
 def get_config(config=None):
     if config:
         conf = config
@@ -148,13 +154,11 @@ def apply(vrf):
     bind_all = '0'
     if 'bind-to-all' in vrf:
         bind_all = '1'
-    call(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}')
-    call(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}')
+    sysctl('net.ipv4.tcp_l3mdev_accept', bind_all)
+    sysctl('net.ipv4.udp_l3mdev_accept', bind_all)
 
     for tmp in (dict_search('vrf_remove', vrf) or []):
         if os.path.isdir(f'/sys/class/net/{tmp}'):
-            call(f'ip -4 route del vrf {tmp} unreachable default metric 4278198272')
-            call(f'ip -6 route del vrf {tmp} unreachable default metric 4278198272')
             call(f'ip link delete dev {tmp}')
             # Remove nftables conntrack zone map item
             nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{tmp}" }}'
@@ -165,31 +169,59 @@ def apply(vrf):
         # check if table already exists
         _, err = popen('nft list table inet vrf_zones')
         # If not, create a table
-        if err:
-            if os.path.exists(nft_vrf_config):
-                cmd(f'nft -f {nft_vrf_config}')
-                os.unlink(nft_vrf_config)
+        if err and os.path.exists(nft_vrf_config):
+            cmd(f'nft -f {nft_vrf_config}')
+            os.unlink(nft_vrf_config)
+
+        # Linux routing uses rules to find tables - routing targets are then
+        # looked up in those tables. If the lookup got a matching route, the
+        # process ends.
+        #
+        # TL;DR; first table with a matching entry wins!
+        #
+        # You can see your routing table lookup rules using "ip rule", sadly the
+        # local lookup is hit before any VRF lookup. Pinging an addresses from the
+        # VRF will usually find a hit in the local table, and never reach the VRF
+        # routing table - this is usually not what you want. Thus we will
+        # re-arrange the tables and move the local lookup further down once VRFs
+        # are enabled.
+        #
+        # Thanks to https://stbuehler.de/blog/article/2020/02/29/using_vrf__virtual_routing_and_forwarding__on_linux.html
+
+        for afi in ['-4', '-6']:
+            # move lookup local to pref 32765 (from 0)
+            if not has_rule(afi, 32765, 'local'):
+                call(f'ip {afi} rule add pref 32765 table local')
+            if has_rule(afi, 0, 'local'):
+                call(f'ip {afi} rule del pref 0')
+            # make sure that in VRFs after failed lookup in the VRF specific table
+            # nothing else is reached
+            if not has_rule(afi, 1000, 'l3mdev'):
+                # this should be added by the kernel when a VRF is created
+                # add it here for completeness
+                call(f'ip {afi} rule add pref 1000 l3mdev protocol kernel')
+
+            # add another rule with an unreachable target which only triggers in VRF context
+            # if a route could not be reached
+            if not has_rule(afi, 2000, 'l3mdev'):
+                call(f'ip {afi} rule add pref 2000 l3mdev unreachable')
 
         for name, config in vrf['name'].items():
             table = config['table']
-
             if not os.path.isdir(f'/sys/class/net/{name}'):
                 # For each VRF apart from your default context create a VRF
                 # interface with a separate routing table
                 call(f'ip link add {name} type vrf table {table}')
-                # The kernel Documentation/networking/vrf.txt also recommends
-                # adding unreachable routes to the VRF routing tables so that routes
-                # afterwards are taken.
-                call(f'ip -4 route add vrf {name} unreachable default metric 4278198272')
-                call(f'ip -6 route add vrf {name} unreachable default metric 4278198272')
-                # We also should add proper loopback IP addresses to the newly
-                # created VRFs for services bound to the loopback address (SNMP, NTP)
-                call(f'ip -4 addr add 127.0.0.1/8 dev {name}')
-                call(f'ip -6 addr add ::1/128 dev {name}')
 
             # set VRF description for e.g. SNMP monitoring
             vrf_if = Interface(name)
+            # We also should add proper loopback IP addresses to the newly
+            # created VRFs for services bound to the loopback address (SNMP, NTP)
+            vrf_if.add_addr('127.0.0.1/8')
+            vrf_if.add_addr('::1/128')
+            # add VRF description if available
             vrf_if.set_alias(config.get('description', ''))
+
             # Enable/Disable of an interface must always be done at the end of the
             # derived class to make use of the ref-counting set_admin_state()
             # function. We will only enable the interface if 'up' was called as
@@ -203,37 +235,9 @@ def apply(vrf):
             nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}'
             cmd(f'nft {nft_add_element}')
 
-    # Linux routing uses rules to find tables - routing targets are then
-    # looked up in those tables. If the lookup got a matching route, the
-    # process ends.
-    #
-    # TL;DR; first table with a matching entry wins!
-    #
-    # You can see your routing table lookup rules using "ip rule", sadly the
-    # local lookup is hit before any VRF lookup. Pinging an addresses from the
-    # VRF will usually find a hit in the local table, and never reach the VRF
-    # routing table - this is usually not what you want. Thus we will
-    # re-arrange the tables and move the local lookup furhter down once VRFs
-    # are enabled.
-
-    # get current preference on local table
-    local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0]
-
-    # change preference when VRFs are enabled and local lookup table is default
-    if not local_pref and 'name' in vrf:
-        for af in ['-4', '-6']:
-            call(f'ip {af} rule add pref 32765 table local')
-            call(f'ip {af} rule del pref 0')
 
     # return to default lookup preference when no VRF is configured
     if 'name' not in vrf:
-        for af in ['-4', '-6']:
-            call(f'ip {af} rule add pref 0 table local')
-            call(f'ip {af} rule del pref 32765')
-
-            # clean out l3mdev-table rule if present
-            if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]:
-                call(f'ip {af} rule del pref 1000')
         # Remove VRF zones table from nftables
         tmp = run('nft list table inet vrf_zones')
         if tmp == 0:
-- 
cgit v1.2.3


From 812d9770619b968b04961aebf3944fde13df491b Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 13 Feb 2022 20:32:10 +0100
Subject: ethernet: T4242: speed/duplex can never be switched back to auto/auto

---
 python/vyos/ethtool.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py
index e45b0f041..80d44eee6 100644
--- a/python/vyos/ethtool.py
+++ b/python/vyos/ethtool.py
@@ -41,7 +41,7 @@ class Ethtool:
     #   '100' : {'full': '', 'half': ''},
     #   '1000': {'full': ''}
     #  }
-    _speed_duplex = { }
+    _speed_duplex = {'auto': {'auto': ''}}
     _ring_buffers = { }
     _ring_buffers_max = { }
     _driver_name = None
-- 
cgit v1.2.3


From 6f1326d6b68f6dcb83843374c876407ef2922bd1 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Mon, 14 Feb 2022 19:07:29 +0100
Subject: tunnel: T4154: verify() no more then one GRE tunnel is used w/o "ip
 key" per interface

It is impossible for the OS kernel to distinguish multiple GRE tunnels when no
"gre key" is configured when sourcing tunnels from the same interface.
---
 src/conf_mode/interfaces-tunnel.py | 39 +++++++++++++++++++++++++-------------
 1 file changed, 26 insertions(+), 13 deletions(-)

diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index 30f57ec0c..c9267e749 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -103,19 +103,22 @@ def verify(tunnel):
         raise ConfigError('Tunnel parameters ip key must be set!')
 
     if tunnel['encapsulation'] in ['gre', 'gretap']:
-        if dict_search('parameters.ip.key', tunnel) != None:
-            # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
-            # Prevent the same key for 2 tunnels with same source-address/encap. T2920
-            for tunnel_if in Section.interfaces('tunnel'):
-                # It makes no sense to run the test for re-used GRE keys on our
-                # own interface we are currently working on
-                if tunnel['ifname'] == tunnel_if:
-                    continue
-                tunnel_cfg = get_interface_config(tunnel_if)
-                # no match on encapsulation - bail out
-                if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']:
-                    continue
-                new_source_address = dict_search('source_address', tunnel)
+        # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
+        # Prevent the same key for 2 tunnels with same source-address/encap. T2920
+        for tunnel_if in Section.interfaces('tunnel'):
+            # It makes no sense to run the test against our own interface we
+            # are currently configuring
+            if tunnel['ifname'] == tunnel_if:
+                continue
+
+            tunnel_cfg = get_interface_config(tunnel_if)
+            # no match on encapsulation - bail out
+            if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']:
+                continue
+
+            new_source_address = dict_search('source_address', tunnel)
+            new_source_interface = dict_search('source_interface', tunnel)
+            if dict_search('parameters.ip.key', tunnel) != None:
                 # Convert tunnel key to ip key, format "ip -j link show"
                 # 1 => 0.0.0.1, 999 => 0.0.3.231
                 orig_new_key = dict_search('parameters.ip.key', tunnel)
@@ -125,6 +128,16 @@ def verify(tunnel):
                    dict_search('linkinfo.info_data.ikey', tunnel_cfg) == new_key:
                     raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \
                                       f'is already used for tunnel "{tunnel_if}"!')
+            else:
+                # If no IP GRE key is used we can not have more then one GRE tunnel
+                # bound to any one interface/IP address. This will result in a OS
+                # PermissionError: add tunnel "gre0" failed: File exists
+                if (dict_search('address', tunnel_cfg) == new_source_address or
+                   (dict_search('address', tunnel_cfg) == '0.0.0.0' and
+                    dict_search('link', tunnel_cfg) == new_source_interface)):
+                    raise ConfigError(f'Missing required "ip key" parameter when \
+                                      running more then one GRE based tunnel on the \
+                                      same source-interface/source-address')
 
     # Keys are not allowed with ipip and sit tunnels
     if tunnel['encapsulation'] in ['ipip', 'sit']:
-- 
cgit v1.2.3


From 122c7a53575f67759f157e02eca776f799658dc1 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Mon, 14 Feb 2022 19:09:09 +0100
Subject: tunnel: T4154: import cleanup

---
 src/conf_mode/interfaces-tunnel.py | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index c9267e749..4c1204b4e 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 yOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -21,9 +21,7 @@ from netifaces import interfaces
 from ipaddress import IPv4Address
 
 from vyos.config import Config
-from vyos.configdict import dict_merge
 from vyos.configdict import get_interface_dict
-from vyos.configdict import node_changed
 from vyos.configdict import leaf_node_changed
 from vyos.configverify import verify_address
 from vyos.configverify import verify_bridge_delete
@@ -34,8 +32,6 @@ from vyos.configverify import verify_tunnel
 from vyos.ifconfig import Interface
 from vyos.ifconfig import Section
 from vyos.ifconfig import TunnelIf
-from vyos.template import is_ipv4
-from vyos.template import is_ipv6
 from vyos.util import get_interface_config
 from vyos.util import dict_search
 from vyos import ConfigError
-- 
cgit v1.2.3


From e00edb0072ceb07b92be826984154afeb6c567d3 Mon Sep 17 00:00:00 2001
From: Andrew Gunnerson <chillermillerlong@hotmail.com>
Date: Mon, 14 Feb 2022 17:02:13 -0500
Subject: pki: eapol: T4244: Fix KeyError when CA cert name differs from client
 cert name

This commit fixes a small typo where the client cert name was being used
to index the CA configuration dict.

Signed-off-by: Andrew Gunnerson <chillermillerlong@hotmail.com>
---
 python/vyos/configverify.py          | 2 +-
 src/conf_mode/interfaces-ethernet.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index 365a28feb..18fb7f9f7 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -173,7 +173,7 @@ def verify_eapol(config):
             if ca_cert_name not in config['pki']['ca']:
                 raise ConfigError('Invalid CA certificate specified for EAPoL')
 
-            ca_cert = config['pki']['ca'][cert_name]
+            ca_cert = config['pki']['ca'][ca_cert_name]
 
             if 'certificate' not in ca_cert:
                 raise ConfigError('Invalid CA certificate specified for EAPoL')
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index e7250fb49..ab8d58f81 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -165,7 +165,7 @@ def generate(ethernet):
         if 'ca_certificate' in ethernet['eapol']:
             ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem')
             ca_cert_name = ethernet['eapol']['ca_certificate']
-            pki_ca_cert = ethernet['pki']['ca'][cert_name]
+            pki_ca_cert = ethernet['pki']['ca'][ca_cert_name]
 
             write_file(ca_cert_file_path,
                        wrap_certificate(pki_ca_cert['certificate']))
-- 
cgit v1.2.3


From 283688fe52bd762d1fadff635de58a87df3da629 Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Fri, 11 Feb 2022 13:17:05 +0000
Subject: conntrack-sync: T4237: Fix checks for listen-address list to str

Verify section conntrack_sync.py funciton 'is_addr_assigned'
should checks address as string not as list

(cherry picked from commit c41c51e4ed7ceb293161014a73bdd350162c3300)
---
 src/conf_mode/conntrack_sync.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/conf_mode/conntrack_sync.py b/src/conf_mode/conntrack_sync.py
index 8f9837c2b..34d1f7398 100755
--- a/src/conf_mode/conntrack_sync.py
+++ b/src/conf_mode/conntrack_sync.py
@@ -93,9 +93,9 @@ def verify(conntrack):
             raise ConfigError('Can not configure expect-sync "all" with other protocols!')
 
     if 'listen_address' in conntrack:
-        address = conntrack['listen_address']
-        if not is_addr_assigned(address):
-            raise ConfigError(f'Specified listen-address {address} not assigned to any interface!')
+        for address in conntrack['listen_address']:
+            if not is_addr_assigned(address):
+                raise ConfigError(f'Specified listen-address {address} not assigned to any interface!')
 
     vrrp_group = dict_search('failover_mechanism.vrrp.sync_group', conntrack)
     if vrrp_group == None:
-- 
cgit v1.2.3


From 1ceaed55a629c92cf42baccdef4106e8d0e4914e Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Wed, 16 Feb 2022 22:16:01 +0100
Subject: wireless: T4240: bugfix interface bridging

VLAN isolation can not be "set" when interface is of type wifi.
---
 python/vyos/ifconfig/bridge.py   | 12 +++++++-----
 python/vyos/ifconfig/wireless.py |  6 +++---
 2 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py
index 27073b266..ffd9c590f 100644
--- a/python/vyos/ifconfig/bridge.py
+++ b/python/vyos/ifconfig/bridge.py
@@ -298,7 +298,6 @@ class BridgeIf(Interface):
 
         tmp = dict_search('member.interface', config)
         if tmp:
-
             for interface, interface_config in tmp.items():
                 # if interface does yet not exist bail out early and
                 # add it later
@@ -316,10 +315,13 @@ class BridgeIf(Interface):
                 # enslave interface port to bridge
                 self.add_port(interface)
 
-                # always set private-vlan/port isolation
-                tmp = dict_search('isolated', interface_config)
-                value = 'on' if (tmp != None) else 'off'
-                lower.set_port_isolation(value)
+                if not interface.startswith('wlan'):
+                    # always set private-vlan/port isolation - this can not be
+                    # done when lower link is a wifi link, as it will trigger:
+                    # RTNETLINK answers: Operation not supported
+                    tmp = dict_search('isolated', interface_config)
+                    value = 'on' if (tmp != None) else 'off'
+                    lower.set_port_isolation(value)
 
                 # set bridge port path cost
                 if 'cost' in interface_config:
diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py
index 748b6e02d..88eaa772b 100644
--- a/python/vyos/ifconfig/wireless.py
+++ b/python/vyos/ifconfig/wireless.py
@@ -49,10 +49,10 @@ class WiFiIf(Interface):
         on any interface. """
 
         # We can not call add_to_bridge() until wpa_supplicant is running, thus
-        # we will remove the key from the config dict and react to this specal
-        # case in thie derived class.
+        # we will remove the key from the config dict and react to this special
+        # case in this derived class.
         # re-add ourselves to any bridge we might have fallen out of
-        bridge_member = ''
+        bridge_member = None
         if 'is_bridge_member' in config:
             bridge_member = config['is_bridge_member']
             del config['is_bridge_member']
-- 
cgit v1.2.3


From f076f9f4cf6e6b7d89eada9f5d59bacea0f3af72 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Wed, 16 Feb 2022 22:17:48 +0100
Subject: policy: T2425: add completion helper script when referencing IP
 addresses

---
 interface-definitions/policy.xml.in | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in
index 61c5ab90a..9767285dd 100644
--- a/interface-definitions/policy.xml.in
+++ b/interface-definitions/policy.xml.in
@@ -1113,6 +1113,9 @@
                   <leafNode name="ip-next-hop">
                     <properties>
                       <help>Nexthop IP address</help>
+                      <completionHelp>
+                        <script>${vyos_completion_dir}/list_local_ips.sh --ipv4</script>
+                      </completionHelp>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>IP address</description>
@@ -1130,6 +1133,9 @@
                       <leafNode name="global">
                         <properties>
                           <help>Nexthop IPv6 global address</help>
+                          <completionHelp>
+                            <script>${vyos_completion_dir}/list_local_ips.sh --ipv6</script>
+                          </completionHelp>
                           <valueHelp>
                             <format>ipv6</format>
                             <description>IPv6 address and prefix length</description>
@@ -1142,6 +1148,9 @@
                       <leafNode name="local">
                         <properties>
                           <help>Nexthop IPv6 local address</help>
+                          <completionHelp>
+                            <script>${vyos_completion_dir}/list_local_ips.sh --ipv6</script>
+                          </completionHelp>
                           <valueHelp>
                             <format>ipv6</format>
                             <description>IPv6 address and prefix length</description>
@@ -1268,6 +1277,9 @@
                   <leafNode name="src">
                     <properties>
                       <help>Source address for route</help>
+                      <completionHelp>
+                        <script>${vyos_completion_dir}/list_local_ips.sh --both</script>
+                      </completionHelp>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>IPv4 address</description>
-- 
cgit v1.2.3


From 3795fdba8edf8e81298370d6cd8d81a779ae2997 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Wed, 16 Feb 2022 11:31:23 -0600
Subject: xml: T3474: add component version include files

Add the include files containing the syntaxVersion element defining the
version of the respective component; these files are included by the top
level file 'xml-component-versions.xml.in'. Processing of these elements
was previously added to the python xml lib in commit 40f5359d. This will
replace the use of 'curver_DATA' in vyatta-cfg-system and other legacy
packages.
---
 .../include/version/bgp-version.xml.i              |  3 ++
 .../include/version/broadcast-relay-version.xml.i  |  3 ++
 .../include/version/cluster-version.xml.i          |  3 ++
 .../version/config-management-version.xml.i        |  3 ++
 .../include/version/conntrack-sync-version.xml.i   |  3 ++
 .../include/version/conntrack-version.xml.i        |  3 ++
 .../include/version/dhcp-relay-version.xml.i       |  3 ++
 .../include/version/dhcp-server-version.xml.i      |  3 ++
 .../include/version/dhcpv6-server-version.xml.i    |  3 ++
 .../include/version/dns-forwarding-version.xml.i   |  3 ++
 .../include/version/firewall-version.xml.i         |  3 ++
 .../include/version/flow-accounting-version.xml.i  |  3 ++
 .../include/version/https-version.xml.i            |  3 ++
 .../include/version/interfaces-version.xml.i       |  3 ++
 .../include/version/ipoe-server-version.xml.i      |  3 ++
 .../include/version/ipsec-version.xml.i            |  3 ++
 .../include/version/isis-version.xml.i             |  3 ++
 .../include/version/l2tp-version.xml.i             |  3 ++
 .../include/version/lldp-version.xml.i             |  3 ++
 .../include/version/mdns-version.xml.i             |  3 ++
 .../include/version/nat-version.xml.i              |  3 ++
 .../include/version/nat66-version.xml.i            |  3 ++
 .../include/version/ntp-version.xml.i              |  3 ++
 .../include/version/openconnect-version.xml.i      |  3 ++
 .../include/version/ospf-version.xml.i             |  3 ++
 .../include/version/policy-version.xml.i           |  3 ++
 .../include/version/pppoe-server-version.xml.i     |  3 ++
 .../include/version/pptp-version.xml.i             |  3 ++
 .../include/version/qos-version.xml.i              |  3 ++
 .../include/version/quagga-version.xml.i           |  3 ++
 .../include/version/rpki-version.xml.i             |  3 ++
 .../include/version/salt-version.xml.i             |  3 ++
 .../include/version/snmp-version.xml.i             |  3 ++
 .../include/version/ssh-version.xml.i              |  3 ++
 .../include/version/sstp-version.xml.i             |  3 ++
 .../include/version/system-version.xml.i           |  3 ++
 .../include/version/vrf-version.xml.i              |  3 ++
 .../include/version/vrrp-version.xml.i             |  3 ++
 .../include/version/vyos-accel-ppp-version.xml.i   |  3 ++
 .../include/version/wanloadbalance-version.xml.i   |  3 ++
 .../include/version/webproxy-version.xml.i         |  3 ++
 interface-definitions/xml-component-version.xml.in | 44 ++++++++++++++++++++++
 python/vyos/xml/__init__.py                        |  4 +-
 python/vyos/xml/definition.py                      |  9 +++--
 44 files changed, 174 insertions(+), 6 deletions(-)
 create mode 100644 interface-definitions/include/version/bgp-version.xml.i
 create mode 100644 interface-definitions/include/version/broadcast-relay-version.xml.i
 create mode 100644 interface-definitions/include/version/cluster-version.xml.i
 create mode 100644 interface-definitions/include/version/config-management-version.xml.i
 create mode 100644 interface-definitions/include/version/conntrack-sync-version.xml.i
 create mode 100644 interface-definitions/include/version/conntrack-version.xml.i
 create mode 100644 interface-definitions/include/version/dhcp-relay-version.xml.i
 create mode 100644 interface-definitions/include/version/dhcp-server-version.xml.i
 create mode 100644 interface-definitions/include/version/dhcpv6-server-version.xml.i
 create mode 100644 interface-definitions/include/version/dns-forwarding-version.xml.i
 create mode 100644 interface-definitions/include/version/firewall-version.xml.i
 create mode 100644 interface-definitions/include/version/flow-accounting-version.xml.i
 create mode 100644 interface-definitions/include/version/https-version.xml.i
 create mode 100644 interface-definitions/include/version/interfaces-version.xml.i
 create mode 100644 interface-definitions/include/version/ipoe-server-version.xml.i
 create mode 100644 interface-definitions/include/version/ipsec-version.xml.i
 create mode 100644 interface-definitions/include/version/isis-version.xml.i
 create mode 100644 interface-definitions/include/version/l2tp-version.xml.i
 create mode 100644 interface-definitions/include/version/lldp-version.xml.i
 create mode 100644 interface-definitions/include/version/mdns-version.xml.i
 create mode 100644 interface-definitions/include/version/nat-version.xml.i
 create mode 100644 interface-definitions/include/version/nat66-version.xml.i
 create mode 100644 interface-definitions/include/version/ntp-version.xml.i
 create mode 100644 interface-definitions/include/version/openconnect-version.xml.i
 create mode 100644 interface-definitions/include/version/ospf-version.xml.i
 create mode 100644 interface-definitions/include/version/policy-version.xml.i
 create mode 100644 interface-definitions/include/version/pppoe-server-version.xml.i
 create mode 100644 interface-definitions/include/version/pptp-version.xml.i
 create mode 100644 interface-definitions/include/version/qos-version.xml.i
 create mode 100644 interface-definitions/include/version/quagga-version.xml.i
 create mode 100644 interface-definitions/include/version/rpki-version.xml.i
 create mode 100644 interface-definitions/include/version/salt-version.xml.i
 create mode 100644 interface-definitions/include/version/snmp-version.xml.i
 create mode 100644 interface-definitions/include/version/ssh-version.xml.i
 create mode 100644 interface-definitions/include/version/sstp-version.xml.i
 create mode 100644 interface-definitions/include/version/system-version.xml.i
 create mode 100644 interface-definitions/include/version/vrf-version.xml.i
 create mode 100644 interface-definitions/include/version/vrrp-version.xml.i
 create mode 100644 interface-definitions/include/version/vyos-accel-ppp-version.xml.i
 create mode 100644 interface-definitions/include/version/wanloadbalance-version.xml.i
 create mode 100644 interface-definitions/include/version/webproxy-version.xml.i
 create mode 100644 interface-definitions/xml-component-version.xml.in

diff --git a/interface-definitions/include/version/bgp-version.xml.i b/interface-definitions/include/version/bgp-version.xml.i
new file mode 100644
index 000000000..15bc5abd4
--- /dev/null
+++ b/interface-definitions/include/version/bgp-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/bgp-version.xml.i -->
+<syntaxVersion component='bgp' version='2'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/broadcast-relay-version.xml.i b/interface-definitions/include/version/broadcast-relay-version.xml.i
new file mode 100644
index 000000000..98481f446
--- /dev/null
+++ b/interface-definitions/include/version/broadcast-relay-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/broadcast-relay-version.xml.i -->
+<syntaxVersion component='broadcast-relay' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/cluster-version.xml.i b/interface-definitions/include/version/cluster-version.xml.i
new file mode 100644
index 000000000..621996df4
--- /dev/null
+++ b/interface-definitions/include/version/cluster-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/cluster-version.xml.i -->
+<syntaxVersion component='cluster' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/config-management-version.xml.i b/interface-definitions/include/version/config-management-version.xml.i
new file mode 100644
index 000000000..695ba09ab
--- /dev/null
+++ b/interface-definitions/include/version/config-management-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/config-management-version.xml.i -->
+<syntaxVersion component='config-management' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/conntrack-sync-version.xml.i b/interface-definitions/include/version/conntrack-sync-version.xml.i
new file mode 100644
index 000000000..f040c29f6
--- /dev/null
+++ b/interface-definitions/include/version/conntrack-sync-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/conntrack-sync-version.xml.i -->
+<syntaxVersion component='conntrack-sync' version='2'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/conntrack-version.xml.i b/interface-definitions/include/version/conntrack-version.xml.i
new file mode 100644
index 000000000..696f76362
--- /dev/null
+++ b/interface-definitions/include/version/conntrack-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/conntrack-version.xml.i -->
+<syntaxVersion component='conntrack' version='3'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/dhcp-relay-version.xml.i b/interface-definitions/include/version/dhcp-relay-version.xml.i
new file mode 100644
index 000000000..75f5d5486
--- /dev/null
+++ b/interface-definitions/include/version/dhcp-relay-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/dhcp-relay-version.xml.i -->
+<syntaxVersion component='dhcp-relay' version='2'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/dhcp-server-version.xml.i b/interface-definitions/include/version/dhcp-server-version.xml.i
new file mode 100644
index 000000000..330cb7d1b
--- /dev/null
+++ b/interface-definitions/include/version/dhcp-server-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/dhcp-server-version.xml.i -->
+<syntaxVersion component='dhcp-server' version='6'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/dhcpv6-server-version.xml.i b/interface-definitions/include/version/dhcpv6-server-version.xml.i
new file mode 100644
index 000000000..4b2cf40aa
--- /dev/null
+++ b/interface-definitions/include/version/dhcpv6-server-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/dhcpv6-server-version.xml.i -->
+<syntaxVersion component='dhcpv6-server' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/dns-forwarding-version.xml.i b/interface-definitions/include/version/dns-forwarding-version.xml.i
new file mode 100644
index 000000000..fe817940a
--- /dev/null
+++ b/interface-definitions/include/version/dns-forwarding-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/dns-forwarding-version.xml.i -->
+<syntaxVersion component='dns-forwarding' version='3'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/firewall-version.xml.i b/interface-definitions/include/version/firewall-version.xml.i
new file mode 100644
index 000000000..059a89f24
--- /dev/null
+++ b/interface-definitions/include/version/firewall-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/firewall-version.xml.i -->
+<syntaxVersion component='firewall' version='7'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/flow-accounting-version.xml.i b/interface-definitions/include/version/flow-accounting-version.xml.i
new file mode 100644
index 000000000..5b01fe4b5
--- /dev/null
+++ b/interface-definitions/include/version/flow-accounting-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/flow-accounting-version.xml.i -->
+<syntaxVersion component='flow-accounting' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/https-version.xml.i b/interface-definitions/include/version/https-version.xml.i
new file mode 100644
index 000000000..586083649
--- /dev/null
+++ b/interface-definitions/include/version/https-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/https-version.xml.i -->
+<syntaxVersion component='https' version='3'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/interfaces-version.xml.i b/interface-definitions/include/version/interfaces-version.xml.i
new file mode 100644
index 000000000..b97971531
--- /dev/null
+++ b/interface-definitions/include/version/interfaces-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/interfaces-version.xml.i -->
+<syntaxVersion component='interfaces' version='25'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/ipoe-server-version.xml.i b/interface-definitions/include/version/ipoe-server-version.xml.i
new file mode 100644
index 000000000..00d2544e6
--- /dev/null
+++ b/interface-definitions/include/version/ipoe-server-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/ipoe-server-version.xml.i -->
+<syntaxVersion component='ipoe-server' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/ipsec-version.xml.i b/interface-definitions/include/version/ipsec-version.xml.i
new file mode 100644
index 000000000..fcdd6c702
--- /dev/null
+++ b/interface-definitions/include/version/ipsec-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/ipsec-version.xml.i -->
+<syntaxVersion component='ipsec' version='8'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/isis-version.xml.i b/interface-definitions/include/version/isis-version.xml.i
new file mode 100644
index 000000000..4a8fef39c
--- /dev/null
+++ b/interface-definitions/include/version/isis-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/isis-version.xml.i -->
+<syntaxVersion component='isis' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/l2tp-version.xml.i b/interface-definitions/include/version/l2tp-version.xml.i
new file mode 100644
index 000000000..86114d676
--- /dev/null
+++ b/interface-definitions/include/version/l2tp-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/l2tp-version.xml.i -->
+<syntaxVersion component='l2tp' version='4'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/lldp-version.xml.i b/interface-definitions/include/version/lldp-version.xml.i
new file mode 100644
index 000000000..0deb73279
--- /dev/null
+++ b/interface-definitions/include/version/lldp-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/lldp-version.xml.i -->
+<syntaxVersion component='lldp' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/mdns-version.xml.i b/interface-definitions/include/version/mdns-version.xml.i
new file mode 100644
index 000000000..b200a68b4
--- /dev/null
+++ b/interface-definitions/include/version/mdns-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/mdns-version.xml.i -->
+<syntaxVersion component='mdns' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/nat-version.xml.i b/interface-definitions/include/version/nat-version.xml.i
new file mode 100644
index 000000000..027216a07
--- /dev/null
+++ b/interface-definitions/include/version/nat-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/nat-version.xml.i -->
+<syntaxVersion component='nat' version='5'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/nat66-version.xml.i b/interface-definitions/include/version/nat66-version.xml.i
new file mode 100644
index 000000000..7b7123dcc
--- /dev/null
+++ b/interface-definitions/include/version/nat66-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/nat66-version.xml.i -->
+<syntaxVersion component='nat66' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/ntp-version.xml.i b/interface-definitions/include/version/ntp-version.xml.i
new file mode 100644
index 000000000..cc4ff9a1c
--- /dev/null
+++ b/interface-definitions/include/version/ntp-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/ntp-version.xml.i -->
+<syntaxVersion component='ntp' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/openconnect-version.xml.i b/interface-definitions/include/version/openconnect-version.xml.i
new file mode 100644
index 000000000..d7d35b321
--- /dev/null
+++ b/interface-definitions/include/version/openconnect-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/openconnect-version.xml.i -->
+<syntaxVersion component='openconnect' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/ospf-version.xml.i b/interface-definitions/include/version/ospf-version.xml.i
new file mode 100644
index 000000000..755965daa
--- /dev/null
+++ b/interface-definitions/include/version/ospf-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/ospf-version.xml.i -->
+<syntaxVersion component='ospf' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/policy-version.xml.i b/interface-definitions/include/version/policy-version.xml.i
new file mode 100644
index 000000000..6d0c80518
--- /dev/null
+++ b/interface-definitions/include/version/policy-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/policy-version.xml.i -->
+<syntaxVersion component='policy' version='2'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/pppoe-server-version.xml.i b/interface-definitions/include/version/pppoe-server-version.xml.i
new file mode 100644
index 000000000..ec81487f8
--- /dev/null
+++ b/interface-definitions/include/version/pppoe-server-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/pppoe-server-version.xml.i -->
+<syntaxVersion component='pppoe-server' version='5'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/pptp-version.xml.i b/interface-definitions/include/version/pptp-version.xml.i
new file mode 100644
index 000000000..0296c44e9
--- /dev/null
+++ b/interface-definitions/include/version/pptp-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/pptp-version.xml.i -->
+<syntaxVersion component='pptp' version='2'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/qos-version.xml.i b/interface-definitions/include/version/qos-version.xml.i
new file mode 100644
index 000000000..e4d139349
--- /dev/null
+++ b/interface-definitions/include/version/qos-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/qos-version.xml.i -->
+<syntaxVersion component='qos' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/quagga-version.xml.i b/interface-definitions/include/version/quagga-version.xml.i
new file mode 100644
index 000000000..bb8ad7f82
--- /dev/null
+++ b/interface-definitions/include/version/quagga-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/quagga-version.xml.i -->
+<syntaxVersion component='quagga' version='9'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/rpki-version.xml.i b/interface-definitions/include/version/rpki-version.xml.i
new file mode 100644
index 000000000..2fff259a8
--- /dev/null
+++ b/interface-definitions/include/version/rpki-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/rpki-version.xml.i -->
+<syntaxVersion component='rpki' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/salt-version.xml.i b/interface-definitions/include/version/salt-version.xml.i
new file mode 100644
index 000000000..fe4684050
--- /dev/null
+++ b/interface-definitions/include/version/salt-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/salt-version.xml.i -->
+<syntaxVersion component='salt' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/snmp-version.xml.i b/interface-definitions/include/version/snmp-version.xml.i
new file mode 100644
index 000000000..0416288f0
--- /dev/null
+++ b/interface-definitions/include/version/snmp-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/snmp-version.xml.i -->
+<syntaxVersion component='snmp' version='2'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/ssh-version.xml.i b/interface-definitions/include/version/ssh-version.xml.i
new file mode 100644
index 000000000..0f25caf98
--- /dev/null
+++ b/interface-definitions/include/version/ssh-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/ssh-version.xml.i -->
+<syntaxVersion component='ssh' version='2'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/sstp-version.xml.i b/interface-definitions/include/version/sstp-version.xml.i
new file mode 100644
index 000000000..79b43a3e7
--- /dev/null
+++ b/interface-definitions/include/version/sstp-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/sstp-version.xml.i -->
+<syntaxVersion component='sstp' version='4'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/system-version.xml.i b/interface-definitions/include/version/system-version.xml.i
new file mode 100644
index 000000000..fb4629bf1
--- /dev/null
+++ b/interface-definitions/include/version/system-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/system-version.xml.i -->
+<syntaxVersion component='system' version='22'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/vrf-version.xml.i b/interface-definitions/include/version/vrf-version.xml.i
new file mode 100644
index 000000000..9d7ff35fe
--- /dev/null
+++ b/interface-definitions/include/version/vrf-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/vrf-version.xml.i -->
+<syntaxVersion component='vrf' version='3'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/vrrp-version.xml.i b/interface-definitions/include/version/vrrp-version.xml.i
new file mode 100644
index 000000000..626dd6cbc
--- /dev/null
+++ b/interface-definitions/include/version/vrrp-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/vrrp-version.xml.i -->
+<syntaxVersion component='vrrp' version='3'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/vyos-accel-ppp-version.xml.i b/interface-definitions/include/version/vyos-accel-ppp-version.xml.i
new file mode 100644
index 000000000..e5a4e1613
--- /dev/null
+++ b/interface-definitions/include/version/vyos-accel-ppp-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/vyos-accel-ppp-version.xml.i -->
+<syntaxVersion component='vyos-accel-ppp' version='2'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/wanloadbalance-version.xml.i b/interface-definitions/include/version/wanloadbalance-version.xml.i
new file mode 100644
index 000000000..59f8729cc
--- /dev/null
+++ b/interface-definitions/include/version/wanloadbalance-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/wanloadbalance-version.xml.i -->
+<syntaxVersion component='wanloadbalance' version='3'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/include/version/webproxy-version.xml.i b/interface-definitions/include/version/webproxy-version.xml.i
new file mode 100644
index 000000000..42dbf3f8b
--- /dev/null
+++ b/interface-definitions/include/version/webproxy-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/webproxy-version.xml.i -->
+<syntaxVersion component='webproxy' version='2'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/xml-component-version.xml.in b/interface-definitions/xml-component-version.xml.in
new file mode 100644
index 000000000..b7f063a6c
--- /dev/null
+++ b/interface-definitions/xml-component-version.xml.in
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  #include <include/version/bgp-version.xml.i>
+  #include <include/version/broadcast-relay-version.xml.i>
+  #include <include/version/cluster-version.xml.i>
+  #include <include/version/config-management-version.xml.i>
+  #include <include/version/conntrack-sync-version.xml.i>
+  #include <include/version/conntrack-version.xml.i>
+  #include <include/version/dhcp-relay-version.xml.i>
+  #include <include/version/dhcp-server-version.xml.i>
+  #include <include/version/dhcpv6-server-version.xml.i>
+  #include <include/version/dns-forwarding-version.xml.i>
+  #include <include/version/firewall-version.xml.i>
+  #include <include/version/flow-accounting-version.xml.i>
+  #include <include/version/https-version.xml.i>
+  #include <include/version/interfaces-version.xml.i>
+  #include <include/version/ipoe-server-version.xml.i>
+  #include <include/version/ipsec-version.xml.i>
+  #include <include/version/isis-version.xml.i>
+  #include <include/version/l2tp-version.xml.i>
+  #include <include/version/lldp-version.xml.i>
+  #include <include/version/mdns-version.xml.i>
+  #include <include/version/nat66-version.xml.i>
+  #include <include/version/nat-version.xml.i>
+  #include <include/version/ntp-version.xml.i>
+  #include <include/version/openconnect-version.xml.i>
+  #include <include/version/ospf-version.xml.i>
+  #include <include/version/policy-version.xml.i>
+  #include <include/version/pppoe-server-version.xml.i>
+  #include <include/version/pptp-version.xml.i>
+  #include <include/version/qos-version.xml.i>
+  #include <include/version/quagga-version.xml.i>
+  #include <include/version/rpki-version.xml.i>
+  #include <include/version/salt-version.xml.i>
+  #include <include/version/snmp-version.xml.i>
+  #include <include/version/ssh-version.xml.i>
+  #include <include/version/sstp-version.xml.i>
+  #include <include/version/system-version.xml.i>
+  #include <include/version/vrf-version.xml.i>
+  #include <include/version/vrrp-version.xml.i>
+  #include <include/version/vyos-accel-ppp-version.xml.i>
+  #include <include/version/wanloadbalance-version.xml.i>
+  #include <include/version/webproxy-version.xml.i>
+</interfaceDefinition>
diff --git a/python/vyos/xml/__init__.py b/python/vyos/xml/__init__.py
index e0eacb2d1..6db446a40 100644
--- a/python/vyos/xml/__init__.py
+++ b/python/vyos/xml/__init__.py
@@ -46,8 +46,8 @@ def is_tag(lpath):
 def is_leaf(lpath, flat=True):
     return load_configuration().is_leaf(lpath, flat)
 
-def component_versions():
-    return load_configuration().component_versions()
+def component_version():
+    return load_configuration().component_version()
 
 def defaults(lpath, flat=False):
     return load_configuration().defaults(lpath, flat)
diff --git a/python/vyos/xml/definition.py b/python/vyos/xml/definition.py
index 5e0d5282c..bc3892b42 100644
--- a/python/vyos/xml/definition.py
+++ b/python/vyos/xml/definition.py
@@ -249,10 +249,11 @@ class XML(dict):
     # @lru_cache(maxsize=100)
     # XXX: need to use cachetool instead - for later
 
-    def component_versions(self) -> dict:
-        sort_component = sorted(self[kw.component_version].items(),
-                                key = lambda kv: kv[0])
-        return dict(sort_component)
+    def component_version(self) -> dict:
+        d = {}
+        for k in sorted(self[kw.component_version]):
+            d[k] = int(self[kw.component_version][k])
+        return d
 
     def defaults(self, lpath, flat):
         d = self[kw.default]
-- 
cgit v1.2.3


From d9a1f8deceec729371004377692d477f5e554cb1 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Wed, 16 Feb 2022 15:29:02 -0600
Subject: xml: T3474: add smoketest to check xml component versions are
 maintained

Add smoketest to catch updates to a component version in legacy
curver_DATA that is not present in xml syntaxVersion.
---
 python/vyos/systemversions.py                   |  7 +++++
 smoketest/scripts/cli/test_component_version.py | 36 +++++++++++++++++++++++++
 2 files changed, 43 insertions(+)
 create mode 100755 smoketest/scripts/cli/test_component_version.py

diff --git a/python/vyos/systemversions.py b/python/vyos/systemversions.py
index 9b3f4f413..f2da76d4f 100644
--- a/python/vyos/systemversions.py
+++ b/python/vyos/systemversions.py
@@ -17,7 +17,10 @@ import os
 import re
 import sys
 import vyos.defaults
+from vyos.xml import component_version
 
+# legacy version, reading from the file names in
+# /opt/vyatta/etc/config-migrate/current
 def get_system_versions():
     """
     Get component versions from running system; critical failure if
@@ -37,3 +40,7 @@ def get_system_versions():
             system_versions[pair[0]] = int(pair[1])
 
     return system_versions
+
+# read from xml cache
+def get_system_component_version():
+    return component_version()
diff --git a/smoketest/scripts/cli/test_component_version.py b/smoketest/scripts/cli/test_component_version.py
new file mode 100755
index 000000000..777379bdd
--- /dev/null
+++ b/smoketest/scripts/cli/test_component_version.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from vyos.systemversions import get_system_versions, get_system_component_version
+
+# After T3474, component versions should be updated in the files in
+# vyos-1x/interface-definitions/include/version/
+# This test verifies that the legacy version in curver_DATA does not exceed
+# that in the xml cache.
+class TestComponentVersion(unittest.TestCase):
+    def setUp(self):
+        self.legacy_d = get_system_versions()
+        self.xml_d = get_system_component_version()
+
+    def test_component_version(self):
+        self.assertTrue(set(self.legacy_d).issubset(set(self.xml_d)))
+        for k, v in self.legacy_d.items():
+            self.assertTrue(v <= self.xml_d[k])
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
-- 
cgit v1.2.3


From 425f8f16caa7e9e5101be3dc8ab32e60db274d7f Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Wed, 16 Feb 2022 20:11:31 -0600
Subject: xml: T3474: get component version dictionary from xml cache, not
 legacy

---
 python/vyos/migrator.py             | 2 +-
 src/helpers/system-versions-foot.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py
index 4574bb6d1..a2e0daabd 100644
--- a/python/vyos/migrator.py
+++ b/python/vyos/migrator.py
@@ -195,7 +195,7 @@ class Migrator(object):
             # This will force calling all migration scripts:
             cfg_versions = {}
 
-        sys_versions = systemversions.get_system_versions()
+        sys_versions = systemversions.get_system_component_version()
 
         # save system component versions in json file for easy reference
         self.save_json_record(sys_versions)
diff --git a/src/helpers/system-versions-foot.py b/src/helpers/system-versions-foot.py
index c33e41d79..2aa687221 100755
--- a/src/helpers/system-versions-foot.py
+++ b/src/helpers/system-versions-foot.py
@@ -21,7 +21,7 @@ import vyos.systemversions as systemversions
 import vyos.defaults
 import vyos.version
 
-sys_versions = systemversions.get_system_versions()
+sys_versions = systemversions.get_system_component_version()
 
 component_string = formatversions.format_versions_string(sys_versions)
 
-- 
cgit v1.2.3


From 1cbcbf40b7721849f9696c05fac65db010a66b7c Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 17 Feb 2022 20:58:02 +0100
Subject: openvpn: T4230: globally enable ip_nonlocal_bind

---
 src/conf_mode/interfaces-openvpn.py         | 7 -------
 src/etc/sysctl.d/33-vyos-nonlocal-bind.conf | 8 ++++++++
 2 files changed, 8 insertions(+), 7 deletions(-)
 create mode 100644 src/etc/sysctl.d/33-vyos-nonlocal-bind.conf

diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 329399274..29a25eedc 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -649,13 +649,6 @@ def apply(openvpn):
 
         return None
 
-    # verify specified IP address is present on any interface on this system
-    # Allow to bind service to nonlocal address, if it virtaual-vrrp address
-    # or if address will be assign later
-    if 'local_host' in openvpn:
-        if not is_addr_assigned(openvpn['local_host']):
-            cmd('sysctl -w net.ipv4.ip_nonlocal_bind=1')
-
     # No matching OpenVPN process running - maybe it got killed or none
     # existed - nevertheless, spawn new OpenVPN process
     call(f'systemctl reload-or-restart openvpn@{interface}.service')
diff --git a/src/etc/sysctl.d/33-vyos-nonlocal-bind.conf b/src/etc/sysctl.d/33-vyos-nonlocal-bind.conf
new file mode 100644
index 000000000..aa81b5336
--- /dev/null
+++ b/src/etc/sysctl.d/33-vyos-nonlocal-bind.conf
@@ -0,0 +1,8 @@
+### Added by vyos-1x ###
+#
+# ip_nonlocal_bind - BOOLEAN
+#     If set, allows processes to bind() to non-local IP addresses,
+#     which can be quite useful - but may break some applications.
+#     Default: 0
+net.ipv4.ip_nonlocal_bind = 1
+net.ipv6.ip_nonlocal_bind = 1
-- 
cgit v1.2.3


From 9e626ce7bad2bd846826822a3622fedf2d937e09 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 17 Feb 2022 21:10:16 +0100
Subject: vyos.configverify: T4255: fix unexpected print of dictionary instead
 of key

---
 python/vyos/configverify.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index 18fb7f9f7..fab88bc72 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -224,9 +224,10 @@ def verify_bridge_delete(config):
     when interface also is part of a bridge.
     """
     if 'is_bridge_member' in config:
-        raise ConfigError(
-            'Interface "{ifname}" cannot be deleted as it is a '
-            'member of bridge "{is_bridge_member}"!'.format(**config))
+        interface = config['ifname']
+        for bridge in config['is_bridge_member']:
+            raise ConfigError(f'Interface "{interface}" cannot be deleted as it '
+                              f'is a member of bridge "{bridge}"!')
 
 def verify_interface_exists(ifname):
     """
-- 
cgit v1.2.3


From 3d1b34bf715e594aa4a013d409bfcc5a4c4ad99c Mon Sep 17 00:00:00 2001
From: Andrew Gunnerson <chillermillerlong@hotmail.com>
Date: Wed, 16 Feb 2022 17:46:06 -0500
Subject: pki: eapol: T4245: Add full CA and client cert chains to
 wpa_supplicant PEM files

This commit updates the eapol code so that it writes the full
certificate chains for both the specified CA and the client certificate
to `<iface>_ca.pem` and `<iface>_cert.pem`, respectively.

The full CA chain is necessary for validating the incoming server
certificate when it is signed by an intermediate CA and the
intermediate CA cert is not included in the EAP-TLS ServerHello. In this
scenario, wpa_supplicant needs to have both the intermediate CA and the
root CA in its `ca_file`.

Similarly, the full client certificate chain is needed when the ISP
expects/requires that the client (wpa_supplicant) sends the client cert
+ the intermediate CA (or even + the root CA) as part of the EAP-TLS
ClientHello.

Signed-off-by: Andrew Gunnerson <chillermillerlong@hotmail.com>
---
 python/vyos/pki.py                                |  26 ++++++
 smoketest/scripts/cli/test_interfaces_ethernet.py | 108 +++++++++++++++++-----
 src/conf_mode/interfaces-ethernet.py              |  18 +++-
 3 files changed, 125 insertions(+), 27 deletions(-)

diff --git a/python/vyos/pki.py b/python/vyos/pki.py
index 68ad73bf2..0b916eaae 100644
--- a/python/vyos/pki.py
+++ b/python/vyos/pki.py
@@ -331,3 +331,29 @@ def verify_certificate(cert, ca_cert):
         return True
     except InvalidSignature:
         return False
+
+# Certificate chain
+
+def find_parent(cert, ca_certs):
+    for ca_cert in ca_certs:
+        if verify_certificate(cert, ca_cert):
+            return ca_cert
+    return None
+
+def find_chain(cert, ca_certs):
+    remaining = ca_certs.copy()
+    chain = [cert]
+
+    while remaining:
+        parent = find_parent(chain[-1], remaining)
+        if parent is None:
+            # No parent in the list of remaining certificates or there's a circular dependency
+            break
+        elif parent == chain[-1]:
+            # Self-signed: must be root CA (end of chain)
+            break
+        else:
+            remaining.remove(parent)
+            chain.append(parent)
+
+    return chain
diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py
index 6d80e4c96..ae3d5ed96 100755
--- a/smoketest/scripts/cli/test_interfaces_ethernet.py
+++ b/smoketest/scripts/cli/test_interfaces_ethernet.py
@@ -21,29 +21,73 @@ import unittest
 from base_interfaces_test import BasicInterfaceTest
 from vyos.configsession import ConfigSessionError
 from vyos.ifconfig import Section
+from vyos.pki import CERT_BEGIN
 from vyos.util import cmd
 from vyos.util import process_named_running
 from vyos.util import read_file
 
-cert_data = """
-MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIw
-WTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv
-bWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIx
-MDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNV
-BAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlP
-UzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
-01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3
-QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E
-BAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu
-+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3Lftz
-ngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93
-+dm/LDnp7C0=
+server_ca_root_cert_data = """
+MIIBcTCCARagAwIBAgIUDcAf1oIQV+6WRaW7NPcSnECQ/lUwCgYIKoZIzj0EAwIw
+HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjBa
+Fw0zMjAyMTUxOTQxMjBaMB4xHDAaBgNVBAMME1Z5T1Mgc2VydmVyIHJvb3QgQ0Ew
+WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ0y24GzKQf4aM2Ir12tI9yITOIzAUj
+ZXyJeCmYI6uAnyAMqc4Q4NKyfq3nBi4XP87cs1jlC1P2BZ8MsjL5MdGWozIwMDAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRwC/YaieMEnjhYa7K3Flw/o0SFuzAK
+BggqhkjOPQQDAgNJADBGAiEAh3qEj8vScsjAdBy5shXzXDVVOKWCPTdGrPKnu8UW
+a2cCIQDlDgkzWmn5ujc5ATKz1fj+Se/aeqwh4QyoWCVTFLIxhQ==
 """
 
-key_data = """
-MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx
-2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7
-u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww
+server_ca_intermediate_cert_data = """
+MIIBmTCCAT+gAwIBAgIUNzrtHzLmi3QpPK57tUgCnJZhXXQwCgYIKoZIzj0EAwIw
+HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjFa
+Fw0zMjAyMTUxOTQxMjFaMCYxJDAiBgNVBAMMG1Z5T1Mgc2VydmVyIGludGVybWVk
+aWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEl2nJ1CzoqPV6hWII2m
+eGN/uieU6wDMECTk/LgG8CCCSYb488dibUiFN/1UFsmoLIdIhkx/6MUCYh62m8U2
+WNujUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMV3YwH88I5gFsFUibbQ
+kMR0ECPsMB8GA1UdIwQYMBaAFHAL9hqJ4wSeOFhrsrcWXD+jRIW7MAoGCCqGSM49
+BAMCA0gAMEUCIQC/ahujD9dp5pMMCd3SZddqGC9cXtOwMN0JR3e5CxP13AIgIMQm
+jMYrinFoInxmX64HfshYqnUY8608nK9D2BNPOHo=
+"""
+
+client_ca_root_cert_data = """
+MIIBcDCCARagAwIBAgIUZmoW2xVdwkZSvglnkCq0AHKa6zIwCgYIKoZIzj0EAwIw
+HjEcMBoGA1UEAwwTVnlPUyBjbGllbnQgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjFa
+Fw0zMjAyMTUxOTQxMjFaMB4xHDAaBgNVBAMME1Z5T1MgY2xpZW50IHJvb3QgQ0Ew
+WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATUpKXzQk2NOVKDN4VULk2yw4mOKPvn
+mg947+VY7lbpfOfAUD0QRg95qZWCw899eKnXp/U4TkAVrmEKhUb6OJTFozIwMDAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTXu6xGWUl25X3sBtrhm3BJSICIATAK
+BggqhkjOPQQDAgNIADBFAiEAnTzEwuTI9bz2Oae3LZbjP6f/f50KFJtjLZFDbQz7
+DpYCIDNRHV8zBUibC+zg5PqMpQBKd/oPfNU76nEv6xkp/ijO
+"""
+
+client_ca_intermediate_cert_data = """
+MIIBmDCCAT+gAwIBAgIUJEMdotgqA7wU4XXJvEzDulUAGqgwCgYIKoZIzj0EAwIw
+HjEcMBoGA1UEAwwTVnlPUyBjbGllbnQgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjJa
+Fw0zMjAyMTUxOTQxMjJaMCYxJDAiBgNVBAMMG1Z5T1MgY2xpZW50IGludGVybWVk
+aWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGyIVIi217s9j3O+WQ2b
+6R65/Z0ZjQpELxPjBRc0CA0GFCo+pI5EvwI+jNFArvTAJ5+ZdEWUJ1DQhBKDDQdI
+avCjUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOUS8oNJjChB1Rb9Blcl
+ETvziHJ9MB8GA1UdIwQYMBaAFNe7rEZZSXblfewG2uGbcElIgIgBMAoGCCqGSM49
+BAMCA0cAMEQCIArhaxWgRsAUbEeNHD/ULtstLHxw/P97qPUSROLQld53AiBjgiiz
+9pDfISmpekZYz6bIDWRIR0cXUToZEMFNzNMrQg==
+"""
+
+client_cert_data = """
+MIIBmTCCAUCgAwIBAgIUV5T77XdE/tV82Tk4Vzhp5BIFFm0wCgYIKoZIzj0EAwIw
+JjEkMCIGA1UEAwwbVnlPUyBjbGllbnQgaW50ZXJtZWRpYXRlIENBMB4XDTIyMDIx
+NzE5NDEyMloXDTMyMDIxNTE5NDEyMlowIjEgMB4GA1UEAwwXVnlPUyBjbGllbnQg
+Y2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARuyynqfc/qJj5e
+KJ03oOH8X4Z8spDeAPO9WYckMM0ldPj+9kU607szFzPwjaPWzPdgyIWz3hcN8yAh
+CIhytmJao1AwTjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTIFKrxZ+PqOhYSUqnl
+TGCUmM7wTjAfBgNVHSMEGDAWgBTlEvKDSYwoQdUW/QZXJRE784hyfTAKBggqhkjO
+PQQDAgNHADBEAiAvO8/jvz05xqmP3OXD53XhfxDLMIxzN4KPoCkFqvjlhQIgIHq2
+/geVx3rAOtSps56q/jiDouN/aw01TdpmGKVAa9U=
+"""
+
+client_key_data = """
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgxaxAQsJwjoOCByQE
++qSYKtKtJzbdbOnTsKNSrfgkFH6hRANCAARuyynqfc/qJj5eKJ03oOH8X4Z8spDe
+APO9WYckMM0ldPj+9kU607szFzPwjaPWzPdgyIWz3hcN8yAhCIhytmJa
 """
 
 def get_wpa_supplicant_value(interface, key):
@@ -51,6 +95,10 @@ def get_wpa_supplicant_value(interface, key):
     tmp = re.findall(r'\n?{}=(.*)'.format(key), tmp)
     return tmp[0]
 
+def get_certificate_count(interface, cert_type):
+    tmp = read_file(f'/run/wpa_supplicant/{interface}_{cert_type}.pem')
+    return tmp.count(CERT_BEGIN)
+
 class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
     @classmethod
     def setUpClass(cls):
@@ -165,16 +213,23 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
             self.cli_commit()
 
     def test_eapol_support(self):
-        ca_name = 'eapol'
-        cert_name = 'eapol'
+        ca_certs = {
+            'eapol-server-ca-root': server_ca_root_cert_data,
+            'eapol-server-ca-intermediate': server_ca_intermediate_cert_data,
+            'eapol-client-ca-root': client_ca_root_cert_data,
+            'eapol-client-ca-intermediate': client_ca_intermediate_cert_data,
+        }
+        cert_name = 'eapol-client'
 
-        self.cli_set(['pki', 'ca', ca_name, 'certificate', cert_data.replace('\n','')])
-        self.cli_set(['pki', 'certificate', cert_name, 'certificate', cert_data.replace('\n','')])
-        self.cli_set(['pki', 'certificate', cert_name, 'private', 'key', key_data.replace('\n','')])
+        for name, data in ca_certs.items():
+            self.cli_set(['pki', 'ca', name, 'certificate', data.replace('\n','')])
+
+        self.cli_set(['pki', 'certificate', cert_name, 'certificate', client_cert_data.replace('\n','')])
+        self.cli_set(['pki', 'certificate', cert_name, 'private', 'key', client_key_data.replace('\n','')])
 
         for interface in self._interfaces:
             # Enable EAPoL
-            self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', ca_name])
+            self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-server-ca-intermediate'])
             self.cli_set(self._base_path + [interface, 'eapol', 'certificate', cert_name])
 
         self.cli_commit()
@@ -206,7 +261,12 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
             tmp = get_wpa_supplicant_value(interface, 'identity')
             self.assertEqual(f'"{mac}"', tmp)
 
-        self.cli_delete(['pki', 'ca', ca_name])
+        # Check certificate files have the full chain
+        self.assertEqual(get_certificate_count(interface, 'ca'), 2)
+        self.assertEqual(get_certificate_count(interface, 'cert'), 3)
+
+        for name in ca_certs:
+            self.cli_delete(['pki', 'ca', name])
         self.cli_delete(['pki', 'certificate', cert_name])
 
 if __name__ == '__main__':
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index ab8d58f81..2a8a126f2 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -32,7 +32,9 @@ from vyos.configverify import verify_vlan_config
 from vyos.configverify import verify_vrf
 from vyos.ethtool import Ethtool
 from vyos.ifconfig import EthernetIf
-from vyos.pki import wrap_certificate
+from vyos.pki import find_chain
+from vyos.pki import encode_certificate
+from vyos.pki import load_certificate
 from vyos.pki import wrap_private_key
 from vyos.template import render
 from vyos.util import call
@@ -159,7 +161,14 @@ def generate(ethernet):
         cert_name = ethernet['eapol']['certificate']
         pki_cert = ethernet['pki']['certificate'][cert_name]
 
-        write_file(cert_file_path, wrap_certificate(pki_cert['certificate']))
+        loaded_pki_cert = load_certificate(pki_cert['certificate'])
+        loaded_ca_certs = {load_certificate(c['certificate'])
+            for c in ethernet['pki']['ca'].values()}
+
+        cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs)
+
+        write_file(cert_file_path,
+                   '\n'.join(encode_certificate(c) for c in cert_full_chain))
         write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))
 
         if 'ca_certificate' in ethernet['eapol']:
@@ -167,8 +176,11 @@ def generate(ethernet):
             ca_cert_name = ethernet['eapol']['ca_certificate']
             pki_ca_cert = ethernet['pki']['ca'][ca_cert_name]
 
+            loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
+            ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
+
             write_file(ca_cert_file_path,
-                       wrap_certificate(pki_ca_cert['certificate']))
+                       '\n'.join(encode_certificate(c) for c in ca_full_chain))
     else:
         # delete configuration on interface removal
         if os.path.isfile(wpa_suppl_conf.format(**ethernet)):
-- 
cgit v1.2.3


From 5fc9ef9e31eb566a601f8a150c69b183a4331564 Mon Sep 17 00:00:00 2001
From: fett0 <fernando.gmaidana@gmail.com>
Date: Fri, 18 Feb 2022 21:08:21 +0000
Subject: DHCP : T4258: Set correct port for dhcp-failover

---
 data/templates/dhcp-server/dhcpd.conf.tmpl | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/data/templates/dhcp-server/dhcpd.conf.tmpl b/data/templates/dhcp-server/dhcpd.conf.tmpl
index da2f28ced..dbd864b5e 100644
--- a/data/templates/dhcp-server/dhcpd.conf.tmpl
+++ b/data/templates/dhcp-server/dhcpd.conf.tmpl
@@ -42,9 +42,9 @@ failover peer "{{ failover.name }}" {
     secondary;
 {%   endif %}
     address {{ failover.source_address }};
-    port 520;
+    port 647;
     peer address {{ failover.remote }};
-    peer port 520;
+    peer port 647;
     max-response-delay 30;
     max-unacked-updates 10;
     load balance max seconds 3;
-- 
cgit v1.2.3


From 29ba813fb65b8b292105cdae4f8f71fcce6350a1 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 19 Feb 2022 13:09:17 +0100
Subject: smoketest: T4258: dhcp: bugfix failover ports

Commit 5fc9ef9e ("DHCP : T4258: Set correct port for dhcp-failover") changed
how the failover port is rendered into the ISC DHCPd configuration - adjustment
of the smoketests was missed out.
---
 smoketest/scripts/cli/test_service_dhcp-server.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py
index 14666db15..9adb9c042 100755
--- a/smoketest/scripts/cli/test_service_dhcp-server.py
+++ b/smoketest/scripts/cli/test_service_dhcp-server.py
@@ -461,12 +461,11 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
         self.assertIn(f'mclt 1800;', config)
         self.assertIn(f'mclt 1800;', config)
         self.assertIn(f'split 128;', config)
-        self.assertIn(f'port 520;', config)
-        self.assertIn(f'peer port 520;', config)
+        self.assertIn(f'port 647;', config)
+        self.assertIn(f'peer port 647;', config)
         self.assertIn(f'max-response-delay 30;', config)
         self.assertIn(f'max-unacked-updates 10;', config)
         self.assertIn(f'load balance max seconds 3;', config)
-        self.assertIn(f'peer port 520;', config)
         self.assertIn(f'address {failover_local};', config)
         self.assertIn(f'peer address {failover_remote};', config)
 
-- 
cgit v1.2.3


From f6c2b5e4762e7713c5868bebf8e482ce732e3302 Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.i>
Date: Thu, 17 Feb 2022 21:18:37 +0000
Subject: vpn: T4254: Add cisco_flexvpn and install_virtual_ip_on options

Ability to set Cisco FlexVPN vendor ID payload:
charon.cisco_flexvpn
charon.install_virtual_ip_on
swanctl.connections.<conn>.vips = x.x.x.x, z.z.z.z

set vpn ipsec options flexvpn
set vpn ipsec options virtual-ip
set vpn ipsec options interface tunX
set vpn ipsec site-to-site peer x.x.x.x virtual-address x.x.x.x
---
 data/templates/ipsec/charon.tmpl        | 11 +++++
 data/templates/ipsec/swanctl/peer.tmpl  |  3 ++
 interface-definitions/vpn_ipsec.xml.in  | 27 +++++++++++++
 smoketest/scripts/cli/test_vpn_ipsec.py | 71 +++++++++++++++++++++++++++++++++
 4 files changed, 112 insertions(+)

diff --git a/data/templates/ipsec/charon.tmpl b/data/templates/ipsec/charon.tmpl
index 4d710921e..b9b020dcd 100644
--- a/data/templates/ipsec/charon.tmpl
+++ b/data/templates/ipsec/charon.tmpl
@@ -20,6 +20,17 @@ charon {
     # Send Cisco Unity vendor ID payload (IKEv1 only).
     # cisco_unity = no
 
+    # Cisco FlexVPN
+{% if options is defined %}
+    cisco_flexvpn = {{ 'yes' if options.flexvpn is defined else 'no' }}
+{%   if options.virtual_ip is defined %}
+    install_virtual_ip = yes
+{%   endif %}
+{%   if options.interface is defined and options.interface is not none %}
+    install_virtual_ip_on = {{ options.interface }}
+{%   endif %}
+{%  endif %}
+
     # Close the IKE_SA if setup of the CHILD_SA along with IKE_AUTH failed.
     # close_ike_on_child_failure = no
 
diff --git a/data/templates/ipsec/swanctl/peer.tmpl b/data/templates/ipsec/swanctl/peer.tmpl
index c6b71f2a1..f4e28d818 100644
--- a/data/templates/ipsec/swanctl/peer.tmpl
+++ b/data/templates/ipsec/swanctl/peer.tmpl
@@ -5,6 +5,9 @@
     peer_{{ name }} {
         proposals = {{ ike | get_esp_ike_cipher | join(',') }}
         version = {{ ike.key_exchange[4:] if ike is defined and ike.key_exchange is defined else "0" }}
+{%   if peer_conf.virtual_address is defined and peer_conf.virtual_address is not none %}
+        vips = {{ peer_conf.virtual_address | join(', ') }}
+{%   endif %}
         local_addrs = {{ peer_conf.local_address if peer_conf.local_address != 'any' else '0.0.0.0/0' }} # dhcp:{{ peer_conf.dhcp_interface if 'dhcp_interface' in peer_conf else 'no' }}
         remote_addrs = {{ peer if peer not in ['any', '0.0.0.0'] and peer[0:1] != '@' else '0.0.0.0/0' }}
 {%   if peer_conf.authentication is defined and peer_conf.authentication.mode is defined and peer_conf.authentication.mode == 'x509' %}
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index afa3d52a0..f7297a6e2 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -622,6 +622,19 @@
                   <valueless/>
                 </properties>
               </leafNode>
+              <leafNode name="flexvpn">
+                <properties>
+                  <help>Allow FlexVPN vendor ID payload (IKEv2 only)</help>
+                  <valueless/>
+                </properties>
+              </leafNode>
+              #include <include/generic-interface.xml.i>
+              <leafNode name="virtual-ip">
+                <properties>
+                  <help>Allow install virtual-ip addresses</help>
+                  <valueless/>
+                </properties>
+              </leafNode>
             </children>
           </node>
           <tagNode name="profile">
@@ -1087,6 +1100,20 @@
                       </node>
                     </children>
                   </tagNode>
+                  <leafNode name="virtual-address">
+                    <properties>
+                      <help>Initiator request virtual-address from peer</help>
+                      <valueHelp>
+                        <format>ipv4</format>
+                        <description>Request IPv4 address from peer</description>
+                      </valueHelp>
+                      <valueHelp>
+                        <format>ipv6</format>
+                        <description>Request IPv6 address from peer</description>
+                      </valueHelp>
+                      <multi/>
+                    </properties>
+                  </leafNode>
                   <node name="vti">
                     <properties>
                       <help>Virtual tunnel interface [REQUIRED]</help>
diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py
index 1433c7329..2c3e55a57 100755
--- a/smoketest/scripts/cli/test_vpn_ipsec.py
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -28,6 +28,7 @@ vti_path = ['interfaces', 'vti']
 nhrp_path = ['protocols', 'nhrp']
 base_path = ['vpn', 'ipsec']
 
+charon_file = '/etc/strongswan.d/charon.conf'
 dhcp_waiting_file = '/tmp/ipsec_dhcp_waiting'
 swanctl_file = '/etc/swanctl/swanctl.conf'
 
@@ -416,5 +417,75 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
         # There is only one VTI test so no need to delete this globally in tearDown()
         self.cli_delete(vti_path)
 
+
+    def test_06_flex_vpn_vips(self):
+        local_address = '192.0.2.5'
+        local_id = 'vyos-r1'
+        remote_id = 'vyos-r2'
+        peer_base_path = base_path + ['site-to-site', 'peer', peer_ip]
+
+        self.cli_set(tunnel_path + ['tun1', 'encapsulation', 'gre'])
+        self.cli_set(tunnel_path + ['tun1', 'source-address', local_address])
+
+        self.cli_set(base_path + ['interface', interface])
+        self.cli_set(base_path + ['options', 'flexvpn'])
+        self.cli_set(base_path + ['options', 'interface', 'tun1'])
+        self.cli_set(base_path + ['ike-group', ike_group, 'ikev2-reauth', 'no'])
+        self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+
+        self.cli_set(peer_base_path + ['authentication', 'id', local_id])
+        self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
+        self.cli_set(peer_base_path + ['authentication', 'pre-shared-secret', secret])
+        self.cli_set(peer_base_path + ['authentication', 'remote-id', remote_id])
+        self.cli_set(peer_base_path + ['connection-type', 'initiate'])
+        self.cli_set(peer_base_path + ['ike-group', ike_group])
+        self.cli_set(peer_base_path + ['default-esp-group', esp_group])
+        self.cli_set(peer_base_path + ['local-address', local_address])
+        self.cli_set(peer_base_path + ['tunnel', '1', 'protocol', 'gre'])
+
+        self.cli_set(peer_base_path + ['virtual-address', '203.0.113.55'])
+        self.cli_set(peer_base_path + ['virtual-address', '203.0.113.56'])
+
+        self.cli_commit()
+
+        # Verify strongSwan configuration
+        swanctl_conf = read_file(swanctl_file)
+        swanctl_conf_lines = [
+            f'version = 2',
+            f'vips = 203.0.113.55, 203.0.113.56',
+            f'life_time = 3600s', # default value
+            f'local_addrs = {local_address} # dhcp:no',
+            f'remote_addrs = {peer_ip}',
+            f'peer_{peer_ip.replace(".","-")}_tunnel_1',
+            f'mode = tunnel',
+        ]
+
+        for line in swanctl_conf_lines:
+            self.assertIn(line, swanctl_conf)
+
+        swanctl_secrets_lines = [
+            f'id-local = {local_address} # dhcp:no',
+            f'id-remote = {peer_ip}',
+            f'id-localid = {local_id}',
+            f'id-remoteid = {remote_id}',
+            f'secret = "{secret}"',
+        ]
+
+        for line in swanctl_secrets_lines:
+            self.assertIn(line, swanctl_conf)
+
+        # Verify charon configuration
+        charon_conf = read_file(charon_file)
+        charon_conf_lines = [
+            f'# Cisco FlexVPN',
+            f'cisco_flexvpn = yes',
+            f'install_virtual_ip = yes',
+            f'install_virtual_ip_on = tun1',
+        ]
+
+        for line in charon_conf_lines:
+            self.assertIn(line, charon_conf)
+
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)
-- 
cgit v1.2.3


From cf36ced75094a519682875e0e73571824f34b6ec Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Sat, 19 Feb 2022 18:06:27 +0000
Subject: containers: T4249: Allow to connect host device to the container

Ability to attach host devices to the container
It can be disk, USB device or any device from the directory /dev

set container name alp01 device disk source '/dev/vdb1'
set container name alp01 device disk destination '/dev/mydisk'
---
 interface-definitions/containers.xml.in | 25 +++++++++++++++++++++++++
 src/conf_mode/containers.py             | 22 +++++++++++++++++++++-
 2 files changed, 46 insertions(+), 1 deletion(-)

diff --git a/interface-definitions/containers.xml.in b/interface-definitions/containers.xml.in
index 30c7110b8..07686b16e 100644
--- a/interface-definitions/containers.xml.in
+++ b/interface-definitions/containers.xml.in
@@ -58,6 +58,31 @@
             </properties>
           </leafNode>
           #include <include/generic-description.xml.i>
+          <tagNode name="device">
+            <properties>
+              <help>Add a host device to the container</help>
+            </properties>
+            <children>
+              <leafNode name="source">
+                <properties>
+                  <help>Source device (Example: "/dev/x")</help>
+                  <valueHelp>
+                    <format>txt</format>
+                    <description>Source device</description>
+                  </valueHelp>
+                </properties>
+              </leafNode>
+              <leafNode name="destination">
+                <properties>
+                  <help>Destination container device (Example: "/dev/x")</help>
+                  <valueHelp>
+                    <format>txt</format>
+                    <description>Destination container device</description>
+                  </valueHelp>
+                </properties>
+              </leafNode>
+            </children>
+          </tagNode>
           #include <include/generic-disable-node.xml.i>
           <tagNode name="environment">
             <properties>
diff --git a/src/conf_mode/containers.py b/src/conf_mode/containers.py
index 26c50cab6..516671844 100755
--- a/src/conf_mode/containers.py
+++ b/src/conf_mode/containers.py
@@ -122,6 +122,18 @@ def verify(container):
                         raise ConfigError(f'IP address "{address}" can not be used for a container, '\
                                           'reserved for the container engine!')
 
+            if 'device' in container_config:
+                for dev, dev_config in container_config['device'].items():
+                    if 'source' not in dev_config:
+                        raise ConfigError(f'Device "{dev}" has no source path configured!')
+
+                    if 'destination' not in dev_config:
+                        raise ConfigError(f'Device "{dev}" has no destination path configured!')
+
+                    source = dev_config['source']
+                    if not os.path.exists(source):
+                        raise ConfigError(f'Device "{dev}" source path "{source}" does not exist!')
+
             if 'environment' in container_config:
                 for var, cfg in container_config['environment'].items():
                     if 'value' not in cfg:
@@ -266,6 +278,14 @@ def apply(container):
                     c = c.replace('-', '_')
                     cap_add += f' --cap-add={c}'
 
+            # Add a host device to the container /dev/x:/dev/x
+            device = ''
+            if 'device' in container_config:
+                for dev, dev_config in container_config['device'].items():
+                    source_dev = dev_config['source']
+                    dest_dev = dev_config['destination']
+                    device += f' --device={source_dev}:{dest_dev}'
+
             # Check/set environment options "-e foo=bar"
             env_opt = ''
             if 'environment' in container_config:
@@ -296,7 +316,7 @@ def apply(container):
 
             container_base_cmd = f'podman run --detach --interactive --tty --replace {cap_add} ' \
                                  f'--memory {memory}m --memory-swap 0 --restart {restart} ' \
-                                 f'--name {name} {port} {volume} {env_opt}'
+                                 f'--name {name} {device} {port} {volume} {env_opt}'
             if 'allow_host_networks' in container_config:
                 run(f'{container_base_cmd} --net host {image}')
             else:
-- 
cgit v1.2.3


From 3a1a7c40a13ee9f5561823a79876d88d3f5bf053 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 20 Feb 2022 10:36:33 +0100
Subject: interface: T4203: prevent DHCP client restart if not necessary

In the past whenever a change happened to any interface and it was configured
as a DHCP client, VyOS always had a breif outage as DHCP released the old lease
and re-aquired a new one - bad!

This commit changes the behavior that DHCP client is only restarted if any one
of the possible options one can set for DHCP client under the "dhcp-options"
node is altered.
---
 python/vyos/configdict.py         | 38 +++++++++++++++++++++++++++++++++++++-
 python/vyos/ifconfig/interface.py | 26 ++++++++++++++------------
 2 files changed, 51 insertions(+), 13 deletions(-)

diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index e7f515ea9..efeb6dc1f 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -423,6 +423,15 @@ def get_interface_dict(config, base, ifname=''):
     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
+    for leaf_node in ['client-id', 'default-route-distance', 'host-name',
+                 'no-default-route', 'vendor-class-id']:
+        dhcp = leaf_node_changed(config, ['dhcp-options', leaf_node])
+        if dhcp:
+            dict.update({'dhcp_options_old' : dhcp})
+            # one option is suffiecient to set 'dhcp_options_old' key
+            break
+
     # 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.
@@ -470,6 +479,15 @@ def get_interface_dict(config, base, ifname=''):
         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
+        for leaf_node in ['client-id', 'default-route-distance', 'host-name',
+                     'no-default-route', 'vendor-class-id']:
+            dhcp = leaf_node_changed(config, ['vif', vif, 'dhcp-options', leaf_node])
+            if dhcp:
+                dict['vif'][vif].update({'dhcp_options_old' : dhcp})
+                # one option is suffiecient to set 'dhcp_options_old' key
+                break
+
     for vif_s, vif_s_config in dict.get('vif_s', {}).items():
         default_vif_s_values = defaults(base + ['vif-s'])
         # XXX: T2665: we only wan't the vif-s defaults - do not care about vif-c
@@ -495,6 +513,15 @@ def get_interface_dict(config, base, ifname=''):
         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
+        for leaf_node in ['client-id', 'default-route-distance', 'host-name',
+                     'no-default-route', 'vendor-class-id']:
+            dhcp = leaf_node_changed(config, ['vif-s', vif_s, 'dhcp-options', leaf_node])
+            if dhcp:
+                dict['vif_s'][vif_s].update({'dhcp_options_old' : dhcp})
+                # one option is suffiecient to set 'dhcp_options_old' key
+                break
+
         for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items():
             default_vif_c_values = defaults(base + ['vif-s', 'vif-c'])
 
@@ -521,6 +548,16 @@ def get_interface_dict(config, base, ifname=''):
             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
+            for leaf_node in ['client-id', 'default-route-distance', 'host-name',
+                         'no-default-route', 'vendor-class-id']:
+                dhcp = leaf_node_changed(config, ['vif-s', vif_s, 'vif-c', vif_c,
+                                                  'dhcp-options', leaf_node])
+                if dhcp:
+                    dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcp_options_old' : dhcp})
+                    # one option is suffiecient to set 'dhcp_options_old' key
+                    break
+
     # Check vif, vif-s/vif-c VLAN interfaces for removal
     dict = get_removed_vlans(config, dict)
     return dict
@@ -545,7 +582,6 @@ def get_vlan_ids(interface):
 
     return vlan_ids
 
-
 def get_accel_dict(config, base, chap_secrets):
     """
     Common utility function to retrieve and mangle the Accel-PPP configuration
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 91c7f0c33..cf1887bf6 100755
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -1228,12 +1228,11 @@ class Interface(Control):
         options_file = f'{config_base}_{ifname}.options'
         pid_file = f'{config_base}_{ifname}.pid'
         lease_file = f'{config_base}_{ifname}.leases'
-
-        # Stop client with old config files to get the right IF_METRIC.
         systemd_service = f'dhclient@{ifname}.service'
-        if is_systemd_service_active(systemd_service):
-            self._cmd(f'systemctl stop {systemd_service}')
 
+        # 'up' check is mandatory b/c even if the interface is A/D, as soon as
+        # the DHCP client is started the interface will be placed in u/u state.
+        # This is not what we intended to do when disabling an interface.
         if enable and 'disable' not in self._config:
             if dict_search('dhcp_options.host_name', self._config) == None:
                 # read configured system hostname.
@@ -1244,16 +1243,19 @@ class Interface(Control):
                     tmp = {'dhcp_options' : { 'host_name' : hostname}}
                     self._config = dict_merge(tmp, self._config)
 
-            render(options_file, 'dhcp-client/daemon-options.tmpl',
-                   self._config)
-            render(config_file, 'dhcp-client/ipv4.tmpl',
-                   self._config)
+            render(options_file, 'dhcp-client/daemon-options.tmpl', self._config)
+            render(config_file, 'dhcp-client/ipv4.tmpl', self._config)
 
-            # 'up' check is mandatory b/c even if the interface is A/D, as soon as
-            # the DHCP client is started the interface will be placed in u/u state.
-            # This is not what we intended to do when disabling an interface.
-            return self._cmd(f'systemctl restart {systemd_service}')
+            # When the DHCP client is restarted a brief outage will occur, as
+            # the old lease is released a new one is acquired (T4203). We will
+            # only restart DHCP client if it's option changed, or if it's not
+            # running, but it should be running (e.g. on system startup)
+            if 'dhcp_options_old' in self._config or not is_systemd_service_active(systemd_service):
+                return self._cmd(f'systemctl restart {systemd_service}')
+            return None
         else:
+            if is_systemd_service_active(systemd_service):
+                self._cmd(f'systemctl stop {systemd_service}')
             # cleanup old config files
             for file in [config_file, options_file, pid_file, lease_file]:
                 if os.path.isfile(file):
-- 
cgit v1.2.3


From b693f929b63c0c847d9a3c6ee9160845ef501be1 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 20 Feb 2022 10:40:38 +0100
Subject: static: T4203: obey interface dhcp default route distance

Commit 05aa22dc ("protocols: static: T3680: do not delete DHCP received routes")
added a bug whenever a static route is modified - the DHCP interface will
always end up with metric 210 - if there was a default route over a DHCP
interface.
---
 data/templates/frr/staticd.frr.tmpl                |  4 +-
 .../include/interface/dhcp-options.xml.i           |  3 +-
 python/vyos/configdict.py                          | 54 ++++++++++++++++++----
 3 files changed, 48 insertions(+), 13 deletions(-)

diff --git a/data/templates/frr/staticd.frr.tmpl b/data/templates/frr/staticd.frr.tmpl
index bfe959c1d..5d833228a 100644
--- a/data/templates/frr/staticd.frr.tmpl
+++ b/data/templates/frr/staticd.frr.tmpl
@@ -17,10 +17,10 @@ vrf {{ vrf }}
 {% endif %}
 {# IPv4 default routes from DHCP interfaces #}
 {% if dhcp is defined and dhcp is not none %}
-{%   for interface in dhcp %}
+{%   for interface, interface_config in dhcp.items() %}
 {%     set next_hop = interface | get_dhcp_router %}
 {%     if next_hop is defined and next_hop is not none %}
-{{ ip_prefix }} route 0.0.0.0/0 {{ next_hop }} {{ interface }} tag 210 210
+{{ ip_prefix }} route 0.0.0.0/0 {{ next_hop }} {{ interface }} tag 210 {{ interface_config.distance }}
 {%     endif %}
 {%   endfor %}
 {% endif %}
diff --git a/interface-definitions/include/interface/dhcp-options.xml.i b/interface-definitions/include/interface/dhcp-options.xml.i
index b65b0802a..f62b06640 100644
--- a/interface-definitions/include/interface/dhcp-options.xml.i
+++ b/interface-definitions/include/interface/dhcp-options.xml.i
@@ -30,12 +30,13 @@
         <help>Distance for the default route from DHCP server</help>
         <valueHelp>
           <format>u32:1-255</format>
-          <description>Distance for the default route from DHCP server (default 210)</description>
+          <description>Distance for the default route from DHCP server (default: 210)</description>
         </valueHelp>
         <constraint>
           <validator name="numeric" argument="--range 1-255"/>
         </constraint>
       </properties>
+      <defaultValue>210</defaultValue>
     </leafNode>
     <leafNode name="reject">
       <properties>
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index efeb6dc1f..f2ec93520 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -319,34 +319,42 @@ def is_source_interface(conf, interface, intftype=None):
 def get_dhcp_interfaces(conf, vrf=None):
     """ Common helper functions to retrieve all interfaces from current CLI
     sessions that have DHCP configured. """
-    dhcp_interfaces = []
+    dhcp_interfaces = {}
     dict = conf.get_config_dict(['interfaces'], get_first_key=True)
     if not dict:
         return dhcp_interfaces
 
     def check_dhcp(config, ifname):
-        out = []
+        tmp = {}
         if 'address' in config and 'dhcp' in config['address']:
+            options = {}
+            if 'dhcp_options' in config and 'default_route_distance' in config['dhcp_options']:
+                options.update({'distance' : config['dhcp_options']['default_route_distance']})
             if 'vrf' in config:
-                if vrf is config['vrf']: out.append(ifname)
-            else: out.append(ifname)
-        return out
+                if vrf is config['vrf']: tmp.update({ifname : options})
+            else: tmp.update({ifname : options})
+        return tmp
 
     for section, interface in dict.items():
-        for ifname, ifconfig in interface.items():
+        for ifname in interface:
+            # 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)
-            dhcp_interfaces.extend(tmp)
+            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}')
-                dhcp_interfaces.extend(tmp)
+                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}')
-                dhcp_interfaces.extend(tmp)
+                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}')
-                    dhcp_interfaces.extend(tmp)
+                    dhcp_interfaces.update(tmp)
 
     return dhcp_interfaces
 
@@ -405,6 +413,12 @@ def get_interface_dict(config, base, ifname=''):
     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)
 
@@ -475,6 +489,12 @@ def get_interface_dict(config, base, ifname=''):
             # 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})
@@ -509,6 +529,13 @@ def get_interface_dict(config, base, ifname=''):
             # 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})
@@ -543,6 +570,13 @@ def get_interface_dict(config, base, ifname=''):
                 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(
-- 
cgit v1.2.3


From 5d14a04b6ffbd592e8257d98d71da5acb1bb45a9 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 20 Feb 2022 18:50:29 +0100
Subject: smoketest: dhcp: T4203: move testcase to base class

We do not only provide DHCP functionality to ethernet interfaces, it's a common
feature so the testcase should be made available for multiple interface types.
---
 smoketest/scripts/cli/base_interfaces_test.py      | 28 +++++++++++++++++++++-
 smoketest/scripts/cli/test_interfaces_bonding.py   |  3 ++-
 smoketest/scripts/cli/test_interfaces_bridge.py    |  3 ++-
 smoketest/scripts/cli/test_interfaces_ethernet.py  | 21 ++--------------
 smoketest/scripts/cli/test_interfaces_macsec.py    |  3 ++-
 .../scripts/cli/test_interfaces_pseudo_ethernet.py |  3 ++-
 6 files changed, 37 insertions(+), 24 deletions(-)

diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py
index 9de961249..013c78837 100644
--- a/smoketest/scripts/cli/base_interfaces_test.py
+++ b/smoketest/scripts/cli/base_interfaces_test.py
@@ -1,4 +1,4 @@
-# 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
@@ -56,6 +56,7 @@ def is_mirrored_to(interface, mirror_if, qdisc):
 
 class BasicInterfaceTest:
     class TestCase(VyOSUnitTestSHIM.TestCase):
+        _test_dhcp = False
         _test_ip = False
         _test_mtu = False
         _test_vlan = False
@@ -96,6 +97,31 @@ class BasicInterfaceTest:
             for intf in self._interfaces:
                 self.assertNotIn(intf, interfaces())
 
+            # No daemon that was started during a test should remain running
+            for daemon in ['dhcp6c', 'dhclient']:
+                self.assertFalse(process_named_running(daemon))
+
+        def test_dhcp_disable_interface(self):
+            if not self._test_dhcp:
+                self.skipTest('not supported')
+
+            # When interface is configured as admin down, it must be admin down
+            # even when dhcpc starts on the given interface
+            for interface in self._interfaces:
+                self.cli_set(self._base_path + [interface, 'disable'])
+
+                # Also enable DHCP (ISC DHCP always places interface in admin up
+                # state so we check that we do not start DHCP client.
+                # https://phabricator.vyos.net/T2767
+                self.cli_set(self._base_path + [interface, 'address', 'dhcp'])
+
+            self.cli_commit()
+
+            # Validate interface state
+            for interface in self._interfaces:
+                flags = read_file(f'/sys/class/net/{interface}/flags')
+                self.assertEqual(int(flags, 16) & 1, 0)
+
         def test_span_mirror(self):
             if not self._mirror_interfaces:
                 self.skipTest('not supported')
diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py
index 1d9a887bd..4f2fe979a 100755
--- a/smoketest/scripts/cli/test_interfaces_bonding.py
+++ b/smoketest/scripts/cli/test_interfaces_bonding.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -28,6 +28,7 @@ from vyos.util import read_file
 class BondingInterfaceTest(BasicInterfaceTest.TestCase):
     @classmethod
     def setUpClass(cls):
+        cls._test_dhcp = True
         cls._test_ip = True
         cls._test_ipv6 = True
         cls._test_ipv6_pd = True
diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py
index 4f7e03298..f2e111425 100755
--- a/smoketest/scripts/cli/test_interfaces_bridge.py
+++ b/smoketest/scripts/cli/test_interfaces_bridge.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -31,6 +31,7 @@ from vyos.validate import is_intf_addr_assigned
 class BridgeInterfaceTest(BasicInterfaceTest.TestCase):
     @classmethod
     def setUpClass(cls):
+        cls._test_dhcp = True
         cls._test_ip = True
         cls._test_ipv6 = True
         cls._test_ipv6_pd = True
diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py
index ae3d5ed96..ee7649af8 100755
--- a/smoketest/scripts/cli/test_interfaces_ethernet.py
+++ b/smoketest/scripts/cli/test_interfaces_ethernet.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -102,6 +102,7 @@ def get_certificate_count(interface, cert_type):
 class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
     @classmethod
     def setUpClass(cls):
+        cls._test_dhcp = True
         cls._test_ip = True
         cls._test_ipv6 = True
         cls._test_ipv6_pd = True
@@ -146,24 +147,6 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
 
         self.cli_commit()
 
-    def test_dhcp_disable_interface(self):
-        # When interface is configured as admin down, it must be admin down
-        # even when dhcpc starts on the given interface
-        for interface in self._interfaces:
-            self.cli_set(self._base_path + [interface, 'disable'])
-
-            # Also enable DHCP (ISC DHCP always places interface in admin up
-            # state so we check that we do not start DHCP client.
-            # https://phabricator.vyos.net/T2767
-            self.cli_set(self._base_path + [interface, 'address', 'dhcp'])
-
-        self.cli_commit()
-
-        # Validate interface state
-        for interface in self._interfaces:
-            flags = read_file(f'/sys/class/net/{interface}/flags')
-            self.assertEqual(int(flags, 16) & 1, 0)
-
     def test_offloading_rps(self):
         # enable RPS on all available CPUs, RPS works woth a CPU bitmask,
         # where each bit represents a CPU (core/thread). The formula below
diff --git a/smoketest/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py
index e4280a5b7..5b10bfa44 100755
--- a/smoketest/scripts/cli/test_interfaces_macsec.py
+++ b/smoketest/scripts/cli/test_interfaces_macsec.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -40,6 +40,7 @@ def get_cipher(interface):
 class MACsecInterfaceTest(BasicInterfaceTest.TestCase):
     @classmethod
     def setUpClass(cls):
+        cls._test_dhcp = True
         cls._test_ip = True
         cls._test_ipv6 = True
         cls._base_path = ['interfaces', 'macsec']
diff --git a/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py b/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py
index ae899cddd..adcadc5eb 100755
--- a/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py
+++ b/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -23,6 +23,7 @@ from base_interfaces_test import BasicInterfaceTest
 class PEthInterfaceTest(BasicInterfaceTest.TestCase):
     @classmethod
     def setUpClass(cls):
+        cls._test_dhcp = True
         cls._test_ip = True
         cls._test_ipv6 = True
         cls._test_ipv6_pd = True
-- 
cgit v1.2.3


From 6bf5a0b0dd489a480dce6030e1c61d29e77fa107 Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Sun, 20 Feb 2022 18:30:44 +0000
Subject: ipsec: T1856: Ability to set SA life bytes and packets

set vpn ipsec esp-group grp-ESP life-bytes '100000'
set vpn ipsec esp-group grp-ESP life-packets '2000000'
---
 data/templates/ipsec/swanctl/peer.tmpl  | 12 ++++++++++++
 interface-definitions/vpn_ipsec.xml.in  | 24 ++++++++++++++++++++++++
 smoketest/scripts/cli/test_vpn_ipsec.py |  7 +++++++
 3 files changed, 43 insertions(+)

diff --git a/data/templates/ipsec/swanctl/peer.tmpl b/data/templates/ipsec/swanctl/peer.tmpl
index c6b71f2a1..481ea7224 100644
--- a/data/templates/ipsec/swanctl/peer.tmpl
+++ b/data/templates/ipsec/swanctl/peer.tmpl
@@ -57,6 +57,12 @@
 {%     set vti_esp = esp_group[ peer_conf.vti.esp_group ] if peer_conf.vti.esp_group is defined else esp_group[ peer_conf.default_esp_group ] %}
             peer_{{ name }}_vti {
                 esp_proposals = {{ vti_esp | get_esp_ike_cipher(ike) | join(',') }}
+{%   if vti_esp.life_bytes is defined and vti_esp.life_bytes is not none %}
+                life_bytes = {{ vti_esp.life_bytes }}
+{%   endif %}
+{%   if vti_esp.life_packets is defined and vti_esp.life_packets is not none %}
+                life_packets = {{ vti_esp.life_packets }}
+{%   endif %}
                 life_time = {{ vti_esp.lifetime }}s
                 local_ts = 0.0.0.0/0,::/0
                 remote_ts = 0.0.0.0/0,::/0
@@ -91,6 +97,12 @@
 {%       set remote_suffix = '[{0}/{1}]'.format(proto, remote_port) if proto or remote_port else '' %}
             peer_{{ name }}_tunnel_{{ tunnel_id }} {
                 esp_proposals = {{ tunnel_esp | get_esp_ike_cipher(ike) | join(',') }}
+{%       if tunnel_esp.life_bytes is defined and tunnel_esp.life_bytes is not none %}
+                life_bytes = {{ tunnel_esp.life_bytes }}
+{%       endif %}
+{%       if tunnel_esp.life_packets is defined and tunnel_esp.life_packets is not none %}
+                life_packets = {{ tunnel_esp.life_packets }}
+{%       endif %}
                 life_time = {{ tunnel_esp.lifetime }}s
 {%       if tunnel_esp.mode is not defined or tunnel_esp.mode == 'tunnel' %}
 {%         if tunnel_conf.local is defined and tunnel_conf.local.prefix is defined %}
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index afa3d52a0..af92eec31 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -55,6 +55,30 @@
                 </properties>
                 <defaultValue>3600</defaultValue>
               </leafNode>
+              <leafNode name="life-bytes">
+                <properties>
+                  <help>ESP life in bytes</help>
+                  <valueHelp>
+                    <format>u32:1024-26843545600000</format>
+                    <description>ESP life in bytes</description>
+                  </valueHelp>
+                  <constraint>
+                    <validator name="numeric" argument="--range 1024-26843545600000"/>
+                  </constraint>
+                </properties>
+              </leafNode>
+              <leafNode name="life-packets">
+                <properties>
+                  <help>ESP life in packets</help>
+                  <valueHelp>
+                    <format>u32:1000-26843545600000</format>
+                    <description>ESP life in packets</description>
+                  </valueHelp>
+                  <constraint>
+                    <validator name="numeric" argument="--range 1000-26843545600000"/>
+                  </constraint>
+                </properties>
+              </leafNode>
               <leafNode name="mode">
                 <properties>
                   <help>ESP mode</help>
diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py
index 1433c7329..14079c905 100755
--- a/smoketest/scripts/cli/test_vpn_ipsec.py
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -171,8 +171,13 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
         # Site to site
         local_address = '192.0.2.10'
         priority = '20'
+        life_bytes = '100000'
+        life_packets = '2000000'
         peer_base_path = base_path + ['site-to-site', 'peer', peer_ip]
 
+        self.cli_set(base_path + ['esp-group', esp_group, 'life-bytes', life_bytes])
+        self.cli_set(base_path + ['esp-group', esp_group, 'life-packets', life_packets])
+
         self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
         self.cli_set(peer_base_path + ['authentication', 'pre-shared-secret', secret])
         self.cli_set(peer_base_path + ['ike-group', ike_group])
@@ -197,6 +202,8 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
         swanctl_conf_lines = [
             f'version = 2',
             f'auth = psk',
+            f'life_bytes = {life_bytes}',
+            f'life_packets = {life_packets}',
             f'rekey_time = 28800s', # default value
             f'proposals = aes128-sha1-modp1024',
             f'esp_proposals = aes128-sha1-modp1024',
-- 
cgit v1.2.3


From 5ae566086c5c190d52b15f64454abcae9c8a1d46 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 20 Feb 2022 20:07:34 +0100
Subject: smoketest: dhcp: T4203: set missing interface options if present

Commit 5d14a04b ("smoketest: dhcp: T4203: move testcase to base class") added
global support in the test case framework for DHCP tests. Some interfaces (e.g.
MACsec) require additional options to be passed before the test can be launched.

In the MACsec case this includes a source interface, or encryption ciphers.
---
 smoketest/scripts/cli/base_interfaces_test.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py
index 013c78837..ba5acf5d6 100644
--- a/smoketest/scripts/cli/base_interfaces_test.py
+++ b/smoketest/scripts/cli/base_interfaces_test.py
@@ -109,6 +109,10 @@ class BasicInterfaceTest:
             # even when dhcpc starts on the given interface
             for interface in self._interfaces:
                 self.cli_set(self._base_path + [interface, 'disable'])
+                for option in self._options.get(interface, []):
+                    self.cli_set(self._base_path + [interface] + option.split())
+
+                self.cli_set(self._base_path + [interface, 'disable'])
 
                 # Also enable DHCP (ISC DHCP always places interface in admin up
                 # state so we check that we do not start DHCP client.
-- 
cgit v1.2.3


From 529af7898d062b42ac33e15bfdc62c14184e098f Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 20 Feb 2022 20:18:02 +0100
Subject: macsec: T4261: add dhcp client support

---
 interface-definitions/interfaces-macsec.xml.in | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in
index d69a093af..598935e51 100644
--- a/interface-definitions/interfaces-macsec.xml.in
+++ b/interface-definitions/interfaces-macsec.xml.in
@@ -16,7 +16,9 @@
           </valueHelp>
         </properties>
         <children>
-          #include <include/interface/address-ipv4-ipv6.xml.i>
+          #include <include/interface/address-ipv4-ipv6-dhcp.xml.i>
+          #include <include/interface/dhcp-options.xml.i>
+          #include <include/interface/dhcpv6-options.xml.i>
           #include <include/interface/ipv4-options.xml.i>
           #include <include/interface/ipv6-options.xml.i>
           #include <include/interface/interface-firewall.xml.i>
-- 
cgit v1.2.3


From f23040a0f7d425550350f91410272196f842308e Mon Sep 17 00:00:00 2001
From: srividya0208 <a.srividya@vyos.io>
Date: Sun, 20 Feb 2022 15:06:21 -0500
Subject: T4115:Reboot:Options "in" and "at" are not working

When reboot is executed with "in" option it only accepts minutes till 99 value
and does not accept greater values and "at" is also working same like in option
where as it should work with exact timings.
---
 op-mode-definitions/reboot.xml.in |  4 ++--
 src/op_mode/powerctrl.py          | 25 +++++++++++++++++++++----
 2 files changed, 23 insertions(+), 6 deletions(-)

diff --git a/op-mode-definitions/reboot.xml.in b/op-mode-definitions/reboot.xml.in
index 2c8daec5d..6414742d9 100644
--- a/op-mode-definitions/reboot.xml.in
+++ b/op-mode-definitions/reboot.xml.in
@@ -25,7 +25,7 @@
             <list>&lt;Minutes&gt;</list>
           </completionHelp>
         </properties>
-        <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3 $4</command>
+        <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot_in $3 $4</command>
       </tagNode>
       <tagNode name="at">
         <properties>
@@ -40,7 +40,7 @@
             <properties>
               <help>Reboot at a specific date</help>
               <completionHelp>
-                <list>&lt;DDMMYYYY&gt; &lt;DD/MM/YYYY&gt; &lt;DD.MM.YYYY&gt; &lt;DD:MM:YYYY&gt;</list>
+                <list>&lt;DD/MM/YYYY&gt; &lt;DD.MM.YYYY&gt; &lt;DD:MM:YYYY&gt;</list>
               </completionHelp>
             </properties>
             <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3 $5</command>
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
index 679b03c0b..fd4f86d88 100755
--- a/src/op_mode/powerctrl.py
+++ b/src/op_mode/powerctrl.py
@@ -33,10 +33,12 @@ def utc2local(datetime):
 
 def parse_time(s):
     try:
-        if re.match(r'^\d{1,2}$', s):
-            if (int(s) > 59):
+        if re.match(r'^\d{1,9999}$', s):
+            if (int(s) > 59) and (int(s) < 1440):
                 s = str(int(s)//60) + ":" + str(int(s)%60)
                 return datetime.strptime(s, "%H:%M").time()
+            if (int(s) >= 1440):
+                return s.split()
             else:
                 return datetime.strptime(s, "%M").time()
         else:
@@ -141,7 +143,7 @@ def execute_shutdown(time, reboot=True, ask=True):
             cmd(f'/usr/bin/wall "{wall_msg}"')
         else:
             if not ts:
-                exit(f'Invalid time "{time[0]}". The valid format is HH:MM')
+                exit(f'Invalid time "{time[0]}". Uses 24 Hour Clock format')
             else:
                 exit(f'Invalid date "{time[1]}". A valid format is YYYY-MM-DD [HH:MM]')
     else:
@@ -172,7 +174,12 @@ def main():
     action.add_argument("--reboot", "-r",
                         help="Reboot the system",
                         nargs="*",
-                        metavar="Minutes|HH:MM")
+                        metavar="HH:MM")
+
+    action.add_argument("--reboot_in", "-i",
+                        help="Reboot the system",
+                        nargs="*",
+                        metavar="Minutes")
 
     action.add_argument("--poweroff", "-p",
                         help="Poweroff the system",
@@ -190,7 +197,17 @@ def main():
 
     try:
         if args.reboot is not None:
+            for r in args.reboot:
+                if ':' not in r and '/' not in r and '.' not in r:
+                    print("Incorrect  format! Use HH:MM")
+                    exit(1)
             execute_shutdown(args.reboot, reboot=True, ask=args.yes)
+        if args.reboot_in is not None:
+            for i in args.reboot_in:
+                if ':' in i:
+                    print("Incorrect format! Use Minutes")
+                    exit(1)
+            execute_shutdown(args.reboot_in, reboot=True, ask=args.yes)
         if args.poweroff is not None:
             execute_shutdown(args.poweroff, reboot=False, ask=args.yes)
         if args.cancel:
-- 
cgit v1.2.3


From 4ec6262629393bd8a88951970c367a5cc3d57a42 Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Sun, 20 Feb 2022 20:32:06 +0000
Subject: ipsec: T3948: Add CLI site-to-site peer connection-type none

set vpn ipsec site-to-site peer 192.0.2.14 connection-type none
---
 data/templates/ipsec/swanctl/peer.tmpl  | 4 ++++
 interface-definitions/vpn_ipsec.xml.in  | 8 ++++++--
 smoketest/scripts/cli/test_vpn_ipsec.py | 2 ++
 3 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/data/templates/ipsec/swanctl/peer.tmpl b/data/templates/ipsec/swanctl/peer.tmpl
index f4e28d818..673dc3375 100644
--- a/data/templates/ipsec/swanctl/peer.tmpl
+++ b/data/templates/ipsec/swanctl/peer.tmpl
@@ -77,6 +77,8 @@
                 start_action = start
 {%     elif peer_conf.connection_type == 'respond' %}
                 start_action = trap
+{%     elif peer_conf.connection_type == 'none' %}
+                start_action = none
 {%     endif %}
 {%     if ike.dead_peer_detection is defined %}
 {%       set dpd_translate = {'clear': 'clear', 'hold': 'trap', 'restart': 'start'} %}
@@ -119,6 +121,8 @@
                 start_action = start
 {%       elif peer_conf.connection_type == 'respond' %}
                 start_action = trap
+{%       elif peer_conf.connection_type == 'none' %}
+                start_action = none
 {%       endif %}
 {%       if ike.dead_peer_detection is defined %}
 {%         set dpd_translate = {'clear': 'clear', 'hold': 'trap', 'restart': 'start'} %}
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index f7297a6e2..7b5074112 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -978,7 +978,7 @@
                     <properties>
                       <help>Connection type</help>
                       <completionHelp>
-                        <list>initiate respond</list>
+                        <list>initiate respond none</list>
                       </completionHelp>
                       <valueHelp>
                         <format>initiate</format>
@@ -988,8 +988,12 @@
                         <format>respond</format>
                         <description>Bring the connection up only if traffic is detected</description>
                       </valueHelp>
+                      <valueHelp>
+                        <format>none</format>
+                        <description>Load the connection only</description>
+                      </valueHelp>
                       <constraint>
-                        <regex>^(initiate|respond)$</regex>
+                        <regex>^(initiate|respond|none)$</regex>
                       </constraint>
                     </properties>
                   </leafNode>
diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py
index 2c3e55a57..699d854bb 100755
--- a/smoketest/scripts/cli/test_vpn_ipsec.py
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -238,6 +238,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
         peer_base_path = base_path + ['site-to-site', 'peer', peer_ip]
         self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
         self.cli_set(peer_base_path + ['authentication', 'pre-shared-secret', secret])
+        self.cli_set(peer_base_path + ['connection-type', 'none'])
         self.cli_set(peer_base_path + ['ike-group', ike_group])
         self.cli_set(peer_base_path + ['default-esp-group', esp_group])
         self.cli_set(peer_base_path + ['local-address', local_address])
@@ -266,6 +267,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
             f'mode = tunnel',
             f'local_ts = 172.16.10.0/24,172.16.11.0/24',
             f'remote_ts = 172.17.10.0/24,172.17.11.0/24',
+            f'start_action = none',
             f'if_id_in = {if_id}', # will be 11 for vti10 - shifted by one
             f'if_id_out = {if_id}',
             f'updown = "/etc/ipsec.d/vti-up-down {vti}"'
-- 
cgit v1.2.3


From 0ecddff7cffa8900d351d5c15e32420f9d780c0b Mon Sep 17 00:00:00 2001
From: Andreas <vyos-git@justsecure.de>
Date: Wed, 29 Dec 2021 18:02:06 +0100
Subject: vxlan: T4120: add ability to set multiple remotes (PR #1127)

VXLAN does support using multiple remotes but VyOS does not. Add the ability
to set multiple remotes and add their flood lists using "bridge" command.
---
 .../include/interface/tunnel-remote.xml.i          |  2 +-
 .../include/interface/tunnel-remotes.xml.i         | 19 ++++++++++++
 interface-definitions/interfaces-vxlan.xml.in      |  2 +-
 python/vyos/ifconfig/vxlan.py                      |  7 +++++
 smoketest/scripts/cli/test_interfaces_vxlan.py     |  2 ++
 src/conf_mode/interfaces-vxlan.py                  | 34 ++++++++++++++++++++++
 6 files changed, 64 insertions(+), 2 deletions(-)
 create mode 100644 interface-definitions/include/interface/tunnel-remotes.xml.i

diff --git a/interface-definitions/include/interface/tunnel-remote.xml.i b/interface-definitions/include/interface/tunnel-remote.xml.i
index 1ba9b0382..2a8891b85 100644
--- a/interface-definitions/include/interface/tunnel-remote.xml.i
+++ b/interface-definitions/include/interface/tunnel-remote.xml.i
@@ -1,4 +1,4 @@
-<!-- include start from rip/tunnel-remote.xml.i -->
+<!-- include start from interface/tunnel-remote.xml.i -->
 <leafNode name="remote">
   <properties>
     <help>Tunnel remote address</help>
diff --git a/interface-definitions/include/interface/tunnel-remotes.xml.i b/interface-definitions/include/interface/tunnel-remotes.xml.i
new file mode 100644
index 000000000..ae8481898
--- /dev/null
+++ b/interface-definitions/include/interface/tunnel-remotes.xml.i
@@ -0,0 +1,19 @@
+<!-- include start from interface/tunnel-remotes.xml.i -->
+<leafNode name="remote">
+  <properties>
+    <help>Tunnel remote address</help>
+    <valueHelp>
+      <format>ipv4</format>
+      <description>Tunnel remote IPv4 address</description>
+    </valueHelp>
+    <valueHelp>
+      <format>ipv6</format>
+      <description>Tunnel remote IPv6 address</description>
+    </valueHelp>
+    <constraint>
+      <validator name="ip-address"/>
+    </constraint>
+    <multi/>
+  </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in
index 4c3c3ac71..559067ea5 100644
--- a/interface-definitions/interfaces-vxlan.xml.in
+++ b/interface-definitions/interfaces-vxlan.xml.in
@@ -98,7 +98,7 @@
           </leafNode>
           #include <include/source-address-ipv4-ipv6.xml.i>
           #include <include/source-interface.xml.i>
-          #include <include/interface/tunnel-remote.xml.i>
+          #include <include/interface/tunnel-remotes.xml.i>
           #include <include/interface/vrf.xml.i>
           #include <include/vni.xml.i>
         </children>
diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py
index 0c5282db4..87b5e40b8 100644
--- a/python/vyos/ifconfig/vxlan.py
+++ b/python/vyos/ifconfig/vxlan.py
@@ -82,3 +82,10 @@ class VXLANIf(Interface):
         self._cmd(cmd.format(**self.config))
         # interface is always A/D down. It needs to be enabled explicitly
         self.set_admin_state('down')
+
+        other_remotes = self.config.get('other_remotes')
+        if other_remotes:
+            for rem in other_remotes:
+                self.config['rem'] = rem
+                cmd2 = 'bridge fdb append to 00:00:00:00:00:00 dst {rem} port {port} dev {ifname}'
+                self._cmd(cmd2.format(**self.config))
diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py
index 9278adadd..12fc463ba 100755
--- a/smoketest/scripts/cli/test_interfaces_vxlan.py
+++ b/smoketest/scripts/cli/test_interfaces_vxlan.py
@@ -33,6 +33,8 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase):
             'vxlan10': ['vni 10', 'remote 127.0.0.2'],
             'vxlan20': ['vni 20', 'group 239.1.1.1', 'source-interface eth0'],
             'vxlan30': ['vni 30', 'remote 2001:db8:2000::1', 'source-address 2001:db8:1000::1', 'parameters ipv6 flowlabel 0x1000'],
+            'vxlan40': ['vni 40', 'remote 127.0.0.2', 'remote 127.0.0.3'],
+            'vxlan50': ['vni 50', 'remote 2001:db8:2000::1', 'remote 2001:db8:2000::2', 'parameters ipv6 flowlabel 0x1000'],
         }
         cls._interfaces = list(cls._options)
         # call base-classes classmethod
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 1f097c4e3..092f249df 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -58,6 +58,13 @@ def get_config(config=None):
     if len(vxlan['other_tunnels']) == 0:
         del vxlan['other_tunnels']
 
+    # leave first remote in dict and put the other ones (if they exists) to "other_remotes"
+    remotes = vxlan.get('remote')
+    if remotes:
+        vxlan['remote'] = remotes[0]
+        if len(remotes) > 1:
+            del remotes[0]
+            vxlan['other_remotes'] = remotes
     return vxlan
 
 def verify(vxlan):
@@ -108,6 +115,33 @@ def verify(vxlan):
             raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\
                               f'bytes) for VXLAN overhead ({vxlan_overhead} bytes!)')
 
+    # Check for mixed IPv4 and IPv6 addresses
+    protocol = None
+    if 'source_address' in vxlan:
+        if is_ipv6(vxlan['source_address']):
+            protocol = 'ipv6'
+        else:
+            protocol = 'ipv4'
+    if 'remote' in vxlan:
+        if is_ipv6(vxlan['remote']):
+            if protocol == 'ipv4':
+                raise ConfigError('IPv4 and IPV6 cannot be mixed')
+            protocol = 'ipv6'
+        else:
+            if protocol == 'ipv6':
+                raise ConfigError('IPv4 and IPV6 cannot be mixed')
+            protocol = 'ipv4'
+    if 'other_remotes' in vxlan:
+        for rem in vxlan['other_remotes']:
+            if is_ipv6(rem):
+                if protocol == 'ipv4':
+                    raise ConfigError('IPv4 and IPV6 cannot be mixed')
+                protocol = 'ipv6'
+            else:
+                if protocol == 'ipv6':
+                    raise ConfigError('IPv4 and IPV6 cannot be mixed')
+                protocol = 'ipv4'
+
     verify_mtu_ipv6(vxlan)
     verify_address(vxlan)
     return None
-- 
cgit v1.2.3


From d418cd36027aef5993122ec62419e8c66fe7a1ed Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 20 Feb 2022 22:06:49 +0100
Subject: vxlan: T4120: rename tunnel-remotes.xml.i ->
 tunnel-remote-multi.xml.i

---
 .../include/interface/tunnel-remote-multi.xml.i       | 19 +++++++++++++++++++
 .../include/interface/tunnel-remotes.xml.i            | 19 -------------------
 interface-definitions/interfaces-vxlan.xml.in         |  2 +-
 3 files changed, 20 insertions(+), 20 deletions(-)
 create mode 100644 interface-definitions/include/interface/tunnel-remote-multi.xml.i
 delete mode 100644 interface-definitions/include/interface/tunnel-remotes.xml.i

diff --git a/interface-definitions/include/interface/tunnel-remote-multi.xml.i b/interface-definitions/include/interface/tunnel-remote-multi.xml.i
new file mode 100644
index 000000000..f672087a4
--- /dev/null
+++ b/interface-definitions/include/interface/tunnel-remote-multi.xml.i
@@ -0,0 +1,19 @@
+<!-- include start from interface/tunnel-remote-multi.xml.i -->
+<leafNode name="remote">
+  <properties>
+    <help>Tunnel remote address</help>
+    <valueHelp>
+      <format>ipv4</format>
+      <description>Tunnel remote IPv4 address</description>
+    </valueHelp>
+    <valueHelp>
+      <format>ipv6</format>
+      <description>Tunnel remote IPv6 address</description>
+    </valueHelp>
+    <constraint>
+      <validator name="ip-address"/>
+    </constraint>
+    <multi/>
+  </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/interface/tunnel-remotes.xml.i b/interface-definitions/include/interface/tunnel-remotes.xml.i
deleted file mode 100644
index ae8481898..000000000
--- a/interface-definitions/include/interface/tunnel-remotes.xml.i
+++ /dev/null
@@ -1,19 +0,0 @@
-<!-- include start from interface/tunnel-remotes.xml.i -->
-<leafNode name="remote">
-  <properties>
-    <help>Tunnel remote address</help>
-    <valueHelp>
-      <format>ipv4</format>
-      <description>Tunnel remote IPv4 address</description>
-    </valueHelp>
-    <valueHelp>
-      <format>ipv6</format>
-      <description>Tunnel remote IPv6 address</description>
-    </valueHelp>
-    <constraint>
-      <validator name="ip-address"/>
-    </constraint>
-    <multi/>
-  </properties>
-</leafNode>
-<!-- include end -->
diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in
index 559067ea5..0546b4199 100644
--- a/interface-definitions/interfaces-vxlan.xml.in
+++ b/interface-definitions/interfaces-vxlan.xml.in
@@ -98,7 +98,7 @@
           </leafNode>
           #include <include/source-address-ipv4-ipv6.xml.i>
           #include <include/source-interface.xml.i>
-          #include <include/interface/tunnel-remotes.xml.i>
+          #include <include/interface/tunnel-remote-multi.xml.i>
           #include <include/interface/vrf.xml.i>
           #include <include/vni.xml.i>
         </children>
-- 
cgit v1.2.3


From 25b2f2a8057260ad0d2c59823618d7c9f0fba707 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 20 Feb 2022 22:10:37 +0100
Subject: bridge: remove unreferenced import -> leaf_node_changed

---
 src/conf_mode/interfaces-bridge.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 4d3ebc587..f4dba9d4a 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -22,7 +22,6 @@ from netifaces import interfaces
 from vyos.config import Config
 from vyos.configdict import get_interface_dict
 from vyos.configdict import node_changed
-from vyos.configdict import leaf_node_changed
 from vyos.configdict import is_member
 from vyos.configdict import is_source_interface
 from vyos.configdict import has_vlan_subinterface_configured
-- 
cgit v1.2.3


From 9b7d0744409b4fc9433a1dc1e83e8c984567a2d0 Mon Sep 17 00:00:00 2001
From: srividya0208 <a.srividya@vyos.io>
Date: Mon, 21 Feb 2022 09:20:23 -0500
Subject: vpn_ipsec: T3656: modified completion help for key-exchange

In latest releases, default IKE version is removed, which allows the
connection to be IKEv1 or IKEv2.
The completion help shows IKEv1 as default so removed it.
---
 interface-definitions/vpn_ipsec.xml.in | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index f7297a6e2..5ec7480cc 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -311,7 +311,7 @@
                   </completionHelp>
                   <valueHelp>
                     <format>ikev1</format>
-                    <description>Use IKEv1 for key exchange [DEFAULT]</description>
+                    <description>Use IKEv1 for key exchange</description>
                   </valueHelp>
                   <valueHelp>
                     <format>ikev2</format>
-- 
cgit v1.2.3


From a3b7e985911eeaccac4fa229563b78c5a64e7e90 Mon Sep 17 00:00:00 2001
From: Daniil Baturin <daniil@vyos.io>
Date: Mon, 21 Feb 2022 08:06:40 -0500
Subject: T2719: initial batch of standardized structure op mode scripts

---
 src/op_mode/cpu_summary.py  | 36 +++++++++++++++++---------
 src/op_mode/show_cpu.py     | 63 ++++++++++++++++++++++++++-------------------
 src/op_mode/show_ram.py     | 19 +++++++++-----
 src/op_mode/show_uptime.py  | 27 ++++++++++++++-----
 src/op_mode/show_version.py | 22 +++++++++++-----
 5 files changed, 109 insertions(+), 58 deletions(-)

diff --git a/src/op_mode/cpu_summary.py b/src/op_mode/cpu_summary.py
index cfd321522..3bdf5a718 100755
--- a/src/op_mode/cpu_summary.py
+++ b/src/op_mode/cpu_summary.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -19,18 +19,30 @@ from vyos.util import colon_separated_to_dict
 
 FILE_NAME = '/proc/cpuinfo'
 
-with open(FILE_NAME, 'r') as f:
-    data_raw = f.read()
+def get_raw_data():
+    with open(FILE_NAME, 'r') as f:
+        data_raw = f.read()
 
-data = colon_separated_to_dict(data_raw)
+    data = colon_separated_to_dict(data_raw)
 
-# Accumulate all data in a dict for future support for machine-readable output
-cpu_data = {}
-cpu_data['cpu_number'] = len(data['processor'])
-cpu_data['models'] = list(set(data['model name']))
+    # Accumulate all data in a dict for future support for machine-readable output
+    cpu_data = {}
+    cpu_data['cpu_number'] = len(data['processor'])
+    cpu_data['models'] = list(set(data['model name']))
 
-# Strip extra whitespace from CPU model names, /proc/cpuinfo is prone to that
-cpu_data['models'] = map(lambda s: re.sub(r'\s+', ' ', s), cpu_data['models'])
+    # Strip extra whitespace from CPU model names, /proc/cpuinfo is prone to that
+    cpu_data['models'] = list(map(lambda s: re.sub(r'\s+', ' ', s), cpu_data['models']))
+
+    return cpu_data
+
+def get_formatted_output():
+    cpu_data = get_raw_data()
+
+    out = "CPU(s): {0}\n".format(cpu_data['cpu_number'])
+    out += "CPU model(s): {0}".format(",".join(cpu_data['models']))
+
+    return out
+
+if __name__ == '__main__':
+    print(get_formatted_output())
 
-print("CPU(s): {0}".format(cpu_data['cpu_number']))
-print("CPU model(s): {0}".format(",".join(cpu_data['models'])))
diff --git a/src/op_mode/show_cpu.py b/src/op_mode/show_cpu.py
index 0040e950d..9973d9789 100755
--- a/src/op_mode/show_cpu.py
+++ b/src/op_mode/show_cpu.py
@@ -21,7 +21,7 @@ from sys import exit
 from vyos.util import popen, DEVNULL
 
 OUT_TMPL_SRC = """
-{% if cpu %}
+{%- if cpu -%}
 {% if 'vendor' in cpu %}CPU Vendor:       {{cpu.vendor}}{% endif %}
 {% if 'model' in cpu %}Model:            {{cpu.model}}{% endif %}
 {% if 'cpus' in cpu %}Total CPUs:       {{cpu.cpus}}{% endif %}
@@ -31,31 +31,42 @@ OUT_TMPL_SRC = """
 {% if 'mhz' in cpu %}Current MHz:      {{cpu.mhz}}{% endif %}
 {% if 'mhz_min' in cpu %}Minimum MHz:      {{cpu.mhz_min}}{% endif %}
 {% if 'mhz_max' in cpu %}Maximum MHz:      {{cpu.mhz_max}}{% endif %}
-{% endif %}
+{%- endif -%}
 """
 
-cpu = {}
-cpu_json, code = popen('lscpu -J', stderr=DEVNULL)
-
-if code == 0:
-    cpu_info = json.loads(cpu_json)
-    if len(cpu_info) > 0 and 'lscpu' in cpu_info:
-        for prop in cpu_info['lscpu']:
-            if (prop['field'].find('Thread(s)') > -1): cpu['threads'] = prop['data']
-            if (prop['field'].find('Core(s)')) > -1: cpu['cores'] = prop['data']
-            if (prop['field'].find('Socket(s)')) > -1: cpu['sockets'] = prop['data']
-            if (prop['field'].find('CPU(s):')) > -1: cpu['cpus'] = prop['data']
-            if (prop['field'].find('CPU MHz')) > -1: cpu['mhz'] = prop['data']
-            if (prop['field'].find('CPU min MHz')) > -1: cpu['mhz_min'] = prop['data']
-            if (prop['field'].find('CPU max MHz')) > -1: cpu['mhz_max'] = prop['data']
-            if (prop['field'].find('Vendor ID')) > -1: cpu['vendor'] = prop['data']
-            if (prop['field'].find('Model name')) > -1: cpu['model'] = prop['data']
-
-if len(cpu) > 0:
-    tmp = { 'cpu':cpu }
+def get_raw_data():
+    cpu = {}
+    cpu_json, code = popen('lscpu -J', stderr=DEVNULL)
+
+    if code == 0:
+        cpu_info = json.loads(cpu_json)
+        if len(cpu_info) > 0 and 'lscpu' in cpu_info:
+            for prop in cpu_info['lscpu']:
+                if (prop['field'].find('Thread(s)') > -1): cpu['threads'] = prop['data']
+                if (prop['field'].find('Core(s)')) > -1: cpu['cores'] = prop['data']
+                if (prop['field'].find('Socket(s)')) > -1: cpu['sockets'] = prop['data']
+                if (prop['field'].find('CPU(s):')) > -1: cpu['cpus'] = prop['data']
+                if (prop['field'].find('CPU MHz')) > -1: cpu['mhz'] = prop['data']
+                if (prop['field'].find('CPU min MHz')) > -1: cpu['mhz_min'] = prop['data']
+                if (prop['field'].find('CPU max MHz')) > -1: cpu['mhz_max'] = prop['data']
+                if (prop['field'].find('Vendor ID')) > -1: cpu['vendor'] = prop['data']
+                if (prop['field'].find('Model name')) > -1: cpu['model'] = prop['data']
+
+    return cpu
+
+def get_formatted_output():
+    cpu = get_raw_data()
+
+    tmp = {'cpu':cpu}
     tmpl = Template(OUT_TMPL_SRC)
-    print(tmpl.render(tmp))
-    exit(0)
-else:
-    print('CPU information could not be determined\n')
-    exit(1)
+    return tmpl.render(tmp)
+
+if __name__ == '__main__':
+    cpu = get_raw_data()
+
+    if len(cpu) > 0:
+        print(get_formatted_output())
+    else:
+        print('CPU information could not be determined\n')
+        exit(1)
+
diff --git a/src/op_mode/show_ram.py b/src/op_mode/show_ram.py
index 5818ec132..2b0be3965 100755
--- a/src/op_mode/show_ram.py
+++ b/src/op_mode/show_ram.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2021 VyOS maintainers and contributors
+# 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
@@ -55,10 +55,17 @@ def get_system_memory_human():
 
     return mem
 
-if __name__ == '__main__':
-    mem = get_system_memory_human()
+def get_raw_data():
+    return get_system_memory_human()
+
+def get_formatted_output():
+    mem = get_raw_data()
 
-    print("Total: {}".format(mem["total"]))
-    print("Free:  {}".format(mem["free"]))
-    print("Used:  {}".format(mem["used"]))
+    out = "Total: {}\n".format(mem["total"])
+    out += "Free:  {}\n".format(mem["free"])
+    out += "Used:  {}".format(mem["used"])
 
+    return out
+
+if __name__ == '__main__':
+    print(get_formatted_output())
diff --git a/src/op_mode/show_uptime.py b/src/op_mode/show_uptime.py
index c3dea52e6..1b5e33fa9 100755
--- a/src/op_mode/show_uptime.py
+++ b/src/op_mode/show_uptime.py
@@ -37,14 +37,27 @@ def get_load_averages():
 
     return res
 
-if __name__ == '__main__':
+def get_raw_data():
     from vyos.util import seconds_to_human
 
-    print("Uptime: {}\n".format(seconds_to_human(get_uptime_seconds())))
+    res = {}
+    res["uptime_seconds"] = get_uptime_seconds()
+    res["uptime"] = seconds_to_human(get_uptime_seconds())
+    res["load_average"] = get_load_averages()
+
+    return res
 
-    avgs = get_load_averages()
+def get_formatted_output():
+    data = get_raw_data()
 
-    print("Load averages:")
-    print("1  minute:   {:.02f}%".format(avgs[1]*100))
-    print("5  minutes:  {:.02f}%".format(avgs[5]*100))
-    print("15 minutes:  {:.02f}%".format(avgs[15]*100))
+    out = "Uptime: {}\n\n".format(data["uptime"])
+    avgs = data["load_average"]
+    out += "Load averages:\n"
+    out += "1  minute:   {:.02f}%\n".format(avgs[1]*100)
+    out += "5  minutes:  {:.02f}%\n".format(avgs[5]*100)
+    out += "15 minutes:  {:.02f}%\n".format(avgs[15]*100)
+
+    return out
+
+if __name__ == '__main__':
+    print(get_formatted_output())
diff --git a/src/op_mode/show_version.py b/src/op_mode/show_version.py
index 7962e1e7b..b82ab6eca 100755
--- a/src/op_mode/show_version.py
+++ b/src/op_mode/show_version.py
@@ -26,10 +26,6 @@ from jinja2 import Template
 from sys import exit
 from vyos.util import call
 
-parser = argparse.ArgumentParser()
-parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output")
-parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output")
-
 version_output_tmpl = """
 Version:          VyOS {{version}}
 Release train:    {{release_train}}
@@ -51,7 +47,20 @@ Hardware UUID:    {{hardware_uuid}}
 Copyright:        VyOS maintainers and contributors
 """
 
+def get_raw_data():
+    version_data = vyos.version.get_full_version_data()
+    return version_data
+
+def get_formatted_output():
+    version_data = get_raw_data()
+    tmpl = Template(version_output_tmpl)
+    return tmpl.render(version_data)
+
 if __name__ == '__main__':
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output")
+    parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output")
+
     args = parser.parse_args()
 
     version_data = vyos.version.get_full_version_data()
@@ -60,9 +69,8 @@ if __name__ == '__main__':
         import json
         print(json.dumps(version_data))
         exit(0)
-
-    tmpl = Template(version_output_tmpl)
-    print(tmpl.render(version_data))
+    else:
+        print(get_formatted_output())
 
     if args.funny:
         print(vyos.limericks.get_random())
-- 
cgit v1.2.3


From 3a605ad020d8d20b08a72cb1284f6e590d1fd7b5 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Mon, 21 Feb 2022 18:23:55 +0100
Subject: vxlan: T4120: code cleanup for multiple remotes

---
 python/vyos/ifconfig/vxlan.py     | 24 +++++++++++++++++-------
 src/conf_mode/interfaces-vxlan.py | 38 ++++++++++----------------------------
 2 files changed, 27 insertions(+), 35 deletions(-)

diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py
index 87b5e40b8..516a19f24 100644
--- a/python/vyos/ifconfig/vxlan.py
+++ b/python/vyos/ifconfig/vxlan.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2022 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
@@ -68,6 +68,16 @@ class VXLANIf(Interface):
             'vni'                        : 'id',
         }
 
+        # IPv6 flowlabels can only be used on IPv6 tunnels, thus we need to
+        # ensure that at least the first remote IP address is passed to the
+        # tunnel creation command. Subsequent tunnel remote addresses can later
+        # be added to the FDB
+        remote_list = None
+        if 'remote' in self.config:
+            # skip first element as this is already configured as remote
+            remote_list = self.config['remote'][1:]
+            self.config['remote'] = self.config['remote'][0]
+
         cmd = 'ip link add {ifname} type {type} dstport {port}'
         for vyos_key, iproute2_key in mapping.items():
             # dict_search will return an empty dict "{}" for valueless nodes like
@@ -83,9 +93,9 @@ class VXLANIf(Interface):
         # interface is always A/D down. It needs to be enabled explicitly
         self.set_admin_state('down')
 
-        other_remotes = self.config.get('other_remotes')
-        if other_remotes:
-            for rem in other_remotes:
-                self.config['rem'] = rem
-                cmd2 = 'bridge fdb append to 00:00:00:00:00:00 dst {rem} port {port} dev {ifname}'
-                self._cmd(cmd2.format(**self.config))
+        # VXLAN tunnel is always recreated on any change - see interfaces-vxlan.py
+        if remote_list:
+            for remote in remote_list:
+                cmd = f'bridge fdb append to 00:00:00:00:00:00 dst {remote} ' \
+                       'port {port} dev {ifname}'
+                self._cmd(cmd.format(**self.config))
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 092f249df..85604508e 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2019-2020 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
@@ -34,8 +34,8 @@ 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
+    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
@@ -58,13 +58,6 @@ def get_config(config=None):
     if len(vxlan['other_tunnels']) == 0:
         del vxlan['other_tunnels']
 
-    # leave first remote in dict and put the other ones (if they exists) to "other_remotes"
-    remotes = vxlan.get('remote')
-    if remotes:
-        vxlan['remote'] = remotes[0]
-        if len(remotes) > 1:
-            del remotes[0]
-            vxlan['other_remotes'] = remotes
     return vxlan
 
 def verify(vxlan):
@@ -77,8 +70,7 @@ def verify(vxlan):
 
     if 'group' in vxlan:
         if 'source_interface' not in vxlan:
-            raise ConfigError('Multicast VXLAN requires an underlaying interface ')
-
+            raise ConfigError('Multicast VXLAN requires an underlaying interface')
         verify_source_interface(vxlan)
 
     if not any(tmp in ['group', 'remote', 'source_address'] for tmp in vxlan):
@@ -122,35 +114,26 @@ def verify(vxlan):
             protocol = 'ipv6'
         else:
             protocol = 'ipv4'
+
     if 'remote' in vxlan:
-        if is_ipv6(vxlan['remote']):
-            if protocol == 'ipv4':
-                raise ConfigError('IPv4 and IPV6 cannot be mixed')
-            protocol = 'ipv6'
-        else:
-            if protocol == 'ipv6':
-                raise ConfigError('IPv4 and IPV6 cannot be mixed')
-            protocol = 'ipv4'
-    if 'other_remotes' in vxlan:
-        for rem in vxlan['other_remotes']:
-            if is_ipv6(rem):
+        error_msg = 'Can not mix both IPv4 and IPv6 for VXLAN underlay'
+        for remote in vxlan['remote']:
+            if is_ipv6(remote):
                 if protocol == 'ipv4':
-                    raise ConfigError('IPv4 and IPV6 cannot be mixed')
+                    raise ConfigError(error_msg)
                 protocol = 'ipv6'
             else:
                 if protocol == 'ipv6':
-                    raise ConfigError('IPv4 and IPV6 cannot be mixed')
+                    raise ConfigError(error_msg)
                 protocol = 'ipv4'
 
     verify_mtu_ipv6(vxlan)
     verify_address(vxlan)
     return None
 
-
 def generate(vxlan):
     return None
 
-
 def apply(vxlan):
     # Check if the VXLAN interface already exists
     if vxlan['ifname'] in interfaces():
@@ -166,7 +149,6 @@ def apply(vxlan):
 
     return None
 
-
 if __name__ == '__main__':
     try:
         c = get_config()
-- 
cgit v1.2.3


From c3661c8d5d7e8f5c1d040cadf134e87f0d77e28e Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Mon, 21 Feb 2022 18:25:05 +0100
Subject: smoketest: vxlan: T4120: verify support for multiple remote addresses

---
 python/vyos/util.py                            |  8 ++++++
 smoketest/scripts/cli/test_interfaces_vxlan.py | 34 ++++++++++++++++++--------
 2 files changed, 32 insertions(+), 10 deletions(-)

diff --git a/python/vyos/util.py b/python/vyos/util.py
index 1767ff9d3..4526375df 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -774,6 +774,14 @@ def dict_search_recursive(dict_object, key):
             for x in dict_search_recursive(j, key):
                 yield x
 
+def get_bridge_fdb(interface):
+    """ Returns the forwarding database entries for a given interface """
+    if not os.path.exists(f'/sys/class/net/{interface}'):
+        return None
+    from json import loads
+    tmp = loads(cmd(f'bridge -j fdb show dev {interface}'))
+    return tmp
+
 def get_interface_config(interface):
     """ Returns the used encapsulation protocol for given interface.
         If interface does not exist, None is returned.
diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py
index 12fc463ba..f34b99ea4 100755
--- a/smoketest/scripts/cli/test_interfaces_vxlan.py
+++ b/smoketest/scripts/cli/test_interfaces_vxlan.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -18,8 +18,9 @@ import unittest
 
 from vyos.configsession import ConfigSessionError
 from vyos.ifconfig import Interface
+from vyos.util import get_bridge_fdb
 from vyos.util import get_interface_config
-
+from vyos.template import is_ipv6
 from base_interfaces_test import BasicInterfaceTest
 
 class VXLANInterfaceTest(BasicInterfaceTest.TestCase):
@@ -57,21 +58,34 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase):
         ttl = 20
         for interface in self._interfaces:
             options = get_interface_config(interface)
+            bridge = get_bridge_fdb(interface)
 
             vni = options['linkinfo']['info_data']['id']
             self.assertIn(f'vni {vni}', self._options[interface])
 
-            if any('link' in s for s in self._options[interface]):
+            if any('source-interface' in s for s in self._options[interface]):
                 link = options['linkinfo']['info_data']['link']
                 self.assertIn(f'source-interface {link}', self._options[interface])
 
-            if any('local6' in s for s in self._options[interface]):
-                remote = options['linkinfo']['info_data']['local6']
-                self.assertIn(f'source-address {local6}', self._options[interface])
-
-            if any('remote6' in s for s in self._options[interface]):
-                remote = options['linkinfo']['info_data']['remote6']
-                self.assertIn(f'remote {remote}', self._options[interface])
+            # Verify source-address setting was properly configured on the Kernel
+            if any('source-address' in s for s in self._options[interface]):
+                for s in self._options[interface]:
+                    if 'source-address' in s:
+                        address = s.split()[-1]
+                        if is_ipv6(address):
+                            tmp = options['linkinfo']['info_data']['local6']
+                        else:
+                            tmp = options['linkinfo']['info_data']['local']
+                        self.assertIn(f'source-address {tmp}', self._options[interface])
+
+            # Verify remote setting was properly configured on the Kernel
+            if any('remote' in s for s in self._options[interface]):
+                for s in self._options[interface]:
+                    if 'remote' in s:
+                        for fdb in bridge:
+                            if 'mac' in fdb and fdb['mac'] == '00:00:00:00:00:00':
+                                remote = fdb['dst']
+                                self.assertIn(f'remote {remote}', self._options[interface])
 
             if any('group' in s for s in self._options[interface]):
                 group = options['linkinfo']['info_data']['group']
-- 
cgit v1.2.3


From 149f704a172fb14f16d0ba00ef237b972539492f Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Tue, 22 Feb 2022 10:29:32 +0100
Subject: vyos.configdict: T4263: leaf_node_changed() must also honor valueLess
 CLI nodes

If a valueLess node is added or removed from the CLI, a call to
leaf_node_changed() will not detect it.

If node is valueLess, on change old or new (depending on addition or deletion)
will be {} and is treated as None.

Add handler for this special case where old or new is an instance of a
dictionary but empty.
---
 python/vyos/configdict.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index f2ec93520..aea22c0c9 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -117,6 +117,12 @@ def leaf_node_changed(conf, path):
     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):
-- 
cgit v1.2.3


From 2373b232849c847717cbdcfac7390d8376e227ca Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Tue, 22 Feb 2022 10:31:32 +0100
Subject: vxlan: T4264: interface is destroyed and rebuild on description
 change

When changing "general" parameters like:
  - interface IP address
  - MTU
  - description

the interface is destroyed and recreated ... this should not happen!
---
 src/conf_mode/interfaces-vxlan.py | 22 +++++++++++++++++-----
 1 file changed, 17 insertions(+), 5 deletions(-)

diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 85604508e..29b16af89 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -21,6 +21,7 @@ from netifaces import interfaces
 
 from vyos.config import Config
 from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
 from vyos.configverify import verify_address
 from vyos.configverify import verify_bridge_delete
 from vyos.configverify import verify_mtu_ipv6
@@ -44,6 +45,16 @@ def get_config(config=None):
     base = ['interfaces', 'vxlan']
     vxlan = get_interface_dict(conf, base)
 
+    # VXLAN interfaces are picky and require recreation if certain parameters
+    # change. But a VXLAN interface should - of course - not be re-created if
+    # it's description or IP address is adjusted. Feels somehow logic doesn't it?
+    for cli_option in ['external', 'gpe', 'group', 'port', 'remote',
+                       'source-address', 'source-interface', 'vni',
+                       'parameters ip dont-fragment', 'parameters ip tos',
+                       'parameters ip ttl']:
+        if leaf_node_changed(conf, cli_option.split()):
+            vxlan.update({'rebuild_required': {}})
+
     # We need to verify that no other VXLAN tunnel is configured when external
     # mode is in use - Linux Kernel limitation
     conf.set_level(base)
@@ -136,11 +147,12 @@ def generate(vxlan):
 
 def apply(vxlan):
     # Check if the VXLAN interface already exists
-    if vxlan['ifname'] in interfaces():
-        v = VXLANIf(vxlan['ifname'])
-        # VXLAN is super picky and the tunnel always needs to be recreated,
-        # thus we can simply always delete it first.
-        v.remove()
+    if 'rebuild_required' in vxlan or 'delete' in vxlan:
+        if vxlan['ifname'] in interfaces():
+            v = VXLANIf(vxlan['ifname'])
+            # VXLAN is super picky and the tunnel always needs to be recreated,
+            # thus we can simply always delete it first.
+            v.remove()
 
     if 'deleted' not in vxlan:
         # Finally create the new interface
-- 
cgit v1.2.3


From e64d45717940aa4fb4a072065bdfa04f884d00cc Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Wed, 23 Feb 2022 18:14:11 +0100
Subject: tunnel: T4267: "parameters ip key" on GRE not required for different
 remotes

---
 smoketest/scripts/cli/test_interfaces_tunnel.py | 98 +++++++++++++++++++++++--
 src/conf_mode/interfaces-tunnel.py              | 82 ++++++++++++---------
 2 files changed, 139 insertions(+), 41 deletions(-)

diff --git a/smoketest/scripts/cli/test_interfaces_tunnel.py b/smoketest/scripts/cli/test_interfaces_tunnel.py
index fc2e254d6..b2c045b56 100755
--- a/smoketest/scripts/cli/test_interfaces_tunnel.py
+++ b/smoketest/scripts/cli/test_interfaces_tunnel.py
@@ -44,14 +44,14 @@ class TunnelInterfaceTest(BasicInterfaceTest.TestCase):
         # call base-classes classmethod
         super(cls, cls).setUpClass()
 
-    def setUp(self):
-        super().setUp()
-        self.cli_set(['interfaces', 'dummy', source_if, 'address', self.local_v4 + '/32'])
-        self.cli_set(['interfaces', 'dummy', source_if, 'address', self.local_v6 + '/128'])
+        # create some test interfaces
+        cls.cli_set(cls, ['interfaces', 'dummy', source_if, 'address', cls.local_v4 + '/32'])
+        cls.cli_set(cls, ['interfaces', 'dummy', source_if, 'address', cls.local_v6 + '/128'])
 
-    def tearDown(self):
-        self.cli_delete(['interfaces', 'dummy', source_if])
-        super().tearDown()
+    @classmethod
+    def tearDownClass(cls):
+        cls.cli_delete(cls, ['interfaces', 'dummy', source_if])
+        super().tearDownClass()
 
     def test_ipv4_encapsulations(self):
         # When running tests ensure that for certain encapsulation types the
@@ -312,5 +312,89 @@ class TunnelInterfaceTest(BasicInterfaceTest.TestCase):
         conf = get_interface_config(interface)
         self.assertEqual(new_remote,    conf['linkinfo']['info_data']['remote'])
 
+    def test_tunnel_src_any_gre_key(self):
+        interface = f'tun1280'
+        encapsulation = 'gre'
+        src_addr = '0.0.0.0'
+        key = '127'
+
+        self.cli_set(self._base_path + [interface, 'encapsulation', encapsulation])
+        self.cli_set(self._base_path + [interface, 'source-address', src_addr])
+        # GRE key must be supplied with a 0.0.0.0 source address
+        with self.assertRaises(ConfigSessionError):
+            self.cli_commit()
+        self.cli_set(self._base_path + [interface, 'parameters', 'ip', 'key', key])
+
+        self.cli_commit()
+
+    def test_multiple_gre_tunnel_same_remote(self):
+        tunnels = {
+            'tun10' : {
+                'encapsulation' : 'gre',
+                'source_interface' : source_if,
+                'remote' : '1.2.3.4',
+            },
+            'tun20' : {
+                'encapsulation' : 'gre',
+                'source_interface' : source_if,
+                'remote' : '1.2.3.4',
+            },
+        }
+
+        for tunnel, tunnel_config in tunnels.items():
+            self.cli_set(self._base_path + [tunnel, 'encapsulation', tunnel_config['encapsulation']])
+            if 'source_interface' in tunnel_config:
+                self.cli_set(self._base_path + [tunnel, 'source-interface', tunnel_config['source_interface']])
+            if 'remote' in tunnel_config:
+                self.cli_set(self._base_path + [tunnel, 'remote', tunnel_config['remote']])
+
+        # GRE key must be supplied when two or more tunnels are formed to the same desitnation
+        with self.assertRaises(ConfigSessionError):
+            self.cli_commit()
+        for tunnel, tunnel_config in tunnels.items():
+            self.cli_set(self._base_path + [tunnel, 'parameters', 'ip', 'key', tunnel.lstrip('tun')])
+
+        self.cli_commit()
+
+        for tunnel, tunnel_config in tunnels.items():
+            conf = get_interface_config(tunnel)
+            ip_key = tunnel.lstrip('tun')
+
+            self.assertEqual(tunnel_config['source_interface'], conf['link'])
+            self.assertEqual(tunnel_config['encapsulation'],    conf['linkinfo']['info_kind'])
+            self.assertEqual(tunnel_config['remote'],           conf['linkinfo']['info_data']['remote'])
+            self.assertEqual(f'0.0.0.{ip_key}',                 conf['linkinfo']['info_data']['ikey'])
+            self.assertEqual(f'0.0.0.{ip_key}',                 conf['linkinfo']['info_data']['okey'])
+
+    def test_multiple_gre_tunnel_different_remote(self):
+        tunnels = {
+            'tun10' : {
+                'encapsulation' : 'gre',
+                'source_interface' : source_if,
+                'remote' : '1.2.3.4',
+            },
+            'tun20' : {
+                'encapsulation' : 'gre',
+                'source_interface' : source_if,
+                'remote' : '1.2.3.5',
+            },
+        }
+
+        for tunnel, tunnel_config in tunnels.items():
+            self.cli_set(self._base_path + [tunnel, 'encapsulation', tunnel_config['encapsulation']])
+            if 'source_interface' in tunnel_config:
+                self.cli_set(self._base_path + [tunnel, 'source-interface', tunnel_config['source_interface']])
+            if 'remote' in tunnel_config:
+                self.cli_set(self._base_path + [tunnel, 'remote', tunnel_config['remote']])
+
+        self.cli_commit()
+
+        for tunnel, tunnel_config in tunnels.items():
+            conf = get_interface_config(tunnel)
+
+            self.assertEqual(tunnel_config['source_interface'], conf['link'])
+            self.assertEqual(tunnel_config['encapsulation'],    conf['linkinfo']['info_kind'])
+            self.assertEqual(tunnel_config['remote'],           conf['linkinfo']['info_data']['remote'])
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index 4c1204b4e..433764b8a 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -18,7 +18,6 @@ import os
 
 from sys import exit
 from netifaces import interfaces
-from ipaddress import IPv4Address
 
 from vyos.config import Config
 from vyos.configdict import get_interface_dict
@@ -50,8 +49,24 @@ def get_config(config=None):
     base = ['interfaces', 'tunnel']
     tunnel = get_interface_dict(conf, base)
 
-    tmp = leaf_node_changed(conf, ['encapsulation'])
-    if tmp: tunnel.update({'encapsulation_changed': {}})
+    if 'deleted' not in tunnel:
+        tmp = leaf_node_changed(conf, ['encapsulation'])
+        if tmp: tunnel.update({'encapsulation_changed': {}})
+
+        # We also need to inspect other configured tunnels as there are Kernel
+        # restrictions where we need to comply. E.g. GRE tunnel key can't be used
+        # twice, or with multiple GRE tunnels to the same location we must specify
+        # a GRE key
+        conf.set_level(base)
+        tunnel['other_tunnels'] = conf.get_config_dict([], key_mangling=('-', '_'),
+                                                      get_first_key=True,
+                                                      no_tag_node_value_mangle=True)
+        # delete our own instance from this dict
+        ifname = tunnel['ifname']
+        del tunnel['other_tunnels'][ifname]
+        # if only one tunnel is present on the system, no need to keep this key
+        if len(tunnel['other_tunnels']) == 0:
+            del tunnel['other_tunnels']
 
     # We must check if our interface is configured to be a DMVPN member
     nhrp_base = ['protocols', 'nhrp', 'tunnel']
@@ -92,48 +107,47 @@ def verify(tunnel):
             if 'direction' not in tunnel['parameters']['erspan']:
                 raise ConfigError('ERSPAN version 2 requires direction to be set!')
 
-    # If tunnel source address any and key not set
+    # If tunnel source is any and gre key is not set
+    interface = tunnel['ifname']
     if tunnel['encapsulation'] in ['gre'] and \
        dict_search('source_address', tunnel) == '0.0.0.0' and \
        dict_search('parameters.ip.key', tunnel) == None:
-        raise ConfigError('Tunnel parameters ip key must be set!')
+        raise ConfigError(f'"parameters ip key" must be set for {interface} when '\
+                           'encapsulation is GRE!')
 
-    if tunnel['encapsulation'] in ['gre', 'gretap']:
+    gre_encapsulations = ['gre', 'gretap']
+    if tunnel['encapsulation'] in gre_encapsulations and 'other_tunnels' in tunnel:
         # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
         # Prevent the same key for 2 tunnels with same source-address/encap. T2920
-        for tunnel_if in Section.interfaces('tunnel'):
-            # It makes no sense to run the test against our own interface we
-            # are currently configuring
-            if tunnel['ifname'] == tunnel_if:
-                continue
-
-            tunnel_cfg = get_interface_config(tunnel_if)
+        for o_tunnel, o_tunnel_conf in tunnel['other_tunnels'].items():
             # no match on encapsulation - bail out
-            if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']:
+            our_encapsulation = tunnel['encapsulation']
+            their_encapsulation = o_tunnel_conf['encapsulation']
+            if our_encapsulation in gre_encapsulations and their_encapsulation \
+                not in gre_encapsulations:
                 continue
 
-            new_source_address = dict_search('source_address', tunnel)
-            new_source_interface = dict_search('source_interface', tunnel)
-            if dict_search('parameters.ip.key', tunnel) != None:
-                # Convert tunnel key to ip key, format "ip -j link show"
-                # 1 => 0.0.0.1, 999 => 0.0.3.231
-                orig_new_key = dict_search('parameters.ip.key', tunnel)
-                new_key = IPv4Address(int(orig_new_key))
-                new_key = str(new_key)
-                if dict_search('address', tunnel_cfg) == new_source_address and \
-                   dict_search('linkinfo.info_data.ikey', tunnel_cfg) == new_key:
-                    raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \
+            our_address = dict_search('source_address', tunnel)
+            our_key = dict_search('parameters.ip.key', tunnel)
+            their_address = dict_search('source_address', o_tunnel_conf)
+            their_key = dict_search('parameters.ip.key', o_tunnel_conf)
+            if our_key != None:
+                if their_address == our_address and their_key == our_key:
+                    raise ConfigError(f'Key "{our_key}" for source-address "{our_address}" ' \
                                       f'is already used for tunnel "{tunnel_if}"!')
             else:
-                # If no IP GRE key is used we can not have more then one GRE tunnel
-                # bound to any one interface/IP address. This will result in a OS
-                # PermissionError: add tunnel "gre0" failed: File exists
-                if (dict_search('address', tunnel_cfg) == new_source_address or
-                   (dict_search('address', tunnel_cfg) == '0.0.0.0' and
-                    dict_search('link', tunnel_cfg) == new_source_interface)):
-                    raise ConfigError(f'Missing required "ip key" parameter when \
-                                      running more then one GRE based tunnel on the \
-                                      same source-interface/source-address')
+                our_source_if = dict_search('source_interface', tunnel)
+                their_source_if = dict_search('source_interface', o_tunnel_conf)
+                our_remote = dict_search('remote', tunnel)
+                their_remote = dict_search('remote', o_tunnel_conf)
+                # If no IP GRE key is defined we can not have more then one GRE tunnel
+                # bound to any one interface/IP address and the same remote. This will
+                # result in a OS  PermissionError: add tunnel "gre0" failed: File exists
+                if (their_address == our_address or our_source_if == their_source_if) and \
+                    our_remote == their_remote:
+                    raise ConfigError(f'Missing required "ip key" parameter when '\
+                                       'running more then one GRE based tunnel on the '\
+                                       'same source-interface/source-address')
 
     # Keys are not allowed with ipip and sit tunnels
     if tunnel['encapsulation'] in ['ipip', 'sit']:
-- 
cgit v1.2.3


From 53517de05e9566c35218d1f07cacb1bff98a46d9 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Wed, 23 Feb 2022 18:14:58 +0100
Subject: smoketest: tunnel: indention fixup

---
 smoketest/scripts/cli/test_interfaces_tunnel.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/smoketest/scripts/cli/test_interfaces_tunnel.py b/smoketest/scripts/cli/test_interfaces_tunnel.py
index b2c045b56..99c25c374 100755
--- a/smoketest/scripts/cli/test_interfaces_tunnel.py
+++ b/smoketest/scripts/cli/test_interfaces_tunnel.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -202,7 +202,7 @@ class TunnelInterfaceTest(BasicInterfaceTest.TestCase):
         self.assertEqual(encapsulation, conf['linkinfo']['info_kind'])
         self.assertEqual(self.local_v4, conf['linkinfo']['info_data']['local'])
         self.assertEqual(remote_ip4,    conf['linkinfo']['info_data']['remote'])
-        self.assertEqual(64,           conf['linkinfo']['info_data']['ttl'])
+        self.assertEqual(64,            conf['linkinfo']['info_data']['ttl'])
 
         # Change remote ip address (inc host by 2
         new_remote = inc_ip(remote_ip4, 2)
@@ -239,7 +239,7 @@ class TunnelInterfaceTest(BasicInterfaceTest.TestCase):
         self.assertEqual(encapsulation,     conf['linkinfo']['info_kind'])
         self.assertEqual(self.local_v4,     conf['linkinfo']['info_data']['local'])
         self.assertEqual(remote_ip4,        conf['linkinfo']['info_data']['remote'])
-        self.assertEqual(64,               conf['linkinfo']['info_data']['ttl'])
+        self.assertEqual(64,                conf['linkinfo']['info_data']['ttl'])
         self.assertEqual(f'0.0.0.{ip_key}', conf['linkinfo']['info_data']['ikey'])
         self.assertEqual(f'0.0.0.{ip_key}', conf['linkinfo']['info_data']['okey'])
         self.assertEqual(int(idx),          conf['linkinfo']['info_data']['erspan_index'])
@@ -295,7 +295,7 @@ class TunnelInterfaceTest(BasicInterfaceTest.TestCase):
         self.assertEqual(encapsulation,     conf['linkinfo']['info_kind'])
         self.assertEqual(self.local_v6,     conf['linkinfo']['info_data']['local'])
         self.assertEqual(remote_ip6,        conf['linkinfo']['info_data']['remote'])
-        self.assertEqual(64,               conf['linkinfo']['info_data']['ttl'])
+        self.assertEqual(64,                conf['linkinfo']['info_data']['ttl'])
         self.assertEqual(f'0.0.0.{ip_key}', conf['linkinfo']['info_data']['ikey'])
         self.assertEqual(f'0.0.0.{ip_key}', conf['linkinfo']['info_data']['okey'])
         self.assertEqual(erspan_ver,        conf['linkinfo']['info_data']['erspan_ver'])
-- 
cgit v1.2.3


From a68c9238111c6caee78bb28f8054b8f0cfa0e374 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 24 Feb 2022 22:47:12 +0100
Subject: scripts: T4269: node.def generator should automatically add default
 values

Since introducing the XML <defaultValue> node it was common, but redundant,
practice to also add a help string indicating which value would be used as
default if the node is unset.

This makes no sense b/c it's duplicated code/value/characters and prone to
error. The node.def scripts should be extended to automatically render the
appropriate default value into the CLI help string.

For e.g. SSH the current PoC renders:

$ cat templates-cfg/service/ssh/port/node.def
  multi:
  type: txt
  help: Port for SSH service (default: 22)
  val_help: u32:1-65535; Numeric IP port
  ...

Not all subsystems are already migrated to get_config_dict() and make use of
the defaults() call - those subsystems need to be migrated, first before the new
default is added to the CLI help.
---
 interface-definitions/containers.xml.in            |  6 ++--
 interface-definitions/dhcp-relay.xml.in            |  6 ++--
 interface-definitions/dhcp-server.xml.in           |  2 +-
 interface-definitions/dhcpv6-relay.xml.in          |  2 +-
 interface-definitions/dns-domain-name.xml.in       |  1 +
 interface-definitions/dns-forwarding.xml.in        |  6 ++--
 interface-definitions/flow-accounting-conf.xml.in  | 26 +++++++++---------
 interface-definitions/high-availability.xml.in     | 16 +++++------
 interface-definitions/igmp-proxy.xml.in            |  8 +++---
 .../include/accel-ppp/client-ipv6-pool.xml.i       |  2 +-
 .../include/accel-ppp/radius-additions.xml.i       |  6 ++--
 interface-definitions/include/bfd/common.xml.i     |  6 ++--
 .../include/bgp/protocol-common-config.xml.i       |  2 +-
 .../include/bgp/timers-keepalive.xml.i             |  2 +-
 .../include/firewall/name-default-action.xml.i     |  2 +-
 .../include/interface/arp-cache-timeout.xml.i      |  2 +-
 .../include/interface/dhcp-options.xml.i           |  2 +-
 .../include/interface/dhcpv6-options.xml.i         |  4 +--
 .../include/nat-translation-options.xml.i          |  4 +--
 interface-definitions/include/ospf/auto-cost.xml.i |  2 +-
 .../include/ospf/interface-common.xml.i            |  2 +-
 interface-definitions/include/ospf/intervals.xml.i |  8 +++---
 .../include/ospf/metric-type.xml.i                 |  2 +-
 .../include/ospf/protocol-common-config.xml.i      | 18 ++++++------
 .../include/ospfv3/protocol-common-config.xml.i    |  2 +-
 .../include/radius-server-port.xml.i               |  2 +-
 interface-definitions/include/rip/rip-timers.xml.i |  6 ++--
 .../include/snmp/access-mode.xml.i                 |  2 +-
 .../include/snmp/authentication-type.xml.i         |  2 +-
 .../include/snmp/privacy-type.xml.i                |  2 +-
 interface-definitions/include/snmp/protocol.xml.i  |  2 +-
 .../include/vpn-ipsec-encryption.xml.i             |  2 +-
 interface-definitions/include/vpn-ipsec-hash.xml.i |  2 +-
 interface-definitions/interfaces-bonding.xml.in    |  6 ++--
 interface-definitions/interfaces-bridge.xml.in     | 10 +++----
 interface-definitions/interfaces-ethernet.xml.in   |  4 +--
 interface-definitions/interfaces-l2tpv3.xml.in     |  6 ++--
 interface-definitions/interfaces-macsec.xml.in     |  4 +--
 interface-definitions/interfaces-openvpn.xml.in    | 22 +++++++--------
 interface-definitions/interfaces-pppoe.xml.in      |  2 +-
 interface-definitions/interfaces-tunnel.xml.in     |  4 +--
 interface-definitions/interfaces-wireless.xml.in   | 10 +++----
 interface-definitions/protocols-rpki.xml.in        |  2 +-
 .../service_console-server.xml.in                  |  6 ++--
 .../service_monitoring_telegraf.xml.in             |  6 ++--
 interface-definitions/service_router-advert.xml.in | 14 +++++-----
 interface-definitions/service_webproxy.xml.in      | 26 ++++++++++--------
 interface-definitions/snmp.xml.in                  |  6 ++--
 interface-definitions/ssh.xml.in                   |  2 +-
 interface-definitions/system-ip.xml.in             |  2 +-
 interface-definitions/system-login.xml.in          |  4 +--
 interface-definitions/system-logs.xml.in           |  8 +++---
 interface-definitions/vpn_ipsec.xml.in             | 32 +++++++++++-----------
 interface-definitions/vpn_l2tp.xml.in              | 10 +++----
 interface-definitions/vpn_openconnect.xml.in       | 12 ++++----
 interface-definitions/zone-policy.xml.in           |  6 ++--
 scripts/build-command-templates                    | 17 +++++++++---
 57 files changed, 197 insertions(+), 183 deletions(-)

diff --git a/interface-definitions/containers.xml.in b/interface-definitions/containers.xml.in
index 07686b16e..9cd2b0902 100644
--- a/interface-definitions/containers.xml.in
+++ b/interface-definitions/containers.xml.in
@@ -111,7 +111,7 @@
           </leafNode>
           <leafNode name="memory">
             <properties>
-              <help>Constrain the memory available to a container (default: 512MB)</help>
+              <help>Constrain the memory available to a container</help>
               <valueHelp>
                 <format>u32:0</format>
                 <description>Unlimited</description>
@@ -212,7 +212,7 @@
               </valueHelp>
               <valueHelp>
                 <format>on-failure</format>
-                <description>Restart containers when they exit with a non-zero exit code, retrying indefinitely (default)</description>
+                <description>Restart containers when they exit with a non-zero exit code, retrying indefinitely</description>
               </valueHelp>
               <valueHelp>
                 <format>always</format>
@@ -283,7 +283,7 @@
       </tagNode>
       <leafNode name="registry">
         <properties>
-          <help>Add registry (default docker.io)</help>
+          <help>Add registry</help>
           <multi/>
         </properties>
         <defaultValue>docker.io</defaultValue>
diff --git a/interface-definitions/dhcp-relay.xml.in b/interface-definitions/dhcp-relay.xml.in
index 483e776a7..a5643add6 100644
--- a/interface-definitions/dhcp-relay.xml.in
+++ b/interface-definitions/dhcp-relay.xml.in
@@ -20,7 +20,7 @@
                   <help>Policy to discard packets that have reached specified hop-count</help>
                   <valueHelp>
                     <format>u32:1-255</format>
-                    <description>Hop count (default: 10)</description>
+                    <description>Hop count</description>
                   </valueHelp>
                   <constraint>
                     <validator name="numeric" argument="--range 1-255"/>
@@ -34,7 +34,7 @@
                   <help>Maximum packet size to send to a DHCPv4/BOOTP server</help>
                   <valueHelp>
                     <format>u32:64-1400</format>
-                    <description>Maximum packet size (default: 576)</description>
+                    <description>Maximum packet size</description>
                   </valueHelp>
                   <constraint>
                     <validator name="numeric" argument="--range 64-1400"/>
@@ -44,7 +44,7 @@
               </leafNode>
               <leafNode name="relay-agents-packets">
                 <properties>
-                  <help>Policy to handle incoming DHCPv4 packets which already contain relay agent options (default: forward)</help>
+                  <help>Policy to handle incoming DHCPv4 packets which already contain relay agent options</help>
                   <completionHelp>
                     <list>append replace forward discard</list>
                   </completionHelp>
diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in
index d1ed579e9..312dcd2a0 100644
--- a/interface-definitions/dhcp-server.xml.in
+++ b/interface-definitions/dhcp-server.xml.in
@@ -198,7 +198,7 @@
                   </leafNode>
                   <leafNode name="lease">
                     <properties>
-                      <help>Lease timeout in seconds (default: 86400)</help>
+                      <help>Lease timeout in seconds</help>
                       <valueHelp>
                         <format>u32</format>
                         <description>DHCP lease time in seconds</description>
diff --git a/interface-definitions/dhcpv6-relay.xml.in b/interface-definitions/dhcpv6-relay.xml.in
index 7162cf353..5abcbe804 100644
--- a/interface-definitions/dhcpv6-relay.xml.in
+++ b/interface-definitions/dhcpv6-relay.xml.in
@@ -36,7 +36,7 @@
               <help>Maximum hop count for which requests will be processed</help>
               <valueHelp>
                 <format>u32:1-255</format>
-                <description>Hop count (default: 10)</description>
+                <description>Hop count</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-255"/>
diff --git a/interface-definitions/dns-domain-name.xml.in b/interface-definitions/dns-domain-name.xml.in
index 005a55ab3..7ae537d00 100644
--- a/interface-definitions/dns-domain-name.xml.in
+++ b/interface-definitions/dns-domain-name.xml.in
@@ -29,6 +29,7 @@
           </constraint>
         </properties>
       </leafNode>
+      <!-- script does not use XML defaults so far -->
       <leafNode name="host-name" owner="${vyos_conf_scripts_dir}/host_name.py">
         <properties>
           <help>System host name (default: vyos)</help>
diff --git a/interface-definitions/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in
index 4faf604ad..a2e809da8 100644
--- a/interface-definitions/dns-forwarding.xml.in
+++ b/interface-definitions/dns-forwarding.xml.in
@@ -16,7 +16,7 @@
             <children>
               <leafNode name="cache-size">
                 <properties>
-                  <help>DNS forwarding cache size (default: 10000)</help>
+                  <help>DNS forwarding cache size</help>
                   <valueHelp>
                     <format>u32:0-2147483647</format>
                     <description>DNS forwarding cache size</description>
@@ -38,7 +38,7 @@
               </leafNode>
               <leafNode name="dnssec">
                 <properties>
-                  <help>DNSSEC mode (default: process-no-validate)</help>
+                  <help>DNSSEC mode</help>
                   <completionHelp>
                     <list>off process-no-validate process log-fail validate</list>
                   </completionHelp>
@@ -587,7 +587,7 @@
               #include <include/listen-address.xml.i>
               <leafNode name="negative-ttl">
                 <properties>
-                  <help>Maximum amount of time negative entries are cached (default: 3600)</help>
+                  <help>Maximum amount of time negative entries are cached</help>
                   <valueHelp>
                     <format>u32:0-7200</format>
                     <description>Seconds to cache NXDOMAIN entries</description>
diff --git a/interface-definitions/flow-accounting-conf.xml.in b/interface-definitions/flow-accounting-conf.xml.in
index 1b57d706c..05cf5e170 100644
--- a/interface-definitions/flow-accounting-conf.xml.in
+++ b/interface-definitions/flow-accounting-conf.xml.in
@@ -14,7 +14,7 @@
               <help>Buffer size</help>
               <valueHelp>
                 <format>u32</format>
-                <description>Buffer size in MiB (default: 10)</description>
+                <description>Buffer size in MiB</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 0-4294967295"/>
@@ -27,7 +27,7 @@
               <help>Specifies the maximum number of bytes to capture for each packet</help>
               <valueHelp>
                 <format>u32:128-750</format>
-                <description>Packet length in bytes (default: 128)</description>
+                <description>Packet length in bytes</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 128-750"/>
@@ -209,7 +209,7 @@
                   </valueHelp>
                   <valueHelp>
                     <format>9</format>
-                    <description>NetFlow version 9 (default)</description>
+                    <description>NetFlow version 9</description>
                   </valueHelp>
                   <valueHelp>
                     <format>10</format>
@@ -240,7 +240,7 @@
                       <help>NetFlow port number</help>
                       <valueHelp>
                         <format>u32:1025-65535</format>
-                        <description>NetFlow port number (default: 2055)</description>
+                        <description>NetFlow port number</description>
                       </valueHelp>
                       <constraint>
                        <validator name="numeric" argument="--range 1025-65535"/>
@@ -260,7 +260,7 @@
                       <help>Expiry scan interval</help>
                       <valueHelp>
                         <format>u32:0-2147483647</format>
-                        <description>Expiry scan interval (default: 60)</description>
+                        <description>Expiry scan interval</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 0-2147483647"/>
@@ -273,7 +273,7 @@
                       <help>Generic flow timeout value</help>
                       <valueHelp>
                         <format>u32:0-2147483647</format>
-                        <description>Generic flow timeout in seconds (default: 3600)</description>
+                        <description>Generic flow timeout in seconds</description>
                       </valueHelp>
                       <constraint>
                        <validator name="numeric" argument="--range 0-2147483647"/>
@@ -286,7 +286,7 @@
                       <help>ICMP timeout value</help>
                       <valueHelp>
                         <format>u32:0-2147483647</format>
-                        <description>ICMP timeout in seconds (default: 300)</description>
+                        <description>ICMP timeout in seconds</description>
                       </valueHelp>
                       <constraint>
                        <validator name="numeric" argument="--range 0-2147483647"/>
@@ -299,7 +299,7 @@
                       <help>Max active timeout value</help>
                       <valueHelp>
                         <format>u32:0-2147483647</format>
-                        <description>Max active timeout in seconds (default: 604800)</description>
+                        <description>Max active timeout in seconds</description>
                       </valueHelp>
                       <constraint>
                        <validator name="numeric" argument="--range 0-2147483647"/>
@@ -312,7 +312,7 @@
                       <help>TCP finish timeout value</help>
                       <valueHelp>
                         <format>u32:0-2147483647</format>
-                        <description>TCP FIN timeout in seconds (default: 300)</description>
+                        <description>TCP FIN timeout in seconds</description>
                       </valueHelp>
                       <constraint>
                        <validator name="numeric" argument="--range 0-2147483647"/>
@@ -325,7 +325,7 @@
                       <help>TCP generic timeout value</help>
                       <valueHelp>
                         <format>u32:0-2147483647</format>
-                        <description>TCP generic timeout in seconds (default: 3600)</description>
+                        <description>TCP generic timeout in seconds</description>
                       </valueHelp>
                       <constraint>
                        <validator name="numeric" argument="--range 0-2147483647"/>
@@ -338,7 +338,7 @@
                       <help>TCP reset timeout value</help>
                       <valueHelp>
                         <format>u32:0-2147483647</format>
-                        <description>TCP RST timeout in seconds (default: 120)</description>
+                        <description>TCP RST timeout in seconds</description>
                       </valueHelp>
                       <constraint>
                        <validator name="numeric" argument="--range 0-2147483647"/>
@@ -351,7 +351,7 @@
                       <help>UDP timeout value</help>
                       <valueHelp>
                         <format>u32:0-2147483647</format>
-                        <description>UDP timeout in seconds (default: 300)</description>
+                        <description>UDP timeout in seconds</description>
                       </valueHelp>
                       <constraint>
                        <validator name="numeric" argument="--range 0-2147483647"/>
@@ -418,7 +418,7 @@
                       <help>sFlow port number</help>
                       <valueHelp>
                         <format>u32:1025-65535</format>
-                        <description>sFlow port number (default: 6343)</description>
+                        <description>sFlow port number</description>
                       </valueHelp>
                       <constraint>
                        <validator name="numeric" argument="--range 1025-65535"/>
diff --git a/interface-definitions/high-availability.xml.in b/interface-definitions/high-availability.xml.in
index ee1d70484..662052e12 100644
--- a/interface-definitions/high-availability.xml.in
+++ b/interface-definitions/high-availability.xml.in
@@ -22,7 +22,7 @@
                   <help>Advertise interval</help>
                   <valueHelp>
                     <format>u32:1-255</format>
-                    <description>Advertise interval in seconds (default: 1)</description>
+                    <description>Advertise interval in seconds</description>
                   </valueHelp>
                   <constraint>
                     <validator name="numeric" argument="--range 1-255"/>
@@ -79,7 +79,7 @@
                 <children>
                   <leafNode name="failure-count">
                     <properties>
-                      <help>Health check failure count required for transition to fault (default: 3)</help>
+                      <help>Health check failure count required for transition to fault</help>
                       <constraint>
                         <validator name="numeric" argument="--positive" />
                       </constraint>
@@ -88,7 +88,7 @@
                   </leafNode>
                   <leafNode name="interval">
                     <properties>
-                      <help>Health check execution interval in seconds (default: 60)</help>
+                      <help>Health check execution interval in seconds</help>
                       <constraint>
                         <validator name="numeric" argument="--positive"/>
                       </constraint>
@@ -160,7 +160,7 @@
               </leafNode>
               <leafNode name="priority">
                 <properties>
-                  <help>Router priority (default: 100)</help>
+                  <help>Router priority</help>
                   <valueHelp>
                     <format>u32:1-255</format>
                     <description>Router priority</description>
@@ -333,7 +333,7 @@
               <help>Interval between health-checks (in seconds)</help>
               <valueHelp>
                 <format>u32:1-600</format>
-                <description>Interval in seconds (default: 10)</description>
+                <description>Interval in seconds</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-3600"/>
@@ -343,7 +343,7 @@
           </leafNode>
           <leafNode name="forward-method">
             <properties>
-              <help>Forwarding method (default: NAT)</help>
+              <help>Forwarding method</help>
               <completionHelp>
                 <list>direct nat tunnel</list>
               </completionHelp>
@@ -371,7 +371,7 @@
               <help>Timeout for persistent connections</help>
               <valueHelp>
                 <format>u32:1-86400</format>
-                <description>Timeout for persistent connections (default: 300)</description>
+                <description>Timeout for persistent connections</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-86400"/>
@@ -381,7 +381,7 @@
           </leafNode>
           <leafNode name="protocol">
             <properties>
-              <help>Protocol for port checks (default: TCP)</help>
+              <help>Protocol for port checks</help>
               <completionHelp>
                 <list>tcp udp</list>
               </completionHelp>
diff --git a/interface-definitions/igmp-proxy.xml.in b/interface-definitions/igmp-proxy.xml.in
index 91c912d8b..c7ab60929 100644
--- a/interface-definitions/igmp-proxy.xml.in
+++ b/interface-definitions/igmp-proxy.xml.in
@@ -39,7 +39,7 @@
               </leafNode>
               <leafNode name="role">
                 <properties>
-                  <help>IGMP interface role (default: downstream)</help>
+                  <help>IGMP interface role</help>
                   <completionHelp>
                     <list>upstream downstream disabled</list>
                   </completionHelp>
@@ -49,7 +49,7 @@
                   </valueHelp>
                   <valueHelp>
                     <format>downstream</format>
-                    <description>Downstream interface(s) (default)</description>
+                    <description>Downstream interface(s)</description>
                   </valueHelp>
                   <valueHelp>
                     <format>disabled</format>
@@ -63,10 +63,10 @@
               </leafNode>
               <leafNode name="threshold">
                 <properties>
-                  <help>TTL threshold (default: 1)</help>
+                  <help>TTL threshold</help>
                   <valueHelp>
                     <format>u32:1-255</format>
-                    <description>TTL threshold for the interfaces (default: 1)</description>
+                    <description>TTL threshold for the interfaces</description>
                   </valueHelp>
                   <constraint>
                     <validator name="numeric" argument="--range 1-255"/>
diff --git a/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i b/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i
index a692f2335..01cf0e040 100644
--- a/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i
+++ b/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i
@@ -21,7 +21,7 @@
               <help>Prefix length used for individual client</help>
               <valueHelp>
                 <format>u32:48-128</format>
-                <description>Client prefix length (default: 64)</description>
+                <description>Client prefix length</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 48-128"/>
diff --git a/interface-definitions/include/accel-ppp/radius-additions.xml.i b/interface-definitions/include/accel-ppp/radius-additions.xml.i
index 258ece2b5..441c9dda5 100644
--- a/interface-definitions/include/accel-ppp/radius-additions.xml.i
+++ b/interface-definitions/include/accel-ppp/radius-additions.xml.i
@@ -21,7 +21,7 @@
             <help>Accounting port</help>
             <valueHelp>
               <format>u32:1-65535</format>
-              <description>Numeric IP port (default: 1813)</description>
+              <description>Numeric IP port</description>
             </valueHelp>
             <constraint>
               <validator name="numeric" argument="--range 1-65535"/>
@@ -62,7 +62,7 @@
     </leafNode>
     <leafNode name="acct-timeout">
       <properties>
-        <help>Timeout for Interim-Update packets, terminate session afterwards (default 3 seconds)</help>
+        <help>Timeout for Interim-Update packets, terminate session afterwards</help>
         <valueHelp>
           <format>u32:0-60</format>
           <description>Timeout in seconds, 0 to keep active</description>
@@ -126,7 +126,7 @@
         </leafNode>
         <leafNode name="port">
           <properties>
-            <help>Port for Dynamic Authorization Extension server (DM/CoA) (default: 1700)</help>
+            <help>Port for Dynamic Authorization Extension server (DM/CoA)</help>
             <valueHelp>
               <format>u32:1-65535</format>
               <description>TCP port</description>
diff --git a/interface-definitions/include/bfd/common.xml.i b/interface-definitions/include/bfd/common.xml.i
index e52221441..126ab9b9a 100644
--- a/interface-definitions/include/bfd/common.xml.i
+++ b/interface-definitions/include/bfd/common.xml.i
@@ -15,7 +15,7 @@
         <help>Minimum interval of receiving control packets</help>
         <valueHelp>
           <format>u32:10-60000</format>
-          <description>Interval in milliseconds (default: 300)</description>
+          <description>Interval in milliseconds</description>
         </valueHelp>
         <constraint>
           <validator name="numeric" argument="--range 10-60000"/>
@@ -28,7 +28,7 @@
         <help>Minimum interval of transmitting control packets</help>
         <valueHelp>
           <format>u32:10-60000</format>
-          <description>Interval in milliseconds (default: 300)</description>
+          <description>Interval in milliseconds</description>
         </valueHelp>
         <constraint>
           <validator name="numeric" argument="--range 10-60000"/>
@@ -41,7 +41,7 @@
         <help>Multiplier to determine packet loss</help>
         <valueHelp>
           <format>u32:2-255</format>
-          <description>Remote transmission interval will be multiplied by this value (default: 3)</description>
+          <description>Remote transmission interval will be multiplied by this value</description>
         </valueHelp>
         <constraint>
           <validator name="numeric" argument="--range 2-255"/>
diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i
index 8214d0779..38337b032 100644
--- a/interface-definitions/include/bgp/protocol-common-config.xml.i
+++ b/interface-definitions/include/bgp/protocol-common-config.xml.i
@@ -1191,7 +1191,7 @@
             <help>Set period to rescan BGP table to check if condition is met</help>
             <valueHelp>
               <format>u32:5-240</format>
-              <description>Period to rerun the conditional advertisement scanner process (default: 60)</description>
+              <description>Period to rerun the conditional advertisement scanner process</description>
             </valueHelp>
             <constraint>
               <validator name="numeric" argument="--range 5-240"/>
diff --git a/interface-definitions/include/bgp/timers-keepalive.xml.i b/interface-definitions/include/bgp/timers-keepalive.xml.i
index b2771e326..b23f96ec8 100644
--- a/interface-definitions/include/bgp/timers-keepalive.xml.i
+++ b/interface-definitions/include/bgp/timers-keepalive.xml.i
@@ -4,7 +4,7 @@
     <help>BGP keepalive interval for this neighbor</help>
     <valueHelp>
       <format>u32:1-65535</format>
-      <description>Keepalive interval in seconds (default 60)</description>
+      <description>Keepalive interval in seconds</description>
     </valueHelp>
     <constraint>
       <validator name="numeric" argument="--range 1-65535"/>
diff --git a/interface-definitions/include/firewall/name-default-action.xml.i b/interface-definitions/include/firewall/name-default-action.xml.i
index 1b61b076f..8470a29a9 100644
--- a/interface-definitions/include/firewall/name-default-action.xml.i
+++ b/interface-definitions/include/firewall/name-default-action.xml.i
@@ -7,7 +7,7 @@
     </completionHelp>
     <valueHelp>
       <format>drop</format>
-      <description>Drop if no prior rules are hit (default)</description>
+      <description>Drop if no prior rules are hit</description>
     </valueHelp>
     <valueHelp>
       <format>reject</format>
diff --git a/interface-definitions/include/interface/arp-cache-timeout.xml.i b/interface-definitions/include/interface/arp-cache-timeout.xml.i
index cb01d0525..06d7ffe96 100644
--- a/interface-definitions/include/interface/arp-cache-timeout.xml.i
+++ b/interface-definitions/include/interface/arp-cache-timeout.xml.i
@@ -4,7 +4,7 @@
     <help>ARP cache entry timeout in seconds</help>
     <valueHelp>
       <format>u32:1-86400</format>
-      <description>ARP cache entry timout in seconds (default 30)</description>
+      <description>ARP cache entry timout in seconds</description>
     </valueHelp>
     <constraint>
       <validator name="numeric" argument="--range 1-86400"/>
diff --git a/interface-definitions/include/interface/dhcp-options.xml.i b/interface-definitions/include/interface/dhcp-options.xml.i
index f62b06640..098d02919 100644
--- a/interface-definitions/include/interface/dhcp-options.xml.i
+++ b/interface-definitions/include/interface/dhcp-options.xml.i
@@ -30,7 +30,7 @@
         <help>Distance for the default route from DHCP server</help>
         <valueHelp>
           <format>u32:1-255</format>
-          <description>Distance for the default route from DHCP server (default: 210)</description>
+          <description>Distance for the default route from DHCP server</description>
         </valueHelp>
         <constraint>
           <validator name="numeric" argument="--range 1-255"/>
diff --git a/interface-definitions/include/interface/dhcpv6-options.xml.i b/interface-definitions/include/interface/dhcpv6-options.xml.i
index d1abf4a90..08e4f5e0a 100644
--- a/interface-definitions/include/interface/dhcpv6-options.xml.i
+++ b/interface-definitions/include/interface/dhcpv6-options.xml.i
@@ -57,10 +57,10 @@
           <children>
             <leafNode name="address">
               <properties>
-                <help>Local interface address assigned to interface</help>
+                <help>Local interface address assigned to interface (default: EUI-64)</help>
                 <valueHelp>
                   <format>&gt;0</format>
-                  <description>Used to form IPv6 interface address (default: EUI-64)</description>
+                  <description>Used to form IPv6 interface address</description>
                 </valueHelp>
                 <constraint>
                   <validator name="numeric" argument="--non-negative"/>
diff --git a/interface-definitions/include/nat-translation-options.xml.i b/interface-definitions/include/nat-translation-options.xml.i
index df2f76397..f1539757b 100644
--- a/interface-definitions/include/nat-translation-options.xml.i
+++ b/interface-definitions/include/nat-translation-options.xml.i
@@ -16,7 +16,7 @@
         </valueHelp>
         <valueHelp>
           <format>random</format>
-          <description>Random source or destination address allocation for each connection (default)</description>
+          <description>Random source or destination address allocation for each connection</description>
         </valueHelp>
         <constraint>
           <regex>^(persistent|random)$</regex>
@@ -39,7 +39,7 @@
         </valueHelp>
         <valueHelp>
           <format>none</format>
-          <description>Do not apply port randomization (default)</description>
+          <description>Do not apply port randomization</description>
         </valueHelp>
         <constraint>
           <regex>^(random|fully-random|none)$</regex>
diff --git a/interface-definitions/include/ospf/auto-cost.xml.i b/interface-definitions/include/ospf/auto-cost.xml.i
index 3e6cc8232..da6483a00 100644
--- a/interface-definitions/include/ospf/auto-cost.xml.i
+++ b/interface-definitions/include/ospf/auto-cost.xml.i
@@ -6,7 +6,7 @@
   <children>
     <leafNode name="reference-bandwidth">
       <properties>
-        <help>Reference bandwidth method to assign cost (default: 100)</help>
+        <help>Reference bandwidth method to assign cost</help>
         <valueHelp>
           <format>u32:1-4294967</format>
           <description>Reference bandwidth cost in Mbits/sec</description>
diff --git a/interface-definitions/include/ospf/interface-common.xml.i b/interface-definitions/include/ospf/interface-common.xml.i
index 738651594..9c8b94f0b 100644
--- a/interface-definitions/include/ospf/interface-common.xml.i
+++ b/interface-definitions/include/ospf/interface-common.xml.i
@@ -20,7 +20,7 @@
 </leafNode>
 <leafNode name="priority">
   <properties>
-    <help>Router priority (default: 1)</help>
+    <help>Router priority</help>
     <valueHelp>
       <format>u32:0-255</format>
       <description>OSPF router priority cost</description>
diff --git a/interface-definitions/include/ospf/intervals.xml.i b/interface-definitions/include/ospf/intervals.xml.i
index fad1a6305..9f6e5df69 100644
--- a/interface-definitions/include/ospf/intervals.xml.i
+++ b/interface-definitions/include/ospf/intervals.xml.i
@@ -1,7 +1,7 @@
 <!-- include start from ospf/intervals.xml.i -->
 <leafNode name="dead-interval">
   <properties>
-    <help>Interval after which a neighbor is declared dead (default: 40)</help>
+    <help>Interval after which a neighbor is declared dead</help>
     <valueHelp>
       <format>u32:1-65535</format>
       <description>Neighbor dead interval (seconds)</description>
@@ -14,7 +14,7 @@
 </leafNode>
 <leafNode name="hello-interval">
   <properties>
-    <help>Interval between hello packets (default: 10)</help>
+    <help>Interval between hello packets</help>
     <valueHelp>
       <format>u32:1-65535</format>
       <description>Hello interval (seconds)</description>
@@ -27,7 +27,7 @@
 </leafNode>
 <leafNode name="retransmit-interval">
   <properties>
-    <help>Interval between retransmitting lost link state advertisements (default: 5)</help>
+    <help>Interval between retransmitting lost link state advertisements</help>
     <valueHelp>
       <format>u32:1-65535</format>
       <description>Retransmit interval (seconds)</description>
@@ -40,7 +40,7 @@
 </leafNode>
 <leafNode name="transmit-delay">
   <properties>
-    <help>Link state transmit delay (default: 1)</help>
+    <help>Link state transmit delay</help>
     <valueHelp>
       <format>u32:1-65535</format>
       <description>Link state transmit delay (seconds)</description>
diff --git a/interface-definitions/include/ospf/metric-type.xml.i b/interface-definitions/include/ospf/metric-type.xml.i
index ef9fd8ac0..de55c7645 100644
--- a/interface-definitions/include/ospf/metric-type.xml.i
+++ b/interface-definitions/include/ospf/metric-type.xml.i
@@ -1,7 +1,7 @@
 <!-- include start from ospf/metric-type.xml.i -->
 <leafNode name="metric-type">
   <properties>
-    <help>OSPF metric type for default routes (default: 2)</help>
+    <help>OSPF metric type for default routes</help>
     <valueHelp>
       <format>u32:1-2</format>
       <description>Set OSPF External Type 1/2 metrics</description>
diff --git a/interface-definitions/include/ospf/protocol-common-config.xml.i b/interface-definitions/include/ospf/protocol-common-config.xml.i
index e783f4bec..088bee2de 100644
--- a/interface-definitions/include/ospf/protocol-common-config.xml.i
+++ b/interface-definitions/include/ospf/protocol-common-config.xml.i
@@ -106,7 +106,7 @@
             </leafNode>
             <leafNode name="translate">
               <properties>
-                <help>Configure NSSA-ABR (default: candidate)</help>
+                <help>Configure NSSA-ABR</help>
                 <completionHelp>
                   <list>always candidate never</list>
                 </completionHelp>
@@ -116,7 +116,7 @@
                 </valueHelp>
                 <valueHelp>
                   <format>candidate</format>
-                  <description>Translate for election (default)</description>
+                  <description>Translate for election</description>
                 </valueHelp>
                 <valueHelp>
                   <format>never</format>
@@ -502,7 +502,7 @@
   <children>
     <leafNode name="poll-interval">
       <properties>
-        <help>Dead neighbor polling interval (default: 60)</help>
+        <help>Dead neighbor polling interval</help>
         <valueHelp>
           <format>u32:1-65535</format>
           <description>Seconds between dead neighbor polling interval</description>
@@ -515,7 +515,7 @@
     </leafNode>
     <leafNode name="priority">
       <properties>
-        <help>Neighbor priority in seconds (default: 0)</help>
+        <help>Neighbor priority in seconds</help>
         <valueHelp>
           <format>u32:0-255</format>
           <description>Neighbor priority</description>
@@ -535,13 +535,13 @@
   <children>
     <leafNode name="abr-type">
       <properties>
-        <help>OSPF ABR type (default: cisco)</help>
+        <help>OSPF ABR type</help>
         <completionHelp>
           <list>cisco ibm shortcut standard</list>
         </completionHelp>
         <valueHelp>
           <format>cisco</format>
-          <description>Cisco ABR type (default)</description>
+          <description>Cisco ABR type</description>
         </valueHelp>
         <valueHelp>
           <format>ibm</format>
@@ -712,7 +712,7 @@
           <children>
             <leafNode name="delay">
               <properties>
-                <help>Delay from the first change received to SPF calculation (default: 200)</help>
+                <help>Delay from the first change received to SPF calculation</help>
                 <valueHelp>
                   <format>u32:0-600000</format>
                   <description>Delay in milliseconds</description>
@@ -725,7 +725,7 @@
             </leafNode>
             <leafNode name="initial-holdtime">
               <properties>
-                <help>Initial hold time between consecutive SPF calculations (default: 1000)</help>
+                <help>Initial hold time between consecutive SPF calculations</help>
                 <valueHelp>
                   <format>u32:0-600000</format>
                   <description>Initial hold time in milliseconds</description>
@@ -738,7 +738,7 @@
             </leafNode>
             <leafNode name="max-holdtime">
               <properties>
-                <help>Maximum hold time (default: 10000)</help>
+                <help>Maximum hold time</help>
                 <valueHelp>
                   <format>u32:0-600000</format>
                   <description>Max hold time in milliseconds</description>
diff --git a/interface-definitions/include/ospfv3/protocol-common-config.xml.i b/interface-definitions/include/ospfv3/protocol-common-config.xml.i
index 5d08debda..792c873c8 100644
--- a/interface-definitions/include/ospfv3/protocol-common-config.xml.i
+++ b/interface-definitions/include/ospfv3/protocol-common-config.xml.i
@@ -158,7 +158,7 @@
     </leafNode>
     <leafNode name="instance-id">
       <properties>
-        <help>Instance Id (default: 0)</help>
+        <help>Instance ID</help>
         <valueHelp>
           <format>u32:0-255</format>
           <description>Instance Id</description>
diff --git a/interface-definitions/include/radius-server-port.xml.i b/interface-definitions/include/radius-server-port.xml.i
index 4e5d906bc..c6b691a0f 100644
--- a/interface-definitions/include/radius-server-port.xml.i
+++ b/interface-definitions/include/radius-server-port.xml.i
@@ -4,7 +4,7 @@
     <help>Authentication port</help>
     <valueHelp>
       <format>u32:1-65535</format>
-      <description>Numeric IP port (default: 1812)</description>
+      <description>Numeric IP port</description>
     </valueHelp>
     <constraint>
       <validator name="numeric" argument="--range 1-65535"/>
diff --git a/interface-definitions/include/rip/rip-timers.xml.i b/interface-definitions/include/rip/rip-timers.xml.i
index 3aaaf8e65..129d9ed23 100644
--- a/interface-definitions/include/rip/rip-timers.xml.i
+++ b/interface-definitions/include/rip/rip-timers.xml.i
@@ -9,7 +9,7 @@
         <help>Garbage collection timer</help>
         <valueHelp>
           <format>u32:5-2147483647</format>
-          <description>Garbage colletion time (default 120)</description>
+          <description>Garbage colletion time</description>
         </valueHelp>
         <constraint>
           <validator name="numeric" argument="--range 5-2147483647"/>
@@ -22,7 +22,7 @@
         <help>Routing information timeout timer</help>
         <valueHelp>
           <format>u32:5-2147483647</format>
-          <description>Routing information timeout timer (default 180)</description>
+          <description>Routing information timeout timer</description>
         </valueHelp>
         <constraint>
           <validator name="numeric" argument="--range 5-2147483647"/>
@@ -35,7 +35,7 @@
         <help>Routing table update timer</help>
         <valueHelp>
           <format>u32:5-2147483647</format>
-          <description>Routing table update timer in seconds (default 30)</description>
+          <description>Routing table update timer in seconds</description>
         </valueHelp>
         <constraint>
           <validator name="numeric" argument="--range 5-2147483647"/>
diff --git a/interface-definitions/include/snmp/access-mode.xml.i b/interface-definitions/include/snmp/access-mode.xml.i
index 1fce2364e..71c766774 100644
--- a/interface-definitions/include/snmp/access-mode.xml.i
+++ b/interface-definitions/include/snmp/access-mode.xml.i
@@ -7,7 +7,7 @@
     </completionHelp>
     <valueHelp>
       <format>ro</format>
-      <description>Read-Only (default)</description>
+      <description>Read-Only</description>
     </valueHelp>
     <valueHelp>
       <format>rw</format>
diff --git a/interface-definitions/include/snmp/authentication-type.xml.i b/interface-definitions/include/snmp/authentication-type.xml.i
index 2a545864a..ca0bb10a6 100644
--- a/interface-definitions/include/snmp/authentication-type.xml.i
+++ b/interface-definitions/include/snmp/authentication-type.xml.i
@@ -7,7 +7,7 @@
     </completionHelp>
     <valueHelp>
       <format>md5</format>
-      <description>Message Digest 5 (default)</description>
+      <description>Message Digest 5</description>
     </valueHelp>
     <valueHelp>
       <format>sha</format>
diff --git a/interface-definitions/include/snmp/privacy-type.xml.i b/interface-definitions/include/snmp/privacy-type.xml.i
index 47a1e632e..94029a6c6 100644
--- a/interface-definitions/include/snmp/privacy-type.xml.i
+++ b/interface-definitions/include/snmp/privacy-type.xml.i
@@ -7,7 +7,7 @@
     </completionHelp>
     <valueHelp>
       <format>des</format>
-      <description>Data Encryption Standard (default)</description>
+      <description>Data Encryption Standard</description>
     </valueHelp>
     <valueHelp>
       <format>aes</format>
diff --git a/interface-definitions/include/snmp/protocol.xml.i b/interface-definitions/include/snmp/protocol.xml.i
index 335736724..ebdeef87e 100644
--- a/interface-definitions/include/snmp/protocol.xml.i
+++ b/interface-definitions/include/snmp/protocol.xml.i
@@ -7,7 +7,7 @@
     </completionHelp>
     <valueHelp>
       <format>udp</format>
-      <description>Listen protocol UDP (default)</description>
+      <description>Listen protocol UDP</description>
     </valueHelp>
     <valueHelp>
       <format>tcp</format>
diff --git a/interface-definitions/include/vpn-ipsec-encryption.xml.i b/interface-definitions/include/vpn-ipsec-encryption.xml.i
index 9ef2f7c90..faa264d2f 100644
--- a/interface-definitions/include/vpn-ipsec-encryption.xml.i
+++ b/interface-definitions/include/vpn-ipsec-encryption.xml.i
@@ -11,7 +11,7 @@
     </valueHelp>
     <valueHelp>
       <format>aes128</format>
-      <description>128 bit AES-CBC (default)</description>
+      <description>128 bit AES-CBC</description>
     </valueHelp>
     <valueHelp>
       <format>aes192</format>
diff --git a/interface-definitions/include/vpn-ipsec-hash.xml.i b/interface-definitions/include/vpn-ipsec-hash.xml.i
index 5a06b290e..b3ef4fb7a 100644
--- a/interface-definitions/include/vpn-ipsec-hash.xml.i
+++ b/interface-definitions/include/vpn-ipsec-hash.xml.i
@@ -15,7 +15,7 @@
     </valueHelp>
     <valueHelp>
       <format>sha1</format>
-      <description>SHA1 HMAC (default)</description>
+      <description>SHA1 HMAC</description>
     </valueHelp>
     <valueHelp>
       <format>sha1_160</format>
diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in
index 723041ca5..b98f4b960 100644
--- a/interface-definitions/interfaces-bonding.xml.in
+++ b/interface-definitions/interfaces-bonding.xml.in
@@ -66,7 +66,7 @@
               </completionHelp>
               <valueHelp>
                 <format>layer2</format>
-                <description>use MAC addresses to generate the hash (802.3ad, default)</description>
+                <description>use MAC addresses to generate the hash</description>
               </valueHelp>
               <valueHelp>
                 <format>layer2+3</format>
@@ -115,7 +115,7 @@
               </completionHelp>
               <valueHelp>
                 <format>slow</format>
-                <description>Request partner to transmit LACPDUs every 30 seconds (default)</description>
+                <description>Request partner to transmit LACPDUs every 30 seconds</description>
               </valueHelp>
               <valueHelp>
                 <format>fast</format>
@@ -135,7 +135,7 @@
               </completionHelp>
               <valueHelp>
                 <format>802.3ad</format>
-                <description>IEEE 802.3ad Dynamic link aggregation (Default)</description>
+                <description>IEEE 802.3ad Dynamic link aggregation</description>
               </valueHelp>
               <valueHelp>
                 <format>active-backup</format>
diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in
index 89a6d2303..fabfb917a 100644
--- a/interface-definitions/interfaces-bridge.xml.in
+++ b/interface-definitions/interfaces-bridge.xml.in
@@ -26,7 +26,7 @@
               </valueHelp>
               <valueHelp>
                 <format>u32:10-1000000</format>
-                <description>MAC address aging time in seconds (default: 300)</description>
+                <description>MAC address aging time in seconds</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 0-0 --range 10-1000000"/>
@@ -48,7 +48,7 @@
               <help>Forwarding delay</help>
               <valueHelp>
                 <format>u32:0-200</format>
-                <description>Spanning Tree Protocol forwarding delay in seconds (default 15)</description>
+                <description>Spanning Tree Protocol forwarding delay in seconds</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 0-200"/>
@@ -62,7 +62,7 @@
               <help>Hello packet advertisement interval</help>
               <valueHelp>
                 <format>u32:1-10</format>
-                <description>Spanning Tree Protocol hello advertisement interval in seconds (default 2)</description>
+                <description>Spanning Tree Protocol hello advertisement interval in seconds</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-10"/>
@@ -99,7 +99,7 @@
               <help>Interval at which neighbor bridges are removed</help>
               <valueHelp>
                 <format>u32:1-40</format>
-                <description>Bridge maximum aging time in seconds (default 20)</description>
+                <description>Bridge maximum aging time in seconds</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-40"/>
@@ -195,7 +195,7 @@
               <help>Priority for this bridge</help>
               <valueHelp>
                 <format>u32:0-65535</format>
-                <description>Bridge priority (default 32768)</description>
+                <description>Bridge priority</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 0-65535"/>
diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in
index 9e113cb71..be7bddfa4 100644
--- a/interface-definitions/interfaces-ethernet.xml.in
+++ b/interface-definitions/interfaces-ethernet.xml.in
@@ -41,7 +41,7 @@
               </completionHelp>
               <valueHelp>
                 <format>auto</format>
-                <description>Auto negotiation (default)</description>
+                <description>Auto negotiation</description>
               </valueHelp>
               <valueHelp>
                 <format>half</format>
@@ -110,7 +110,7 @@
           </node>
           <leafNode name="speed">
             <properties>
-              <help>Link speed (default: auto)</help>
+              <help>Link speed</help>
               <completionHelp>
                 <list>auto 10 100 1000 2500 5000 10000 25000 40000 50000 100000</list>
               </completionHelp>
diff --git a/interface-definitions/interfaces-l2tpv3.xml.in b/interface-definitions/interfaces-l2tpv3.xml.in
index 85d4ab992..ba9bcb0a2 100644
--- a/interface-definitions/interfaces-l2tpv3.xml.in
+++ b/interface-definitions/interfaces-l2tpv3.xml.in
@@ -20,7 +20,7 @@
           #include <include/interface/description.xml.i>
           <leafNode name="destination-port">
             <properties>
-              <help>UDP destination port for L2TPv3 tunnel (default: 5000)</help>
+              <help>UDP destination port for L2TPv3 tunnel</help>
               <valueHelp>
                 <format>u32:1-65535</format>
                 <description>Numeric IP port</description>
@@ -36,7 +36,7 @@
           #include <include/interface/interface-policy.xml.i>
           <leafNode name="encapsulation">
             <properties>
-              <help>Encapsulation type (default: UDP)</help>
+              <help>Encapsulation type</help>
               <completionHelp>
                 <list>udp ip</list>
               </completionHelp>
@@ -102,7 +102,7 @@
           </leafNode>
           <leafNode name="source-port">
             <properties>
-              <help>UDP source port for L2TPv3 tunnel (default: 5000)</help>
+              <help>UDP source port for L2TPv3 tunnel</help>
               <valueHelp>
                 <format>u32:1-65535</format>
                 <description>Numeric IP port</description>
diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in
index 598935e51..7206e57b1 100644
--- a/interface-definitions/interfaces-macsec.xml.in
+++ b/interface-definitions/interfaces-macsec.xml.in
@@ -36,7 +36,7 @@
                   </completionHelp>
                   <valueHelp>
                     <format>gcm-aes-128</format>
-                    <description>Galois/Counter Mode of AES cipher with 128-bit key (default)</description>
+                    <description>Galois/Counter Mode of AES cipher with 128-bit key</description>
                   </valueHelp>
                   <valueHelp>
                     <format>gcm-aes-256</format>
@@ -84,7 +84,7 @@
                   </leafNode>
                   <leafNode name="priority">
                     <properties>
-                      <help>Priority of MACsec Key Agreement protocol (MKA) actor (default: 255)</help>
+                      <help>Priority of MACsec Key Agreement protocol (MKA) actor</help>
                       <valueHelp>
                         <format>u32:0-255</format>
                         <description>MACsec Key Agreement protocol (MKA) priority</description>
diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in
index 16d91145f..eb574eb52 100644
--- a/interface-definitions/interfaces-openvpn.xml.in
+++ b/interface-definitions/interfaces-openvpn.xml.in
@@ -38,7 +38,7 @@
           #include <include/interface/interface-policy.xml.i>
           <leafNode name="device-type">
             <properties>
-              <help>OpenVPN interface device-type (default: tun)</help>
+              <help>OpenVPN interface device-type</help>
               <completionHelp>
                 <list>tun tap</list>
               </completionHelp>
@@ -206,7 +206,7 @@
             <children>
               <leafNode name="failure-count">
                 <properties>
-                  <help>Maximum number of keepalive packet failures (default: 60)</help>
+                  <help>Maximum number of keepalive packet failures</help>
                   <valueHelp>
                     <format>u32:0-1000</format>
                     <description>Maximum number of keepalive packet failures</description>
@@ -219,7 +219,7 @@
               </leafNode>
               <leafNode name="interval">
                 <properties>
-                  <help>Keepalive packet interval in seconds (default: 10)</help>
+                  <help>Keepalive packet interval in seconds</help>
                   <valueHelp>
                     <format>u32:0-600</format>
                     <description>Keepalive packet interval (seconds)</description>
@@ -613,13 +613,13 @@
               </leafNode>
               <leafNode name="topology">
                 <properties>
-                  <help>Topology for clients (default: net30)</help>
+                  <help>Topology for clients</help>
                   <completionHelp>
                     <list>net30 point-to-point subnet</list>
                   </completionHelp>
                   <valueHelp>
                     <format>net30</format>
-                    <description>net30 topology (default)</description>
+                    <description>net30 topology</description>
                   </valueHelp>
                   <valueHelp>
                     <format>point-to-point</format>
@@ -647,7 +647,7 @@
                     <children>
                       <leafNode name="slop">
                         <properties>
-                          <help>Maximum allowed clock slop in seconds (default: 180)</help>
+                          <help>Maximum allowed clock slop in seconds</help>
                           <valueHelp>
                             <format>1-65535</format>
                             <description>Seconds</description>
@@ -660,7 +660,7 @@
                       </leafNode>
                       <leafNode name="drift">
                         <properties>
-                          <help>Time drift in seconds (default: 0)</help>
+                          <help>Time drift in seconds</help>
                           <valueHelp>
                             <format>1-65535</format>
                             <description>Seconds</description>
@@ -673,7 +673,7 @@
                       </leafNode>
                       <leafNode name="step">
                         <properties>
-                          <help>Step value for totp in seconds (default: 30)</help>
+                          <help>Step value for totp in seconds</help>
                           <valueHelp>
                             <format>1-65535</format>
                             <description>Seconds</description>
@@ -686,7 +686,7 @@
                       </leafNode>
                       <leafNode name="digits">
                         <properties>
-                          <help>Number of digits to use for totp hash (default: 6)</help>
+                          <help>Number of digits to use for totp hash</help>
                           <valueHelp>
                             <format>1-65535</format>
                             <description>Seconds</description>
@@ -699,7 +699,7 @@
                       </leafNode>
                       <leafNode name="challenge">
                         <properties>
-                          <help>Expect password as result of a challenge response protocol (default: enabled)</help>
+                          <help>Expect password as result of a challenge response protocol</help>
                           <completionHelp>
                             <list>disable enable</list>
                           </completionHelp>
@@ -709,7 +709,7 @@
                           </valueHelp>
                           <valueHelp>
                             <format>enable</format>
-                            <description>Enable chalenge-response (default)</description>
+                            <description>Enable chalenge-response</description>
                           </valueHelp>
                           <constraint>
                             <regex>^(disable|enable)$</regex>
diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in
index 80a890940..ed0e45840 100644
--- a/interface-definitions/interfaces-pppoe.xml.in
+++ b/interface-definitions/interfaces-pppoe.xml.in
@@ -23,7 +23,7 @@
           #include <include/interface/interface-policy.xml.i>
           <leafNode name="default-route">
             <properties>
-              <help>Default route insertion behaviour (default: auto)</help>
+              <help>Default route insertion behaviour</help>
               <completionHelp>
                 <list>auto none force</list>
               </completionHelp>
diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in
index fd69fd177..eb1708aaa 100644
--- a/interface-definitions/interfaces-tunnel.xml.in
+++ b/interface-definitions/interfaces-tunnel.xml.in
@@ -241,7 +241,7 @@
                       </completionHelp>
                       <valueHelp>
                         <format>u32:0-255</format>
-                        <description>Encapsulation limit (default: 4)</description>
+                        <description>Encapsulation limit</description>
                       </valueHelp>
                       <valueHelp>
                         <format>none</format>
@@ -261,7 +261,7 @@
                       <help>Hoplimit</help>
                       <valueHelp>
                         <format>u32:0-255</format>
-                        <description>Hop limit (default: 64)</description>
+                        <description>Hop limit</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 0-255"/>
diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in
index a2d1439a3..5b79ac671 100644
--- a/interface-definitions/interfaces-wireless.xml.in
+++ b/interface-definitions/interfaces-wireless.xml.in
@@ -291,7 +291,7 @@
                       </completionHelp>
                       <valueHelp>
                         <format>0</format>
-                        <description>20 or 40 MHz channel width (default)</description>
+                        <description>20 or 40 MHz channel width</description>
                       </valueHelp>
                       <valueHelp>
                         <format>1</format>
@@ -431,7 +431,7 @@
           </node>
           <leafNode name="channel">
             <properties>
-              <help>Wireless radio channel (default: 0)</help>
+              <help>Wireless radio channel</help>
               <valueHelp>
                 <format>0</format>
                 <description>Automatic Channel Selection (ACS)</description>
@@ -515,7 +515,7 @@
               </completionHelp>
               <valueHelp>
                 <format>disabled</format>
-                <description>no MFP (hostapd default)</description>
+                <description>no MFP</description>
               </valueHelp>
               <valueHelp>
                 <format>optional</format>
@@ -546,7 +546,7 @@
               </valueHelp>
               <valueHelp>
                 <format>g</format>
-                <description>802.11g - 54 Mbits/sec (default)</description>
+                <description>802.11g - 54 Mbits/sec</description>
               </valueHelp>
               <valueHelp>
                 <format>n</format>
@@ -564,7 +564,7 @@
           </leafNode>
           <leafNode name="physical-device">
             <properties>
-              <help>Wireless physical device (default: phy0)</help>
+              <help>Wireless physical device</help>
               <completionHelp>
                 <script>${vyos_completion_dir}/list_wireless_phys.sh</script>
               </completionHelp>
diff --git a/interface-definitions/protocols-rpki.xml.in b/interface-definitions/protocols-rpki.xml.in
index a73d0aae4..68762ff9a 100644
--- a/interface-definitions/protocols-rpki.xml.in
+++ b/interface-definitions/protocols-rpki.xml.in
@@ -82,7 +82,7 @@
           </tagNode>
           <leafNode name="polling-period">
             <properties>
-              <help>RPKI cache polling period (default: 300)</help>
+              <help>RPKI cache polling period</help>
               <valueHelp>
                 <format>u32:1-86400</format>
                 <description>Polling period in seconds</description>
diff --git a/interface-definitions/service_console-server.xml.in b/interface-definitions/service_console-server.xml.in
index 28aa7ea71..549edb813 100644
--- a/interface-definitions/service_console-server.xml.in
+++ b/interface-definitions/service_console-server.xml.in
@@ -41,7 +41,7 @@
               </leafNode>
               <leafNode name="data-bits">
                 <properties>
-                  <help>Serial port data bits (default: 8)</help>
+                  <help>Serial port data bits</help>
                   <completionHelp>
                     <list>7 8</list>
                   </completionHelp>
@@ -53,7 +53,7 @@
               </leafNode>
               <leafNode name="stop-bits">
                 <properties>
-                  <help>Serial port stop bits (default: 1)</help>
+                  <help>Serial port stop bits</help>
                   <completionHelp>
                     <list>1 2</list>
                   </completionHelp>
@@ -65,7 +65,7 @@
               </leafNode>
               <leafNode name="parity">
                 <properties>
-                  <help>Parity setting (default: none)</help>
+                  <help>Parity setting</help>
                   <completionHelp>
                     <list>even odd none</list>
                   </completionHelp>
diff --git a/interface-definitions/service_monitoring_telegraf.xml.in b/interface-definitions/service_monitoring_telegraf.xml.in
index 0db9052ff..f0a94d6a9 100644
--- a/interface-definitions/service_monitoring_telegraf.xml.in
+++ b/interface-definitions/service_monitoring_telegraf.xml.in
@@ -44,19 +44,19 @@
               </node>
               <leafNode name="bucket">
                 <properties>
-                  <help>Remote bucket, by default (main)</help>
+                  <help>Remote bucket</help>
                 </properties>
                 <defaultValue>main</defaultValue>
               </leafNode>
               <leafNode name="source">
                 <properties>
-                  <help>Source parameters for monitoring (default: all)</help>
+                  <help>Source parameters for monitoring</help>
                   <completionHelp>
                     <list>all hardware-utilization logs network system telegraf</list>
                   </completionHelp>
                   <valueHelp>
                     <format>all</format>
-                    <description>All parameters (default)</description>
+                    <description>All parameters</description>
                   </valueHelp>
                   <valueHelp>
                     <format>hardware-utilization</format>
diff --git a/interface-definitions/service_router-advert.xml.in b/interface-definitions/service_router-advert.xml.in
index 0f4009f5c..ce1da85aa 100644
--- a/interface-definitions/service_router-advert.xml.in
+++ b/interface-definitions/service_router-advert.xml.in
@@ -18,7 +18,7 @@
             <children>
               <leafNode name="hop-limit">
                 <properties>
-                  <help>Set Hop Count field of the IP header for outgoing packets (default: 64)</help>
+                  <help>Set Hop Count field of the IP header for outgoing packets</help>
                   <valueHelp>
                     <format>u32:0</format>
                     <description>Unspecified (by this router)</description>
@@ -63,7 +63,7 @@
                   </valueHelp>
                   <valueHelp>
                     <format>medium</format>
-                    <description>Default router has medium preference (default)</description>
+                    <description>Default router has medium preference</description>
                   </valueHelp>
                   <valueHelp>
                     <format>high</format>
@@ -108,7 +108,7 @@
                 <children>
                   <leafNode name="max">
                     <properties>
-                      <help>Maximum interval between unsolicited multicast RAs (default: 600)</help>
+                      <help>Maximum interval between unsolicited multicast RAs</help>
                       <valueHelp>
                         <format>u32:4-1800</format>
                         <description>Maximum interval in seconds</description>
@@ -156,7 +156,7 @@
                 <children>
                   <leafNode name="valid-lifetime">
                     <properties>
-                      <help>Time in seconds that the route will remain valid (default: 1800 seconds)</help>
+                      <help>Time in seconds that the route will remain valid</help>
                       <completionHelp>
                         <list>infinity</list>
                       </completionHelp>
@@ -187,7 +187,7 @@
                       </valueHelp>
                       <valueHelp>
                         <format>medium</format>
-                        <description>Route has medium preference (default)</description>
+                        <description>Route has medium preference</description>
                       </valueHelp>
                       <valueHelp>
                         <format>high</format>
@@ -234,7 +234,7 @@
                   </leafNode>
                   <leafNode name="preferred-lifetime">
                     <properties>
-                      <help>Time in seconds that the prefix will remain preferred (default 4 hours)</help>
+                      <help>Time in seconds that the prefix will remain preferred</help>
                       <completionHelp>
                         <list>infinity</list>
                       </completionHelp>
@@ -255,7 +255,7 @@
                   </leafNode>
                   <leafNode name="valid-lifetime">
                     <properties>
-                      <help>Time in seconds that the prefix will remain valid (default: 30 days)</help>
+                      <help>Time in seconds that the prefix will remain valid</help>
                       <completionHelp>
                         <list>infinity</list>
                       </completionHelp>
diff --git a/interface-definitions/service_webproxy.xml.in b/interface-definitions/service_webproxy.xml.in
index 03f504ac7..92e5ca37b 100644
--- a/interface-definitions/service_webproxy.xml.in
+++ b/interface-definitions/service_webproxy.xml.in
@@ -28,7 +28,7 @@
             <children>
               <leafNode name="children">
                 <properties>
-                  <help>Number of authentication helper processes (default: 5)</help>
+                  <help>Number of authentication helper processes</help>
                   <valueHelp>
                     <format>n</format>
                     <description>Number of authentication helper processes</description>
@@ -41,7 +41,7 @@
               </leafNode>
               <leafNode name="credentials-ttl">
                 <properties>
-                  <help>Authenticated session time to live in minutes (default: 60)</help>
+                  <help>Authenticated session time to live in minutes</help>
                   <valueHelp>
                     <format>n</format>
                     <description>Authenticated session timeout</description>
@@ -105,7 +105,7 @@
                   </leafNode>
                   <leafNode name="version">
                     <properties>
-                      <help>LDAP protocol version (default: 3)</help>
+                      <help>LDAP protocol version</help>
                       <completionHelp>
                           <list>2 3</list>
                       </completionHelp>
@@ -177,7 +177,7 @@
               </leafNode>
               <leafNode name="http-port">
                 <properties>
-                  <help>Default Proxy Port (default: 3128)</help>
+                  <help>Default Proxy Port</help>
                   <valueHelp>
                     <format>u32:1025-65535</format>
                     <description>Default port number</description>
@@ -190,7 +190,11 @@
               </leafNode>
                <leafNode name="icp-port">
                 <properties>
-                  <help>Cache peer ICP port (default: disabled)</help>
+                  <help>Cache peer ICP port</help>
+                  <valueHelp>
+                    <format>u32:0</format>
+                    <description>Cache peer disabled</description>
+                  </valueHelp>
                   <valueHelp>
                     <format>u32:1-65535</format>
                     <description>Cache peer ICP port</description>
@@ -203,7 +207,7 @@
               </leafNode>
               <leafNode name="options">
                 <properties>
-                  <help>Cache peer options (default: "no-query default")</help>
+                  <help>Cache peer options</help>
                   <valueHelp>
                     <format>txt</format>
                     <description>Cache peer options</description>
@@ -239,7 +243,7 @@
           </tagNode>
           <leafNode name="cache-size">
             <properties>
-              <help>Disk cache size in MB (default: 100)</help>
+              <help>Disk cache size in MB</help>
                <valueHelp>
                 <format>u32</format>
                 <description>Disk cache size in MB</description>
@@ -253,7 +257,7 @@
           </leafNode>
           <leafNode name="default-port">
             <properties>
-              <help>Default Proxy Port (default: 3128)</help>
+              <help>Default Proxy Port</help>
               <valueHelp>
                 <format>u32:1025-65535</format>
                 <description>Default port number</description>
@@ -296,7 +300,7 @@
             <children>
               <leafNode name="port">
                 <properties>
-                  <help>Default Proxy Port (default: 3128)</help>
+                  <help>Default Proxy Port</help>
                   <valueHelp>
                     <format>u32:1025-65535</format>
                     <description>Default port number</description>
@@ -399,7 +403,7 @@
                     <children>
                       <leafNode name="update-hour">
                         <properties>
-                          <help>Hour of day for database update [REQUIRED]</help>
+                          <help>Hour of day for database update</help>
                           <valueHelp>
                             <format>u32:0-23</format>
                             <description>Hour for database update</description>
@@ -414,7 +418,7 @@
                   </node>
                   <leafNode name="redirect-url">
                     <properties>
-                      <help>Redirect URL for filtered websites (default: block.vyos.net)</help>
+                      <help>Redirect URL for filtered websites</help>
                       <valueHelp>
                         <format>url</format>
                         <description>URL for redirect</description>
diff --git a/interface-definitions/snmp.xml.in b/interface-definitions/snmp.xml.in
index 67d3aef9a..b9e0f4cc5 100644
--- a/interface-definitions/snmp.xml.in
+++ b/interface-definitions/snmp.xml.in
@@ -26,7 +26,7 @@
                   </completionHelp>
                   <valueHelp>
                     <format>ro</format>
-                    <description>Read-Only (default)</description>
+                    <description>Read-Only</description>
                   </valueHelp>
                   <valueHelp>
                     <format>rw</format>
@@ -226,7 +226,7 @@
                       </valueHelp>
                       <valueHelp>
                         <format>auth</format>
-                        <description>Messages are authenticated but not encrypted (authNoPriv, default)</description>
+                        <description>Messages are authenticated but not encrypted (authNoPriv)</description>
                       </valueHelp>
                       <valueHelp>
                         <format>priv</format>
@@ -329,7 +329,7 @@
                         <list>inform trap</list>
                       </completionHelp>
                       <valueHelp>
-                        <format>inform (default)</format>
+                        <format>inform</format>
                         <description>Use INFORM</description>
                       </valueHelp>
                       <valueHelp>
diff --git a/interface-definitions/ssh.xml.in b/interface-definitions/ssh.xml.in
index e3b9d16e1..187e5f8e8 100644
--- a/interface-definitions/ssh.xml.in
+++ b/interface-definitions/ssh.xml.in
@@ -105,7 +105,7 @@
                 <regex>^(quiet|fatal|error|info|verbose)$</regex>
               </constraint>
             </properties>
-            <defaultValue>INFO</defaultValue>
+            <defaultValue>info</defaultValue>
           </leafNode>
           <leafNode name="mac">
             <properties>
diff --git a/interface-definitions/system-ip.xml.in b/interface-definitions/system-ip.xml.in
index 86fbe5701..1fa63d517 100644
--- a/interface-definitions/system-ip.xml.in
+++ b/interface-definitions/system-ip.xml.in
@@ -15,7 +15,7 @@
             <children>
               <leafNode name="table-size">
                 <properties>
-                  <help>Maximum number of entries to keep in the ARP cache (default: 8192)</help>
+                  <help>Maximum number of entries to keep in the ARP cache</help>
                   <completionHelp>
                     <list>1024 2048 4096 8192 16384 32768</list>
                   </completionHelp>
diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in
index 4bfe82268..a5519ee88 100644
--- a/interface-definitions/system-login.xml.in
+++ b/interface-definitions/system-login.xml.in
@@ -124,7 +124,7 @@
                       <help>Session timeout</help>
                       <valueHelp>
                         <format>u32:1-30</format>
-                        <description>Session timeout in seconds (default: 2)</description>
+                        <description>Session timeout in seconds</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-30"/>
@@ -138,7 +138,7 @@
                       <help>Server priority</help>
                       <valueHelp>
                         <format>u32:1-255</format>
-                        <description>Server priority (default: 255)</description>
+                        <description>Server priority</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-255"/>
diff --git a/interface-definitions/system-logs.xml.in b/interface-definitions/system-logs.xml.in
index 8b6c7c399..1caa7abb6 100644
--- a/interface-definitions/system-logs.xml.in
+++ b/interface-definitions/system-logs.xml.in
@@ -23,7 +23,7 @@
                       <help>Size of a single log file that triggers rotation</help>
                       <valueHelp>
                         <format>u32:1-1024</format>
-                        <description>Size in MB (default: 10)</description>
+                        <description>Size in MB</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-1024" />
@@ -37,7 +37,7 @@
                       <help>Count of rotations before old logs will be deleted</help>
                       <valueHelp>
                         <format>u32:1-100</format>
-                        <description>Rotations (default: 10)</description>
+                        <description>Rotations</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-100" />
@@ -58,7 +58,7 @@
                       <help>Size of a single log file that triggers rotation</help>
                       <valueHelp>
                         <format>u32:1-1024</format>
-                        <description>Size in MB (default: 1)</description>
+                        <description>Size in MB</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-1024" />
@@ -72,7 +72,7 @@
                       <help>Count of rotations before old logs will be deleted</help>
                       <valueHelp>
                         <format>u32:1-100</format>
-                        <description>Rotations (default: 10)</description>
+                        <description>Rotations</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-100" />
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index dae76218f..147bb99ba 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -30,7 +30,7 @@
                   </completionHelp>
                   <valueHelp>
                     <format>disable</format>
-                    <description>Disable ESP compression (default)</description>
+                    <description>Disable ESP compression</description>
                   </valueHelp>
                   <valueHelp>
                     <format>enable</format>
@@ -47,7 +47,7 @@
                   <help>ESP lifetime</help>
                   <valueHelp>
                     <format>u32:30-86400</format>
-                    <description>ESP lifetime in seconds (default: 3600)</description>
+                    <description>ESP lifetime in seconds</description>
                   </valueHelp>
                   <constraint>
                     <validator name="numeric" argument="--range 30-86400"/>
@@ -87,7 +87,7 @@
                   </completionHelp>
                   <valueHelp>
                     <format>tunnel</format>
-                    <description>Tunnel mode (default)</description>
+                    <description>Tunnel mode</description>
                   </valueHelp>
                   <valueHelp>
                     <format>transport</format>
@@ -107,7 +107,7 @@
                   </completionHelp>
                   <valueHelp>
                     <format>enable</format>
-                    <description>Inherit Diffie-Hellman group from the IKE group (default)</description>
+                    <description>Inherit Diffie-Hellman group from the IKE group</description>
                   </valueHelp>
                   <valueHelp>
                     <format>dh-group1</format>
@@ -235,7 +235,7 @@
                   </completionHelp>
                   <valueHelp>
                     <format>none</format>
-                    <description>Do nothing (default)</description>
+                    <description>Do nothing</description>
                   </valueHelp>
                   <valueHelp>
                     <format>hold</format>
@@ -267,7 +267,7 @@
                       </completionHelp>
                       <valueHelp>
                         <format>hold</format>
-                        <description>Attempt to re-negotiate the connection when matching traffic is seen (default)</description>
+                        <description>Attempt to re-negotiate the connection when matching traffic is seen</description>
                       </valueHelp>
                       <valueHelp>
                         <format>clear</format>
@@ -287,7 +287,7 @@
                       <help>Keep-alive interval</help>
                       <valueHelp>
                         <format>u32:2-86400</format>
-                        <description>Keep-alive interval in seconds (default: 30)</description>
+                        <description>Keep-alive interval in seconds</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 2-86400"/>
@@ -299,7 +299,7 @@
                       <help>Dead Peer Detection keep-alive timeout (IKEv1 only)</help>
                       <valueHelp>
                         <format>u32:2-86400</format>
-                        <description>Keep-alive timeout in seconds (default 120)</description>
+                        <description>Keep-alive timeout in seconds</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 2-86400"/>
@@ -310,7 +310,7 @@
               </node>
               <leafNode name="ikev2-reauth">
                 <properties>
-                  <help>Re-authentication of the remote peer during an IKE re-key. IKEv2 option only</help>
+                  <help>Re-authentication of the remote peer during an IKE re-key - IKEv2 only</help>
                   <completionHelp>
                     <list>yes no</list>
                   </completionHelp>
@@ -320,7 +320,7 @@
                   </valueHelp>
                   <valueHelp>
                     <format>no</format>
-                    <description>Disable remote host re-authenticaton during an IKE rekey. (default)</description>
+                    <description>Disable remote host re-authenticaton during an IKE rekey</description>
                   </valueHelp>
                   <constraint>
                     <regex>^(yes|no)$</regex>
@@ -351,7 +351,7 @@
                   <help>IKE lifetime</help>
                   <valueHelp>
                     <format>u32:30-86400</format>
-                    <description>IKE lifetime in seconds (default: 28800)</description>
+                    <description>IKE lifetime in seconds</description>
                   </valueHelp>
                   <constraint>
                     <validator name="numeric" argument="--range 30-86400"/>
@@ -367,7 +367,7 @@
                   </completionHelp>
                   <valueHelp>
                     <format>enable</format>
-                    <description>Enable MOBIKE (default for IKEv2)</description>
+                    <description>Enable MOBIKE</description>
                   </valueHelp>
                   <valueHelp>
                     <format>disable</format>
@@ -386,7 +386,7 @@
                   </completionHelp>
                   <valueHelp>
                     <format>main</format>
-                    <description>Use the main mode (recommended, default)</description>
+                    <description>Use the main mode (recommended)</description>
                   </valueHelp>
                   <valueHelp>
                     <format>aggressive</format>
@@ -533,7 +533,7 @@
                   <help>strongSwan logging Level</help>
                   <valueHelp>
                     <format>0</format>
-                    <description>Very basic auditing logs e.g. SA up/SA down (default)</description>
+                    <description>Very basic auditing logs e.g. SA up/SA down</description>
                   </valueHelp>
                   <valueHelp>
                     <format>1</format>
@@ -791,7 +791,7 @@
                       </valueHelp>
                       <valueHelp>
                         <format>u32:1-86400</format>
-                        <description>Timeout in seconds (default: 28800)</description>
+                        <description>Timeout in seconds</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 0-86400"/>
@@ -1067,7 +1067,7 @@
                       </valueHelp>
                       <valueHelp>
                         <format>inherit</format>
-                        <description>Inherit the reauth configuration form your IKE-group (default)</description>
+                        <description>Inherit the reauth configuration form your IKE-group</description>
                       </valueHelp>
                       <constraint>
                         <regex>^(yes|no|inherit)$</regex>
diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in
index 6a88756a7..9ca7b1fad 100644
--- a/interface-definitions/vpn_l2tp.xml.in
+++ b/interface-definitions/vpn_l2tp.xml.in
@@ -88,7 +88,7 @@
                       <help>IKE lifetime</help>
                       <valueHelp>
                         <format>u32:30-86400</format>
-                        <description>IKE lifetime in seconds (default 3600)</description>
+                        <description>IKE lifetime in seconds</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 30-86400"/>
@@ -101,7 +101,7 @@
                       <help>ESP lifetime</help>
                       <valueHelp>
                         <format>u32:30-86400</format>
-                        <description>IKE lifetime in seconds (default 3600)</description>
+                        <description>IKE lifetime in seconds</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 30-86400"/>
@@ -135,7 +135,7 @@
                   <help>PPP idle timeout</help>
                   <valueHelp>
                     <format>u32:30-86400</format>
-                    <description>PPP idle timeout in seconds (default 1800)</description>
+                    <description>PPP idle timeout in seconds</description>
                   </valueHelp>
                     <constraint>
                       <validator name="numeric" argument="--range 30-86400"/>
@@ -206,7 +206,7 @@
                       </leafNode>
                       <leafNode name="acct-timeout">
                         <properties>
-                          <help>Timeout to wait reply for Interim-Update packets. (default 3 seconds)</help>
+                          <help>Timeout to wait reply for Interim-Update packets</help>
                         </properties>
                       </leafNode>
                       <leafNode name="max-try">
@@ -244,7 +244,7 @@
                         <children>
                           <leafNode name="attribute">
                             <properties>
-                              <help>Specifies which radius attribute contains rate information. (default is Filter-Id)</help>
+                              <help>Specifies which radius attribute contains rate information</help>
                             </properties>
                           </leafNode>
                           <leafNode name="vendor">
diff --git a/interface-definitions/vpn_openconnect.xml.in b/interface-definitions/vpn_openconnect.xml.in
index 0db5e79d0..3fc34bacc 100644
--- a/interface-definitions/vpn_openconnect.xml.in
+++ b/interface-definitions/vpn_openconnect.xml.in
@@ -41,7 +41,7 @@
                       <help>Session timeout</help>
                       <valueHelp>
                         <format>u32:1-30</format>
-                        <description>Session timeout in seconds (default: 2)</description>
+                        <description>Session timeout in seconds</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-30"/>
@@ -61,10 +61,10 @@
             <children>
               <leafNode name="tcp">
                 <properties>
-                  <help>tcp port number to accept connections (default: 443)</help>
+                  <help>tcp port number to accept connections</help>
                   <valueHelp>
                     <format>u32:1-65535</format>
-                    <description>Numeric IP port (default: 443)</description>
+                    <description>Numeric IP port</description>
                   </valueHelp>
                   <constraint>
                     <validator name="numeric" argument="--range 1-65535"/>
@@ -74,10 +74,10 @@
               </leafNode>
               <leafNode name="udp">
                 <properties>
-                  <help>udp port number to accept connections (default: 443)</help>
+                  <help>udp port number to accept connections</help>
                   <valueHelp>
                     <format>u32:1-65535</format>
-                    <description>Numeric IP port (default: 443)</description>
+                    <description>Numeric IP port</description>
                   </valueHelp>
                   <constraint>
                     <validator name="numeric" argument="--range 1-65535"/>
@@ -160,7 +160,7 @@
                       <help>Prefix length used for individual client</help>
                       <valueHelp>
                         <format>u32:48-128</format>
-                        <description>Client prefix length (default: 64)</description>
+                        <description>Client prefix length</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 48-128"/>
diff --git a/interface-definitions/zone-policy.xml.in b/interface-definitions/zone-policy.xml.in
index 69ee031c7..b898c3ecd 100644
--- a/interface-definitions/zone-policy.xml.in
+++ b/interface-definitions/zone-policy.xml.in
@@ -27,7 +27,7 @@
               </completionHelp>
               <valueHelp>
                 <format>drop</format>
-                <description>Drop silently (default)</description>
+                <description>Drop silently</description>
               </valueHelp>
               <valueHelp>
                 <format>reject</format>
@@ -97,7 +97,7 @@
                   </completionHelp>
                   <valueHelp>
                     <format>accept</format>
-                    <description>Accept traffic (default)</description>
+                    <description>Accept traffic</description>
                   </valueHelp>
                   <valueHelp>
                     <format>drop</format>
@@ -138,7 +138,7 @@
               <help>Zone to be local-zone</help>
               <valueless/>
             </properties>
-          </leafNode> 
+          </leafNode>
         </children>
       </tagNode>
     </children>
diff --git a/scripts/build-command-templates b/scripts/build-command-templates
index d8abb0a13..876f5877c 100755
--- a/scripts/build-command-templates
+++ b/scripts/build-command-templates
@@ -117,7 +117,7 @@ def collect_validators(ve):
 
     return regex_args + " " + validator_args
 
-def get_properties(p):
+def get_properties(p, default=None):
     props = {}
 
     if p is None:
@@ -125,7 +125,12 @@ def get_properties(p):
 
     # Get the help string
     try:
-        props["help"] = p.find("help").text
+        help = p.find("help").text
+        if default != None:
+            # DNS forwarding for instance has multiple defaults - specified as whitespace separated list
+            tmp = ', '.join(default.text.split())
+            help += f' (default: {tmp})'
+        props["help"] = help
     except:
         pass
 
@@ -134,7 +139,11 @@ def get_properties(p):
         vhe = p.findall("valueHelp")
         vh = []
         for v in vhe:
-            vh.append( (v.find("format").text, v.find("description").text) )
+            format = v.find("format").text
+            description = v.find("description").text
+            if default != None and default.text == format:
+                description += f' (default)'
+            vh.append( (format, description) )
         props["val_help"] = vh
     except:
         props["val_help"] = []
@@ -271,7 +280,7 @@ def process_node(n, tmpl_dir):
         print("Name of the node: {0}. Created directory: {1}\n".format(name, "/".join(my_tmpl_dir)), end="")
     os.makedirs(make_path(my_tmpl_dir), exist_ok=True)
 
-    props = get_properties(props_elem)
+    props = get_properties(props_elem, n.find("defaultValue"))
     if owner:
         props["owner"] = owner
     # Type should not be set for non-tag, non-leaf nodes
-- 
cgit v1.2.3


From faa63999ca1fe11cc25e8a241e75a451a53ffa26 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Feb 2022 21:46:18 +0100
Subject: dhcp-relay: T3095: add missing max-size default value

---
 interface-definitions/dhcp-relay.xml.in | 1 +
 1 file changed, 1 insertion(+)

diff --git a/interface-definitions/dhcp-relay.xml.in b/interface-definitions/dhcp-relay.xml.in
index a5643add6..339941e65 100644
--- a/interface-definitions/dhcp-relay.xml.in
+++ b/interface-definitions/dhcp-relay.xml.in
@@ -41,6 +41,7 @@
                   </constraint>
                   <constraintErrorMessage>max-size must be a value between 64 and 1400</constraintErrorMessage>
                 </properties>
+                <defaultValue>576</defaultValue>
               </leafNode>
               <leafNode name="relay-agents-packets">
                 <properties>
-- 
cgit v1.2.3


From 15eff1682613ad20f83c46fded866b132a1fb814 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Feb 2022 21:47:26 +0100
Subject: smoketest: webproxy: use setUpClass() over setUp()

---
 smoketest/scripts/cli/test_service_webproxy.py | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/smoketest/scripts/cli/test_service_webproxy.py b/smoketest/scripts/cli/test_service_webproxy.py
index 8a1a03ce7..ebbd9fe55 100755
--- a/smoketest/scripts/cli/test_service_webproxy.py
+++ b/smoketest/scripts/cli/test_service_webproxy.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -30,11 +30,19 @@ listen_if = 'dum3632'
 listen_ip = '192.0.2.1'
 
 class TestServiceWebProxy(VyOSUnitTestSHIM.TestCase):
-    def setUp(self):
-        self.cli_set(['interfaces', 'dummy', listen_if, 'address', listen_ip + '/32'])
+    @classmethod
+    def setUpClass(cls):
+        # call base-classes classmethod
+        super(cls, cls).setUpClass()
+        # create a test interfaces
+        cls.cli_set(cls, ['interfaces', 'dummy', listen_if, 'address', listen_ip + '/32'])
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.cli_delete(cls, ['interfaces', 'dummy', listen_if])
+        super().tearDownClass()
 
     def tearDown(self):
-        self.cli_delete(['interfaces', 'dummy', listen_if])
         self.cli_delete(base_path)
         self.cli_commit()
 
-- 
cgit v1.2.3


From be60d39332b753f5fe35101efe3463eebea2cb9d Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Feb 2022 21:50:09 +0100
Subject: wireless: ifconfig: T2653: add missing defaultValue for
 mgmt-frame-protection

---
 interface-definitions/interfaces-wireless.xml.in | 1 +
 1 file changed, 1 insertion(+)

diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in
index 5b79ac671..9db9fd757 100644
--- a/interface-definitions/interfaces-wireless.xml.in
+++ b/interface-definitions/interfaces-wireless.xml.in
@@ -529,6 +529,7 @@
                 <regex>^(disabled|optional|required)$</regex>
               </constraint>
             </properties>
+            <defaultValue>disabled</defaultValue>
           </leafNode>
           <leafNode name="mode">
             <properties>
-- 
cgit v1.2.3


From 8cb7c2288585b81f29111e7fdc36c34e62b7db13 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Feb 2022 21:52:57 +0100
Subject: xml: webproxy: add comment about explicitly not set defaultValue

---
 interface-definitions/service_webproxy.xml.in | 1 +
 1 file changed, 1 insertion(+)

diff --git a/interface-definitions/service_webproxy.xml.in b/interface-definitions/service_webproxy.xml.in
index 92e5ca37b..89c4c3910 100644
--- a/interface-definitions/service_webproxy.xml.in
+++ b/interface-definitions/service_webproxy.xml.in
@@ -309,6 +309,7 @@
                     <validator name="numeric" argument="--range 1025-65535"/>
                   </constraint>
                 </properties>
+                <!-- no defaultValue specified as there is default-port -->
               </leafNode>
               <leafNode name="disable-transparent">
                 <properties>
-- 
cgit v1.2.3


From 0ec8927476e7d654d52df4c803a6694be0b1e9e2 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Feb 2022 21:54:25 +0100
Subject: monitoring: T3872: re-use "port" building block from
 port-number.xml.i

---
 interface-definitions/service_monitoring_telegraf.xml.in | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/interface-definitions/service_monitoring_telegraf.xml.in b/interface-definitions/service_monitoring_telegraf.xml.in
index f0a94d6a9..7db9de9f8 100644
--- a/interface-definitions/service_monitoring_telegraf.xml.in
+++ b/interface-definitions/service_monitoring_telegraf.xml.in
@@ -98,10 +98,8 @@
                   <constraintErrorMessage>Incorrect URL format.</constraintErrorMessage>
                 </properties>
               </leafNode>
+              #include <include/port-number.xml.i>
               <leafNode name="port">
-                <properties>
-                  <help>Remote port (default: 8086)</help>
-                </properties>
                 <defaultValue>8086</defaultValue>
               </leafNode>
             </children>
-- 
cgit v1.2.3


From ae51162283826e1a510aed1609778eb0223c8462 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Feb 2022 21:57:09 +0100
Subject: vpn: ipsec: T3093: add missing defaultValue entries

---
 interface-definitions/include/vpn-ipsec-encryption.xml.i | 1 +
 interface-definitions/include/vpn-ipsec-hash.xml.i       | 1 +
 interface-definitions/vpn_ipsec.xml.in                   | 4 ++++
 3 files changed, 6 insertions(+)

diff --git a/interface-definitions/include/vpn-ipsec-encryption.xml.i b/interface-definitions/include/vpn-ipsec-encryption.xml.i
index faa264d2f..eb0678aa9 100644
--- a/interface-definitions/include/vpn-ipsec-encryption.xml.i
+++ b/interface-definitions/include/vpn-ipsec-encryption.xml.i
@@ -229,5 +229,6 @@
       <regex>^(null|aes128|aes192|aes256|aes128ctr|aes192ctr|aes256ctr|aes128ccm64|aes192ccm64|aes256ccm64|aes128ccm96|aes192ccm96|aes256ccm96|aes128ccm128|aes192ccm128|aes256ccm128|aes128gcm64|aes192gcm64|aes256gcm64|aes128gcm96|aes192gcm96|aes256gcm96|aes128gcm128|aes192gcm128|aes256gcm128|aes128gmac|aes192gmac|aes256gmac|3des|blowfish128|blowfish192|blowfish256|camellia128|camellia192|camellia256|camellia128ctr|camellia192ctr|camellia256ctr|camellia128ccm64|camellia192ccm64|camellia256ccm64|camellia128ccm96|camellia192ccm96|camellia256ccm96|camellia128ccm128|camellia192ccm128|camellia256ccm128|serpent128|serpent192|serpent256|twofish128|twofish192|twofish256|cast128|chacha20poly1305)$</regex>
     </constraint>
   </properties>
+  <defaultValue>aes128</defaultValue>
 </leafNode>
 <!-- include end -->
diff --git a/interface-definitions/include/vpn-ipsec-hash.xml.i b/interface-definitions/include/vpn-ipsec-hash.xml.i
index b3ef4fb7a..d6259574a 100644
--- a/interface-definitions/include/vpn-ipsec-hash.xml.i
+++ b/interface-definitions/include/vpn-ipsec-hash.xml.i
@@ -61,5 +61,6 @@
       <regex>^(md5|md5_128|sha1|sha1_160|sha256|sha256_96|sha384|sha512|aesxcbc|aescmac|aes128gmac|aes192gmac|aes256gmac)$</regex>
     </constraint>
   </properties>
+  <defaultValue>sha1</defaultValue>
 </leafNode>
 <!-- include end -->
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index 147bb99ba..885bac979 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -293,6 +293,7 @@
                         <validator name="numeric" argument="--range 2-86400"/>
                       </constraint>
                     </properties>
+                    <defaultValue>30</defaultValue>
                   </leafNode>
                   <leafNode name="timeout">
                     <properties>
@@ -305,6 +306,7 @@
                         <validator name="numeric" argument="--range 2-86400"/>
                       </constraint>
                     </properties>
+                    <defaultValue>120</defaultValue>
                   </leafNode>
                 </children>
               </node>
@@ -377,6 +379,7 @@
                     <regex>^(enable|disable)$</regex>
                   </constraint>
                 </properties>
+                <defaultValue>enable</defaultValue>
               </leafNode>
               <leafNode name="mode">
                 <properties>
@@ -396,6 +399,7 @@
                     <regex>^(main|aggressive)$</regex>
                   </constraint>
                 </properties>
+                <defaultValue>main</defaultValue>
               </leafNode>
               <tagNode name="proposal">
                 <properties>
-- 
cgit v1.2.3


From 0daf168d3d7583984431de2ef97682ff4c986f74 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Feb 2022 22:30:34 +0100
Subject: zone-policy: T2199: bugfix defaultValue usage

Instead of hardcoding the default behavior inside the Jinaj2 template, all
defaults are required to be specified inside teh XML definition. This is
required to automatically render the appropriate CLI tab completion commands.
---
 data/templates/zone_policy/nftables.tmpl | 12 ++++++------
 interface-definitions/zone-policy.xml.in |  1 +
 src/conf_mode/zone_policy.py             | 24 ++++++++++++++++++------
 3 files changed, 25 insertions(+), 12 deletions(-)

diff --git a/data/templates/zone_policy/nftables.tmpl b/data/templates/zone_policy/nftables.tmpl
index 093da6bd8..4a6bd2772 100644
--- a/data/templates/zone_policy/nftables.tmpl
+++ b/data/templates/zone_policy/nftables.tmpl
@@ -16,7 +16,7 @@ table ip filter {
         iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME_{{ from_conf.firewall.name }}
         iifname { {{ zone[from_zone].interface | join(",") }} } counter return
 {%       endfor %}
-        counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
+        counter {{ zone_conf.default_action }}
     }
     chain VZONE_{{ zone_name }}_OUT {
         oifname lo counter return
@@ -24,7 +24,7 @@ table ip filter {
         oifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME_{{ from_conf.firewall.name }}
         oifname { {{ zone[from_zone].interface | join(",") }} } counter return
 {%         endfor %}
-        counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
+        counter {{ zone_conf.default_action }}
     }
 {%     else %}
     chain VZONE_{{ zone_name }} {
@@ -38,7 +38,7 @@ table ip filter {
         iifname { {{ zone[from_zone].interface | join(",") }} } counter return
 {%         endif %}
 {%       endfor %}
-        counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
+        counter {{ zone_conf.default_action }}
     }
 {%     endif %}
 {%   endfor %}
@@ -53,7 +53,7 @@ table ip6 filter {
         iifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME6_{{ from_conf.firewall.ipv6_name }}
         iifname { {{ zone[from_zone].interface | join(",") }} } counter return
 {%       endfor %}
-        counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
+        counter {{ zone_conf.default_action }}
     }
     chain VZONE6_{{ zone_name }}_OUT {
         oifname lo counter return
@@ -61,7 +61,7 @@ table ip6 filter {
         oifname { {{ zone[from_zone].interface | join(",") }} } counter jump NAME6_{{ from_conf.firewall.ipv6_name }}
         oifname { {{ zone[from_zone].interface | join(",") }} } counter return
 {%         endfor %}
-        counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
+        counter {{ zone_conf.default_action }}
     }
 {%     else %}
     chain VZONE6_{{ zone_name }} {
@@ -75,7 +75,7 @@ table ip6 filter {
         iifname { {{ zone[from_zone].interface | join(",") }} } counter return
 {%         endif %}
 {%       endfor %}
-        counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
+        counter {{ zone_conf.default_action }}
     }
 {%     endif %}
 {%   endfor %}
diff --git a/interface-definitions/zone-policy.xml.in b/interface-definitions/zone-policy.xml.in
index b898c3ecd..eac63fa6b 100644
--- a/interface-definitions/zone-policy.xml.in
+++ b/interface-definitions/zone-policy.xml.in
@@ -37,6 +37,7 @@
                 <regex>^(drop|reject)$</regex>
               </constraint>
             </properties>
+            <defaultValue>drop</defaultValue>
           </leafNode>
           <tagNode name="from">
             <properties>
diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py
index 683f8f034..dc0617353 100755
--- a/src/conf_mode/zone_policy.py
+++ b/src/conf_mode/zone_policy.py
@@ -1,6 +1,6 @@
 #!/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
@@ -20,10 +20,12 @@ from json import loads
 from sys import exit
 
 from vyos.config import Config
+from vyos.configdict import dict_merge
 from vyos.template import render
 from vyos.util import cmd
 from vyos.util import dict_search_args
 from vyos.util import run
+from vyos.xml import defaults
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
@@ -36,12 +38,22 @@ def get_config(config=None):
     else:
         conf = Config()
     base = ['zone-policy']
-    zone_policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
-                                    no_tag_node_value_mangle=True)
+    zone_policy = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                       get_first_key=True,
+                                       no_tag_node_value_mangle=True)
 
-    if zone_policy:
-        zone_policy['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True,
-                                    no_tag_node_value_mangle=True)
+    zone_policy['firewall'] = conf.get_config_dict(['firewall'],
+                                                   key_mangling=('-', '_'),
+                                                   get_first_key=True,
+                                                   no_tag_node_value_mangle=True)
+
+    if 'zone' in zone_policy:
+        # 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 + ['zone'])
+        for zone in zone_policy['zone']:
+            zone_policy['zone'][zone] = dict_merge(default_values,
+                                                   zone_policy['zone'][zone])
 
     return zone_policy
 
-- 
cgit v1.2.3


From d25e07fd2e965b1193ad4d625167e890b0e36f6a Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Feb 2022 22:31:45 +0100
Subject: smoketest: zone-policy: use setUpClass() over setUp()

---
 smoketest/scripts/cli/test_zone_policy.py | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/smoketest/scripts/cli/test_zone_policy.py b/smoketest/scripts/cli/test_zone_policy.py
index 00dfe0182..6e34f3179 100755
--- a/smoketest/scripts/cli/test_zone_policy.py
+++ b/smoketest/scripts/cli/test_zone_policy.py
@@ -1,6 +1,6 @@
 #!/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
@@ -21,12 +21,18 @@ from base_vyostest_shim import VyOSUnitTestSHIM
 from vyos.util import cmd
 
 class TestZonePolicy(VyOSUnitTestSHIM.TestCase):
-    def setUp(self):
-        self.cli_set(['firewall', 'name', 'smoketest', 'default-action', 'drop'])
+    @classmethod
+    def setUpClass(cls):
+        super(cls, cls).setUpClass()
+        cls.cli_set(cls, ['firewall', 'name', 'smoketest', 'default-action', 'drop'])
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.cli_delete(cls, ['firewall'])
+        super(cls, cls).tearDownClass()
 
     def tearDown(self):
         self.cli_delete(['zone-policy'])
-        self.cli_delete(['firewall'])
         self.cli_commit()
 
     def test_basic_zone(self):
-- 
cgit v1.2.3


From e1c5f629fa310251e0516ac59fb5429b9e83d7fa Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Feb 2022 22:33:16 +0100
Subject: nat: T1083: use defaultValue from XML when handling translations

---
 interface-definitions/include/nat-translation-options.xml.i | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/interface-definitions/include/nat-translation-options.xml.i b/interface-definitions/include/nat-translation-options.xml.i
index f1539757b..925f90106 100644
--- a/interface-definitions/include/nat-translation-options.xml.i
+++ b/interface-definitions/include/nat-translation-options.xml.i
@@ -22,7 +22,8 @@
           <regex>^(persistent|random)$</regex>
         </constraint>
       </properties>
-    </leafNode> 
+      <defaultValue>random</defaultValue>
+    </leafNode>
     <leafNode name="port-mapping">
       <properties>
         <help>Port mapping options</help>
@@ -45,7 +46,8 @@
           <regex>^(random|fully-random|none)$</regex>
         </constraint>
       </properties>
-    </leafNode> 
+      <defaultValue>none</defaultValue>
+    </leafNode>
   </children>
 </node>
 <!-- include end -->
-- 
cgit v1.2.3


From 291558023bbd1bdbef6e00c4eec173cf5c9575d8 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 26 Feb 2022 16:30:10 +0100
Subject: lldp: T4272: migrate to get_config_dict()

---
 data/templates/lldp/lldpd.tmpl     |   3 +-
 data/templates/lldp/vyos.conf.tmpl |  35 +++---
 interface-definitions/lldp.xml.in  |  22 ++--
 src/conf_mode/lldp.py              | 235 ++++++++++---------------------------
 4 files changed, 94 insertions(+), 201 deletions(-)

diff --git a/data/templates/lldp/lldpd.tmpl b/data/templates/lldp/lldpd.tmpl
index 3db955b48..819e70c84 100644
--- a/data/templates/lldp/lldpd.tmpl
+++ b/data/templates/lldp/lldpd.tmpl
@@ -1,3 +1,2 @@
 ### Autogenerated by lldp.py ###
-DAEMON_ARGS="-M 4{% if options.snmp %} -x{% endif %}{% if options.cdp %} -c{% endif %}{% if options.edp %} -e{% endif %}{% if options.fdp %} -f{% endif %}{% if options.sonmp %} -s{% endif %}"
-
+DAEMON_ARGS="-M 4{% if snmp is defined and snmp.enable is defined %} -x{% endif %}{% if legacy_protocols is defined and legacy_protocols.cdp is defined %} -c{% endif %}{% if legacy_protocols is defined and legacy_protocols.edp is defined %} -e{% endif %}{% if legacy_protocols is defined and legacy_protocols.fdp is defined %} -f{% endif %}{% if legacy_protocols is defined and legacy_protocols.sonmp is defined %} -s{% endif %}"
diff --git a/data/templates/lldp/vyos.conf.tmpl b/data/templates/lldp/vyos.conf.tmpl
index 07bbaf604..592dcf61f 100644
--- a/data/templates/lldp/vyos.conf.tmpl
+++ b/data/templates/lldp/vyos.conf.tmpl
@@ -1,20 +1,25 @@
 ### Autogenerated by lldp.py ###
 
 configure system platform VyOS
-configure system description "VyOS {{ options.description }}"
-{% if options.listen_on %}
-configure system interface pattern "{{ ( options.listen_on | select('equalto','all') | map('replace','all','*') | list + options.listen_on | select('equalto','!all') | map('replace','!all','!*') | list + options.listen_on | reject('equalto','all') | reject('equalto','!all') | list ) | unique | join(",") }}"
+configure system description "VyOS {{ version }}"
+{% if interface is defined and interface is not none %}
+{%   set tmp = [] %}
+{%   for iface, iface_options in interface.items() if not iface_options.disable %}
+{%     if iface == 'all' %}
+{%       set iface = '*' %}
+{%     endif %}
+{%     set _ = tmp.append(iface) %}
+{%     if iface_options.location is defined and iface_options.location is not none %}
+{%       if iface_options.location.elin is defined and iface_options.location.elin is not none %}
+configure ports {{ iface }} med location elin "{{ iface_options.location.elin }}"
+{%       endif %}
+{%       if iface_options.location is defined and iface_options.location.coordinate_based is not none and iface_options.location.coordinate_based is not none %}
+configure ports {{ iface }} med location coordinate latitude "{{ iface_options.location.coordinate_based.latitude }}" longitude "{{ iface_options.location.coordinate_based.longitude }}" altitude "{{ iface_options.location.coordinate_based.altitude }}m" datum "{{ iface_options.location.coordinate_based.datum }}"
+{%       endif %}
+{%     endif %}
+{%   endfor %}
+configure system interface pattern "{{ tmp | join(",") }}"
 {% endif %}
-{% if options.mgmt_addr %}
-configure system ip management pattern {{ options.mgmt_addr | join(",") }}
+{% if management_address is defined and management_address is not none %}
+configure system ip management pattern {{ management_address | join(",") }}
 {% endif %}
-{% for loc in location %}
-{% if loc.elin %}
-configure ports {{ loc.name }} med location elin "{{ loc.elin }}"
-{% endif %}
-{% if loc.coordinate_based %}
-configure ports {{ loc.name }} med location coordinate {% if loc.coordinate_based.latitude %}latitude {{ loc.coordinate_based.latitude }}{% endif %} {% if loc.coordinate_based.longitude %}longitude {{ loc.coordinate_based.longitude }}{% endif %} {% if loc.coordinate_based.altitude %}altitude {{ loc.coordinate_based.altitude }} m{% endif %} {% if loc.coordinate_based.datum %}datum {{ loc.coordinate_based.datum }}{% endif %}
-{% endif %}
-
-
-{% endfor %}
diff --git a/interface-definitions/lldp.xml.in b/interface-definitions/lldp.xml.in
index 32ef0ad14..b9ffe234c 100644
--- a/interface-definitions/lldp.xml.in
+++ b/interface-definitions/lldp.xml.in
@@ -28,7 +28,7 @@
               #include <include/generic-disable-node.xml.i>
               <node name="location">
                 <properties>
-                  <help>LLDP-MED location data [REQUIRED]</help>
+                  <help>LLDP-MED location data</help>
                 </properties>
                 <children>
                   <node name="coordinate-based">
@@ -39,6 +39,10 @@
                       <leafNode name="altitude">
                         <properties>
                           <help>Altitude in meters</help>
+                          <valueHelp>
+                            <format>0</format>
+                            <description>No altitude</description>
+                          </valueHelp>
                           <valueHelp>
                             <format>[+-]&lt;meters&gt;</format>
                             <description>Altitude in meters</description>
@@ -48,13 +52,14 @@
                             <validator name="numeric"/>
                           </constraint>
                         </properties>
+                        <defaultValue>0</defaultValue>
                       </leafNode>
                       <leafNode name="datum">
                         <properties>
                           <help>Coordinate datum type</help>
                           <valueHelp>
                             <format>WGS84</format>
-                            <description>WGS84 (default)</description>
+                            <description>WGS84</description>
                           </valueHelp>
                           <valueHelp>
                             <format>NAD83</format>
@@ -69,33 +74,34 @@
                           </completionHelp>
                           <constraintErrorMessage>Datum should be WGS84, NAD83, or MLLW</constraintErrorMessage>
                           <constraint>
-                            <regex>^(WGS84|NAD83|MLLW)$</regex>
+                            <regex>(WGS84|NAD83|MLLW)</regex>
                           </constraint>
                         </properties>
+                        <defaultValue>WGS84</defaultValue>
                       </leafNode>
                       <leafNode name="latitude">
                         <properties>
-                          <help>Latitude [REQUIRED]</help>
+                          <help>Latitude</help>
                           <valueHelp>
                             <format>&lt;latitude&gt;</format>
                             <description>Latitude (example "37.524449N")</description>
                           </valueHelp>
                           <constraintErrorMessage>Latitude should be a number followed by S or N</constraintErrorMessage>
                           <constraint>
-                            <regex>(\d+)(\.\d+)?[nNsS]$</regex>
+                            <regex>(\d+)(\.\d+)?[nNsS]</regex>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="longitude">
                         <properties>
-                          <help>Longitude [REQUIRED]</help>
+                          <help>Longitude</help>
                           <valueHelp>
                             <format>&lt;longitude&gt;</format>
                             <description>Longitude (example "122.267255W")</description>
                           </valueHelp>
                           <constraintErrorMessage>Longiture should be a number followed by E or W</constraintErrorMessage>
                           <constraint>
-                            <regex>(\d+)(\.\d+)?[eEwW]$</regex>
+                            <regex>(\d+)(\.\d+)?[eEwW]</regex>
                           </constraint>
                         </properties>
                       </leafNode>
@@ -109,7 +115,7 @@
                         <description>Emergency Call Service ELIN number (between 10-25 numbers)</description>
                       </valueHelp>
                       <constraint>
-                        <regex>[0-9]{10,25}$</regex>
+                        <regex>[0-9]{10,25}</regex>
                       </constraint>
                       <constraintErrorMessage>ELIN number must be between 10-25 numbers</constraintErrorMessage>
                     </properties>
diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py
index 082c3e128..db8328259 100755
--- a/src/conf_mode/lldp.py
+++ b/src/conf_mode/lldp.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2017-2020 VyOS maintainers and contributors
+# Copyright (C) 2017-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -15,19 +15,19 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
-import re
 
-from copy import deepcopy
 from sys import exit
 
 from vyos.config import Config
+from vyos.configdict import dict_merge
 from vyos.validate import is_addr_assigned
 from vyos.validate import is_loopback_addr
 from vyos.version import get_version_data
-from vyos import ConfigError
 from vyos.util import call
+from vyos.util import dict_search
+from vyos.xml import defaults
 from vyos.template import render
-
+from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
@@ -35,178 +35,73 @@ config_file = "/etc/default/lldpd"
 vyos_config_file = "/etc/lldpd.d/01-vyos.conf"
 base = ['service', 'lldp']
 
-default_config_data = {
-    "options": '',
-    "interface_list": '',
-    "location": ''
-}
-
-def get_options(config):
-    options = {}
-    config.set_level(base)
-
-    options['listen_vlan'] = config.exists('listen-vlan')
-    options['mgmt_addr'] = []
-    for addr in config.return_values('management-address'):
-        if is_addr_assigned(addr) and not is_loopback_addr(addr):
-            options['mgmt_addr'].append(addr)
-        else:
-            message = 'WARNING: LLDP management address {0} invalid - '.format(addr)
-            if is_loopback_addr(addr):
-                message += '(loopback address).'
-            else:
-                message += 'address not found.'
-            print(message)
-
-    snmp = config.exists('snmp enable')
-    options["snmp"] = snmp
-    if snmp:
-        config.set_level('')
-        options["sys_snmp"] = config.exists('service snmp')
-        config.set_level(base)
-
-    config.set_level(base + ['legacy-protocols'])
-    options['cdp'] = config.exists('cdp')
-    options['edp'] = config.exists('edp')
-    options['fdp'] = config.exists('fdp')
-    options['sonmp'] = config.exists('sonmp')
-
-    # start with an unknown version information
-    version_data = get_version_data()
-    options['description'] = version_data['version']
-    options['listen_on'] = []
-
-    return options
-
-def get_interface_list(config):
-    config.set_level(base)
-    intfs_names = config.list_nodes(['interface'])
-    if len(intfs_names) < 0:
-        return 0
-
-    interface_list = []
-    for name in intfs_names:
-        config.set_level(base + ['interface', name])
-        disable = config.exists(['disable'])
-        intf = {
-            'name': name,
-            'disable': disable
-        }
-        interface_list.append(intf)
-    return interface_list
-
-
-def get_location_intf(config, name):
-    path = base + ['interface', name]
-    config.set_level(path)
-
-    config.set_level(path + ['location'])
-    elin = ''
-    coordinate_based = {}
-
-    if config.exists('elin'):
-        elin = config.return_value('elin')
-
-    if config.exists('coordinate-based'):
-        config.set_level(path + ['location', 'coordinate-based'])
-
-        coordinate_based['latitude'] = config.return_value(['latitude'])
-        coordinate_based['longitude'] = config.return_value(['longitude'])
-
-        coordinate_based['altitude'] = '0'
-        if config.exists(['altitude']):
-            coordinate_based['altitude'] = config.return_value(['altitude'])
-
-        coordinate_based['datum'] = 'WGS84'
-        if config.exists(['datum']):
-            coordinate_based['datum'] = config.return_value(['datum'])
-
-    intf = {
-        'name': name,
-        'elin': elin,
-        'coordinate_based': coordinate_based
-
-    }
-    return intf
-
-
-def get_location(config):
-    config.set_level(base)
-    intfs_names = config.list_nodes(['interface'])
-    if len(intfs_names) < 0:
-        return 0
-
-    if config.exists('disable'):
-        return 0
-
-    intfs_location = []
-    for name in intfs_names:
-        intf = get_location_intf(config, name)
-        intfs_location.append(intf)
-
-    return intfs_location
-
-
 def get_config(config=None):
-    lldp = deepcopy(default_config_data)
     if config:
         conf = config
     else:
         conf = Config()
+
     if not conf.exists(base):
-        return None
-    else:
-        lldp['options'] = get_options(conf)
-        lldp['interface_list'] = get_interface_list(conf)
-        lldp['location'] = get_location(conf)
+        return {}
 
-        return lldp
+    lldp = conf.get_config_dict(base, key_mangling=('-', '_'),
+                                get_first_key=True, no_tag_node_value_mangle=True)
 
+    if conf.exists(['service', 'snmp']):
+        lldp['system_snmp_enabled'] = ''
+
+    version_data = get_version_data()
+    lldp['version'] = version_data['version']
+
+    # We have gathered the dict representation of the CLI, but there are default
+    # options which we need to update into the dictionary retrived.
+    # location coordinates have a default value
+    if 'interface' in lldp:
+        for interface, interface_config in lldp['interface'].items():
+            default_values = defaults(base + ['interface'])
+            if dict_search('location.coordinate_based', interface_config) == None:
+                # no location specified - no need to add defaults
+                del default_values['location']['coordinate_based']['datum']
+                del default_values['location']['coordinate_based']['altitude']
+
+            # cleanup default_values dictionary from inner to outer
+            # this might feel overkill here, but it does support easy extension
+            # in the future with additional default values
+            if len(default_values['location']['coordinate_based']) == 0:
+                del default_values['location']['coordinate_based']
+            if len(default_values['location']) == 0:
+                del default_values['location']
+
+            lldp['interface'][interface] = dict_merge(default_values,
+                                                   lldp['interface'][interface])
+
+    return lldp
 
 def verify(lldp):
     # bail out early - looks like removal from running config
     if lldp is None:
         return
 
-    # check location
-    for location in lldp['location']:
-        # check coordinate-based
-        if len(location['coordinate_based']) > 0:
-            # check longitude and latitude
-            if not location['coordinate_based']['longitude']:
-                raise ConfigError('Must define longitude for interface {0}'.format(location['name']))
-
-            if not location['coordinate_based']['latitude']:
-                raise ConfigError('Must define latitude for interface {0}'.format(location['name']))
-
-            if not re.match(r'^(\d+)(\.\d+)?[nNsS]$', location['coordinate_based']['latitude']):
-                raise ConfigError('Invalid location for interface {0}:\n' \
-                                  'latitude should be a number followed by S or N'.format(location['name']))
-
-            if not re.match(r'^(\d+)(\.\d+)?[eEwW]$', location['coordinate_based']['longitude']):
-                raise ConfigError('Invalid location for interface {0}:\n' \
-                                  'longitude should be a number followed by E or W'.format(location['name']))
-
-            # check altitude and datum if exist
-            if location['coordinate_based']['altitude']:
-                if not re.match(r'^[-+0-9\.]+$', location['coordinate_based']['altitude']):
-                    raise ConfigError('Invalid location for interface {0}:\n' \
-                                      'altitude should be a positive or negative number'.format(location['name']))
-
-            if location['coordinate_based']['datum']:
-                if not re.match(r'^(WGS84|NAD83|MLLW)$', location['coordinate_based']['datum']):
-                    raise ConfigError("Invalid location for interface {0}:\n' \
-                                      'datum should be WGS84, NAD83, or MLLW".format(location['name']))
-
-        # check elin
-        elif location['elin']:
-            if not re.match(r'^[0-9]{10,25}$', location['elin']):
-                raise ConfigError('Invalid location for interface {0}:\n' \
-                                  'ELIN number must be between 10-25 numbers'.format(location['name']))
+    if 'management_address' in lldp:
+        for address in lldp['management_address']:
+            message = f'WARNING: LLDP management address "{address}" is invalid'
+            if is_loopback_addr(address):
+                print(f'{message} - loopback address')
+            elif not is_addr_assigned(address):
+                print(f'{message} - not assigned to any interface')
+
+    if 'interface' in lldp:
+        for interface, interface_config in lldp['interface'].items():
+            # bail out early if no location info present in interface config
+            if 'location' not in interface_config:
+                continue
+            if 'coordinate_based' in interface_config['location']:
+                if not {'latitude', 'latitude'} <= set(interface_config['location']['coordinate_based']):
+                    raise ConfigError(f'Must define both longitude and latitude for "{interface}" location!')
 
     # check options
-    if lldp['options']['snmp']:
-        if not lldp['options']['sys_snmp']:
+    if 'snmp' in lldp and 'enable' in lldp['snmp']:
+        if 'system_snmp_enabled' not in lldp:
             raise ConfigError('SNMP must be configured to enable LLDP SNMP')
 
 
@@ -215,29 +110,17 @@ def generate(lldp):
     if lldp is None:
         return
 
-    # generate listen on interfaces
-    for intf in lldp['interface_list']:
-        tmp = ''
-        # add exclamation mark if interface is disabled
-        if intf['disable']:
-            tmp = '!'
-
-        tmp += intf['name']
-        lldp['options']['listen_on'].append(tmp)
-
-    # generate /etc/default/lldpd
     render(config_file, 'lldp/lldpd.tmpl', lldp)
-    # generate /etc/lldpd.d/01-vyos.conf
     render(vyos_config_file, 'lldp/vyos.conf.tmpl', lldp)
 
-
 def apply(lldp):
+    systemd_service = 'lldpd.service'
     if lldp:
         # start/restart lldp service
-        call('systemctl restart lldpd.service')
+        call(f'systemctl restart {systemd_service}')
     else:
         # LLDP service has been terminated
-        call('systemctl stop lldpd.service')
+        call(f'systemctl stop {systemd_service}')
         if os.path.isfile(config_file):
             os.unlink(config_file)
         if os.path.isfile(vyos_config_file):
-- 
cgit v1.2.3


From 3b72f115807d84a08000d8ddbbadb521e671e697 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 26 Feb 2022 22:38:52 +0100
Subject: smoketest: lldp: add testcase

(cherry picked from commit 2fd5eea801bb524c12217c26d98c44a819b2086e)
---
 smoketest/scripts/cli/test_service_lldp.py | 127 +++++++++++++++++++++++++++++
 1 file changed, 127 insertions(+)
 create mode 100755 smoketest/scripts/cli/test_service_lldp.py

diff --git a/smoketest/scripts/cli/test_service_lldp.py b/smoketest/scripts/cli/test_service_lldp.py
new file mode 100755
index 000000000..64fdd9d1b
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_lldp.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import os
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.util import cmd
+from vyos.util import process_named_running
+from vyos.util import read_file
+from vyos.version import get_version_data
+
+PROCESS_NAME = 'lldpd'
+LLDPD_CONF = '/etc/lldpd.d/01-vyos.conf'
+base_path = ['service', 'lldp']
+mgmt_if = 'dum83513'
+mgmt_addr = ['1.2.3.4', '1.2.3.5']
+
+class TestServiceLLDP(VyOSUnitTestSHIM.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        # call base-classes classmethod
+        super(cls, cls).setUpClass()
+
+        # create a test interfaces
+        for addr in mgmt_addr:
+            cls.cli_set(cls, ['interfaces', 'dummy', mgmt_if, 'address', addr + '/32'])
+
+        # ensure we can also run this test on a live system - so lets clean
+        # out the current configuration :)
+        cls.cli_delete(cls, base_path)
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.cli_delete(cls, ['interfaces', 'dummy', mgmt_if])
+        super().tearDownClass()
+
+    def tearDown(self):
+        # service must be running after it was configured
+        self.assertTrue(process_named_running(PROCESS_NAME))
+
+        # delete/stop LLDP service
+        self.cli_delete(base_path)
+        self.cli_commit()
+
+        # service is no longer allowed to run after it was removed
+        self.assertFalse(process_named_running(PROCESS_NAME))
+
+    def test_01_lldp_basic(self):
+        self.cli_set(base_path)
+        self.cli_commit()
+
+        config = read_file(LLDPD_CONF)
+        version_data = get_version_data()
+        version = version_data['version']
+        self.assertIn(f'configure system platform VyOS', config)
+        self.assertIn(f'configure system description "VyOS {version}"', config)
+
+    def test_02_lldp_mgmt_address(self):
+        for addr in mgmt_addr:
+            self.cli_set(base_path + ['management-address', addr])
+        self.cli_commit()
+
+        config = read_file(LLDPD_CONF)
+        self.assertIn(f'configure system ip management pattern {",".join(mgmt_addr)}', config)
+
+    def test_03_lldp_interfaces(self):
+        for interface in Section.interfaces('ethernet'):
+            if not '.' in interface:
+                self.cli_set(base_path + ['interface', interface])
+
+        # commit changes
+        self.cli_commit()
+
+        # verify configuration
+        config = read_file(LLDPD_CONF)
+
+        interface_list = []
+        for interface in Section.interfaces('ethernet'):
+            if not '.' in interface:
+                interface_list.append(interface)
+        tmp = ','.join(interface_list)
+        self.assertIn(f'configure system interface pattern "{tmp}"', config)
+
+    def test_04_lldp_all_interfaces(self):
+        self.cli_set(base_path + ['interface', 'all'])
+        # commit changes
+        self.cli_commit()
+
+        # verify configuration
+        config = read_file(LLDPD_CONF)
+        self.assertIn(f'configure system interface pattern "*"', config)
+
+    def test_05_lldp_location(self):
+        interface = 'eth0'
+        elin = '1234567890'
+        self.cli_set(base_path + ['interface', interface, 'location', 'elin', elin])
+
+        # commit changes
+        self.cli_commit()
+
+        # verify configuration
+        config = read_file(LLDPD_CONF)
+
+        self.assertIn(f'configure ports {interface} med location elin "{elin}"', config)
+        self.assertIn(f'configure system interface pattern "{interface}"', config)
+
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
-- 
cgit v1.2.3


From 17602c2d63aacc972d4e2f6f21aeeded243d4fa1 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 26 Feb 2022 23:06:37 +0100
Subject: lldp: T4272: minor bugfix in Jinja2 template for location

---
 data/templates/lldp/vyos.conf.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/data/templates/lldp/vyos.conf.tmpl b/data/templates/lldp/vyos.conf.tmpl
index 592dcf61f..14395a223 100644
--- a/data/templates/lldp/vyos.conf.tmpl
+++ b/data/templates/lldp/vyos.conf.tmpl
@@ -13,7 +13,7 @@ configure system description "VyOS {{ version }}"
 {%       if iface_options.location.elin is defined and iface_options.location.elin is not none %}
 configure ports {{ iface }} med location elin "{{ iface_options.location.elin }}"
 {%       endif %}
-{%       if iface_options.location is defined and iface_options.location.coordinate_based is not none and iface_options.location.coordinate_based is not none %}
+{%       if iface_options.location is defined and iface_options.location.coordinate_based is defined and iface_options.location.coordinate_based is not none %}
 configure ports {{ iface }} med location coordinate latitude "{{ iface_options.location.coordinate_based.latitude }}" longitude "{{ iface_options.location.coordinate_based.longitude }}" altitude "{{ iface_options.location.coordinate_based.altitude }}m" datum "{{ iface_options.location.coordinate_based.datum }}"
 {%       endif %}
 {%     endif %}
-- 
cgit v1.2.3


From 61fa1c95164e4222e79b078b1a796f41397e0ee3 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Mon, 28 Feb 2022 14:28:55 +0100
Subject: ssh: T4273: bugfix cipher and key-exchange multi nodes

After hardning the regex validator to be preceeded with ^ and ending with $
it was no longer possible to have a comma separated list as SSH ciphers. The
migrations cript is altered to migrate the previous comma separated list
to individual multi node entries - cipher and key-exchange always had been
multinodes - so this just re-arranges some values and does not break CLI
compatibility
---
 interface-definitions/ssh.xml.in |  8 ++--
 smoketest/configs/basic-vyos     | 88 ++++++++++++++++++++++++++++++++++++++++
 src/migration-scripts/ssh/1-to-2 | 50 +++++++++++++++++------
 3 files changed, 130 insertions(+), 16 deletions(-)
 create mode 100644 smoketest/configs/basic-vyos

diff --git a/interface-definitions/ssh.xml.in b/interface-definitions/ssh.xml.in
index 187e5f8e8..8edbad110 100644
--- a/interface-definitions/ssh.xml.in
+++ b/interface-definitions/ssh.xml.in
@@ -44,7 +44,7 @@
                 <list>3des-cbc aes128-cbc aes192-cbc aes256-cbc rijndael-cbc@lysator.liu.se aes128-ctr aes192-ctr aes256-ctr aes128-gcm@openssh.com aes256-gcm@openssh.com chacha20-poly1305@openssh.com</list>
               </completionHelp>
                 <constraint>
-                  <regex>^(3des-cbc|aes128-cbc|aes192-cbc|aes256-cbc|rijndael-cbc@lysator.liu.se|aes128-ctr|aes192-ctr|aes256-ctr|aes128-gcm@openssh.com|aes256-gcm@openssh.com|chacha20-poly1305@openssh.com)$</regex>
+                  <regex>(3des-cbc|aes128-cbc|aes192-cbc|aes256-cbc|rijndael-cbc@lysator.liu.se|aes128-ctr|aes192-ctr|aes256-ctr|aes128-gcm@openssh.com|aes256-gcm@openssh.com|chacha20-poly1305@openssh.com)</regex>
                 </constraint>
               <multi/>
             </properties>
@@ -70,7 +70,7 @@
               </completionHelp>
               <multi/>
               <constraint>
-                <regex>^(diffie-hellman-group1-sha1|diffie-hellman-group14-sha1|diffie-hellman-group14-sha256|diffie-hellman-group16-sha512|diffie-hellman-group18-sha512|diffie-hellman-group-exchange-sha1|diffie-hellman-group-exchange-sha256|ecdh-sha2-nistp256|ecdh-sha2-nistp384|ecdh-sha2-nistp521|curve25519-sha256|curve25519-sha256@libssh.org)$</regex>
+                <regex>(diffie-hellman-group1-sha1|diffie-hellman-group14-sha1|diffie-hellman-group14-sha256|diffie-hellman-group16-sha512|diffie-hellman-group18-sha512|diffie-hellman-group-exchange-sha1|diffie-hellman-group-exchange-sha256|ecdh-sha2-nistp256|ecdh-sha2-nistp384|ecdh-sha2-nistp521|curve25519-sha256|curve25519-sha256@libssh.org)</regex>
               </constraint>
             </properties>
           </leafNode>
@@ -102,7 +102,7 @@
                 <description>enable logging of failed login attempts</description>
               </valueHelp>
               <constraint>
-                <regex>^(quiet|fatal|error|info|verbose)$</regex>
+                <regex>(quiet|fatal|error|info|verbose)</regex>
               </constraint>
             </properties>
             <defaultValue>info</defaultValue>
@@ -115,7 +115,7 @@
                 <list>hmac-sha1 hmac-sha1-96 hmac-sha2-256 hmac-sha2-512 hmac-md5 hmac-md5-96 umac-64@openssh.com umac-128@openssh.com hmac-sha1-etm@openssh.com hmac-sha1-96-etm@openssh.com hmac-sha2-256-etm@openssh.com hmac-sha2-512-etm@openssh.com hmac-md5-etm@openssh.com hmac-md5-96-etm@openssh.com umac-64-etm@openssh.com umac-128-etm@openssh.com</list>
               </completionHelp>
               <constraint>
-                <regex>^(hmac-sha1|hmac-sha1-96|hmac-sha2-256|hmac-sha2-512|hmac-md5|hmac-md5-96|umac-64@openssh.com|umac-128@openssh.com|hmac-sha1-etm@openssh.com|hmac-sha1-96-etm@openssh.com|hmac-sha2-256-etm@openssh.com|hmac-sha2-512-etm@openssh.com|hmac-md5-etm@openssh.com|hmac-md5-96-etm@openssh.com|umac-64-etm@openssh.com|umac-128-etm@openssh.com)$</regex>
+                <regex>(hmac-sha1|hmac-sha1-96|hmac-sha2-256|hmac-sha2-512|hmac-md5|hmac-md5-96|umac-64@openssh.com|umac-128@openssh.com|hmac-sha1-etm@openssh.com|hmac-sha1-96-etm@openssh.com|hmac-sha2-256-etm@openssh.com|hmac-sha2-512-etm@openssh.com|hmac-md5-etm@openssh.com|hmac-md5-96-etm@openssh.com|umac-64-etm@openssh.com|umac-128-etm@openssh.com)</regex>
               </constraint>
               <multi/>
             </properties>
diff --git a/smoketest/configs/basic-vyos b/smoketest/configs/basic-vyos
new file mode 100644
index 000000000..493feed5b
--- /dev/null
+++ b/smoketest/configs/basic-vyos
@@ -0,0 +1,88 @@
+interfaces {
+    ethernet eth0 {
+        address 192.168.0.1/24
+        duplex auto
+        smp-affinity auto
+        speed auto
+    }
+    ethernet eth1 {
+        address 100.64.0.0/31
+        duplex auto
+        smp-affinity auto
+        speed auto
+    }
+    loopback lo {
+    }
+}
+protocols {
+    static {
+        route 0.0.0.0/0 {
+            next-hop 100.64.0.1 {
+            }
+        }
+    }
+}
+service {
+    dhcp-server {
+        shared-network-name LAN {
+            authoritative
+            subnet 192.168.0.0/24 {
+                default-router 192.168.0.1
+                dns-server 192.168.0.1
+                domain-name vyos.net
+                domain-search vyos.net
+                range LANDynamic {
+                    start 192.168.0.20
+                    stop 192.168.0.240
+                }
+            }
+        }
+    }
+    dns {
+        forwarding {
+            allow-from 192.168.0.0/16
+            cache-size 10000
+            dnssec off
+            listen-address 192.168.0.1
+        }
+    }
+    ssh {
+        ciphers aes128-ctr,aes192-ctr,aes256-ctr
+        ciphers chacha20-poly1305@openssh.com,rijndael-cbc@lysator.liu.se
+        listen-address 192.168.0.1
+        key-exchange curve25519-sha256@libssh.org
+        key-exchange diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group-exchange-sha256
+        port 22
+    }
+}
+system {
+    config-management {
+        commit-revisions 100
+    }
+    console {
+        device ttyS0 {
+            speed 115200
+        }
+    }
+    host-name vyos
+    login {
+        user vyos {
+            authentication {
+                encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0
+                plaintext-password ""
+            }
+        }
+    }
+    name-server 192.168.0.1
+    syslog {
+        global {
+            facility all {
+                level info
+            }
+        }
+    }
+    time-zone Europe/Berlin
+}
+/* Warning: Do not remove the following line. */
+/* === vyatta-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@2:dhcp-server@5:dns-forwarding@1:firewall@5:ipsec@5:l2tp@1:mdns@1:nat@4:ntp@1:pptp@1:qos@1:quagga@6:snmp@1:ssh@1:system@9:vrrp@2:wanloadbalance@3:webgui@1:webproxy@1:webproxy@2:zone-policy@1" === */
+/* Release version: 1.2.6 */
diff --git a/src/migration-scripts/ssh/1-to-2 b/src/migration-scripts/ssh/1-to-2
index bc8815753..31c40df16 100755
--- a/src/migration-scripts/ssh/1-to-2
+++ b/src/migration-scripts/ssh/1-to-2
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -30,26 +30,52 @@ file_name = argv[1]
 with open(file_name, 'r') as f:
     config_file = f.read()
 
-base = ['service', 'ssh', 'loglevel']
+base = ['service', 'ssh']
 config = ConfigTree(config_file)
 
 if not config.exists(base):
     # Nothing to do
     exit(0)
-else:
-    # red in configured loglevel and convert it to lower case
-    tmp = config.return_value(base).lower()
 
+path_loglevel = base + ['loglevel']
+if config.exists(path_loglevel):
+    # red in configured loglevel and convert it to lower case
+    tmp = config.return_value(path_loglevel).lower()
     # VyOS 1.2 had no proper value validation on the CLI thus the
     # user could use any arbitrary values - sanitize them
     if tmp not in ['quiet', 'fatal', 'error', 'info', 'verbose']:
         tmp = 'info'
+    config.set(path_loglevel, value=tmp)
+
+# T4273: migrate ssh cipher list to multi node
+path_ciphers = base + ['ciphers']
+if config.exists(path_ciphers):
+    tmp = []
+    # get curtrent cipher list - comma delimited
+    for cipher in config.return_values(path_ciphers):
+        tmp.extend(cipher.split(','))
+    # delete old cipher suite representation
+    config.delete(path_ciphers)
 
-    config.set(base, value=tmp)
+    for cipher in tmp:
+        config.set(path_ciphers, value=cipher, replace=False)
 
-    try:
-        with open(file_name, 'w') as f:
-            f.write(config.to_string())
-    except OSError as e:
-        print("Failed to save the modified config: {}".format(e))
-        exit(1)
+# T4273: migrate ssh key-exchange list to multi node
+path_kex = base + ['key-exchange']
+if config.exists(path_kex):
+    tmp = []
+    # get curtrent cipher list - comma delimited
+    for kex in config.return_values(path_kex):
+        tmp.extend(kex.split(','))
+    # delete old cipher suite representation
+    config.delete(path_kex)
+
+    for kex in tmp:
+        config.set(path_kex, value=kex, replace=False)
+
+try:
+    with open(file_name, 'w') as f:
+        f.write(config.to_string())
+except OSError as e:
+    print("Failed to save the modified config: {}".format(e))
+    exit(1)
-- 
cgit v1.2.3


From 257345cd152c23a465332dea4af034244007aaa7 Mon Sep 17 00:00:00 2001
From: RageLtMan <sempervictus@users.noreply.github.com>
Date: Mon, 28 Feb 2022 08:32:30 -0500
Subject: open-connect: T4274: extend RADIUS authentication timeout

RADIUS authentication can be handled by a variety of mechanisms,
including proxy for 2FA systems requiring user interaction with a
separate device, token acquisition, or other time-consuming action.

Given the delays required for certain 2FA implementations, a thirty
second timeout can range from onerous to untenable. Accomodate the
2FA time requirements by extending the hard-coded RADIUS time limit
from 30 seconds to 240.

Co-authored-by: RageLtMan <rageltman [at] sempervictus>
---
 interface-definitions/vpn_openconnect.xml.in | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/interface-definitions/vpn_openconnect.xml.in b/interface-definitions/vpn_openconnect.xml.in
index 3fc34bacc..f418f5d75 100644
--- a/interface-definitions/vpn_openconnect.xml.in
+++ b/interface-definitions/vpn_openconnect.xml.in
@@ -40,13 +40,13 @@
                     <properties>
                       <help>Session timeout</help>
                       <valueHelp>
-                        <format>u32:1-30</format>
-                        <description>Session timeout in seconds</description>
+                        <format>u32:1-240</format>
+                        <description>Session timeout in seconds (default: 2)</description>
                       </valueHelp>
                       <constraint>
-                        <validator name="numeric" argument="--range 1-30"/>
+                        <validator name="numeric" argument="--range 1-240"/>
                       </constraint>
-                      <constraintErrorMessage>Timeout must be between 1 and 30 seconds</constraintErrorMessage>
+                      <constraintErrorMessage>Timeout must be between 1 and 240 seconds</constraintErrorMessage>
                     </properties>
                     <defaultValue>2</defaultValue>
                   </leafNode>
-- 
cgit v1.2.3


From a37f0db280ffc662e1b51ec2ae479ff6318b0b3c Mon Sep 17 00:00:00 2001
From: srividya0208 <a.srividya@vyos.io>
Date: Mon, 28 Feb 2022 12:04:31 -0500
Subject: ipsec prefix: T4275: Incorrect val_help for local/remote prefix

It accepts network as the input value but the completion help is showing
ip address
---
 interface-definitions/vpn_ipsec.xml.in | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index 885bac979..0ad69c637 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -879,11 +879,11 @@
                     <properties>
                       <help>Local IPv4 or IPv6 pool prefix exclusions</help>
                       <valueHelp>
-                        <format>ipv4</format>
+                        <format>ipv4net</format>
                         <description>Local IPv4 pool prefix exclusion</description>
                       </valueHelp>
                       <valueHelp>
-                        <format>ipv6</format>
+                        <format>ipv6net</format>
                         <description>Local IPv6 pool prefix exclusion</description>
                       </valueHelp>
                       <constraint>
-- 
cgit v1.2.3


From ff7e2cd622cf3679cd9265b2cb766395a1830f50 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Fri, 25 Feb 2022 16:05:03 -0600
Subject: configtree: T4235: add utility get_subtree

---
 python/vyos/configtree.py | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 866f24e47..0ec15cf40 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -126,6 +126,10 @@ class ConfigTree(object):
         self.__set_tag.argtypes = [c_void_p, c_char_p]
         self.__set_tag.restype = c_int
 
+        self.__get_subtree = self.__lib.get_subtree
+        self.__get_subtree.argtypes = [c_void_p, c_char_p]
+        self.__get_subtree.restype = c_void_p
+
         self.__destroy = self.__lib.destroy
         self.__destroy.argtypes = [c_void_p]
 
@@ -291,6 +295,14 @@ class ConfigTree(object):
         else:
             raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
 
+    def get_subtree(self, path, with_node=False):
+        check_path(path)
+        path_str = " ".join(map(str, path)).encode()
+
+        res = self.__get_subtree(self.__config, path_str, with_node)
+        subt = ConfigTree(address=res)
+        return subt
+
 class Diff:
     def __init__(self, left, right, path=[], libpath=LIBPATH):
         if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)):
-- 
cgit v1.2.3


From 3d28528ff84b4e874faf80028709bd08b2956933 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Fri, 25 Feb 2022 16:10:12 -0600
Subject: configtree: T4235: simplification of diff_tree class

The return value of diff_tree is now a single config_tree, with initial
children of names: ["add", "delete", "inter"] containing the config
sub-trees of added paths; deleted paths; and intersection, respectively.
The simplifies dumping to json, and checking existence of paths, hence,
of node changes.
---
 python/vyos/configtree.py | 31 +++++++++++++++++++------------
 1 file changed, 19 insertions(+), 12 deletions(-)

diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 0ec15cf40..f5e697137 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -15,7 +15,7 @@
 import re
 import json
 
-from ctypes import cdll, c_char_p, c_void_p, c_int, POINTER
+from ctypes import cdll, c_char_p, c_void_p, c_int
 
 LIBPATH = '/usr/lib/libvyosconfig.so.0'
 
@@ -303,7 +303,7 @@ class ConfigTree(object):
         subt = ConfigTree(address=res)
         return subt
 
-class Diff:
+class DiffTree:
     def __init__(self, left, right, path=[], libpath=LIBPATH):
         if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)):
             raise TypeError("Arguments must be instances of ConfigTree")
@@ -312,21 +312,28 @@ class Diff:
                 raise ConfigTreeError(f"Path {path} doesn't exist in lhs tree")
             if not right.exists(path):
                 raise ConfigTreeError(f"Path {path} doesn't exist in rhs tree")
+
         self.left = left
         self.right = right
 
+        self.__lib = cdll.LoadLibrary(libpath)
+
+        self.__diff_tree = self.__lib.diff_tree
+        self.__diff_tree.argtypes = [c_char_p, c_void_p, c_void_p]
+        self.__diff_tree.restype = c_void_p
+
         check_path(path)
         path_str = " ".join(map(str, path)).encode()
-        df = cdll.LoadLibrary(libpath).diffs
-        df.restype = POINTER(c_void_p * 3)
-        res = list(df(path_str, left._get_config(), right._get_config()).contents)
-        self._diff = {'add': ConfigTree(address=res[0]),
-                      'del': ConfigTree(address=res[1]),
-                      'int': ConfigTree(address=res[2]) }
-
-        self.add = self._diff['add']
-        self.delete = self._diff['del']
-        self.inter = self._diff['int']
+        res = self.__diff_tree(path_str, left._get_config(), right._get_config())
+
+        # full diff config_tree and python dict representation
+        self.full = ConfigTree(address=res)
+        self.dict = json.loads(self.full.to_json())
+
+        # config_tree sub-trees
+        self.add = self.full.get_subtree(['add'])
+        self.delete = self.full.get_subtree(['delete'])
+        self.inter = self.full.get_subtree(['inter'])
 
     def to_commands(self):
         add = self.add.to_commands()
-- 
cgit v1.2.3


From 4625fd41f99ddf77c104a657cd90a1ddf5449dd8 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Fri, 25 Feb 2022 16:12:29 -0600
Subject: configtree: T4235: allow empty arguments

---
 python/vyos/configtree.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index f5e697137..5ba829a4c 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -305,6 +305,10 @@ class ConfigTree(object):
 
 class DiffTree:
     def __init__(self, left, right, path=[], libpath=LIBPATH):
+        if left is None:
+            left = ConfigTree(config_string='\n')
+        if right is None:
+            right = ConfigTree(config_string='\n')
         if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)):
             raise TypeError("Arguments must be instances of ConfigTree")
         if path:
-- 
cgit v1.2.3


From 193cbd15ba39a41614c63b997e6a62254589158a Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Sun, 27 Feb 2022 10:05:40 -0600
Subject: configtree: T4235: distinguish sub(-tract) tree from delete tree

The DiffTree class maintains both the 'sub'(-tract) configtree,
containing all paths in the LHS of the comparison that are not in the
RHS, and the 'delete' configtree: the delete tree is the minimal subtree
containing only the first node of a path not present in the RHS. It is
the delete tree that is needed to produce 'delete' commands for config
mode, whereas the 'sub' tree contains full information, needed for
recursively detecting changes to a node.
---
 python/vyos/configtree.py | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 5ba829a4c..e9cdb69e4 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -326,8 +326,13 @@ class DiffTree:
         self.__diff_tree.argtypes = [c_char_p, c_void_p, c_void_p]
         self.__diff_tree.restype = c_void_p
 
+        self.__trim_tree = self.__lib.trim_tree
+        self.__trim_tree.argtypes = [c_void_p, c_void_p]
+        self.__trim_tree.restype = c_void_p
+
         check_path(path)
         path_str = " ".join(map(str, path)).encode()
+
         res = self.__diff_tree(path_str, left._get_config(), right._get_config())
 
         # full diff config_tree and python dict representation
@@ -336,9 +341,14 @@ class DiffTree:
 
         # config_tree sub-trees
         self.add = self.full.get_subtree(['add'])
-        self.delete = self.full.get_subtree(['delete'])
+        self.sub = self.full.get_subtree(['sub'])
         self.inter = self.full.get_subtree(['inter'])
 
+        # trim sub(-tract) tree to get delete tree for commands
+        ref = self.right.get_subtree(path, with_node=True) if path else self.right
+        res = self.__trim_tree(self.sub._get_config(), ref._get_config())
+        self.delete = ConfigTree(address=res)
+
     def to_commands(self):
         add = self.add.to_commands()
         delete = self.delete.to_commands(op="delete")
-- 
cgit v1.2.3


From 42c011224e5aef3c27f9de6b5a74e594a404131e Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Tue, 1 Mar 2022 19:09:12 +0100
Subject: flow-accounting: T4277: support sending flow-data via VRF interface

It should be possible to send the gathered data via a VRF bound interface to
the collector. This is somehow related to T3981 but it's the opposite side of
the netflow process.

set system flow-accounting vrf <name>
---
 data/templates/netflow/uacctd.conf.tmpl            | 74 ----------------------
 data/templates/pmacct/override.conf.tmpl           | 17 +++++
 data/templates/pmacct/uacctd.conf.tmpl             | 74 ++++++++++++++++++++++
 interface-definitions/flow-accounting-conf.xml.in  |  1 +
 .../scripts/cli/test_system_flow-accounting.py     |  5 +-
 src/conf_mode/flow_accounting_conf.py              | 14 ++--
 .../systemd/system/uacctd.service.d/override.conf  | 14 ----
 7 files changed, 106 insertions(+), 93 deletions(-)
 delete mode 100644 data/templates/netflow/uacctd.conf.tmpl
 create mode 100644 data/templates/pmacct/override.conf.tmpl
 create mode 100644 data/templates/pmacct/uacctd.conf.tmpl
 delete mode 100644 src/etc/systemd/system/uacctd.service.d/override.conf

diff --git a/data/templates/netflow/uacctd.conf.tmpl b/data/templates/netflow/uacctd.conf.tmpl
deleted file mode 100644
index f81002dc1..000000000
--- a/data/templates/netflow/uacctd.conf.tmpl
+++ /dev/null
@@ -1,74 +0,0 @@
-# Genereated from VyOS configuration
-daemonize: true
-promisc: false
-pidfile: /run/pmacct/uacctd.pid
-uacctd_group: 2
-uacctd_nl_size: 2097152
-snaplen: {{ packet_length }}
-aggregate: in_iface{{ ',out_iface' if enable_egress is defined }},src_mac,dst_mac,vlan,src_host,dst_host,src_port,dst_port,proto,tos,flows
-{% set pipe_size = buffer_size | int *1024 *1024 %}
-plugin_pipe_size: {{ pipe_size }}
-{# We need an integer division (//) without any remainder or fraction #}
-plugin_buffer_size: {{ pipe_size // 1000 }}
-{% if syslog_facility is defined and syslog_facility is not none %}
-syslog: {{ syslog_facility }}
-{% endif %}
-{% if disable_imt is not defined %}
-imt_path: /tmp/uacctd.pipe
-imt_mem_pools_number: 169
-{% endif %}
-
-{% set plugin = [] %}
-{% if disable_imt is not defined %}
-{%   set plugin = ['memory'] %}
-{% endif %}
-{% if netflow is defined and netflow.server is defined and netflow.server is not none %}
-{%   for server in netflow.server %}
-{%     set plugin = plugin.append('nfprobe[nf_' ~ server ~ ']') %}
-{%   endfor %}
-{% endif %}
-{% if sflow is defined and sflow.server is defined and sflow.server is not none %}
-{%   for server in sflow.server %}
-{%     set plugin = plugin.append('sfprobe[sf_' ~ server ~ ']') %}
-{%   endfor %}
-{% endif %}
-plugins: {{ plugin | join(',') }}
-
-{% if netflow is defined and netflow.server is defined and netflow.server is not none %}
-# NetFlow servers
-{%   for server, server_config in netflow.server.items() %}
-nfprobe_receiver[nf_{{ server }}]: {{ server }}:{{ server_config.port }}
-nfprobe_version[nf_{{ server }}]: {{ netflow.version }}
-{%     if netflow.engine_id is defined and netflow.engine_id is not none %}
-nfprobe_engine[nf_{{ server }}]: {{ netflow.engine_id }}
-{%     endif %}
-{%     if netflow.max_flows is defined and netflow.max_flows is not none %}
-nfprobe_maxflows[nf_{{ server }}]: {{ netflow.max_flows }}
-{%     endif %}
-{%     if netflow.sampling_rate is defined and netflow.sampling_rate is not none %}
-sampling_rate[nf_{{ server }}]: {{ netflow.sampling_rate }}
-{%     endif %}
-{%     if netflow.source_address is defined and netflow.source_address is not none %}
-nfprobe_source_ip[nf_{{ server }}]: {{ netflow.source_address }}
-{%     endif %}
-{%     if netflow.timeout is defined and netflow.timeout is not none %}
-nfprobe_timeouts[nf_{{ server }}]: expint={{ netflow.timeout.expiry_interval }}:general={{ netflow.timeout.flow_generic }}:icmp={{ netflow.timeout.icmp }}:maxlife={{ netflow.timeout.max_active_life }}:tcp.fin={{ netflow.timeout.tcp_fin }}:tcp={{ netflow.timeout.tcp_generic }}:tcp.rst={{ netflow.timeout.tcp_rst }}:udp={{ netflow.timeout.udp }}
-{%     endif %}
-
-{%   endfor %}
-{% endif %}
-
-{% if sflow is defined and sflow.server is defined and sflow.server is not none %}
-# sFlow servers
-{%   for server, server_config in sflow.server.items() %}
-sfprobe_receiver[sf_{{ server }}]: {{ server }}:{{ server_config.port }}
-sfprobe_agentip[sf_{{ server }}]: {{ sflow.agent_address }}
-{%     if sflow.sampling_rate is defined and sflow.sampling_rate is not none %}
-sampling_rate[sf_{{ server }}]: {{ sflow.sampling_rate }}
-{%     endif %}
-{%     if sflow.source_address is defined and sflow.source_address is not none %}
-sfprobe_source_ip[sf_{{ server }}]: {{ sflow.source_address }}
-{%     endif %}
-
-{%   endfor %}
-{% endif %}
diff --git a/data/templates/pmacct/override.conf.tmpl b/data/templates/pmacct/override.conf.tmpl
new file mode 100644
index 000000000..216927666
--- /dev/null
+++ b/data/templates/pmacct/override.conf.tmpl
@@ -0,0 +1,17 @@
+{% set vrf_command = 'ip vrf exec ' + vrf + ' ' if vrf is defined else '' %}
+[Unit]
+After=
+After=vyos-router.service
+ConditionPathExists=
+ConditionPathExists=/run/pmacct/uacctd.conf
+
+[Service]
+EnvironmentFile=
+ExecStart=
+ExecStart={{vrf_command}}/usr/sbin/uacctd -f /run/pmacct/uacctd.conf
+WorkingDirectory=
+WorkingDirectory=/run/pmacct
+PIDFile=
+PIDFile=/run/pmacct/uacctd.pid
+Restart=always
+RestartSec=10
diff --git a/data/templates/pmacct/uacctd.conf.tmpl b/data/templates/pmacct/uacctd.conf.tmpl
new file mode 100644
index 000000000..b58f7c796
--- /dev/null
+++ b/data/templates/pmacct/uacctd.conf.tmpl
@@ -0,0 +1,74 @@
+# Genereated from VyOS configuration
+daemonize: true
+promisc: false
+pidfile: /run/pmacct/uacctd.pid
+uacctd_group: 2
+uacctd_nl_size: 2097152
+snaplen: {{ packet_length }}
+aggregate: in_iface{{ ',out_iface' if enable_egress is defined }},src_mac,dst_mac,vlan,src_host,dst_host,src_port,dst_port,proto,tos,flows
+{% set pipe_size = buffer_size | int *1024 *1024 %}
+plugin_pipe_size: {{ pipe_size }}
+{# We need an integer division (//) without any remainder or fraction #}
+plugin_buffer_size: {{ pipe_size // 1000 }}
+{% if syslog_facility is defined and syslog_facility is not none %}
+syslog: {{ syslog_facility }}
+{% endif %}
+{% if disable_imt is not defined %}
+imt_path: /tmp/uacctd.pipe
+imt_mem_pools_number: 169
+{% endif %}
+
+{% set plugin = [] %}
+{% if netflow is defined and netflow.server is defined and netflow.server is not none %}
+{%   for server in netflow.server %}
+{%     set _ = plugin.append('nfprobe[nf_' ~ server ~ ']') %}
+{%   endfor %}
+{% endif %}
+{% if sflow is defined and sflow.server is defined and sflow.server is not none %}
+{%   for server in sflow.server %}
+{%     set _ = plugin.append('sfprobe[sf_' ~ server ~ ']') %}
+{%   endfor %}
+{% endif %}
+{% if disable_imt is not defined %}
+{%     set _ = plugin.append('memory') %}
+{% endif %}
+plugins: {{ plugin | join(',') }}
+
+{% if netflow is defined and netflow.server is defined and netflow.server is not none %}
+# NetFlow servers
+{%   for server, server_config in netflow.server.items() %}
+nfprobe_receiver[nf_{{ server }}]: {{ server }}:{{ server_config.port }}
+nfprobe_version[nf_{{ server }}]: {{ netflow.version }}
+{%     if netflow.engine_id is defined and netflow.engine_id is not none %}
+nfprobe_engine[nf_{{ server }}]: {{ netflow.engine_id }}
+{%     endif %}
+{%     if netflow.max_flows is defined and netflow.max_flows is not none %}
+nfprobe_maxflows[nf_{{ server }}]: {{ netflow.max_flows }}
+{%     endif %}
+{%     if netflow.sampling_rate is defined and netflow.sampling_rate is not none %}
+sampling_rate[nf_{{ server }}]: {{ netflow.sampling_rate }}
+{%     endif %}
+{%     if netflow.source_address is defined and netflow.source_address is not none %}
+nfprobe_source_ip[nf_{{ server }}]: {{ netflow.source_address }}
+{%     endif %}
+{%     if netflow.timeout is defined and netflow.timeout is not none %}
+nfprobe_timeouts[nf_{{ server }}]: expint={{ netflow.timeout.expiry_interval }}:general={{ netflow.timeout.flow_generic }}:icmp={{ netflow.timeout.icmp }}:maxlife={{ netflow.timeout.max_active_life }}:tcp.fin={{ netflow.timeout.tcp_fin }}:tcp={{ netflow.timeout.tcp_generic }}:tcp.rst={{ netflow.timeout.tcp_rst }}:udp={{ netflow.timeout.udp }}
+{%     endif %}
+
+{%   endfor %}
+{% endif %}
+
+{% if sflow is defined and sflow.server is defined and sflow.server is not none %}
+# sFlow servers
+{%   for server, server_config in sflow.server.items() %}
+sfprobe_receiver[sf_{{ server }}]: {{ server }}:{{ server_config.port }}
+sfprobe_agentip[sf_{{ server }}]: {{ sflow.agent_address }}
+{%     if sflow.sampling_rate is defined and sflow.sampling_rate is not none %}
+sampling_rate[sf_{{ server }}]: {{ sflow.sampling_rate }}
+{%     endif %}
+{%     if sflow.source_address is defined and sflow.source_address is not none %}
+sfprobe_source_ip[sf_{{ server }}]: {{ sflow.source_address }}
+{%     endif %}
+
+{%   endfor %}
+{% endif %}
diff --git a/interface-definitions/flow-accounting-conf.xml.in b/interface-definitions/flow-accounting-conf.xml.in
index 05cf5e170..133e45c72 100644
--- a/interface-definitions/flow-accounting-conf.xml.in
+++ b/interface-definitions/flow-accounting-conf.xml.in
@@ -431,6 +431,7 @@
               #include <include/source-address-ipv4-ipv6.xml.i>
             </children>
           </node>
+          #include <include/interface/vrf.xml.i>
         </children>
       </node>
     </children>
diff --git a/smoketest/scripts/cli/test_system_flow-accounting.py b/smoketest/scripts/cli/test_system_flow-accounting.py
index 857df1be6..84f17bcb0 100755
--- a/smoketest/scripts/cli/test_system_flow-accounting.py
+++ b/smoketest/scripts/cli/test_system_flow-accounting.py
@@ -39,6 +39,9 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
         cls.cli_delete(cls, base_path)
 
     def tearDown(self):
+        # after service removal process must no longer run
+        self.assertTrue(process_named_running(PROCESS_NAME))
+
         self.cli_delete(base_path)
         self.cli_commit()
 
@@ -213,9 +216,9 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase):
         uacctd = read_file(uacctd_conf)
 
         tmp = []
-        tmp.append('memory')
         for server, server_config in netflow_server.items():
             tmp.append(f'nfprobe[nf_{server}]')
+        tmp.append('memory')
         self.assertIn('plugins: ' + ','.join(tmp), uacctd)
 
         for server, server_config in netflow_server.items():
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 975f19acf..25bf54790 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -27,6 +27,7 @@ from vyos.configdict import dict_merge
 from vyos.ifconfig import Section
 from vyos.ifconfig import Interface
 from vyos.template import render
+from vyos.util import call
 from vyos.util import cmd
 from vyos.validate import is_addr_assigned
 from vyos.xml import defaults
@@ -35,6 +36,8 @@ from vyos import airbag
 airbag.enable()
 
 uacctd_conf_path = '/run/pmacct/uacctd.conf'
+systemd_service = 'uacctd.service'
+systemd_override = f'/etc/systemd/system/{systemd_service}.d/override.conf'
 nftables_nflog_table = 'raw'
 nftables_nflog_chain = 'VYOS_CT_PREROUTING_HOOK'
 egress_nftables_nflog_table = 'inet mangle'
@@ -236,7 +239,10 @@ def generate(flow_config):
     if not flow_config:
         return None
 
-    render(uacctd_conf_path, 'netflow/uacctd.conf.tmpl', flow_config)
+    render(uacctd_conf_path, 'pmacct/uacctd.conf.tmpl', flow_config)
+    render(systemd_override, 'pmacct/override.conf.tmpl', flow_config)
+    # Reload systemd manager configuration
+    call('systemctl daemon-reload')
 
 def apply(flow_config):
     action = 'restart'
@@ -246,13 +252,13 @@ def apply(flow_config):
         _nftables_config([], 'egress')
 
         # Stop flow-accounting daemon and remove configuration file
-        cmd('systemctl stop uacctd.service')
+        call(f'systemctl stop {systemd_service}')
         if os.path.exists(uacctd_conf_path):
             os.unlink(uacctd_conf_path)
         return
 
     # Start/reload flow-accounting daemon
-    cmd(f'systemctl restart uacctd.service')
+    call(f'systemctl restart {systemd_service}')
 
     # configure nftables rules for defined interfaces
     if 'interface' in flow_config:
diff --git a/src/etc/systemd/system/uacctd.service.d/override.conf b/src/etc/systemd/system/uacctd.service.d/override.conf
deleted file mode 100644
index 38bcce515..000000000
--- a/src/etc/systemd/system/uacctd.service.d/override.conf
+++ /dev/null
@@ -1,14 +0,0 @@
-[Unit]
-After=
-After=vyos-router.service
-ConditionPathExists=
-ConditionPathExists=/run/pmacct/uacctd.conf
-
-[Service]
-EnvironmentFile=
-ExecStart=
-ExecStart=/usr/sbin/uacctd -f /run/pmacct/uacctd.conf
-WorkingDirectory=
-WorkingDirectory=/run/pmacct
-PIDFile=
-PIDFile=/run/pmacct/uacctd.pid
-- 
cgit v1.2.3


From e5d04b20be0ef270d20f1d5ac9203b3a03649135 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Wed, 2 Mar 2022 07:59:47 -0600
Subject: configdiff: T4260: add support for diff_tree class

Add support for the configtree diff algorithm. A new function
ConfigDiff().is_node_changed(path) -> bool
is added to recursively detect changes in the tree below the node at
path; existing functions take the keyword argument 'recursive: bool' to
apply the algorithm in place of the existing, non-recursive, comparison.
---
 python/vyos/configdiff.py | 84 ++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 80 insertions(+), 4 deletions(-)

diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py
index 4ad7443d7..9185575df 100644
--- a/python/vyos/configdiff.py
+++ b/python/vyos/configdiff.py
@@ -16,6 +16,7 @@
 from enum import IntFlag, auto
 
 from vyos.config import Config
+from vyos.configtree import DiffTree
 from vyos.configdict import dict_merge
 from vyos.configdict import list_diff
 from vyos.util import get_sub_dict, mangle_dict_keys
@@ -38,6 +39,8 @@ class Diff(IntFlag):
     ADD = auto()
     STABLE = auto()
 
+ALL = Diff.MERGE | Diff.DELETE | Diff.ADD | Diff.STABLE
+
 requires_effective = [enum_to_key(Diff.DELETE)]
 target_defaults = [enum_to_key(Diff.MERGE)]
 
@@ -75,19 +78,24 @@ def get_config_diff(config, key_mangling=None):
             isinstance(key_mangling[1], str)):
         raise ValueError("key_mangling must be a tuple of two strings")
 
-    return ConfigDiff(config, key_mangling)
+    diff_t = DiffTree(config._running_config, config._session_config)
+
+    return ConfigDiff(config, key_mangling, diff_tree=diff_t)
 
 class ConfigDiff(object):
     """
     The class of config changes as represented by comparison between the
     session config dict and the effective config dict.
     """
-    def __init__(self, config, key_mangling=None):
+    def __init__(self, config, key_mangling=None, diff_tree=None):
         self._level = config.get_level()
         self._session_config_dict = config.get_cached_root_dict(effective=False)
         self._effective_config_dict = config.get_cached_root_dict(effective=True)
         self._key_mangling = key_mangling
 
+        self._diff_tree = diff_tree
+        self._diff_dict = diff_tree.dict if diff_tree else {}
+
     # mirrored from Config; allow path arguments relative to level
     def _make_path(self, path):
         if isinstance(path, str):
@@ -136,6 +144,15 @@ class ConfigDiff(object):
                                                     self._key_mangling[1])
         return config_dict
 
+    def is_node_changed(self, path=[]):
+        if self._diff_tree is None:
+            raise NotImplementedError("diff_tree class not available")
+
+        if (self._diff_tree.add.exists(self._make_path(path)) or
+            self._diff_tree.sub.exists(self._make_path(path))):
+            return True
+        return False
+
     def get_child_nodes_diff_str(self, path=[]):
         ret = {'add': {}, 'change': {}, 'delete': {}}
 
@@ -164,7 +181,8 @@ class ConfigDiff(object):
 
         return ret
 
-    def get_child_nodes_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False):
+    def get_child_nodes_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False,
+                             recursive=False):
         """
         Args:
             path (str|list): config path
@@ -174,6 +192,8 @@ class ConfigDiff(object):
                                   value
             no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default
                                values to ret['merge']
+            recursive: if true, use config_tree diff algorithm provided by
+                       diff_tree class
 
         Returns: dict of lists, representing differences between session
                                 and effective config, under path
@@ -184,6 +204,34 @@ class ConfigDiff(object):
         """
         session_dict = get_sub_dict(self._session_config_dict,
                                     self._make_path(path), get_first_key=True)
+
+        if recursive:
+            if self._diff_tree is None:
+                raise NotImplementedError("diff_tree class not available")
+            else:
+                add = get_sub_dict(self._diff_tree.dict, ['add'], get_first_key=True)
+                sub = get_sub_dict(self._diff_tree.dict, ['sub'], get_first_key=True)
+                inter = get_sub_dict(self._diff_tree.dict, ['inter'], get_first_key=True)
+                ret = {}
+                ret[enum_to_key(Diff.MERGE)] = session_dict
+                ret[enum_to_key(Diff.DELETE)] = get_sub_dict(sub, self._make_path(path),
+                                                             get_first_key=True)
+                ret[enum_to_key(Diff.ADD)] = get_sub_dict(add, self._make_path(path),
+                                                          get_first_key=True)
+                ret[enum_to_key(Diff.STABLE)] = get_sub_dict(inter, self._make_path(path),
+                                                             get_first_key=True)
+                for e in Diff:
+                    k = enum_to_key(e)
+                    if not (e & expand_nodes):
+                        ret[k] = list(ret[k])
+                    else:
+                        if self._key_mangling:
+                            ret[k] = self._mangle_dict_keys(ret[k])
+                        if k in target_defaults and not no_defaults:
+                            default_values = defaults(self._make_path(path))
+                            ret[k] = dict_merge(default_values, ret[k])
+                return ret
+
         effective_dict = get_sub_dict(self._effective_config_dict,
                                       self._make_path(path), get_first_key=True)
 
@@ -209,7 +257,8 @@ class ConfigDiff(object):
 
         return ret
 
-    def get_node_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False):
+    def get_node_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False,
+                      recursive=False):
         """
         Args:
             path (str|list): config path
@@ -219,6 +268,8 @@ class ConfigDiff(object):
                                   value
             no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default
                                values to ret['merge']
+            recursive: if true, use config_tree diff algorithm provided by
+                       diff_tree class
 
         Returns: dict of lists, representing differences between session
                                 and effective config, at path
@@ -228,6 +279,31 @@ class ConfigDiff(object):
                  dict['stable'] = config values in both session and effective
         """
         session_dict = get_sub_dict(self._session_config_dict, self._make_path(path))
+
+        if recursive:
+            if self._diff_tree is None:
+                raise NotImplementedError("diff_tree class not available")
+            else:
+                add = get_sub_dict(self._diff_tree.dict, ['add'], get_first_key=True)
+                sub = get_sub_dict(self._diff_tree.dict, ['sub'], get_first_key=True)
+                inter = get_sub_dict(self._diff_tree.dict, ['inter'], get_first_key=True)
+                ret = {}
+                ret[enum_to_key(Diff.MERGE)] = session_dict
+                ret[enum_to_key(Diff.DELETE)] = get_sub_dict(sub, self._make_path(path))
+                ret[enum_to_key(Diff.ADD)] = get_sub_dict(add, self._make_path(path))
+                ret[enum_to_key(Diff.STABLE)] = get_sub_dict(inter, self._make_path(path))
+                for e in Diff:
+                    k = enum_to_key(e)
+                    if not (e & expand_nodes):
+                        ret[k] = list(ret[k])
+                    else:
+                        if self._key_mangling:
+                            ret[k] = self._mangle_dict_keys(ret[k])
+                        if k in target_defaults and not no_defaults:
+                            default_values = defaults(self._make_path(path))
+                            ret[k] = dict_merge(default_values, ret[k])
+                return ret
+
         effective_dict = get_sub_dict(self._effective_config_dict, self._make_path(path))
 
         ret = _key_sets_from_dicts(session_dict, effective_dict)
-- 
cgit v1.2.3


From 0e23fc10581cde7b31101990566f59eded826233 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 3 Mar 2022 18:15:32 +0100
Subject: interface: T4203: switch to new recursive node_changed()
 implementation

---
 python/vyos/configdict.py         | 42 ++++++++++-----------------------------
 python/vyos/ifconfig/interface.py |  4 ++--
 2 files changed, 13 insertions(+), 33 deletions(-)

diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index aea22c0c9..b8f428c1c 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -136,7 +136,7 @@ def leaf_node_changed(conf, path):
 
     return None
 
-def node_changed(conf, path, key_mangling=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
@@ -146,7 +146,7 @@ def node_changed(conf, path, key_mangling=None):
     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)['delete'].keys()
+    keys = D.get_child_nodes_diff(path, expand_nodes=Diff.DELETE, recursive=recursive)['delete'].keys()
     return list(keys)
 
 def get_removed_vlans(conf, dict):
@@ -444,13 +444,8 @@ def get_interface_dict(config, base, ifname=''):
     if bond: dict.update({'is_bond_member' : bond})
 
     # Check if any DHCP options changed which require a client restat
-    for leaf_node in ['client-id', 'default-route-distance', 'host-name',
-                 'no-default-route', 'vendor-class-id']:
-        dhcp = leaf_node_changed(config, ['dhcp-options', leaf_node])
-        if dhcp:
-            dict.update({'dhcp_options_old' : dhcp})
-            # one option is suffiecient to set 'dhcp_options_old' key
-            break
+    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
@@ -506,13 +501,8 @@ def get_interface_dict(config, base, ifname=''):
         if bridge: dict['vif'][vif].update({'is_bridge_member' : bridge})
 
         # Check if any DHCP options changed which require a client restat
-        for leaf_node in ['client-id', 'default-route-distance', 'host-name',
-                     'no-default-route', 'vendor-class-id']:
-            dhcp = leaf_node_changed(config, ['vif', vif, 'dhcp-options', leaf_node])
-            if dhcp:
-                dict['vif'][vif].update({'dhcp_options_old' : dhcp})
-                # one option is suffiecient to set 'dhcp_options_old' key
-                break
+        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():
         default_vif_s_values = defaults(base + ['vif-s'])
@@ -547,13 +537,8 @@ def get_interface_dict(config, base, ifname=''):
         if bridge: dict['vif_s'][vif_s].update({'is_bridge_member' : bridge})
 
         # Check if any DHCP options changed which require a client restat
-        for leaf_node in ['client-id', 'default-route-distance', 'host-name',
-                     'no-default-route', 'vendor-class-id']:
-            dhcp = leaf_node_changed(config, ['vif-s', vif_s, 'dhcp-options', leaf_node])
-            if dhcp:
-                dict['vif_s'][vif_s].update({'dhcp_options_old' : dhcp})
-                # one option is suffiecient to set 'dhcp_options_old' key
-                break
+        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():
             default_vif_c_values = defaults(base + ['vif-s', 'vif-c'])
@@ -589,14 +574,9 @@ def get_interface_dict(config, base, ifname=''):
                 {'is_bridge_member' : bridge})
 
             # Check if any DHCP options changed which require a client restat
-            for leaf_node in ['client-id', 'default-route-distance', 'host-name',
-                         'no-default-route', 'vendor-class-id']:
-                dhcp = leaf_node_changed(config, ['vif-s', vif_s, 'vif-c', vif_c,
-                                                  'dhcp-options', leaf_node])
-                if dhcp:
-                    dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcp_options_old' : dhcp})
-                    # one option is suffiecient to set 'dhcp_options_old' key
-                    break
+            dhcp = leaf_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)
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index cf1887bf6..8c5ff576e 100755
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2022 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
@@ -1250,7 +1250,7 @@ class Interface(Control):
             # the old lease is released a new one is acquired (T4203). We will
             # only restart DHCP client if it's option changed, or if it's not
             # running, but it should be running (e.g. on system startup)
-            if 'dhcp_options_old' in self._config or not is_systemd_service_active(systemd_service):
+            if 'dhcp_options_changed' in self._config or not is_systemd_service_active(systemd_service):
                 return self._cmd(f'systemctl restart {systemd_service}')
             return None
         else:
-- 
cgit v1.2.3


From 9d2fa6f85847bbb59af42809c50e4547542e4845 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 3 Mar 2022 19:10:07 +0100
Subject: static: T4283: fix help string for route/route6

---
 interface-definitions/include/static/static-route.xml.i  | 2 +-
 interface-definitions/include/static/static-route6.xml.i | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/interface-definitions/include/static/static-route.xml.i b/interface-definitions/include/static/static-route.xml.i
index 21babc015..903915066 100644
--- a/interface-definitions/include/static/static-route.xml.i
+++ b/interface-definitions/include/static/static-route.xml.i
@@ -1,7 +1,7 @@
 <!-- include start from static/static-route.xml.i -->
 <tagNode name="route">
   <properties>
-    <help>VRF static IPv4 route</help>
+    <help>Static IPv4 route</help>
     <valueHelp>
       <format>ipv4net</format>
       <description>IPv4 static route</description>
diff --git a/interface-definitions/include/static/static-route6.xml.i b/interface-definitions/include/static/static-route6.xml.i
index 0ea995588..e705c45fa 100644
--- a/interface-definitions/include/static/static-route6.xml.i
+++ b/interface-definitions/include/static/static-route6.xml.i
@@ -1,7 +1,7 @@
 <!-- include start from static/static-route6.xml.i -->
 <tagNode name="route6">
   <properties>
-    <help>VRF static IPv6 route</help>
+    <help>Static IPv6 route</help>
     <valueHelp>
       <format>ipv6net</format>
       <description>IPv6 static route</description>
-- 
cgit v1.2.3


From e3f86ce0d65fe8fe0c5eebebdfd3ab3723e2e539 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 3 Mar 2022 19:10:38 +0100
Subject: static: T4283: create re-usable XML interface definitions for
 blackhole

---
 .../include/static/static-route-blackhole.xml.i     |  3 ++-
 .../include/static/static-route-tag.xml.i           | 14 ++++++++++++++
 .../include/static/static-route.xml.i               | 21 +--------------------
 .../include/static/static-route6.xml.i              | 21 +--------------------
 4 files changed, 18 insertions(+), 41 deletions(-)
 create mode 100644 interface-definitions/include/static/static-route-tag.xml.i

diff --git a/interface-definitions/include/static/static-route-blackhole.xml.i b/interface-definitions/include/static/static-route-blackhole.xml.i
index f2ad23e69..487f775f5 100644
--- a/interface-definitions/include/static/static-route-blackhole.xml.i
+++ b/interface-definitions/include/static/static-route-blackhole.xml.i
@@ -1,10 +1,11 @@
 <!-- include start from static/static-route-blackhole.xml.i -->
 <node name="blackhole">
   <properties>
-    <help>Silently discard packets when matched</help>
+    <help>Silently discard pkts when matched</help>
   </properties>
   <children>
     #include <include/static/static-route-distance.xml.i>
+    #include <include/static/static-route-tag.xml.i>
   </children>
 </node>
 <!-- include end -->
diff --git a/interface-definitions/include/static/static-route-tag.xml.i b/interface-definitions/include/static/static-route-tag.xml.i
new file mode 100644
index 000000000..24bfa732e
--- /dev/null
+++ b/interface-definitions/include/static/static-route-tag.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from static/static-route-tag.xml.i -->
+<leafNode name="tag">
+  <properties>
+    <help>Tag value for this route</help>
+    <valueHelp>
+      <format>u32:1-4294967295</format>
+      <description>Tag value for this route</description>
+    </valueHelp>
+    <constraint>
+      <validator name="numeric" argument="--range 1-4294967295"/>
+    </constraint>
+  </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/static/static-route.xml.i b/interface-definitions/include/static/static-route.xml.i
index 903915066..8433703a5 100644
--- a/interface-definitions/include/static/static-route.xml.i
+++ b/interface-definitions/include/static/static-route.xml.i
@@ -11,26 +11,7 @@
     </constraint>
   </properties>
   <children>
-    <node name="blackhole">
-      <properties>
-        <help>Silently discard pkts when matched</help>
-      </properties>
-      <children>
-        #include <include/static/static-route-distance.xml.i>
-        <leafNode name="tag">
-          <properties>
-            <help>Tag value for this route</help>
-            <valueHelp>
-              <format>u32:1-4294967295</format>
-              <description>Tag value for this route</description>
-            </valueHelp>
-            <constraint>
-              <validator name="numeric" argument="--range 1-4294967295"/>
-            </constraint>
-          </properties>
-        </leafNode>
-      </children>
-    </node>
+    #include <include/static/static-route-blackhole.xml.i>
     #include <include/dhcp-interface.xml.i>
     <tagNode name="interface">
       <properties>
diff --git a/interface-definitions/include/static/static-route6.xml.i b/interface-definitions/include/static/static-route6.xml.i
index e705c45fa..124b2b062 100644
--- a/interface-definitions/include/static/static-route6.xml.i
+++ b/interface-definitions/include/static/static-route6.xml.i
@@ -11,26 +11,7 @@
     </constraint>
   </properties>
   <children>
-    <node name="blackhole">
-      <properties>
-        <help>Silently discard pkts when matched</help>
-      </properties>
-      <children>
-        #include <include/static/static-route-distance.xml.i>
-        <leafNode name="tag">
-          <properties>
-            <help>Tag value for this route</help>
-            <valueHelp>
-              <format>u32:1-4294967295</format>
-              <description>Tag value for this route</description>
-            </valueHelp>
-            <constraint>
-              <validator name="numeric" argument="--range 1-4294967295"/>
-            </constraint>
-          </properties>
-        </leafNode>
-      </children>
-    </node>
+    #include <include/static/static-route-blackhole.xml.i>
     <tagNode name="interface">
       <properties>
         <help>IPv6 gateway interface name</help>
-- 
cgit v1.2.3


From bb78f3a9ad28f62896a536719783011794deb64c Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 3 Mar 2022 20:23:09 +0100
Subject: static: T4283: support "reject" routes - emit an ICMP unreachable
 when matched

---
 data/templates/frr/static_routes_macro.j2          |  3 ++
 .../include/static/static-route-reject.xml.i       | 12 +++++
 .../include/static/static-route.xml.i              |  1 +
 .../include/static/static-route6.xml.i             |  1 +
 smoketest/scripts/cli/test_protocols_static.py     | 57 +++++++++++++++++++---
 src/conf_mode/protocols_static.py                  |  4 ++
 6 files changed, 70 insertions(+), 8 deletions(-)
 create mode 100644 interface-definitions/include/static/static-route-reject.xml.i

diff --git a/data/templates/frr/static_routes_macro.j2 b/data/templates/frr/static_routes_macro.j2
index 86c7470ca..8359357b7 100644
--- a/data/templates/frr/static_routes_macro.j2
+++ b/data/templates/frr/static_routes_macro.j2
@@ -2,6 +2,9 @@
 {%   if prefix_config.blackhole is defined %}
 {{ ip_ipv6 }} route {{ prefix }} blackhole {{ prefix_config.blackhole.distance if prefix_config.blackhole.distance is defined }} {{ 'tag ' + prefix_config.blackhole.tag if prefix_config.blackhole.tag is defined }} {{ 'table ' + table if table is defined and table is not none }}
 {%   endif %}
+{%   if prefix_config.reject is defined %}
+{{ ip_ipv6 }} route {{ prefix }} reject {{ prefix_config.reject.distance if prefix_config.reject.distance is defined }} {{ 'tag ' + prefix_config.reject.tag if prefix_config.reject.tag is defined }} {{ 'table ' + table if table is defined and table is not none }}
+{%   endif %}
 {%   if prefix_config.dhcp_interface is defined and prefix_config.dhcp_interface is not none %}
 {%     set next_hop = prefix_config.dhcp_interface | get_dhcp_router %}
 {%     if next_hop is defined and next_hop is not none %}
diff --git a/interface-definitions/include/static/static-route-reject.xml.i b/interface-definitions/include/static/static-route-reject.xml.i
new file mode 100644
index 000000000..81d4f9afd
--- /dev/null
+++ b/interface-definitions/include/static/static-route-reject.xml.i
@@ -0,0 +1,12 @@
+<!-- include start from static/static-route-blackhole.xml.i -->
+<node name="reject">
+  <properties>
+    <help>Emit an ICMP unreachable when matched</help>
+  </properties>
+  <children>
+    #include <include/static/static-route-distance.xml.i>
+    #include <include/static/static-route-tag.xml.i>
+  </children>
+</node>
+<!-- include end -->
+
diff --git a/interface-definitions/include/static/static-route.xml.i b/interface-definitions/include/static/static-route.xml.i
index 8433703a5..2de5dc58f 100644
--- a/interface-definitions/include/static/static-route.xml.i
+++ b/interface-definitions/include/static/static-route.xml.i
@@ -12,6 +12,7 @@
   </properties>
   <children>
     #include <include/static/static-route-blackhole.xml.i>
+    #include <include/static/static-route-reject.xml.i>
     #include <include/dhcp-interface.xml.i>
     <tagNode name="interface">
       <properties>
diff --git a/interface-definitions/include/static/static-route6.xml.i b/interface-definitions/include/static/static-route6.xml.i
index 124b2b062..35feef41c 100644
--- a/interface-definitions/include/static/static-route6.xml.i
+++ b/interface-definitions/include/static/static-route6.xml.i
@@ -12,6 +12,7 @@
   </properties>
   <children>
     #include <include/static/static-route-blackhole.xml.i>
+    #include <include/static/static-route-reject.xml.i>
     <tagNode name="interface">
       <properties>
         <help>IPv6 gateway interface name</help>
diff --git a/smoketest/scripts/cli/test_protocols_static.py b/smoketest/scripts/cli/test_protocols_static.py
index 4c4eb5a7c..3ef9c76d8 100755
--- a/smoketest/scripts/cli/test_protocols_static.py
+++ b/smoketest/scripts/cli/test_protocols_static.py
@@ -1,6 +1,6 @@
 #!/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
@@ -52,9 +52,16 @@ routes = {
         },
         'blackhole' : { 'distance' : '90' },
     },
-    '100.64.0.0/10' : {
+    '100.64.0.0/16' : {
         'blackhole' : { },
     },
+    '100.65.0.0/16' : {
+        'reject'    : { 'distance' : '10', 'tag' : '200' },
+    },
+    '100.66.0.0/16' : {
+        'blackhole' : { },
+        'reject'    : { 'distance' : '10', 'tag' : '200' },
+    },
     '2001:db8:100::/40' : {
         'next_hop' : {
             '2001:db8::1' : { 'distance' : '10' },
@@ -74,6 +81,9 @@ routes = {
         },
         'blackhole' : { 'distance' : '250', 'tag' : '500' },
     },
+    '2001:db8:300::/40' : {
+        'reject'    : { 'distance' : '250', 'tag' : '500' },
+    },
     '2001:db8::/32' : {
         'blackhole' : { 'distance' : '200', 'tag' : '600' },
     },
@@ -82,9 +92,15 @@ routes = {
 tables = ['80', '81', '82']
 
 class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
-    def setUp(self):
-        # This is our "target" VRF when leaking routes:
-        self.cli_set(['vrf', 'name', 'black', 'table', '43210'])
+    @classmethod
+    def setUpClass(cls):
+        super(cls, cls).setUpClass()
+        cls.cli_set(cls, ['vrf', 'name', 'black', 'table', '43210'])
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.cli_delete(cls, ['vrf'])
+        super(cls, cls).tearDownClass()
 
     def tearDown(self):
         for route, route_config in routes.items():
@@ -135,6 +151,20 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
                 if 'tag' in route_config['blackhole']:
                     self.cli_set(base + ['blackhole', 'tag', route_config['blackhole']['tag']])
 
+            if 'reject' in route_config:
+                self.cli_set(base + ['reject'])
+                if 'distance' in route_config['reject']:
+                    self.cli_set(base + ['reject', 'distance', route_config['reject']['distance']])
+                if 'tag' in route_config['reject']:
+                    self.cli_set(base + ['reject', 'tag', route_config['reject']['tag']])
+
+            if {'blackhole', 'reject'} <= set(route_config):
+                # Can not use blackhole and reject at the same time
+                with self.assertRaises(ConfigSessionError):
+                    self.cli_commit()
+                self.cli_delete(base + ['blackhole'])
+                self.cli_delete(base + ['reject'])
+
         # commit changes
         self.cli_commit()
 
@@ -177,6 +207,11 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
                     else:
                         self.assertIn(tmp, frrconfig)
 
+            if {'blackhole', 'reject'} <= set(route_config):
+                # Can not use blackhole and reject at the same time
+                # Config error validated above - skip this route
+                continue
+
             if 'blackhole' in route_config:
                 tmp = f'{ip_ipv6} route {route} blackhole'
                 if 'tag' in route_config['blackhole']:
@@ -186,6 +221,15 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
 
                 self.assertIn(tmp, frrconfig)
 
+            if 'reject' in route_config:
+                tmp = f'{ip_ipv6} route {route} reject'
+                if 'tag' in route_config['reject']:
+                    tmp += ' tag ' + route_config['reject']['tag']
+                if 'distance' in route_config['reject']:
+                    tmp += ' ' + route_config['reject']['distance']
+
+                self.assertIn(tmp, frrconfig)
+
     def test_02_static_table(self):
         for table in tables:
             for route, route_config in routes.items():
@@ -389,11 +433,8 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
 
                     self.assertIn(tmp, frrconfig)
 
-        self.cli_delete(['vrf'])
-
     def test_04_static_zebra_route_map(self):
         # Implemented because of T3328
-        self.debug = True
         route_map = 'foo-static-in'
         self.cli_set(['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit'])
 
diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py
index c1e427b16..f0ec48de4 100755
--- a/src/conf_mode/protocols_static.py
+++ b/src/conf_mode/protocols_static.py
@@ -82,6 +82,10 @@ def verify(static):
                     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):
-- 
cgit v1.2.3


From a929d58f333c39272580a48b2fc467118105bbcf Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 4 Mar 2022 06:48:43 +0100
Subject: interface: T4203: bugfix Q-in-Q interface parsing

Commit 0e23fc10 ("interface: T4203: switch to new recursive node_changed()
implementation") switched to a new implementation to retrieve nested changes
under a CLI node. Unfortunately the new API was not called - instead the
old one was used.
---
 python/vyos/configdict.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index b8f428c1c..551c27b67 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -574,8 +574,7 @@ def get_interface_dict(config, base, ifname=''):
                 {'is_bridge_member' : bridge})
 
             # Check if any DHCP options changed which require a client restat
-            dhcp = leaf_node_changed(config, ['vif-s', vif_s, 'vif-c', vif_c,
-                                              'dhcp-options'], recursive=True)
+            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
-- 
cgit v1.2.3


From c06861440cd21ff7c668b35ed1039f5fac4101b9 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 4 Mar 2022 20:08:27 +0100
Subject: op-mode: lldp: T3999: bugfix cap' referenced before assignment

---
 src/op_mode/lldp_op.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py
index b9ebc991a..17f6bf552 100755
--- a/src/op_mode/lldp_op.py
+++ b/src/op_mode/lldp_op.py
@@ -54,6 +54,7 @@ def parse_data(data, interface):
         for local_if, values in neighbor.items():
             if interface is not None and local_if != interface:
                 continue
+            cap = ''
             for chassis, c_value in values.get('chassis', {}).items():
                 # bail out early if no capabilities found
                 if 'capability' not in c_value:
@@ -62,7 +63,6 @@ def parse_data(data, interface):
                 if isinstance(capabilities, dict):
                     capabilities = [capabilities]
 
-                cap = ''
                 for capability in capabilities:
                     if capability['enabled']:
                         if capability['type'] == 'Router':
-- 
cgit v1.2.3


From 2c94c3ec72a559de405b29b4399250db3085717e Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 5 Mar 2022 09:40:32 +0100
Subject: conntrackd: T4259: prevent startup of multiple daemon instances

---
 debian/vyos-1x.preinst         | 2 +-
 src/etc/logrotate.d/conntrackd | 9 +++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)
 create mode 100644 src/etc/logrotate.d/conntrackd

diff --git a/debian/vyos-1x.preinst b/debian/vyos-1x.preinst
index 45440bf64..71750b3a1 100644
--- a/debian/vyos-1x.preinst
+++ b/debian/vyos-1x.preinst
@@ -1,4 +1,4 @@
 dpkg-divert --package vyos-1x --add --rename /etc/securetty
 dpkg-divert --package vyos-1x --add --rename /etc/security/capability.conf
 dpkg-divert --package vyos-1x --add --rename /lib/systemd/system/lcdproc.service
-
+dpkg-divert --package vyos-1x --add --rename /etc/logrotate.d/conntrackd
diff --git a/src/etc/logrotate.d/conntrackd b/src/etc/logrotate.d/conntrackd
new file mode 100644
index 000000000..b0b09dec1
--- /dev/null
+++ b/src/etc/logrotate.d/conntrackd
@@ -0,0 +1,9 @@
+/var/log/conntrackd-stats.log {
+    weekly
+    rotate 2
+    missingok
+
+    postrotate
+        systemctl restart conntrackd.service > /dev/null
+    endscript
+}
-- 
cgit v1.2.3


From aa8080d316dbeb4d26bf67f6d67efeda43b2bc07 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 5 Mar 2022 20:51:13 +0100
Subject: conntrackd: T4259: fix daemon configuration path

---
 debian/vyos-1x.postinst                | 9 +++++++++
 src/helpers/vyos-vrrp-conntracksync.sh | 8 +++++---
 2 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index 4b4c4c13e..ffd631b1f 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -83,3 +83,12 @@ fi
 if [ -L /etc/init.d/README ]; then
     rm -f /etc/init.d/README
 fi
+
+# Remove unwanted daemon files from /etc
+# conntackd
+DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd"
+for file in $DELETE; do
+    if [ -f ${file} ]; then
+        rm -f ${file}
+    fi
+done
diff --git a/src/helpers/vyos-vrrp-conntracksync.sh b/src/helpers/vyos-vrrp-conntracksync.sh
index 4501aa63e..0cc718938 100755
--- a/src/helpers/vyos-vrrp-conntracksync.sh
+++ b/src/helpers/vyos-vrrp-conntracksync.sh
@@ -14,12 +14,10 @@
 # Modified by : Mohit Mehta <mohit@vyatta.com>
 # Slight modifications were made to this script for running with Vyatta
 # The original script came from 0.9.14 debian conntrack-tools package
-#
-#
 
 CONNTRACKD_BIN=/usr/sbin/conntrackd
 CONNTRACKD_LOCK=/var/lock/conntrack.lock
-CONNTRACKD_CONFIG=/etc/conntrackd/conntrackd.conf
+CONNTRACKD_CONFIG=/run/conntrackd/conntrackd.conf
 FACILITY=daemon
 LEVEL=notice
 TAG=conntrack-tools
@@ -29,6 +27,10 @@ FAILOVER_STATE="/var/run/vyatta-conntrackd-failover-state"
 
 $LOGCMD "vyatta-vrrp-conntracksync invoked at `date`"
 
+if ! systemctl is-active --quiet conntrackd.service; then
+    echo "conntrackd service not running"
+    exit 1
+fi
 
 if [ ! -e $FAILOVER_STATE ]; then
 	mkdir -p /var/run
-- 
cgit v1.2.3


From 1073df8c3aa2a56af861155290a77a59bf5739bf Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 5 Mar 2022 20:52:00 +0100
Subject: flow-accounting: T4277: delete Debian common configs

---
 debian/vyos-1x.postinst | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index ffd631b1f..1a4c830cc 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -86,7 +86,8 @@ fi
 
 # Remove unwanted daemon files from /etc
 # conntackd
-DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd"
+DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd
+        /etc/default/pmacctd /etc/pmacct"
 for file in $DELETE; do
     if [ -f ${file} ]; then
         rm -f ${file}
-- 
cgit v1.2.3


From 1d0d4e83d8413c1b389be763cadd5d150d4be982 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sun, 6 Mar 2022 09:58:22 +0100
Subject: smoketest: config: add "recent" firewall rule to dialup-router

---
 smoketest/configs/dialup-router-complex | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/smoketest/configs/dialup-router-complex b/smoketest/configs/dialup-router-complex
index fef79ea56..1b62deb5c 100644
--- a/smoketest/configs/dialup-router-complex
+++ b/smoketest/configs/dialup-router-complex
@@ -267,6 +267,22 @@ firewall {
             }
             protocol udp
         }
+        rule 800 {
+            action drop
+            description "SSH anti brute force"
+            destination {
+                port ssh
+            }
+            log enable
+            protocol tcp
+            recent {
+                count 4
+                time 60
+            }
+            state {
+                new enable
+            }
+        }
     }
     name DMZ-WAN {
         default-action accept
-- 
cgit v1.2.3


From 27404f71c85187403b3ae1b73b95e6347e07ea97 Mon Sep 17 00:00:00 2001
From: srividya0208 <a.srividya@vyos.io>
Date: Mon, 7 Mar 2022 04:44:08 -0500
Subject: ipsec prefix: T4275: Fix for prefix val_help of remote-access and s2s
 vpn

It accepts network as the input value but the completion help is showing
ip address, continuation of previous commit
---
 interface-definitions/include/ipsec/local-traffic-selector.xml.i | 4 ++--
 interface-definitions/vpn_ipsec.xml.in                           | 8 ++++----
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/interface-definitions/include/ipsec/local-traffic-selector.xml.i b/interface-definitions/include/ipsec/local-traffic-selector.xml.i
index d30a6d11a..9ae67f583 100644
--- a/interface-definitions/include/ipsec/local-traffic-selector.xml.i
+++ b/interface-definitions/include/ipsec/local-traffic-selector.xml.i
@@ -9,11 +9,11 @@
       <properties>
         <help>Local IPv4 or IPv6 prefix</help>
         <valueHelp>
-          <format>ipv4</format>
+          <format>ipv4net</format>
           <description>Local IPv4 prefix</description>
         </valueHelp>
         <valueHelp>
-          <format>ipv6</format>
+          <format>ipv6net</format>
           <description>Local IPv6 prefix</description>
         </valueHelp>
         <constraint>
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index 0ad69c637..d8c06a310 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -897,11 +897,11 @@
                     <properties>
                       <help>Local IPv4 or IPv6 pool prefix</help>
                       <valueHelp>
-                        <format>ipv4</format>
+                        <format>ipv4net</format>
                         <description>Local IPv4 pool prefix</description>
                       </valueHelp>
                       <valueHelp>
-                        <format>ipv6</format>
+                        <format>ipv6net</format>
                         <description>Local IPv6 pool prefix</description>
                       </valueHelp>
                       <constraint>
@@ -1114,11 +1114,11 @@
                             <properties>
                               <help>Remote IPv4 or IPv6 prefix</help>
                               <valueHelp>
-                                <format>ipv4</format>
+                                <format>ipv4net</format>
                                 <description>Remote IPv4 prefix</description>
                               </valueHelp>
                               <valueHelp>
-                                <format>ipv6</format>
+                                <format>ipv6net</format>
                                 <description>Remote IPv6 prefix</description>
                               </valueHelp>
                               <constraint>
-- 
cgit v1.2.3


From ebb524702e1cd60a74b00727b7bd24d375648c78 Mon Sep 17 00:00:00 2001
From: zsdc <taras@vyos.io>
Date: Mon, 7 Mar 2022 18:20:53 +0200
Subject: logrotate: T4250: Fixed logrotate config generation

* Removed `/var/log/auth.log` and `/var/log/messages` from
`/etc/logrotate.d/rsyslog`, because they conflict with VyOS-controlled
items what leads to service error.
* Removed generation config file for `/var/log/messages` from
`system-syslog.py` - this should be done from `syslom logs` now.
* Generate each logfile from `system syslog file` to a dedicated
logrotate config file.
* Fixed logrotate config file names in
`/etc/rsyslog.d/vyos-rsyslog.conf`.
* Added default logrotate settins for `/var/log/messages`
---
 data/templates/syslog/logrotate.tmpl |  9 ++++-----
 debian/vyos-1x.postinst              |  4 ++++
 src/conf_mode/system-syslog.py       | 14 +++++++++++---
 src/etc/logrotate.d/vyos-rsyslog     | 12 ++++++++++++
 4 files changed, 31 insertions(+), 8 deletions(-)
 create mode 100644 src/etc/logrotate.d/vyos-rsyslog

diff --git a/data/templates/syslog/logrotate.tmpl b/data/templates/syslog/logrotate.tmpl
index f758265e4..c1b951e8b 100644
--- a/data/templates/syslog/logrotate.tmpl
+++ b/data/templates/syslog/logrotate.tmpl
@@ -1,12 +1,11 @@
-{% for file in files %}
-{{files[file]['log-file']}} {
+{{ config_render['log-file'] }} {
   missingok
   notifempty
   create
-  rotate {{files[file]['max-files']}}
-  size={{files[file]['max-size']//1024}}k
+  rotate {{ config_render['max-files'] }}
+  size={{ config_render['max-size'] // 1024 }}k
   postrotate
     invoke-rc.d rsyslog rotate > /dev/null
   endscript
 }
-{% endfor %}
+
diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index 1a4c830cc..1ca6687a3 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -93,3 +93,7 @@ for file in $DELETE; do
         rm -f ${file}
     fi
 done
+
+# Remove logrotate items controlled via CLI and VyOS defaults
+sed -i '/^\/var\/log\/messages$/d' /etc/logrotate.d/rsyslog
+sed -i '/^\/var\/log\/auth.log$/d' /etc/logrotate.d/rsyslog
diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py
index 3d8a51cd8..309b4bdb0 100755
--- a/src/conf_mode/system-syslog.py
+++ b/src/conf_mode/system-syslog.py
@@ -17,6 +17,7 @@
 import os
 import re
 
+from pathlib import Path
 from sys import exit
 
 from vyos.config import Config
@@ -89,7 +90,7 @@ def get_config(config=None):
                     filename: {
                         'log-file': '/var/log/user/' + filename,
                         'max-files': '5',
-                        'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/' + filename,
+                        'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog-generated-' + filename,
                         'selectors': '*.err',
                         'max-size': 262144
                     }
@@ -205,10 +206,17 @@ def generate(c):
     conf = '/etc/rsyslog.d/vyos-rsyslog.conf'
     render(conf, 'syslog/rsyslog.conf.tmpl', c)
 
+    # cleanup current logrotate config files
+    logrotate_files = Path('/etc/logrotate.d/').glob('vyos-rsyslog-generated-*')
+    for file in logrotate_files:
+        file.unlink()
+
     # eventually write for each file its own logrotate file, since size is
     # defined it shouldn't matter
-    conf = '/etc/logrotate.d/vyos-rsyslog'
-    render(conf, 'syslog/logrotate.tmpl', c)
+    for filename, fileconfig in c.get('files', {}).items():
+        if fileconfig['log-file'].startswith('/var/log/user/'):
+            conf = '/etc/logrotate.d/vyos-rsyslog-generated-' + filename
+            render(conf, 'syslog/logrotate.tmpl', { 'config_render': fileconfig })
 
 
 def verify(c):
diff --git a/src/etc/logrotate.d/vyos-rsyslog b/src/etc/logrotate.d/vyos-rsyslog
new file mode 100644
index 000000000..3c087b94e
--- /dev/null
+++ b/src/etc/logrotate.d/vyos-rsyslog
@@ -0,0 +1,12 @@
+/var/log/messages {
+    create
+    missingok
+    nomail
+    notifempty
+    rotate 10
+    size 1M
+    postrotate
+        # inform rsyslog service about rotation
+        /usr/lib/rsyslog/rsyslog-rotate
+    endscript
+}
-- 
cgit v1.2.3


From 534f677d36285863decb2cdff179687b4fd690cb Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Tue, 8 Mar 2022 11:46:18 -0600
Subject: component_version: T4291: consolidate read/write functions

---
 python/vyos/component_version.py                | 174 ++++++++++++++++++++++++
 python/vyos/component_versions.py               |  57 --------
 python/vyos/formatversions.py                   | 109 ---------------
 python/vyos/migrator.py                         |  26 ++--
 python/vyos/systemversions.py                   |  46 -------
 smoketest/scripts/cli/test_component_version.py |   6 +-
 src/helpers/system-versions-foot.py             |  19 +--
 7 files changed, 189 insertions(+), 248 deletions(-)
 create mode 100644 python/vyos/component_version.py
 delete mode 100644 python/vyos/component_versions.py
 delete mode 100644 python/vyos/formatversions.py
 delete mode 100644 python/vyos/systemversions.py

diff --git a/python/vyos/component_version.py b/python/vyos/component_version.py
new file mode 100644
index 000000000..b1554828d
--- /dev/null
+++ b/python/vyos/component_version.py
@@ -0,0 +1,174 @@
+# Copyright 2022 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/>.
+
+"""
+Functions for reading/writing component versions.
+
+The config file version string has the following form:
+
+VyOS 1.3/1.4:
+
+// Warning: Do not remove the following line.
+// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1"
+// Release version: 1.3.0
+
+VyOS 1.2:
+
+/* Warning: Do not remove the following line. */
+/* === vyatta-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@2:dhcp-server@5:dns-forwarding@1:firewall@5:ipsec@5:l2tp@1:mdns@1:nat@4:ntp@1:pppoe-server@2:pptp@1:qos@1:quagga@7:snmp@1:ssh@1:system@10:vrrp@2:wanloadbalance@3:webgui@1:webproxy@2:zone-policy@1" === */
+/* Release version: 1.2.8 */
+
+"""
+
+import os
+import re
+import sys
+import fileinput
+from vyos.xml import component_version
+from vyos.version import get_version
+
+def from_string(string_line, vintage='vyos'):
+    """
+    Get component version dictionary from string.
+    Return empty dictionary if string contains no config information
+    or raise error if component version string malformed.
+    """
+    version_dict = {}
+
+    if vintage == 'vyos':
+        if re.match(r'// vyos-config-version:.+', string_line):
+            if not re.match(r'// vyos-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s*', string_line):
+                raise ValueError(f"malformed configuration string: {string_line}")
+
+            for pair in re.findall(r'([\w,-]+)@(\d+)', string_line):
+                version_dict[pair[0]] = int(pair[1])
+
+    elif vintage == 'vyatta':
+        if re.match(r'/\* === vyatta-config-version:.+=== \*/$', string_line):
+            if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', string_line):
+                raise ValueError(f"malformed configuration string: {string_line}")
+
+            for pair in re.findall(r'([\w,-]+)@(\d+)', string_line):
+                version_dict[pair[0]] = int(pair[1])
+    else:
+        raise ValueError("Unknown config string vintage")
+
+    return version_dict
+
+def from_file(config_file_name='/opt/vyatta/etc/config/config.boot',
+                      vintage='vyos'):
+    """
+    Get component version dictionary parsing config file line by line
+    """
+    f = open(config_file_name, 'r')
+    for line_in_config in f:
+        version_dict = from_string(line_in_config, vintage=vintage)
+        if version_dict:
+            return version_dict
+
+    # no version information
+    return {}
+
+def from_system():
+    """
+    Get system component version dict.
+    """
+    return component_version()
+
+def legacy_from_system():
+    """
+    legacy function; imported as-is.
+
+    Get component versions from running system; critical failure if
+    unable to read migration directory.
+    """
+    import vyos.defaults
+
+    system_versions = {}
+
+    try:
+        version_info = os.listdir(vyos.defaults.directories['current'])
+    except OSError as err:
+        print("OS error: {}".format(err))
+        sys.exit(1)
+
+    for info in version_info:
+        if re.match(r'[\w,-]+@\d+', info):
+            pair = info.split('@')
+            system_versions[pair[0]] = int(pair[1])
+
+    return system_versions
+
+def format_string(ver: dict) -> str:
+    """
+    Version dict to string.
+    """
+    keys = list(ver)
+    keys.sort()
+    l = []
+    for k in keys:
+        v = ver[k]
+        l.append(f'{k}@{v}')
+    sep = ':'
+    return sep.join(l)
+
+def system_footer(vintage='vyos') -> str:
+    """
+    Version footer as string.
+    """
+    ver_str = format_string(from_system())
+    release = get_version()
+    if vintage == 'vyos':
+        ret_str = (f'// Warning: Do not remove the following line.\n'
+                +  f'// vyos-config-version: "{ver_str}"\n'
+                +  f'// Release version: {release}\n')
+    elif vintage == 'vyatta':
+        ret_str = (f'/* Warning: Do not remove the following line. */\n'
+                +  f'/* === vyatta-config-version: "{ver_str}" === */\n'
+                +  f'/* Release version: {release} */\n')
+    else:
+        raise ValueError("Unknown config string vintage")
+
+    return ret_str
+
+def write_footer(file_name, vintage='vyos'):
+    """
+    Write version footer to file.
+    """
+    footer = system_footer(vintage=vintage)
+    if file_name:
+        with open(file_name, 'a') as f:
+            f.write(footer)
+    else:
+        sys.stdout.write(footer)
+
+def remove_footer(file_name):
+    """
+    Remove old version footer.
+    """
+    for line in fileinput.input(file_name, inplace=True):
+        if re.match(r'/\* Warning:.+ \*/$', line):
+            continue
+        if re.match(r'/\* === vyatta-config-version:.+=== \*/$', line):
+            continue
+        if re.match(r'/\* Release version:.+ \*/$', line):
+            continue
+        if re.match('// vyos-config-version:.+', line):
+            continue
+        if re.match('// Warning:.+', line):
+            continue
+        if re.match('// Release version:.+', line):
+            continue
+        sys.stdout.write(line)
diff --git a/python/vyos/component_versions.py b/python/vyos/component_versions.py
deleted file mode 100644
index 90b458aae..000000000
--- a/python/vyos/component_versions.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# Copyright 2017 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/>.
-
-"""
-The version data looks like:
-
-/* Warning: Do not remove the following line. */
-/* === vyatta-config-version:
-"cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@1:dhcp-server@4:firewall@5:ipsec@4:nat@4:qos@1:quagga@2:system@8:vrrp@1:wanloadbalance@3:webgui@1:webproxy@1:zone-policy@1"
-=== */
-/* Release version: 1.2.0-rolling+201806131737 */
-"""
-
-import re
-
-def get_component_version(string_line):
-    """
-    Get component version dictionary from string
-    return empty dictionary if string contains no config information
-    or raise error if component version string malformed
-    """
-    return_value = {}
-    if re.match(r'/\* === vyatta-config-version:.+=== \*/$', string_line):
-
-        if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', string_line):
-            raise ValueError("malformed configuration string: " + str(string_line))
-
-        for pair in re.findall(r'([\w,-]+)@(\d+)', string_line):
-            if pair[0] in return_value.keys():
-                raise ValueError("duplicate unit name: \"" + str(pair[0]) + "\" in string: \"" + string_line + "\"")
-            return_value[pair[0]] = int(pair[1])
-
-    return return_value
-
-
-def get_component_versions_from_file(config_file_name='/opt/vyatta/etc/config/config.boot'):
-    """
-    Get component version dictionary parsing config file line by line
-    """
-    f = open(config_file_name, 'r')
-    for line_in_config in f:
-        component_version = get_component_version(line_in_config)
-        if component_version:
-            return component_version
-    raise ValueError("no config string in file:", config_file_name)
diff --git a/python/vyos/formatversions.py b/python/vyos/formatversions.py
deleted file mode 100644
index 29117a5d3..000000000
--- a/python/vyos/formatversions.py
+++ /dev/null
@@ -1,109 +0,0 @@
-# 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/>.
-
-import sys
-import os
-import re
-import fileinput
-
-def read_vyatta_versions(config_file):
-    config_file_versions = {}
-
-    with open(config_file, 'r') as config_file_handle:
-        for config_line in config_file_handle:
-            if re.match(r'/\* === vyatta-config-version:.+=== \*/$', config_line):
-                if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', config_line):
-                    raise ValueError("malformed configuration string: "
-                            "{}".format(config_line))
-
-                for pair in re.findall(r'([\w,-]+)@(\d+)', config_line):
-                    config_file_versions[pair[0]] = int(pair[1])
-
-
-    return config_file_versions
-
-def read_vyos_versions(config_file):
-    config_file_versions = {}
-
-    with open(config_file, 'r') as config_file_handle:
-        for config_line in config_file_handle:
-            if re.match(r'// vyos-config-version:.+', config_line):
-                if not re.match(r'// vyos-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s*', config_line):
-                    raise ValueError("malformed configuration string: "
-                            "{}".format(config_line))
-
-                for pair in re.findall(r'([\w,-]+)@(\d+)', config_line):
-                    config_file_versions[pair[0]] = int(pair[1])
-
-    return config_file_versions
-
-def remove_versions(config_file):
-    """
-    Remove old version string.
-    """
-    for line in fileinput.input(config_file, inplace=True):
-        if re.match(r'/\* Warning:.+ \*/$', line):
-            continue
-        if re.match(r'/\* === vyatta-config-version:.+=== \*/$', line):
-            continue
-        if re.match(r'/\* Release version:.+ \*/$', line):
-            continue
-        if re.match('// vyos-config-version:.+', line):
-            continue
-        if re.match('// Warning:.+', line):
-            continue
-        if re.match('// Release version:.+', line):
-            continue
-        sys.stdout.write(line)
-
-def format_versions_string(config_versions):
-    cfg_keys = list(config_versions.keys())
-    cfg_keys.sort()
-
-    component_version_strings = []
-
-    for key in cfg_keys:
-        cfg_vers = config_versions[key]
-        component_version_strings.append('{}@{}'.format(key, cfg_vers))
-
-    separator = ":"
-    component_version_string = separator.join(component_version_strings)
-
-    return component_version_string
-
-def write_vyatta_versions_foot(config_file, component_version_string,
-                                 os_version_string):
-    if config_file:
-        with open(config_file, 'a') as config_file_handle:
-            config_file_handle.write('/* Warning: Do not remove the following line. */\n')
-            config_file_handle.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string))
-            config_file_handle.write('/* Release version: {} */\n'.format(os_version_string))
-    else:
-        sys.stdout.write('/* Warning: Do not remove the following line. */\n')
-        sys.stdout.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string))
-        sys.stdout.write('/* Release version: {} */\n'.format(os_version_string))
-
-def write_vyos_versions_foot(config_file, component_version_string,
-                               os_version_string):
-    if config_file:
-        with open(config_file, 'a') as config_file_handle:
-            config_file_handle.write('// Warning: Do not remove the following line.\n')
-            config_file_handle.write('// vyos-config-version: "{}"\n'.format(component_version_string))
-            config_file_handle.write('// Release version: {}\n'.format(os_version_string))
-    else:
-        sys.stdout.write('// Warning: Do not remove the following line.\n')
-        sys.stdout.write('// vyos-config-version: "{}"\n'.format(component_version_string))
-        sys.stdout.write('// Release version: {}\n'.format(os_version_string))
-
diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py
index a2e0daabd..266a2e58e 100644
--- a/python/vyos/migrator.py
+++ b/python/vyos/migrator.py
@@ -17,10 +17,8 @@ import sys
 import os
 import json
 import subprocess
-import vyos.version
 import vyos.defaults
-import vyos.systemversions as systemversions
-import vyos.formatversions as formatversions
+import vyos.component_version as component_version
 
 class MigratorError(Exception):
     pass
@@ -42,13 +40,13 @@ class Migrator(object):
         cfg_file = self._config_file
         component_versions = {}
 
-        cfg_versions = formatversions.read_vyatta_versions(cfg_file)
+        cfg_versions = component_version.from_file(cfg_file, vintage='vyatta')
 
         if cfg_versions:
             self._config_file_vintage = 'vyatta'
             component_versions = cfg_versions
 
-        cfg_versions = formatversions.read_vyos_versions(cfg_file)
+        cfg_versions = component_version.from_file(cfg_file, vintage='vyos')
 
         if cfg_versions:
             self._config_file_vintage = 'vyos'
@@ -152,19 +150,11 @@ class Migrator(object):
         """
         Write new versions string.
         """
-        versions_string = formatversions.format_versions_string(cfg_versions)
-
-        os_version_string = vyos.version.get_version()
-
         if self._config_file_vintage == 'vyatta':
-            formatversions.write_vyatta_versions_foot(self._config_file,
-                                                      versions_string,
-                                                      os_version_string)
+            component_version.write_footer(self._config_file, vintage='vyatta')
 
         if self._config_file_vintage == 'vyos':
-            formatversions.write_vyos_versions_foot(self._config_file,
-                                                    versions_string,
-                                                    os_version_string)
+            component_version.write_footer(self._config_file, vintage='vyos')
 
     def save_json_record(self, component_versions: dict):
         """
@@ -195,7 +185,7 @@ class Migrator(object):
             # This will force calling all migration scripts:
             cfg_versions = {}
 
-        sys_versions = systemversions.get_system_component_version()
+        sys_versions = component_version.from_system()
 
         # save system component versions in json file for easy reference
         self.save_json_record(sys_versions)
@@ -211,7 +201,7 @@ class Migrator(object):
         if not self._changed:
             return
 
-        formatversions.remove_versions(cfg_file)
+        component_version.remove_footer(cfg_file)
 
         self.write_config_file_versions(rev_versions)
 
@@ -232,7 +222,7 @@ class VirtualMigrator(Migrator):
         if not self._changed:
             return
 
-        formatversions.remove_versions(cfg_file)
+        component_version.remove_footer(cfg_file)
 
         self.write_config_file_versions(cfg_versions)
 
diff --git a/python/vyos/systemversions.py b/python/vyos/systemversions.py
deleted file mode 100644
index f2da76d4f..000000000
--- a/python/vyos/systemversions.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# 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/>.
-
-import os
-import re
-import sys
-import vyos.defaults
-from vyos.xml import component_version
-
-# legacy version, reading from the file names in
-# /opt/vyatta/etc/config-migrate/current
-def get_system_versions():
-    """
-    Get component versions from running system; critical failure if
-    unable to read migration directory.
-    """
-    system_versions = {}
-
-    try:
-        version_info = os.listdir(vyos.defaults.directories['current'])
-    except OSError as err:
-        print("OS error: {}".format(err))
-        sys.exit(1)
-
-    for info in version_info:
-        if re.match(r'[\w,-]+@\d+', info):
-            pair = info.split('@')
-            system_versions[pair[0]] = int(pair[1])
-
-    return system_versions
-
-# read from xml cache
-def get_system_component_version():
-    return component_version()
diff --git a/smoketest/scripts/cli/test_component_version.py b/smoketest/scripts/cli/test_component_version.py
index 777379bdd..21cc1c761 100755
--- a/smoketest/scripts/cli/test_component_version.py
+++ b/smoketest/scripts/cli/test_component_version.py
@@ -16,7 +16,7 @@
 
 import unittest
 
-from vyos.systemversions import get_system_versions, get_system_component_version
+from vyos.component_version import legacy_from_system, from_system
 
 # After T3474, component versions should be updated in the files in
 # vyos-1x/interface-definitions/include/version/
@@ -24,8 +24,8 @@ from vyos.systemversions import get_system_versions, get_system_component_versio
 # that in the xml cache.
 class TestComponentVersion(unittest.TestCase):
     def setUp(self):
-        self.legacy_d = get_system_versions()
-        self.xml_d = get_system_component_version()
+        self.legacy_d = legacy_from_system()
+        self.xml_d = from_system()
 
     def test_component_version(self):
         self.assertTrue(set(self.legacy_d).issubset(set(self.xml_d)))
diff --git a/src/helpers/system-versions-foot.py b/src/helpers/system-versions-foot.py
index 2aa687221..b44408542 100755
--- a/src/helpers/system-versions-foot.py
+++ b/src/helpers/system-versions-foot.py
@@ -16,24 +16,13 @@
 # along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
 import sys
-import vyos.formatversions as formatversions
-import vyos.systemversions as systemversions
 import vyos.defaults
-import vyos.version
-
-sys_versions = systemversions.get_system_component_version()
-
-component_string = formatversions.format_versions_string(sys_versions)
-
-os_version_string = vyos.version.get_version()
+from vyos.component_version import write_footer
 
 sys.stdout.write("\n\n")
 if vyos.defaults.cfg_vintage == 'vyos':
-    formatversions.write_vyos_versions_foot(None, component_string,
-                                            os_version_string)
+    write_footer(None, vintage='vyos')
 elif vyos.defaults.cfg_vintage == 'vyatta':
-    formatversions.write_vyatta_versions_foot(None, component_string,
-                                              os_version_string)
+    write_footer(None, vintage='vyatta')
 else:
-    formatversions.write_vyatta_versions_foot(None, component_string,
-                                              os_version_string)
+    write_footer(None, vintage='vyatta')
-- 
cgit v1.2.3


From c4d389488970c8510200cac96a67182e9333b891 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Tue, 8 Mar 2022 11:48:29 -0600
Subject: save-config: T4292: rewrite vyatta-save-config.pl to Python

---
 python/vyos/config.py           |  5 ++++
 python/vyos/configsession.py    |  2 +-
 src/helpers/vyos-save-config.py | 54 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 60 insertions(+), 1 deletion(-)
 create mode 100755 src/helpers/vyos-save-config.py

diff --git a/python/vyos/config.py b/python/vyos/config.py
index a5c1ad122..829167820 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -93,6 +93,11 @@ class Config(object):
         (self._running_config,
          self._session_config) = self._config_source.get_configtree_tuple()
 
+    def get_config_tree(self, effective=False):
+        if effective:
+            return self._running_config
+        return self._session_config
+
     def _make_path(self, path):
         # Backwards-compatibility stuff: original implementation used string paths
         # libvyosconfig paths are lists, but since node names cannot contain whitespace,
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index f28ad09c5..a7eda6e61 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -28,7 +28,7 @@ DISCARD = '/opt/vyatta/sbin/my_discard'
 SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig']
 LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile']
 MIGRATE_LOAD_CONFIG = ['/usr/libexec/vyos/vyos-load-config.py']
-SAVE_CONFIG = ['/opt/vyatta/sbin/vyatta-save-config.pl']
+SAVE_CONFIG = ['/usr/libexec/vyos/vyos-save-config.py']
 INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image', '--url']
 REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del']
 GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate']
diff --git a/src/helpers/vyos-save-config.py b/src/helpers/vyos-save-config.py
new file mode 100755
index 000000000..628d510dd
--- /dev/null
+++ b/src/helpers/vyos-save-config.py
@@ -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/>.
+#
+#
+import os
+import re
+import sys
+from tempfile import NamedTemporaryFile
+
+from vyos.config import Config
+from vyos.remote import urlc
+from vyos.component_version import system_footer
+
+DEFAULT_CONFIG_FILE = '/opt/vyatta/etc/config/config.boot'
+remote_save = None
+
+if len(sys.argv) > 1:
+    save_file = sys.argv[1]
+else:
+    save_file = DEFAULT_CONFIG_FILE
+
+if re.match(r'\w+:/', save_file):
+    try:
+        remote_save = urlc(save_file)
+    except ValueError as e:
+        sys.exit(e)
+
+config = Config()
+ct = config.get_config_tree(effective=True)
+
+write_file = save_file if remote_save is None else NamedTemporaryFile(delete=False).name
+with open(write_file, 'w') as f:
+    f.write(ct.to_string())
+    f.write("\n")
+    f.write(system_footer())
+
+if remote_save is not None:
+    try:
+        remote_save.upload(write_file)
+    finally:
+        os.remove(write_file)
-- 
cgit v1.2.3


From 2a4b45ba7fa4dabf7e592f499cfb06a7ae38cdea Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Wed, 9 Mar 2022 11:00:10 -0600
Subject: load-config: T4295: use config_tree instead of legacy loadFile

---
 src/helpers/vyos-load-config.py | 38 ++++++++++++++++++++++++++++++--------
 1 file changed, 30 insertions(+), 8 deletions(-)

diff --git a/src/helpers/vyos-load-config.py b/src/helpers/vyos-load-config.py
index e579e81b2..08f28b24e 100755
--- a/src/helpers/vyos-load-config.py
+++ b/src/helpers/vyos-load-config.py
@@ -29,15 +29,40 @@ import gzip
 import tempfile
 import vyos.defaults
 import vyos.remote
-from vyos.configsource import ConfigSourceSession, VyOSError
+from vyos.configsource import ConfigSourceSession
+from vyos.configtree import ConfigTree, DiffTree
 from vyos.migrator import Migrator, VirtualMigrator, MigratorError
+from vyos.util import cmd, DEVNULL
 
 class LoadConfig(ConfigSourceSession):
-    """A subclass for calling 'loadFile'.
+    """A subclass for loading a config file.
     This does not belong in configsource.py, and only has a single caller.
     """
-    def load_config(self, path):
-        return self._run(['/bin/cli-shell-api','loadFile',path])
+    def load_config(self, file_path):
+        try:
+            with open(file_path) as f:
+                config_file = f.read()
+            load_ct = ConfigTree(config_file)
+        except (OSError, ValueError) as e:
+            print(e)
+            return
+
+        eff_ct, _ = self.get_configtree_tuple()
+        diff = DiffTree(eff_ct, load_ct)
+        commands = diff.to_commands()
+        # on an empty set of 'add' or 'delete' commands, to_commands
+        # returns '\n'; prune below
+        command_list = commands.splitlines()
+        command_list = [c for c in command_list if c]
+
+        if not command_list:
+            return
+        for op in command_list:
+            try:
+                cmd(f'/opt/vyatta/sbin/my_{op}', shell=True, stderr=DEVNULL)
+            except OSError as e:
+                print(e)
+                return
 
 file_name = sys.argv[1] if len(sys.argv) > 1 else 'config.boot'
 configdir = vyos.defaults.directories['config']
@@ -93,10 +118,7 @@ with tempfile.NamedTemporaryFile() as fp:
     except MigratorError as err:
         sys.exit('{}'.format(err))
 
-    try:
-        config.load_config(fp.name)
-    except VyOSError as err:
-        sys.exit('{}'.format(err))
+    config.load_config(fp.name)
 
 if config.session_changed():
     print("Load complete. Use 'commit' to make changes effective.")
-- 
cgit v1.2.3


From 7549c847c3df9155c4315efcccfaf798af9fb402 Mon Sep 17 00:00:00 2001
From: Paul Lettington <paul@plett.co.uk>
Date: Wed, 9 Mar 2022 14:24:16 +0000
Subject: policy: T2493 ip-next-hop unchanged & peer-address

Also add ipv6-next-hop peer-address
---
 data/templates/frr/policy.frr.tmpl  |  3 +++
 interface-definitions/policy.xml.in | 16 ++++++++++++++++
 2 files changed, 19 insertions(+)

diff --git a/data/templates/frr/policy.frr.tmpl b/data/templates/frr/policy.frr.tmpl
index d3d3957a5..97eb15331 100644
--- a/data/templates/frr/policy.frr.tmpl
+++ b/data/templates/frr/policy.frr.tmpl
@@ -276,6 +276,9 @@ route-map {{ route_map }} {{ rule_config.action }} {{ rule }}
 {%           if rule_config.set.ipv6_next_hop is defined and rule_config.set.ipv6_next_hop.local is defined and rule_config.set.ipv6_next_hop.local is not none %}
  set ipv6 next-hop local {{ rule_config.set.ipv6_next_hop.local }}
 {%           endif %}
+{%           if rule_config.set.ipv6_next_hop is defined and rule_config.set.ipv6_next_hop.peer_address is defined %}
+ set ipv6 next-hop peer-address
+{%           endif %}
 {%           if rule_config.set.ipv6_next_hop is defined and rule_config.set.ipv6_next_hop.prefer_global is defined %}
  set ipv6 next-hop prefer-global
 {%           endif %}
diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in
index 9767285dd..5e037b558 100644
--- a/interface-definitions/policy.xml.in
+++ b/interface-definitions/policy.xml.in
@@ -1115,13 +1115,23 @@
                       <help>Nexthop IP address</help>
                       <completionHelp>
                         <script>${vyos_completion_dir}/list_local_ips.sh --ipv4</script>
+                        <list>unchanged peer-address</list>
                       </completionHelp>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>IP address</description>
                       </valueHelp>
+                      <valueHelp>
+                        <format>unchanged</format>
+                        <description>Set the BGP nexthop address as unchanged</description>
+                      </valueHelp>
+                      <valueHelp>
+                        <format>peer-address</format>
+                        <description>Set the BGP nexthop address to the address of the peer</description>
+                      </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
+                        <regex>^(unchanged|peer-address)$</regex>
                       </constraint>
                     </properties>
                   </leafNode>
@@ -1160,6 +1170,12 @@
                           </constraint>
                         </properties>
                       </leafNode>
+                      <leafNode name="peer-address">
+                        <properties>
+                          <help>Use peer address (for BGP only)</help>
+                          <valueless/>
+                        </properties>
+                      </leafNode>
                       <leafNode name="prefer-global">
                         <properties>
                           <help>Prefer global address as the nexthop</help>
-- 
cgit v1.2.3


From fde48b4d303b471f8c56b81bc7fcef0a028bb60a Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Thu, 10 Mar 2022 09:34:00 -0600
Subject: Revert "load-config: T4295: use config_tree instead of legacy
 loadFile"

This reverts commit 2a4b45ba7fa4dabf7e592f499cfb06a7ae38cdea.
Revert while investigating failure in vyos-configtest.
---
 src/helpers/vyos-load-config.py | 38 ++++++++------------------------------
 1 file changed, 8 insertions(+), 30 deletions(-)

diff --git a/src/helpers/vyos-load-config.py b/src/helpers/vyos-load-config.py
index 08f28b24e..e579e81b2 100755
--- a/src/helpers/vyos-load-config.py
+++ b/src/helpers/vyos-load-config.py
@@ -29,40 +29,15 @@ import gzip
 import tempfile
 import vyos.defaults
 import vyos.remote
-from vyos.configsource import ConfigSourceSession
-from vyos.configtree import ConfigTree, DiffTree
+from vyos.configsource import ConfigSourceSession, VyOSError
 from vyos.migrator import Migrator, VirtualMigrator, MigratorError
-from vyos.util import cmd, DEVNULL
 
 class LoadConfig(ConfigSourceSession):
-    """A subclass for loading a config file.
+    """A subclass for calling 'loadFile'.
     This does not belong in configsource.py, and only has a single caller.
     """
-    def load_config(self, file_path):
-        try:
-            with open(file_path) as f:
-                config_file = f.read()
-            load_ct = ConfigTree(config_file)
-        except (OSError, ValueError) as e:
-            print(e)
-            return
-
-        eff_ct, _ = self.get_configtree_tuple()
-        diff = DiffTree(eff_ct, load_ct)
-        commands = diff.to_commands()
-        # on an empty set of 'add' or 'delete' commands, to_commands
-        # returns '\n'; prune below
-        command_list = commands.splitlines()
-        command_list = [c for c in command_list if c]
-
-        if not command_list:
-            return
-        for op in command_list:
-            try:
-                cmd(f'/opt/vyatta/sbin/my_{op}', shell=True, stderr=DEVNULL)
-            except OSError as e:
-                print(e)
-                return
+    def load_config(self, path):
+        return self._run(['/bin/cli-shell-api','loadFile',path])
 
 file_name = sys.argv[1] if len(sys.argv) > 1 else 'config.boot'
 configdir = vyos.defaults.directories['config']
@@ -118,7 +93,10 @@ with tempfile.NamedTemporaryFile() as fp:
     except MigratorError as err:
         sys.exit('{}'.format(err))
 
-    config.load_config(fp.name)
+    try:
+        config.load_config(fp.name)
+    except VyOSError as err:
+        sys.exit('{}'.format(err))
 
 if config.session_changed():
     print("Load complete. Use 'commit' to make changes effective.")
-- 
cgit v1.2.3


From ef4870e66f8c1cd6df5fba2abbfdde0eac152e0a Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Thu, 10 Mar 2022 10:07:19 -0600
Subject: Revert "save-config: T4292: rewrite vyatta-save-config.pl to Python"

This reverts commit c4d389488970c8510200cac96a67182e9333b891.
Revert while investigating failure in vyos-configtest.
---
 python/vyos/config.py           |  5 ----
 python/vyos/configsession.py    |  2 +-
 src/helpers/vyos-save-config.py | 54 -----------------------------------------
 3 files changed, 1 insertion(+), 60 deletions(-)
 delete mode 100755 src/helpers/vyos-save-config.py

diff --git a/python/vyos/config.py b/python/vyos/config.py
index 829167820..a5c1ad122 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -93,11 +93,6 @@ class Config(object):
         (self._running_config,
          self._session_config) = self._config_source.get_configtree_tuple()
 
-    def get_config_tree(self, effective=False):
-        if effective:
-            return self._running_config
-        return self._session_config
-
     def _make_path(self, path):
         # Backwards-compatibility stuff: original implementation used string paths
         # libvyosconfig paths are lists, but since node names cannot contain whitespace,
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index a7eda6e61..f28ad09c5 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -28,7 +28,7 @@ DISCARD = '/opt/vyatta/sbin/my_discard'
 SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig']
 LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile']
 MIGRATE_LOAD_CONFIG = ['/usr/libexec/vyos/vyos-load-config.py']
-SAVE_CONFIG = ['/usr/libexec/vyos/vyos-save-config.py']
+SAVE_CONFIG = ['/opt/vyatta/sbin/vyatta-save-config.pl']
 INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image', '--url']
 REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del']
 GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate']
diff --git a/src/helpers/vyos-save-config.py b/src/helpers/vyos-save-config.py
deleted file mode 100755
index 628d510dd..000000000
--- a/src/helpers/vyos-save-config.py
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2022 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-#
-import os
-import re
-import sys
-from tempfile import NamedTemporaryFile
-
-from vyos.config import Config
-from vyos.remote import urlc
-from vyos.component_version import system_footer
-
-DEFAULT_CONFIG_FILE = '/opt/vyatta/etc/config/config.boot'
-remote_save = None
-
-if len(sys.argv) > 1:
-    save_file = sys.argv[1]
-else:
-    save_file = DEFAULT_CONFIG_FILE
-
-if re.match(r'\w+:/', save_file):
-    try:
-        remote_save = urlc(save_file)
-    except ValueError as e:
-        sys.exit(e)
-
-config = Config()
-ct = config.get_config_tree(effective=True)
-
-write_file = save_file if remote_save is None else NamedTemporaryFile(delete=False).name
-with open(write_file, 'w') as f:
-    f.write(ct.to_string())
-    f.write("\n")
-    f.write(system_footer())
-
-if remote_save is not None:
-    try:
-        remote_save.upload(write_file)
-    finally:
-        os.remove(write_file)
-- 
cgit v1.2.3


From c1e04cea0817db07b22a8bd8e6b2f2c0a1e682f4 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Thu, 10 Mar 2022 10:07:31 -0600
Subject: Revert "component_version: T4291: consolidate read/write functions"

This reverts commit 534f677d36285863decb2cdff179687b4fd690cb.
Revert while investigating failure in vyos-configtest.
---
 python/vyos/component_version.py                | 174 ------------------------
 python/vyos/component_versions.py               |  57 ++++++++
 python/vyos/formatversions.py                   | 109 +++++++++++++++
 python/vyos/migrator.py                         |  26 ++--
 python/vyos/systemversions.py                   |  46 +++++++
 smoketest/scripts/cli/test_component_version.py |   6 +-
 src/helpers/system-versions-foot.py             |  19 ++-
 7 files changed, 248 insertions(+), 189 deletions(-)
 delete mode 100644 python/vyos/component_version.py
 create mode 100644 python/vyos/component_versions.py
 create mode 100644 python/vyos/formatversions.py
 create mode 100644 python/vyos/systemversions.py

diff --git a/python/vyos/component_version.py b/python/vyos/component_version.py
deleted file mode 100644
index b1554828d..000000000
--- a/python/vyos/component_version.py
+++ /dev/null
@@ -1,174 +0,0 @@
-# Copyright 2022 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/>.
-
-"""
-Functions for reading/writing component versions.
-
-The config file version string has the following form:
-
-VyOS 1.3/1.4:
-
-// Warning: Do not remove the following line.
-// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1"
-// Release version: 1.3.0
-
-VyOS 1.2:
-
-/* Warning: Do not remove the following line. */
-/* === vyatta-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@2:dhcp-server@5:dns-forwarding@1:firewall@5:ipsec@5:l2tp@1:mdns@1:nat@4:ntp@1:pppoe-server@2:pptp@1:qos@1:quagga@7:snmp@1:ssh@1:system@10:vrrp@2:wanloadbalance@3:webgui@1:webproxy@2:zone-policy@1" === */
-/* Release version: 1.2.8 */
-
-"""
-
-import os
-import re
-import sys
-import fileinput
-from vyos.xml import component_version
-from vyos.version import get_version
-
-def from_string(string_line, vintage='vyos'):
-    """
-    Get component version dictionary from string.
-    Return empty dictionary if string contains no config information
-    or raise error if component version string malformed.
-    """
-    version_dict = {}
-
-    if vintage == 'vyos':
-        if re.match(r'// vyos-config-version:.+', string_line):
-            if not re.match(r'// vyos-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s*', string_line):
-                raise ValueError(f"malformed configuration string: {string_line}")
-
-            for pair in re.findall(r'([\w,-]+)@(\d+)', string_line):
-                version_dict[pair[0]] = int(pair[1])
-
-    elif vintage == 'vyatta':
-        if re.match(r'/\* === vyatta-config-version:.+=== \*/$', string_line):
-            if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', string_line):
-                raise ValueError(f"malformed configuration string: {string_line}")
-
-            for pair in re.findall(r'([\w,-]+)@(\d+)', string_line):
-                version_dict[pair[0]] = int(pair[1])
-    else:
-        raise ValueError("Unknown config string vintage")
-
-    return version_dict
-
-def from_file(config_file_name='/opt/vyatta/etc/config/config.boot',
-                      vintage='vyos'):
-    """
-    Get component version dictionary parsing config file line by line
-    """
-    f = open(config_file_name, 'r')
-    for line_in_config in f:
-        version_dict = from_string(line_in_config, vintage=vintage)
-        if version_dict:
-            return version_dict
-
-    # no version information
-    return {}
-
-def from_system():
-    """
-    Get system component version dict.
-    """
-    return component_version()
-
-def legacy_from_system():
-    """
-    legacy function; imported as-is.
-
-    Get component versions from running system; critical failure if
-    unable to read migration directory.
-    """
-    import vyos.defaults
-
-    system_versions = {}
-
-    try:
-        version_info = os.listdir(vyos.defaults.directories['current'])
-    except OSError as err:
-        print("OS error: {}".format(err))
-        sys.exit(1)
-
-    for info in version_info:
-        if re.match(r'[\w,-]+@\d+', info):
-            pair = info.split('@')
-            system_versions[pair[0]] = int(pair[1])
-
-    return system_versions
-
-def format_string(ver: dict) -> str:
-    """
-    Version dict to string.
-    """
-    keys = list(ver)
-    keys.sort()
-    l = []
-    for k in keys:
-        v = ver[k]
-        l.append(f'{k}@{v}')
-    sep = ':'
-    return sep.join(l)
-
-def system_footer(vintage='vyos') -> str:
-    """
-    Version footer as string.
-    """
-    ver_str = format_string(from_system())
-    release = get_version()
-    if vintage == 'vyos':
-        ret_str = (f'// Warning: Do not remove the following line.\n'
-                +  f'// vyos-config-version: "{ver_str}"\n'
-                +  f'// Release version: {release}\n')
-    elif vintage == 'vyatta':
-        ret_str = (f'/* Warning: Do not remove the following line. */\n'
-                +  f'/* === vyatta-config-version: "{ver_str}" === */\n'
-                +  f'/* Release version: {release} */\n')
-    else:
-        raise ValueError("Unknown config string vintage")
-
-    return ret_str
-
-def write_footer(file_name, vintage='vyos'):
-    """
-    Write version footer to file.
-    """
-    footer = system_footer(vintage=vintage)
-    if file_name:
-        with open(file_name, 'a') as f:
-            f.write(footer)
-    else:
-        sys.stdout.write(footer)
-
-def remove_footer(file_name):
-    """
-    Remove old version footer.
-    """
-    for line in fileinput.input(file_name, inplace=True):
-        if re.match(r'/\* Warning:.+ \*/$', line):
-            continue
-        if re.match(r'/\* === vyatta-config-version:.+=== \*/$', line):
-            continue
-        if re.match(r'/\* Release version:.+ \*/$', line):
-            continue
-        if re.match('// vyos-config-version:.+', line):
-            continue
-        if re.match('// Warning:.+', line):
-            continue
-        if re.match('// Release version:.+', line):
-            continue
-        sys.stdout.write(line)
diff --git a/python/vyos/component_versions.py b/python/vyos/component_versions.py
new file mode 100644
index 000000000..90b458aae
--- /dev/null
+++ b/python/vyos/component_versions.py
@@ -0,0 +1,57 @@
+# Copyright 2017 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/>.
+
+"""
+The version data looks like:
+
+/* Warning: Do not remove the following line. */
+/* === vyatta-config-version:
+"cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@1:dhcp-server@4:firewall@5:ipsec@4:nat@4:qos@1:quagga@2:system@8:vrrp@1:wanloadbalance@3:webgui@1:webproxy@1:zone-policy@1"
+=== */
+/* Release version: 1.2.0-rolling+201806131737 */
+"""
+
+import re
+
+def get_component_version(string_line):
+    """
+    Get component version dictionary from string
+    return empty dictionary if string contains no config information
+    or raise error if component version string malformed
+    """
+    return_value = {}
+    if re.match(r'/\* === vyatta-config-version:.+=== \*/$', string_line):
+
+        if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', string_line):
+            raise ValueError("malformed configuration string: " + str(string_line))
+
+        for pair in re.findall(r'([\w,-]+)@(\d+)', string_line):
+            if pair[0] in return_value.keys():
+                raise ValueError("duplicate unit name: \"" + str(pair[0]) + "\" in string: \"" + string_line + "\"")
+            return_value[pair[0]] = int(pair[1])
+
+    return return_value
+
+
+def get_component_versions_from_file(config_file_name='/opt/vyatta/etc/config/config.boot'):
+    """
+    Get component version dictionary parsing config file line by line
+    """
+    f = open(config_file_name, 'r')
+    for line_in_config in f:
+        component_version = get_component_version(line_in_config)
+        if component_version:
+            return component_version
+    raise ValueError("no config string in file:", config_file_name)
diff --git a/python/vyos/formatversions.py b/python/vyos/formatversions.py
new file mode 100644
index 000000000..29117a5d3
--- /dev/null
+++ b/python/vyos/formatversions.py
@@ -0,0 +1,109 @@
+# 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/>.
+
+import sys
+import os
+import re
+import fileinput
+
+def read_vyatta_versions(config_file):
+    config_file_versions = {}
+
+    with open(config_file, 'r') as config_file_handle:
+        for config_line in config_file_handle:
+            if re.match(r'/\* === vyatta-config-version:.+=== \*/$', config_line):
+                if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', config_line):
+                    raise ValueError("malformed configuration string: "
+                            "{}".format(config_line))
+
+                for pair in re.findall(r'([\w,-]+)@(\d+)', config_line):
+                    config_file_versions[pair[0]] = int(pair[1])
+
+
+    return config_file_versions
+
+def read_vyos_versions(config_file):
+    config_file_versions = {}
+
+    with open(config_file, 'r') as config_file_handle:
+        for config_line in config_file_handle:
+            if re.match(r'// vyos-config-version:.+', config_line):
+                if not re.match(r'// vyos-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s*', config_line):
+                    raise ValueError("malformed configuration string: "
+                            "{}".format(config_line))
+
+                for pair in re.findall(r'([\w,-]+)@(\d+)', config_line):
+                    config_file_versions[pair[0]] = int(pair[1])
+
+    return config_file_versions
+
+def remove_versions(config_file):
+    """
+    Remove old version string.
+    """
+    for line in fileinput.input(config_file, inplace=True):
+        if re.match(r'/\* Warning:.+ \*/$', line):
+            continue
+        if re.match(r'/\* === vyatta-config-version:.+=== \*/$', line):
+            continue
+        if re.match(r'/\* Release version:.+ \*/$', line):
+            continue
+        if re.match('// vyos-config-version:.+', line):
+            continue
+        if re.match('// Warning:.+', line):
+            continue
+        if re.match('// Release version:.+', line):
+            continue
+        sys.stdout.write(line)
+
+def format_versions_string(config_versions):
+    cfg_keys = list(config_versions.keys())
+    cfg_keys.sort()
+
+    component_version_strings = []
+
+    for key in cfg_keys:
+        cfg_vers = config_versions[key]
+        component_version_strings.append('{}@{}'.format(key, cfg_vers))
+
+    separator = ":"
+    component_version_string = separator.join(component_version_strings)
+
+    return component_version_string
+
+def write_vyatta_versions_foot(config_file, component_version_string,
+                                 os_version_string):
+    if config_file:
+        with open(config_file, 'a') as config_file_handle:
+            config_file_handle.write('/* Warning: Do not remove the following line. */\n')
+            config_file_handle.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string))
+            config_file_handle.write('/* Release version: {} */\n'.format(os_version_string))
+    else:
+        sys.stdout.write('/* Warning: Do not remove the following line. */\n')
+        sys.stdout.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string))
+        sys.stdout.write('/* Release version: {} */\n'.format(os_version_string))
+
+def write_vyos_versions_foot(config_file, component_version_string,
+                               os_version_string):
+    if config_file:
+        with open(config_file, 'a') as config_file_handle:
+            config_file_handle.write('// Warning: Do not remove the following line.\n')
+            config_file_handle.write('// vyos-config-version: "{}"\n'.format(component_version_string))
+            config_file_handle.write('// Release version: {}\n'.format(os_version_string))
+    else:
+        sys.stdout.write('// Warning: Do not remove the following line.\n')
+        sys.stdout.write('// vyos-config-version: "{}"\n'.format(component_version_string))
+        sys.stdout.write('// Release version: {}\n'.format(os_version_string))
+
diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py
index 266a2e58e..a2e0daabd 100644
--- a/python/vyos/migrator.py
+++ b/python/vyos/migrator.py
@@ -17,8 +17,10 @@ import sys
 import os
 import json
 import subprocess
+import vyos.version
 import vyos.defaults
-import vyos.component_version as component_version
+import vyos.systemversions as systemversions
+import vyos.formatversions as formatversions
 
 class MigratorError(Exception):
     pass
@@ -40,13 +42,13 @@ class Migrator(object):
         cfg_file = self._config_file
         component_versions = {}
 
-        cfg_versions = component_version.from_file(cfg_file, vintage='vyatta')
+        cfg_versions = formatversions.read_vyatta_versions(cfg_file)
 
         if cfg_versions:
             self._config_file_vintage = 'vyatta'
             component_versions = cfg_versions
 
-        cfg_versions = component_version.from_file(cfg_file, vintage='vyos')
+        cfg_versions = formatversions.read_vyos_versions(cfg_file)
 
         if cfg_versions:
             self._config_file_vintage = 'vyos'
@@ -150,11 +152,19 @@ class Migrator(object):
         """
         Write new versions string.
         """
+        versions_string = formatversions.format_versions_string(cfg_versions)
+
+        os_version_string = vyos.version.get_version()
+
         if self._config_file_vintage == 'vyatta':
-            component_version.write_footer(self._config_file, vintage='vyatta')
+            formatversions.write_vyatta_versions_foot(self._config_file,
+                                                      versions_string,
+                                                      os_version_string)
 
         if self._config_file_vintage == 'vyos':
-            component_version.write_footer(self._config_file, vintage='vyos')
+            formatversions.write_vyos_versions_foot(self._config_file,
+                                                    versions_string,
+                                                    os_version_string)
 
     def save_json_record(self, component_versions: dict):
         """
@@ -185,7 +195,7 @@ class Migrator(object):
             # This will force calling all migration scripts:
             cfg_versions = {}
 
-        sys_versions = component_version.from_system()
+        sys_versions = systemversions.get_system_component_version()
 
         # save system component versions in json file for easy reference
         self.save_json_record(sys_versions)
@@ -201,7 +211,7 @@ class Migrator(object):
         if not self._changed:
             return
 
-        component_version.remove_footer(cfg_file)
+        formatversions.remove_versions(cfg_file)
 
         self.write_config_file_versions(rev_versions)
 
@@ -222,7 +232,7 @@ class VirtualMigrator(Migrator):
         if not self._changed:
             return
 
-        component_version.remove_footer(cfg_file)
+        formatversions.remove_versions(cfg_file)
 
         self.write_config_file_versions(cfg_versions)
 
diff --git a/python/vyos/systemversions.py b/python/vyos/systemversions.py
new file mode 100644
index 000000000..f2da76d4f
--- /dev/null
+++ b/python/vyos/systemversions.py
@@ -0,0 +1,46 @@
+# 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/>.
+
+import os
+import re
+import sys
+import vyos.defaults
+from vyos.xml import component_version
+
+# legacy version, reading from the file names in
+# /opt/vyatta/etc/config-migrate/current
+def get_system_versions():
+    """
+    Get component versions from running system; critical failure if
+    unable to read migration directory.
+    """
+    system_versions = {}
+
+    try:
+        version_info = os.listdir(vyos.defaults.directories['current'])
+    except OSError as err:
+        print("OS error: {}".format(err))
+        sys.exit(1)
+
+    for info in version_info:
+        if re.match(r'[\w,-]+@\d+', info):
+            pair = info.split('@')
+            system_versions[pair[0]] = int(pair[1])
+
+    return system_versions
+
+# read from xml cache
+def get_system_component_version():
+    return component_version()
diff --git a/smoketest/scripts/cli/test_component_version.py b/smoketest/scripts/cli/test_component_version.py
index 21cc1c761..777379bdd 100755
--- a/smoketest/scripts/cli/test_component_version.py
+++ b/smoketest/scripts/cli/test_component_version.py
@@ -16,7 +16,7 @@
 
 import unittest
 
-from vyos.component_version import legacy_from_system, from_system
+from vyos.systemversions import get_system_versions, get_system_component_version
 
 # After T3474, component versions should be updated in the files in
 # vyos-1x/interface-definitions/include/version/
@@ -24,8 +24,8 @@ from vyos.component_version import legacy_from_system, from_system
 # that in the xml cache.
 class TestComponentVersion(unittest.TestCase):
     def setUp(self):
-        self.legacy_d = legacy_from_system()
-        self.xml_d = from_system()
+        self.legacy_d = get_system_versions()
+        self.xml_d = get_system_component_version()
 
     def test_component_version(self):
         self.assertTrue(set(self.legacy_d).issubset(set(self.xml_d)))
diff --git a/src/helpers/system-versions-foot.py b/src/helpers/system-versions-foot.py
index b44408542..2aa687221 100755
--- a/src/helpers/system-versions-foot.py
+++ b/src/helpers/system-versions-foot.py
@@ -16,13 +16,24 @@
 # along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
 import sys
+import vyos.formatversions as formatversions
+import vyos.systemversions as systemversions
 import vyos.defaults
-from vyos.component_version import write_footer
+import vyos.version
+
+sys_versions = systemversions.get_system_component_version()
+
+component_string = formatversions.format_versions_string(sys_versions)
+
+os_version_string = vyos.version.get_version()
 
 sys.stdout.write("\n\n")
 if vyos.defaults.cfg_vintage == 'vyos':
-    write_footer(None, vintage='vyos')
+    formatversions.write_vyos_versions_foot(None, component_string,
+                                            os_version_string)
 elif vyos.defaults.cfg_vintage == 'vyatta':
-    write_footer(None, vintage='vyatta')
+    formatversions.write_vyatta_versions_foot(None, component_string,
+                                              os_version_string)
 else:
-    write_footer(None, vintage='vyatta')
+    formatversions.write_vyatta_versions_foot(None, component_string,
+                                              os_version_string)
-- 
cgit v1.2.3


From 2894b52454311f8e011bed910704064be7471275 Mon Sep 17 00:00:00 2001
From: Daniel Berlin <dberlin@dberlin.org>
Date: Fri, 11 Mar 2022 11:01:04 -0500
Subject: [Ethtool]  T4297: Update drivers supporting speed/flow/duplex

The iavf, ice, and i40e drivers do not support speed, flow, or duplex control using ethtool.
As a result, interface configuration changes fail to commit when using those drivers.
This patch fixes that by correctly marking those drivers as not supporting those controls.
---
 python/vyos/ethtool.py | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py
index 80d44eee6..672ee2cc9 100644
--- a/python/vyos/ethtool.py
+++ b/python/vyos/ethtool.py
@@ -18,6 +18,9 @@ import re
 
 from vyos.util import popen
 
+# These drivers do not support using ethtool to change the speed, duplex, or flow control settings
+_drivers_without_speed_duplex_flow = ['vmxnet3', 'virtio_net', 'xen_netfront', 'iavf', 'ice', 'i40e']
+
 class Ethtool:
     """
     Class is used to retrive and cache information about an ethernet adapter
@@ -188,7 +191,7 @@ class Ethtool:
         if duplex not in ['auto', 'full', 'half']:
             raise ValueError(f'Value "{duplex}" for duplex is invalid!')
 
-        if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']:
+        if self.get_driver_name() in _drivers_without_speed_duplex_flow:
             return False
 
         if speed in self._speed_duplex:
@@ -198,7 +201,7 @@ class Ethtool:
 
     def check_flow_control(self):
         """ Check if the NIC supports flow-control """
-        if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']:
+        if self.get_driver_name() in _drivers_without_speed_duplex_flow:
             return False
         return self._flow_control
 
-- 
cgit v1.2.3


From b1d4be53cd133e9a63f8e29e400f1d7bf18b8384 Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <vgletenko@vyos.io>
Date: Fri, 11 Mar 2022 18:11:44 +0000
Subject: bgp: T4265: Add op-mode for bgp flowspec routes

---
 .../include/bgp/afi-ipv4-ipv6-flowspec.xml.i       | 25 ++++++++++++++++++++++
 .../include/bgp/show-bgp-common.xml.i              |  1 +
 2 files changed, 26 insertions(+)
 create mode 100644 op-mode-definitions/include/bgp/afi-ipv4-ipv6-flowspec.xml.i

diff --git a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-flowspec.xml.i b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-flowspec.xml.i
new file mode 100644
index 000000000..34228fdd1
--- /dev/null
+++ b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-flowspec.xml.i
@@ -0,0 +1,25 @@
+<!-- included start from bgp/afi-ipv4-ipv6-flowspec.xml.i -->
+<tagNode name="flowspec">
+  <properties>
+    <help>Network in the BGP routing table to display</help>
+    <completionHelp>
+      <list>&lt;x.x.x.x&gt; &lt;x.x.x.x/x&gt; &lt;h:h:h:h:h:h:h:h&gt; &lt;h:h:h:h:h:h:h:h/x&gt;</list>
+    </completionHelp>
+  </properties>
+  <children>
+    #include <include/bgp/prefix-bestpath-multipath.xml.i>
+  </children>
+  <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+</tagNode>
+<node name="flowspec">
+  <properties>
+    <help>Flowspec Address Family modifier</help>
+  </properties>
+  <children>
+    #include <include/bgp/afi-common.xml.i>
+    #include <include/bgp/afi-ipv4-ipv6-common.xml.i>
+    #include <include/vtysh-generic-detail.xml.i>
+  </children>
+  <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+</node>
+<!-- included end -->
diff --git a/op-mode-definitions/include/bgp/show-bgp-common.xml.i b/op-mode-definitions/include/bgp/show-bgp-common.xml.i
index e81b26b3e..c9a112fca 100644
--- a/op-mode-definitions/include/bgp/show-bgp-common.xml.i
+++ b/op-mode-definitions/include/bgp/show-bgp-common.xml.i
@@ -20,6 +20,7 @@
   <children>
     #include <include/bgp/afi-common.xml.i>
     #include <include/bgp/afi-ipv4-ipv6-common.xml.i>
+    #include <include/bgp/afi-ipv4-ipv6-flowspec.xml.i>
     #include <include/bgp/afi-ipv4-ipv6-vpn.xml.i>
   </children>
   <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
-- 
cgit v1.2.3


From ff0e43807789f3c5c228683eaeb5fc4fbb8f75ce Mon Sep 17 00:00:00 2001
From: Nicolas Fort <nicolasfort1988@gmail.com>
Date: Sat, 12 Mar 2022 15:10:52 +0000
Subject: Firewall: T4286: Correct ipv6-range validator

---
 src/validators/ipv6-range | 31 +++++++++++++++++--------------
 1 file changed, 17 insertions(+), 14 deletions(-)

diff --git a/src/validators/ipv6-range b/src/validators/ipv6-range
index a3c401281..7080860c4 100755
--- a/src/validators/ipv6-range
+++ b/src/validators/ipv6-range
@@ -1,17 +1,20 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
 
-import sys
-import re
-from vyos.template import is_ipv6
+from ipaddress import IPv6Address
+from sys import argv, exit
 
 if __name__ == '__main__':
-    if len(sys.argv)>1:
-        ipv6_range = sys.argv[1]
-        # Regex for ipv6-ipv6 https://regexr.com/
-        if re.search('([a-f0-9:]+:+)+[a-f0-9]+-([a-f0-9:]+:+)+[a-f0-9]+', ipv6_range):
-            for tmp in ipv6_range.split('-'):
-                if not is_ipv6(tmp):
-                    print(f'Error: {ipv6_range} is not a valid IPv6 range')
-                    sys.exit(1)
-
-    sys.exit(0)
+    if len(argv) > 1:
+        # try to pass validation and raise an error if failed
+        try:
+            ipv6_range = argv[1]
+            range_left = ipv6_range.split('-')[0]
+            range_right = ipv6_range.split('-')[1]
+            if not IPv6Address(range_left) < IPv6Address(range_right):
+                raise ValueError(f'left element {range_left} must be less than right element {range_right}')
+        except Exception as err:
+            print(f'Error: {ipv6_range} is not a valid IPv6 range: {err}')
+            exit(1)
+    else:
+        print('Error: an IPv6 range argument must be provided')
+        exit(1)
-- 
cgit v1.2.3


From a7a7e38049d4601d55dd032b7d3aecf96c7e8781 Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <vgletenko@vyos.io>
Date: Sun, 13 Mar 2022 12:48:46 +0000
Subject: bgp: T4290: Add verify source-interface for none ip neighbor

When we use neighbor as interface we must not use option
'source-interface'
for example:

neighbor eth0 source-interface eth0

Such option can be used for IP/IPv6 neighbors
---
 src/conf_mode/protocols_bgp.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index d8704727c..9e59177a8 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -166,6 +166,8 @@ def verify(bgp):
                         raise ConfigError(f'peer-group must be set under the interface node of "{peer}"')
                     if 'remote_as' in peer_config:
                         raise ConfigError(f'remote-as must be set under the interface node of "{peer}"')
+                    if 'source_interface' in peer_config['interface']:
+                        raise ConfigError(f'"source-interface" option not allowed for neighbor "{peer}"')
 
             for afi in ['ipv4_unicast', 'ipv4_multicast', 'ipv4_labeled_unicast', 'ipv4_flowspec',
                         'ipv6_unicast', 'ipv6_multicast', 'ipv6_labeled_unicast', 'ipv6_flowspec',
-- 
cgit v1.2.3


From df4b544c29974e36b52fc42bcbf617f50738a4a6 Mon Sep 17 00:00:00 2001
From: zsdc <taras@vyos.io>
Date: Tue, 15 Mar 2022 18:29:12 +0200
Subject: bonding: T4301: Fixed arp-monitor option

In verify function for arp-monitor option was used by mistake an extra
conversion for incoming data before comparing items. This commit removed
these unnecessary conversions and makes the option operable.
---
 src/conf_mode/interfaces-bonding.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 431d65f1f..d5be21949 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -132,10 +132,10 @@ def verify(bond):
         return None
 
     if 'arp_monitor' in bond:
-        if 'target' in bond['arp_monitor'] and len(int(bond['arp_monitor']['target'])) > 16:
+        if 'target' in bond['arp_monitor'] and len(bond['arp_monitor']['target']) > 16:
             raise ConfigError('The maximum number of arp-monitor targets is 16')
 
-        if 'interval' in bond['arp_monitor'] and len(int(bond['arp_monitor']['interval'])) > 0:
+        if 'interval' in bond['arp_monitor'] and int(bond['arp_monitor']['interval']) > 0:
             if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']:
                 raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \
                                   'transmit-load-balance or adaptive-load-balance')
-- 
cgit v1.2.3


From fd9cb1574f2ef9bca648c040074820635ad301b1 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Tue, 15 Mar 2022 21:07:11 +0100
Subject: frr: T4302: upgrade to version 8.2

---
 data/templates/frr/policy.frr.tmpl   | 2 +-
 smoketest/scripts/cli/test_policy.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/data/templates/frr/policy.frr.tmpl b/data/templates/frr/policy.frr.tmpl
index 97eb15331..60e15f4fd 100644
--- a/data/templates/frr/policy.frr.tmpl
+++ b/data/templates/frr/policy.frr.tmpl
@@ -204,7 +204,7 @@ route-map {{ route_map }} {{ rule_config.action }} {{ rule }}
  match ipv6 address prefix-list {{ rule_config.match.ipv6.address.prefix_list }}
 {%           endif %}
 {%           if rule_config.match.ipv6 is defined and rule_config.match.ipv6.nexthop is defined and rule_config.match.ipv6.nexthop is not none %}
- match ipv6 next-hop {{ rule_config.match.ipv6.nexthop }}
+ match ipv6 next-hop address {{ rule_config.match.ipv6.nexthop }}
 {%           endif %}
 {%           if rule_config.match.large_community is defined and rule_config.match.large_community.large_community_list is defined and rule_config.match.large_community.large_community_list is not none %}
  match large-community {{ rule_config.match.large_community.large_community_list }}
diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py
index 491f1766d..0acd41903 100755
--- a/smoketest/scripts/cli/test_policy.py
+++ b/smoketest/scripts/cli/test_policy.py
@@ -1030,7 +1030,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):
                         tmp = f'match ipv6 address prefix-list {rule_config["match"]["ipv6-address-pfx"]}'
                         self.assertIn(tmp, config)
                     if 'ipv6-nexthop' in rule_config['match']:
-                        tmp = f'match ipv6 next-hop {rule_config["match"]["ipv6-nexthop"]}'
+                        tmp = f'match ipv6 next-hop address {rule_config["match"]["ipv6-nexthop"]}'
                         self.assertIn(tmp, config)
                     if 'large-community' in rule_config['match']:
                         tmp = f'match large-community {rule_config["match"]["large-community"]}'
-- 
cgit v1.2.3


From a5ae8f831fef4d7f9778f25c1c60e75d50c190ac Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Wed, 16 Mar 2022 07:38:13 +0100
Subject: smoketest: remove failfast=True from certian tests

---
 smoketest/scripts/cli/test_nat66.py            | 2 +-
 smoketest/scripts/cli/test_protocols_mpls.py   | 2 +-
 smoketest/scripts/cli/test_protocols_ospfv3.py | 2 +-
 smoketest/scripts/cli/test_system_login.py     | 2 +-
 smoketest/scripts/cli/test_system_logs.py      | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py
index 8afe0da26..6b7b49792 100755
--- a/smoketest/scripts/cli/test_nat66.py
+++ b/smoketest/scripts/cli/test_nat66.py
@@ -185,4 +185,4 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase):
         self.cli_commit()
 
 if __name__ == '__main__':
-    unittest.main(verbosity=2, failfast=True)
+    unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_mpls.py b/smoketest/scripts/cli/test_protocols_mpls.py
index 13d38d01b..b60f0725a 100755
--- a/smoketest/scripts/cli/test_protocols_mpls.py
+++ b/smoketest/scripts/cli/test_protocols_mpls.py
@@ -114,4 +114,4 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase):
             self.assertIn(f'  interface {interface}', afiv4_config)
 
 if __name__ == '__main__':
-    unittest.main(verbosity=2, failfast=True)
+    unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_ospfv3.py b/smoketest/scripts/cli/test_protocols_ospfv3.py
index 1327fd910..114733002 100755
--- a/smoketest/scripts/cli/test_protocols_ospfv3.py
+++ b/smoketest/scripts/cli/test_protocols_ospfv3.py
@@ -278,4 +278,4 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
         self.cli_delete(['interfaces', 'ethernet', vrf_iface, 'vrf'])
 
 if __name__ == '__main__':
-    unittest.main(verbosity=2, failfast=True)
+    unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py
index 69a06eeac..095300de3 100755
--- a/smoketest/scripts/cli/test_system_login.py
+++ b/smoketest/scripts/cli/test_system_login.py
@@ -235,4 +235,4 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
         self.assertTrue(tmp)
 
 if __name__ == '__main__':
-    unittest.main(verbosity=2, failfast=True)
+    unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_logs.py b/smoketest/scripts/cli/test_system_logs.py
index 0c11c4663..92fa9c3d9 100755
--- a/smoketest/scripts/cli/test_system_logs.py
+++ b/smoketest/scripts/cli/test_system_logs.py
@@ -114,4 +114,4 @@ class TestSystemLogs(VyOSUnitTestSHIM.TestCase):
 
 
 if __name__ == '__main__':
-    unittest.main(verbosity=2, failfast=True)
+    unittest.main(verbosity=2)
-- 
cgit v1.2.3


From 71805191d1e663af47ac1c2c11f7861d84677525 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Wed, 16 Mar 2022 20:32:28 +0100
Subject: frr: T4302: fix Jinja2 template to match new FRR syntax

According to a wrong bug [1] there is no longer a vrf suffix available for
interfaces. This got changed in [2] which no longer print vrf name for
interface config when using vrf-lite.

1: https://github.com/FRRouting/frr/issues/10805
2: https://github.com/FRRouting/frr/pull/10411
---
 data/templates/frr/isisd.frr.tmpl              | 2 +-
 data/templates/frr/ospf6d.frr.tmpl             | 2 +-
 data/templates/frr/ospfd.frr.tmpl              | 2 +-
 smoketest/scripts/cli/test_protocols_ospfv3.py | 4 ++--
 4 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/data/templates/frr/isisd.frr.tmpl b/data/templates/frr/isisd.frr.tmpl
index b1e3f825b..c68dda443 100644
--- a/data/templates/frr/isisd.frr.tmpl
+++ b/data/templates/frr/isisd.frr.tmpl
@@ -1,7 +1,7 @@
 !
 {% if interface is defined and interface is not none %}
 {%   for iface, iface_config in interface.items() %}
-interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
+interface {{ iface }}
  ip router isis VyOS
  ipv6 router isis VyOS
 {%     if iface_config.bfd is defined %}
diff --git a/data/templates/frr/ospf6d.frr.tmpl b/data/templates/frr/ospf6d.frr.tmpl
index 8279e5abb..a73c6cac3 100644
--- a/data/templates/frr/ospf6d.frr.tmpl
+++ b/data/templates/frr/ospf6d.frr.tmpl
@@ -1,7 +1,7 @@
 !
 {% if interface is defined and interface is not none %}
 {%   for iface, iface_config in interface.items() %}
-interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
+interface {{ iface }}
 {%     if iface_config.area is defined and iface_config.area is not none %}
  ipv6 ospf6 area {{ iface_config.area }}
 {%     endif %}
diff --git a/data/templates/frr/ospfd.frr.tmpl b/data/templates/frr/ospfd.frr.tmpl
index a6618b6af..12213f162 100644
--- a/data/templates/frr/ospfd.frr.tmpl
+++ b/data/templates/frr/ospfd.frr.tmpl
@@ -1,7 +1,7 @@
 !
 {% if interface is defined and interface is not none %}
 {%   for iface, iface_config in interface.items() %}
-interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
+interface {{ iface }}
 {%     if iface_config.authentication is defined and iface_config.authentication is not none %}
 {%       if iface_config.authentication.plaintext_password is defined and iface_config.authentication.plaintext_password is not none %}
  ip ospf authentication-key {{ iface_config.authentication.plaintext_password }}
diff --git a/smoketest/scripts/cli/test_protocols_ospfv3.py b/smoketest/scripts/cli/test_protocols_ospfv3.py
index 114733002..2fc694fd7 100755
--- a/smoketest/scripts/cli/test_protocols_ospfv3.py
+++ b/smoketest/scripts/cli/test_protocols_ospfv3.py
@@ -265,8 +265,8 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase):
         self.assertIn(f'router ospf6', frrconfig)
         self.assertIn(f' ospf6 router-id {router_id}', frrconfig)
 
-        frrconfig = self.getFRRconfig(f'interface {vrf_iface} vrf {vrf}')
-        self.assertIn(f'interface {vrf_iface} vrf {vrf}', frrconfig)
+        frrconfig = self.getFRRconfig(f'interface {vrf_iface}')
+        self.assertIn(f'interface {vrf_iface}', frrconfig)
         self.assertIn(f' ipv6 ospf6 bfd', frrconfig)
 
         frrconfig = self.getFRRconfig(f'router ospf6 vrf {vrf}')
-- 
cgit v1.2.3


From c29c6d3d654c7280fdd4ea9fa66b5e84ef267285 Mon Sep 17 00:00:00 2001
From: fett0 <fernando.gmaidana@gmail.com>
Date: Thu, 17 Mar 2022 17:35:02 +0000
Subject: OSPF : T4304: Set import/export filter inter-area prefix

---
 data/templates/frr/ospfd.frr.tmpl                  |  6 +++++
 .../include/ospf/protocol-common-config.xml.i      | 30 ++++++++++++++++++++++
 2 files changed, 36 insertions(+)

diff --git a/data/templates/frr/ospfd.frr.tmpl b/data/templates/frr/ospfd.frr.tmpl
index 12213f162..59d936b55 100644
--- a/data/templates/frr/ospfd.frr.tmpl
+++ b/data/templates/frr/ospfd.frr.tmpl
@@ -97,6 +97,12 @@ router ospf {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
 {%         endif %}
 {%       endfor %}
 {%     endif %}
+{%     if area_config.export_list is defined and area_config.export_list is not none %}
+ area {{ area_id }} export-list {{ area_config.export_list }}
+{%     endif %}
+{%     if area_config.import_list is defined and area_config.import_list is not none %}
+ area {{ area_id }} import-list {{ area_config.import_list }}
+{%     endif %}
 {%     if area_config.shortcut is defined and area_config.shortcut is not none %}
  area {{ area_id }} shortcut {{ area_config.shortcut }}
 {%     endif %}
diff --git a/interface-definitions/include/ospf/protocol-common-config.xml.i b/interface-definitions/include/ospf/protocol-common-config.xml.i
index 088bee2de..3a3372e47 100644
--- a/interface-definitions/include/ospf/protocol-common-config.xml.i
+++ b/interface-definitions/include/ospf/protocol-common-config.xml.i
@@ -256,6 +256,36 @@
         </constraint>
       </properties>
     </leafNode>
+    <leafNode name="export-list">
+      <properties>
+        <help>Set the filter for networks announced to other areas</help>
+        <completionHelp>
+          <path>policy access-list</path>
+        </completionHelp>
+        <valueHelp>
+          <format>u32</format>
+          <description>Access-list number</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 0-4294967295"/>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="import-list">
+      <properties>
+        <help>Set the filter for networks from other areas announced</help>
+        <completionHelp>
+          <path>policy access-list</path>
+        </completionHelp>
+        <valueHelp>
+          <format>u32</format>
+          <description>Access-list number</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 0-4294967295"/>
+        </constraint>
+      </properties>
+    </leafNode>
     <tagNode name="virtual-link">
       <properties>
         <help>Virtual link</help>
-- 
cgit v1.2.3


From 91d19038f9e31657e660a88cbfc1443e454177ef Mon Sep 17 00:00:00 2001
From: fett0 <fernando.gmaidana@gmail.com>
Date: Fri, 18 Mar 2022 13:00:04 +0000
Subject: OSPF : T4304: add check access-list is defined

---
 src/conf_mode/protocols_ospf.py | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index 4895cde6f..26d491838 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.py
@@ -25,6 +25,7 @@ from vyos.configdict import node_changed
 from vyos.configverify import verify_common_route_maps
 from vyos.configverify import verify_route_map
 from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_access_list
 from vyos.template import render_to_string
 from vyos.util import dict_search
 from vyos.util import get_interface_config
@@ -159,6 +160,16 @@ def verify(ospf):
     route_map_name = dict_search('default_information.originate.route_map', ospf)
     if route_map_name: verify_route_map(route_map_name, ospf)
 
+    # Validate if configured Access-list exists 
+    if 'area' in ospf:
+          for area, area_config in ospf['area'].items():
+              if 'import_list' in area_config:
+                  acl_import = area_config['import_list']
+                  if acl_import: verify_access_list(acl_import, ospf)
+              if 'export_list' in area_config:
+                  acl_export = area_config['export_list']
+                  if acl_export: verify_access_list(acl_export, ospf)
+
     if 'interface' in ospf:
         for interface, interface_config in ospf['interface'].items():
             verify_interface_exists(interface)
-- 
cgit v1.2.3


From 496d2a5fd8c3bcbd0e7102c88eaf66d432cbb678 Mon Sep 17 00:00:00 2001
From: fett0 <fernando.gmaidana@gmail.com>
Date: Sat, 19 Mar 2022 18:50:03 +0000
Subject: smoketest: Verify export-list rule to ospf-area

---
 smoketest/scripts/cli/test_protocols_ospf.py | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py
index ee58b0fe2..5d8e9cff2 100755
--- a/smoketest/scripts/cli/test_protocols_ospf.py
+++ b/smoketest/scripts/cli/test_protocols_ospf.py
@@ -368,6 +368,30 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
         self.cli_delete(['vrf', 'name', vrf])
         self.cli_delete(['interfaces', 'ethernet', vrf_iface, 'vrf'])
 
+    def test_ospf_13_export_list(self):
+        # Verify explort-list works on ospf-area
+        acl = '100'
+        seq = '10'
+        area = '0.0.0.10'
+        network = '10.0.0.0/8'
+
+
+        self.cli_set(['policy', 'access-list', acl, 'rule', seq, 'action', 'permit'])
+        self.cli_set(['policy', 'access-list', acl, 'rule', seq, 'source', 'any'])
+        self.cli_set(['policy', 'access-list', acl, 'rule', seq, 'destination', 'any'])
+        self.cli_set(base_path + ['area', area, 'network', network])
+        self.cli_set(base_path + ['area', area, 'export-list', acl])
+
+        # commit changes
+        self.cli_commit()
+
+        # Verify FRR ospfd configuration
+        frrconfig = self.getFRRconfig('router ospf')
+        self.assertIn(f'router ospf', frrconfig)
+        self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # default
+        self.assertIn(f' network {network} area {area}', frrconfig)
+        self.assertIn(f' area {area} export-list {acl}', frrconfig)
+
 if __name__ == '__main__':
     logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
     unittest.main(verbosity=2)
-- 
cgit v1.2.3


From 18483a2f7d18aecb40b2003cec9a9dae6bcfaa24 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Mon, 21 Mar 2022 18:52:12 +0100
Subject: mirror: T3089: add verify_mirror() also for bond and bridge
 interfaces

---
 src/conf_mode/interfaces-bonding.py | 2 ++
 src/conf_mode/interfaces-bridge.py  | 2 ++
 2 files changed, 4 insertions(+)

diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index d5be21949..bb53cd6c2 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -28,6 +28,7 @@ from vyos.configverify import verify_address
 from vyos.configverify import verify_bridge_delete
 from vyos.configverify import verify_dhcpv6
 from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_mirror
 from vyos.configverify import verify_mtu_ipv6
 from vyos.configverify import verify_vlan_config
 from vyos.configverify import verify_vrf
@@ -149,6 +150,7 @@ def verify(bond):
     verify_address(bond)
     verify_dhcpv6(bond)
     verify_vrf(bond)
+    verify_mirror(bond)
 
     # use common function to verify VLAN configuration
     verify_vlan_config(bond)
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index f4dba9d4a..9f840cb58 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -27,6 +27,7 @@ from vyos.configdict import is_source_interface
 from vyos.configdict import has_vlan_subinterface_configured
 from vyos.configdict import dict_merge
 from vyos.configverify import verify_dhcpv6
+from vyos.configverify import verify_mirror
 from vyos.configverify import verify_vrf
 from vyos.ifconfig import BridgeIf
 from vyos.validate import has_address_configured
@@ -105,6 +106,7 @@ def verify(bridge):
 
     verify_dhcpv6(bridge)
     verify_vrf(bridge)
+    verify_mirror(bridge)
 
     ifname = bridge['ifname']
 
-- 
cgit v1.2.3


From 3584691b35f35e40a1bfc22c34da031141fd0dfa Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Mon, 21 Mar 2022 21:41:41 +0100
Subject: qos: T4284: initial XML interface definitions for rewrite

---
 Makefile                                           |   6 +
 data/configd-include.json                          |   1 +
 .../include/interface/redirect.xml.i               |  17 +
 .../include/interface/traffic-policy.xml.i         |  43 ++
 .../include/interface/vif-s.xml.i                  |   4 +
 interface-definitions/include/interface/vif.xml.i  |   4 +-
 interface-definitions/include/qos/bandwidth.xml.i  |  15 +
 interface-definitions/include/qos/burst.xml.i      |  16 +
 .../include/qos/codel-quantum.xml.i                |  16 +
 interface-definitions/include/qos/dscp.xml.i       | 143 ++++
 interface-definitions/include/qos/flows.xml.i      |  16 +
 interface-definitions/include/qos/hfsc-d.xml.i     |  15 +
 interface-definitions/include/qos/hfsc-m1.xml.i    |  32 +
 interface-definitions/include/qos/hfsc-m2.xml.i    |  32 +
 interface-definitions/include/qos/interval.xml.i   |  16 +
 interface-definitions/include/qos/match.xml.i      | 221 +++++++
 interface-definitions/include/qos/max-length.xml.i |  15 +
 .../include/qos/queue-limit-1-4294967295.xml.i     |  15 +
 .../include/qos/queue-limit-2-10999.xml.i          |  16 +
 interface-definitions/include/qos/queue-type.xml.i |  30 +
 interface-definitions/include/qos/set-dscp.xml.i   |  63 ++
 interface-definitions/include/qos/target.xml.i     |  16 +
 interface-definitions/include/qos/tcp-flags.xml.i  |  21 +
 interface-definitions/interfaces-bonding.xml.in    |   2 +
 interface-definitions/interfaces-bridge.xml.in     |   2 +
 interface-definitions/interfaces-dummy.xml.in      |   2 +
 interface-definitions/interfaces-ethernet.xml.in   |   2 +
 interface-definitions/interfaces-geneve.xml.in     |   2 +
 interface-definitions/interfaces-input.xml.in      |  30 +
 interface-definitions/interfaces-l2tpv3.xml.in     |   1 +
 interface-definitions/interfaces-loopback.xml.in   |   2 +
 interface-definitions/interfaces-macsec.xml.in     |   2 +
 interface-definitions/interfaces-openvpn.xml.in    |   2 +
 interface-definitions/interfaces-pppoe.xml.in      |   4 +-
 .../interfaces-pseudo-ethernet.xml.in              |   2 +
 interface-definitions/interfaces-tunnel.xml.in     |   4 +-
 interface-definitions/interfaces-vti.xml.in        |   2 +
 interface-definitions/interfaces-vxlan.xml.in      |   2 +
 interface-definitions/interfaces-wireguard.xml.in  |   4 +-
 interface-definitions/interfaces-wireless.xml.in   |   2 +
 interface-definitions/interfaces-wwan.xml.in       |   4 +-
 interface-definitions/qos.xml.in                   | 721 +++++++++++++++++++++
 python/vyos/configverify.py                        |  16 +
 src/conf_mode/interfaces-bonding.py                |   4 +-
 src/conf_mode/interfaces-bridge.py                 |   2 +
 src/conf_mode/interfaces-dummy.py                  |   2 +
 src/conf_mode/interfaces-ethernet.py               |   2 +
 src/conf_mode/interfaces-geneve.py                 |   2 +
 src/conf_mode/interfaces-l2tpv3.py                 |   2 +
 src/conf_mode/interfaces-loopback.py               |   2 +
 src/conf_mode/interfaces-macsec.py                 |   2 +
 src/conf_mode/interfaces-pppoe.py                  |   2 +
 src/conf_mode/interfaces-pseudo-ethernet.py        |   2 +
 src/conf_mode/interfaces-tunnel.py                 |   2 +
 src/conf_mode/interfaces-vti.py                    |   2 +
 src/conf_mode/interfaces-vxlan.py                  |   2 +
 src/conf_mode/interfaces-wireguard.py              |   2 +
 src/conf_mode/interfaces-wireless.py               |   2 +
 src/conf_mode/interfaces-wwan.py                   |   2 +
 src/conf_mode/qos.py                               |  90 +++
 60 files changed, 1699 insertions(+), 6 deletions(-)
 create mode 100644 interface-definitions/include/interface/redirect.xml.i
 create mode 100644 interface-definitions/include/interface/traffic-policy.xml.i
 create mode 100644 interface-definitions/include/qos/bandwidth.xml.i
 create mode 100644 interface-definitions/include/qos/burst.xml.i
 create mode 100644 interface-definitions/include/qos/codel-quantum.xml.i
 create mode 100644 interface-definitions/include/qos/dscp.xml.i
 create mode 100644 interface-definitions/include/qos/flows.xml.i
 create mode 100644 interface-definitions/include/qos/hfsc-d.xml.i
 create mode 100644 interface-definitions/include/qos/hfsc-m1.xml.i
 create mode 100644 interface-definitions/include/qos/hfsc-m2.xml.i
 create mode 100644 interface-definitions/include/qos/interval.xml.i
 create mode 100644 interface-definitions/include/qos/match.xml.i
 create mode 100644 interface-definitions/include/qos/max-length.xml.i
 create mode 100644 interface-definitions/include/qos/queue-limit-1-4294967295.xml.i
 create mode 100644 interface-definitions/include/qos/queue-limit-2-10999.xml.i
 create mode 100644 interface-definitions/include/qos/queue-type.xml.i
 create mode 100644 interface-definitions/include/qos/set-dscp.xml.i
 create mode 100644 interface-definitions/include/qos/target.xml.i
 create mode 100644 interface-definitions/include/qos/tcp-flags.xml.i
 create mode 100644 interface-definitions/interfaces-input.xml.in
 create mode 100644 interface-definitions/qos.xml.in
 create mode 100755 src/conf_mode/qos.py

diff --git a/Makefile b/Makefile
index 29744b323..431f3a8c2 100644
--- a/Makefile
+++ b/Makefile
@@ -29,6 +29,12 @@ interface_definitions: $(config_xml_obj)
 	# XXX: delete top level node.def's that now live in other packages
 	# IPSec VPN EAP-RADIUS does not support source-address
 	rm -rf $(TMPL_DIR)/vpn/ipsec/remote-access/radius/source-address
+
+	# T4284 neq QoS implementation is not yet live
+	find $(TMPL_DIR)/interfaces -name traffic-policy -type d -exec rm -rf {} \;
+	find $(TMPL_DIR)/interfaces -name redirect -type d -exec rm -rf {} \;
+	rm -rf $(TMPL_DIR)/interfaces/input
+
 	# XXX: test if there are empty node.def files - this is not allowed as these
 	# could mask help strings or mandatory priority statements
 	find $(TMPL_DIR) -name node.def -type f -empty -exec false {} + || sh -c 'echo "There are empty node.def files! Check your interface definitions." && exit 1'
diff --git a/data/configd-include.json b/data/configd-include.json
index c85ab0725..b77d48001 100644
--- a/data/configd-include.json
+++ b/data/configd-include.json
@@ -48,6 +48,7 @@
 "protocols_ripng.py",
 "protocols_static.py",
 "protocols_static_multicast.py",
+"qos.py",
 "salt-minion.py",
 "service_console-server.py",
 "service_ids_fastnetmon.py",
diff --git a/interface-definitions/include/interface/redirect.xml.i b/interface-definitions/include/interface/redirect.xml.i
new file mode 100644
index 000000000..3be9ee16b
--- /dev/null
+++ b/interface-definitions/include/interface/redirect.xml.i
@@ -0,0 +1,17 @@
+<!-- include start from interface/redirect.xml.i -->
+<leafNode name="redirect">
+  <properties>
+    <help>Incoming packet redirection destination</help>
+    <completionHelp>
+      <script>${vyos_completion_dir}/list_interfaces.py</script>
+    </completionHelp>
+    <valueHelp>
+      <format>txt</format>
+      <description>Interface name</description>
+    </valueHelp>
+    <constraint>
+      <validator name="interface-name"/>
+    </constraint>
+  </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/interface/traffic-policy.xml.i b/interface-definitions/include/interface/traffic-policy.xml.i
new file mode 100644
index 000000000..cd60b62a5
--- /dev/null
+++ b/interface-definitions/include/interface/traffic-policy.xml.i
@@ -0,0 +1,43 @@
+<!-- include start from interface/traffic-policy.xml.i -->
+<node name="traffic-policy">
+  <properties>
+    <help>Traffic-policy for interface</help>
+  </properties>
+  <children>
+    <leafNode name="in">
+      <properties>
+        <help>Ingress traffic policy for interface</help>
+        <completionHelp>
+          <path>traffic-policy drop-tail</path>
+          <path>traffic-policy fair-queue</path>
+          <path>traffic-policy fq-codel</path>
+          <path>traffic-policy limiter</path>
+          <path>traffic-policy network-emulator</path>
+          <path>traffic-policy priority-queue</path>
+          <path>traffic-policy random-detect</path>
+          <path>traffic-policy rate-control</path>
+          <path>traffic-policy round-robin</path>
+          <path>traffic-policy shaper</path>
+          <path>traffic-policy shaper-hfsc</path>
+        </completionHelp>
+        <valueHelp>
+          <format>txt</format>
+          <description>Policy name</description>
+        </valueHelp>
+      </properties>
+    </leafNode>
+    <leafNode name="out">
+      <properties>
+        <help>Egress traffic policy for interface</help>
+        <completionHelp>
+          <path>traffic-policy</path>
+        </completionHelp>
+        <valueHelp>
+          <format>txt</format>
+          <description>Policy name</description>
+        </valueHelp>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<!-- include end -->
\ No newline at end of file
diff --git a/interface-definitions/include/interface/vif-s.xml.i b/interface-definitions/include/interface/vif-s.xml.i
index f1a61ff64..59a47b5ff 100644
--- a/interface-definitions/include/interface/vif-s.xml.i
+++ b/interface-definitions/include/interface/vif-s.xml.i
@@ -64,11 +64,15 @@
         #include <include/interface/ipv6-options.xml.i>
         #include <include/interface/mac.xml.i>
         #include <include/interface/mtu-68-16000.xml.i>
+        #include <include/interface/redirect.xml.i>
+        #include <include/interface/traffic-policy.xml.i>
         #include <include/interface/vrf.xml.i>
         #include <include/interface/interface-firewall-vif-c.xml.i>
         #include <include/interface/interface-policy-vif-c.xml.i>
       </children>
     </tagNode>
+    #include <include/interface/redirect.xml.i>
+    #include <include/interface/traffic-policy.xml.i>
     #include <include/interface/vrf.xml.i>
   </children>
 </tagNode>
diff --git a/interface-definitions/include/interface/vif.xml.i b/interface-definitions/include/interface/vif.xml.i
index 11ba7e2f8..8a1475711 100644
--- a/interface-definitions/include/interface/vif.xml.i
+++ b/interface-definitions/include/interface/vif.xml.i
@@ -18,7 +18,6 @@
     #include <include/interface/dhcpv6-options.xml.i>
     #include <include/interface/disable-link-detect.xml.i>
     #include <include/interface/disable.xml.i>
-    #include <include/interface/vrf.xml.i>
     #include <include/interface/interface-firewall-vif.xml.i>
     #include <include/interface/interface-policy-vif.xml.i>
     <leafNode name="egress-qos">
@@ -51,6 +50,9 @@
     #include <include/interface/ipv6-options.xml.i>
     #include <include/interface/mac.xml.i>
     #include <include/interface/mtu-68-16000.xml.i>
+    #include <include/interface/redirect.xml.i>
+    #include <include/interface/traffic-policy.xml.i>
+    #include <include/interface/vrf.xml.i>
   </children>
 </tagNode>
 <!-- include end -->
diff --git a/interface-definitions/include/qos/bandwidth.xml.i b/interface-definitions/include/qos/bandwidth.xml.i
new file mode 100644
index 000000000..82af22f42
--- /dev/null
+++ b/interface-definitions/include/qos/bandwidth.xml.i
@@ -0,0 +1,15 @@
+<!-- include start from qos/bandwidth.xml.i -->
+<leafNode name="bandwidth">
+  <properties>
+    <help>Traffic-limit used for this class</help>
+    <valueHelp>
+      <format>&lt;number&gt;</format>
+      <description>Rate in kbit (kilobit per second)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>&lt;number&gt;&lt;suffix&gt;</format>
+      <description>Rate with scaling suffix (mbit, mbps, ...)</description>
+    </valueHelp>
+  </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/burst.xml.i b/interface-definitions/include/qos/burst.xml.i
new file mode 100644
index 000000000..761618027
--- /dev/null
+++ b/interface-definitions/include/qos/burst.xml.i
@@ -0,0 +1,16 @@
+<!-- include start from qos/burst.xml.i -->
+<leafNode name="burst">
+  <properties>
+    <help>Burst size for this class</help>
+    <valueHelp>
+      <format>&lt;number&gt;</format>
+      <description>Bytes</description>
+    </valueHelp>
+    <valueHelp>
+      <format>&lt;number&gt;&lt;suffix&gt;</format>
+      <description>Bytes with scaling suffix (kb, mb, gb)</description>
+    </valueHelp>
+  </properties>
+  <defaultValue>15k</defaultValue>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/codel-quantum.xml.i b/interface-definitions/include/qos/codel-quantum.xml.i
new file mode 100644
index 000000000..bc24630b6
--- /dev/null
+++ b/interface-definitions/include/qos/codel-quantum.xml.i
@@ -0,0 +1,16 @@
+<!-- include start from qos/codel-quantum.xml.i -->
+<leafNode name="codel-quantum">
+  <properties>
+    <help>Deficit in the fair queuing algorithm</help>
+    <valueHelp>
+      <format>u32:0-1048576</format>
+      <description>Number of bytes used as 'deficit'</description>
+    </valueHelp>
+    <constraint>
+      <validator name="numeric" argument="--range 0-1048576"/>
+    </constraint>
+    <constraintErrorMessage>Interval must be in range 0 to 1048576</constraintErrorMessage>
+  </properties>
+  <defaultValue>1514</defaultValue>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/dscp.xml.i b/interface-definitions/include/qos/dscp.xml.i
new file mode 100644
index 000000000..bb90850ac
--- /dev/null
+++ b/interface-definitions/include/qos/dscp.xml.i
@@ -0,0 +1,143 @@
+<!-- include start from qos/dscp.xml.i -->
+<leafNode name="dscp">
+  <properties>
+    <help>Match on Differentiated Services Codepoint (DSCP)</help>
+    <completionHelp>
+      <list>default reliability throughput lowdelay priority immediate flash flash-override critical internet network AF11 AF12 AF13 AF21 AF22 AF23 AF31 AF32 AF33 AF41 AF42 AF43 CS1 CS2 CS3 CS4 CS5 CS6 CS7 EF</list>
+    </completionHelp>
+    <valueHelp>
+      <format>u32:0-63</format>
+      <description>Differentiated Services Codepoint (DSCP) value </description>
+    </valueHelp>
+    <valueHelp>
+      <format>default</format>
+      <description>match DSCP (000000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>reliability</format>
+      <description>match DSCP (000001)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>throughput</format>
+      <description>match DSCP (000010)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>lowdelay</format>
+      <description>match DSCP (000100)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>priority</format>
+      <description>match DSCP (001000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>immediate</format>
+      <description>match DSCP (010000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>flash</format>
+      <description>match DSCP (011000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>flash-override</format>
+      <description>match DSCP (100000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>critical</format>
+      <description>match DSCP (101000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>internet</format>
+      <description>match DSCP (110000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>network</format>
+      <description>match DSCP (111000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>AF11</format>
+      <description>High-throughput data</description>
+    </valueHelp>
+    <valueHelp>
+      <format>AF12</format>
+      <description>High-throughput data</description>
+    </valueHelp>
+    <valueHelp>
+      <format>AF13</format>
+      <description>High-throughput data</description>
+    </valueHelp>
+    <valueHelp>
+      <format>AF21</format>
+      <description>Low-latency data</description>
+    </valueHelp>
+    <valueHelp>
+      <format>AF22</format>
+      <description>Low-latency data</description>
+    </valueHelp>
+    <valueHelp>
+      <format>AF23</format>
+      <description>Low-latency data</description>
+    </valueHelp>
+    <valueHelp>
+      <format>AF31</format>
+      <description>Multimedia streaming</description>
+    </valueHelp>
+    <valueHelp>
+      <format>AF32</format>
+      <description>Multimedia streaming</description>
+    </valueHelp>
+    <valueHelp>
+      <format>AF33</format>
+      <description>Multimedia streaming</description>
+    </valueHelp>
+    <valueHelp>
+      <format>AF41</format>
+      <description>Multimedia conferencing</description>
+    </valueHelp>
+    <valueHelp>
+      <format>AF42</format>
+      <description>Multimedia conferencing</description>
+    </valueHelp>
+    <valueHelp>
+      <format>AF43</format>
+      <description>Multimedia conferencing</description>
+    </valueHelp>
+    <valueHelp>
+      <format>CS1</format>
+      <description>Low-priority data</description>
+    </valueHelp>
+    <valueHelp>
+      <format>CS2</format>
+      <description>OAM</description>
+    </valueHelp>
+    <valueHelp>
+      <format>CS3</format>
+      <description>Broadcast video</description>
+    </valueHelp>
+    <valueHelp>
+      <format>CS4</format>
+      <description>Real-time interactive</description>
+    </valueHelp>
+    <valueHelp>
+      <format>CS5</format>
+      <description>Signaling</description>
+    </valueHelp>
+    <valueHelp>
+      <format>CS6</format>
+      <description>Network control</description>
+    </valueHelp>
+    <valueHelp>
+      <format>CS7</format>
+      <description></description>
+    </valueHelp>
+    <valueHelp>
+      <format>EF</format>
+      <description>Expedited Forwarding</description>
+    </valueHelp>
+    <constraint>
+      <validator name="numeric" argument="--range 0-63"/>
+      <regex>(default|reliability|throughput|lowdelay|priority|immediate|flash|flash-override|critical|internet|network|AF11|AF12|AF13|AF21|AF22|AF23|AF31|AF32|AF33|AF41|AF42|AF43|CS1|CS2|CS3|CS4|CS5|CS6|CS7|EF)</regex>
+    </constraint>
+    <constraintErrorMessage>Priority must be between 0 and 63</constraintErrorMessage>
+  </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/flows.xml.i b/interface-definitions/include/qos/flows.xml.i
new file mode 100644
index 000000000..a7d7c6422
--- /dev/null
+++ b/interface-definitions/include/qos/flows.xml.i
@@ -0,0 +1,16 @@
+<!-- include start from qos/flows.xml.i -->
+<leafNode name="flows">
+  <properties>
+    <help>Number of flows into which the incoming packets are classified</help>
+    <valueHelp>
+      <format>u32:1-65536</format>
+      <description>Number of flows</description>
+    </valueHelp>
+    <constraint>
+      <validator name="numeric" argument="--range 1-65536"/>
+    </constraint>
+    <constraintErrorMessage>Interval must be in range 1 to 65536</constraintErrorMessage>
+  </properties>
+  <defaultValue>1024</defaultValue>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/hfsc-d.xml.i b/interface-definitions/include/qos/hfsc-d.xml.i
new file mode 100644
index 000000000..2a513509c
--- /dev/null
+++ b/interface-definitions/include/qos/hfsc-d.xml.i
@@ -0,0 +1,15 @@
+<!-- include start from qos/hfsc-d.xml.i -->
+<leafNode name="d">
+  <properties>
+    <help>Service curve delay</help>
+    <valueHelp>
+      <format>&lt;number&gt;</format>
+      <description>Time in milliseconds</description>
+    </valueHelp>
+    <constraint>
+      <validator name="numeric" argument="--range 0-65535"/>
+    </constraint>
+    <constraintErrorMessage>Priority must be between 0 and 65535</constraintErrorMessage>
+  </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/hfsc-m1.xml.i b/interface-definitions/include/qos/hfsc-m1.xml.i
new file mode 100644
index 000000000..749d01f57
--- /dev/null
+++ b/interface-definitions/include/qos/hfsc-m1.xml.i
@@ -0,0 +1,32 @@
+<!-- include start from qos/hfsc-m1.xml.i -->
+<leafNode name="m1">
+  <properties>
+    <help>Linkshare m1 parameter for class traffic</help>
+    <valueHelp>
+      <format>&lt;number&gt;</format>
+      <description>Rate in kbit (kilobit per second)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>&lt;number&gt;%%</format>
+      <description>Percentage of overall rate</description>
+    </valueHelp>
+    <valueHelp>
+      <format>&lt;number&gt;bit</format>
+      <description>bit(1), kbit(10^3), mbit(10^6), gbit, tbit</description>
+    </valueHelp>
+    <valueHelp>
+      <format>&lt;number&gt;ibit</format>
+      <description>kibit(1024), mibit(1024^2), gibit(1024^3), tbit(1024^4)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>&lt;number&gt;ibps</format>
+      <description>kibps(1024*8), mibps(1024^2*8), gibps, tibps - Byte/sec</description>
+    </valueHelp>
+    <valueHelp>
+      <format>&lt;number&gt;bps</format>
+      <description>bps(8),kbps(8*10^3),mbps(8*10^6), gbps, tbps - Byte/sec</description>
+    </valueHelp>
+  </properties>
+  <defaultValue>100%</defaultValue>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/hfsc-m2.xml.i b/interface-definitions/include/qos/hfsc-m2.xml.i
new file mode 100644
index 000000000..24e8f5d63
--- /dev/null
+++ b/interface-definitions/include/qos/hfsc-m2.xml.i
@@ -0,0 +1,32 @@
+<!-- include start from qos/hfsc-m2.xml.i -->
+<leafNode name="m2">
+  <properties>
+    <help>Linkshare m2 parameter for class traffic</help>
+    <valueHelp>
+      <format>&lt;number&gt;</format>
+      <description>Rate in kbit (kilobit per second)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>&lt;number&gt;%%</format>
+      <description>Percentage of overall rate</description>
+    </valueHelp>
+    <valueHelp>
+      <format>&lt;number&gt;bit</format>
+      <description>bit(1), kbit(10^3), mbit(10^6), gbit, tbit</description>
+    </valueHelp>
+    <valueHelp>
+      <format>&lt;number&gt;ibit</format>
+      <description>kibit(1024), mibit(1024^2), gibit(1024^3), tbit(1024^4)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>&lt;number&gt;ibps</format>
+      <description>kibps(1024*8), mibps(1024^2*8), gibps, tibps - Byte/sec</description>
+    </valueHelp>
+    <valueHelp>
+      <format>&lt;number&gt;bps</format>
+      <description>bps(8),kbps(8*10^3),mbps(8*10^6), gbps, tbps - Byte/sec</description>
+    </valueHelp>
+  </properties>
+  <defaultValue>100%</defaultValue>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/interval.xml.i b/interface-definitions/include/qos/interval.xml.i
new file mode 100644
index 000000000..41896ac9c
--- /dev/null
+++ b/interface-definitions/include/qos/interval.xml.i
@@ -0,0 +1,16 @@
+<!-- include start from qos/interval.xml.i -->
+<leafNode name="interval">
+  <properties>
+    <help>Interval used to measure the delay</help>
+    <valueHelp>
+      <format>u32</format>
+      <description>Interval in milliseconds</description>
+    </valueHelp>
+    <constraint>
+      <validator name="numeric" argument="--range 0-4294967295"/>
+    </constraint>
+    <constraintErrorMessage>Interval must be in range 0 to 4294967295</constraintErrorMessage>
+  </properties>
+  <defaultValue>100</defaultValue>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/match.xml.i b/interface-definitions/include/qos/match.xml.i
new file mode 100644
index 000000000..7d89e4460
--- /dev/null
+++ b/interface-definitions/include/qos/match.xml.i
@@ -0,0 +1,221 @@
+<!-- include start from qos/match.xml.i -->
+<tagNode name="match">
+  <properties>
+    <help>Class matching rule name</help>
+    <constraint>
+      <regex>[^-].*</regex>
+    </constraint>
+    <constraintErrorMessage>Match queue name cannot start with hyphen (-)</constraintErrorMessage>
+  </properties>
+  <children>
+    #include <include/generic-description.xml.i>
+    <node name="ether">
+      <properties>
+        <help>Ethernet header match</help>
+      </properties>
+      <children>
+        <leafNode name="destination">
+          <properties>
+            <help>Ethernet destination address for this match</help>
+            <valueHelp>
+              <format>macaddr</format>
+              <description>MAC address to match</description>
+            </valueHelp>
+            <constraint>
+              <validator name="mac-address"/>
+            </constraint>
+          </properties>
+        </leafNode>
+        <leafNode name="protocol">
+          <properties>
+            <help>Ethernet protocol for this match</help>
+            <!-- this refers to /etc/protocols -->
+            <completionHelp>
+              <list>all 802.1Q 802_2 802_3 aarp aoe arp atalk dec ip ipv6 ipx lat localtalk rarp snap x25</list>
+            </completionHelp>
+            <valueHelp>
+              <format>u32:0-65535</format>
+              <description>Ethernet protocol number</description>
+            </valueHelp>
+            <valueHelp>
+              <format>txt</format>
+              <description>Ethernet protocol name</description>
+            </valueHelp>
+            <valueHelp>
+              <format>all</format>
+              <description>Any protocol</description>
+            </valueHelp>
+            <valueHelp>
+              <format>ip</format>
+              <description>Internet IP (IPv4)</description>
+            </valueHelp>
+            <valueHelp>
+              <format>ipv6</format>
+              <description>Internet IP (IPv6)</description>
+            </valueHelp>
+            <valueHelp>
+              <format>arp</format>
+              <description>Address Resolution Protocol</description>
+            </valueHelp>
+            <valueHelp>
+              <format>atalk</format>
+              <description>Appletalk</description>
+            </valueHelp>
+            <valueHelp>
+              <format>ipx</format>
+              <description>Novell Internet Packet Exchange</description>
+            </valueHelp>
+            <valueHelp>
+              <format>802.1Q</format>
+              <description>802.1Q VLAN tag</description>
+            </valueHelp>
+            <constraint>
+              <validator name="ip-protocol"/>
+            </constraint>
+          </properties>
+        </leafNode>
+        <leafNode name="source">
+          <properties>
+            <help>Ethernet source address for this match</help>
+            <valueHelp>
+              <format>macaddr</format>
+              <description>MAC address to match</description>
+            </valueHelp>
+            <constraint>
+              <validator name="mac-address"/>
+            </constraint>
+          </properties>
+        </leafNode>
+      </children>
+    </node>
+    #include <include/generic-interface.xml.i>
+    <node name="ip">
+      <properties>
+        <help>Match IP protocol header</help>
+      </properties>
+      <children>
+        <node name="destination">
+          <properties>
+            <help>Match on destination port or address</help>
+          </properties>
+          <children>
+            <leafNode name="address">
+              <properties>
+                <help>IPv4 destination address for this match</help>
+                <valueHelp>
+                  <format>ipv4net</format>
+                  <description>IPv4 address and prefix length</description>
+                </valueHelp>
+                <constraint>
+                  <validator name="ipv4"/>
+                </constraint>
+              </properties>
+            </leafNode>
+            #include <include/port-number.xml.i>
+          </children>
+        </node>
+        #include <include/qos/dscp.xml.i>
+        #include <include/qos/max-length.xml.i>
+        #include <include/ip-protocol.xml.i>
+        <node name="source">
+          <properties>
+            <help>Match on source port or address</help>
+          </properties>
+          <children>
+            <leafNode name="address">
+              <properties>
+                <help>IPv4 source address for this match</help>
+                <valueHelp>
+                  <format>ipv4net</format>
+                  <description>IPv4 address and prefix length</description>
+                </valueHelp>
+                <constraint>
+                  <validator name="ipv4"/>
+                </constraint>
+              </properties>
+            </leafNode>
+            #include <include/port-number.xml.i>
+          </children>
+        </node>
+        #include <include/qos/tcp-flags.xml.i>
+      </children>
+    </node>
+    <node name="ipv6">
+      <properties>
+        <help>Match IPv6 protocol header</help>
+      </properties>
+      <children>
+        <node name="destination">
+          <properties>
+            <help>Match on destination port or address</help>
+          </properties>
+          <children>
+            <leafNode name="address">
+              <properties>
+                <help>IPv6 destination address for this match</help>
+                <valueHelp>
+                  <format>ipv6net</format>
+                  <description>IPv6 address and prefix length</description>
+                </valueHelp>
+                <constraint>
+                  <validator name="ipv6"/>
+                </constraint>
+              </properties>
+            </leafNode>
+            #include <include/port-number.xml.i>
+          </children>
+        </node>
+        #include <include/qos/dscp.xml.i>
+        #include <include/qos/max-length.xml.i>
+        #include <include/ip-protocol.xml.i>
+        <node name="source">
+          <properties>
+            <help>Match on source port or address</help>
+          </properties>
+          <children>
+            <leafNode name="address">
+              <properties>
+                <help>IPv6 source address for this match</help>
+                <valueHelp>
+                  <format>ipv6net</format>
+                  <description>IPv6 address and prefix length</description>
+                </valueHelp>
+                <constraint>
+                  <validator name="ipv6"/>
+                </constraint>
+              </properties>
+            </leafNode>
+            #include <include/port-number.xml.i>
+          </children>
+        </node>
+        #include <include/qos/tcp-flags.xml.i>
+      </children>
+    </node>
+    <leafNode name="mark">
+      <properties>
+        <help>Match on mark applied by firewall</help>
+        <valueHelp>
+          <format>txt</format>
+          <description>FW mark to match</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 0x0-0xffff"/>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="vif">
+      <properties>
+        <help>Virtual Local Area Network (VLAN) ID for this match</help>
+        <valueHelp>
+          <format>u32:0-4095</format>
+          <description>Virtual Local Area Network (VLAN) tag </description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 0-4095"/>
+        </constraint>
+        <constraintErrorMessage>VLAN ID must be between 0 and 4095</constraintErrorMessage>
+      </properties>
+    </leafNode>
+  </children>
+</tagNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/max-length.xml.i b/interface-definitions/include/qos/max-length.xml.i
new file mode 100644
index 000000000..4cc20f8c4
--- /dev/null
+++ b/interface-definitions/include/qos/max-length.xml.i
@@ -0,0 +1,15 @@
+<!-- include start from qos/max-length.xml.i -->
+<leafNode name="max-length">
+  <properties>
+    <help>Maximum packet length (ipv4)</help>
+    <valueHelp>
+      <format>u32:0-65535</format>
+      <description>Maximum packet/payload length</description>
+    </valueHelp>
+    <constraint>
+      <validator name="numeric" argument="--range 0-65535"/>
+    </constraint>
+    <constraintErrorMessage>Maximum IPv4 total packet length is 65535</constraintErrorMessage>
+  </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/queue-limit-1-4294967295.xml.i b/interface-definitions/include/qos/queue-limit-1-4294967295.xml.i
new file mode 100644
index 000000000..2f2d44631
--- /dev/null
+++ b/interface-definitions/include/qos/queue-limit-1-4294967295.xml.i
@@ -0,0 +1,15 @@
+<!-- include start from qos/queue-limit-1-4294967295.xml.i -->
+<leafNode name="queue-limit">
+  <properties>
+    <help>Maximum queue size</help>
+    <valueHelp>
+      <format>u32:1-4294967295</format>
+      <description>Queue size in packets</description>
+    </valueHelp>
+    <constraint>
+      <validator name="numeric" argument="--range 1-4294967295"/>
+    </constraint>
+    <constraintErrorMessage>Queue limit must be greater than zero</constraintErrorMessage>
+  </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/queue-limit-2-10999.xml.i b/interface-definitions/include/qos/queue-limit-2-10999.xml.i
new file mode 100644
index 000000000..7a9c8266b
--- /dev/null
+++ b/interface-definitions/include/qos/queue-limit-2-10999.xml.i
@@ -0,0 +1,16 @@
+<!-- include start from qos/queue-limit.xml.i -->
+<leafNode name="queue-limit">
+  <properties>
+    <help>Upper limit of the queue</help>
+    <valueHelp>
+      <format>u32:2-10999</format>
+      <description>Queue size in packets</description>
+    </valueHelp>
+    <constraint>
+      <validator name="numeric" argument="--range 2-10999"/>
+    </constraint>
+    <constraintErrorMessage>Queue limit must greater than 1 and less than 11000</constraintErrorMessage>
+  </properties>
+  <defaultValue>10240</defaultValue>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/queue-type.xml.i b/interface-definitions/include/qos/queue-type.xml.i
new file mode 100644
index 000000000..634f61024
--- /dev/null
+++ b/interface-definitions/include/qos/queue-type.xml.i
@@ -0,0 +1,30 @@
+<!-- include start from qos/queue-type.xml.i -->
+<leafNode name="queue-type">
+  <properties>
+    <help>Queue type for default traffic</help>
+    <completionHelp>
+      <list>fq-codel fair-queue drop-tail random-detect</list>
+    </completionHelp>
+    <valueHelp>
+      <format>fq-codel</format>
+      <description>Fair Queue Codel</description>
+    </valueHelp>
+    <valueHelp>
+      <format>fair-queue</format>
+      <description>Stochastic Fair Queue (SFQ)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>drop-tail</format>
+      <description>First-In-First-Out (FIFO)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>random-detect</format>
+      <description>Random Early Detection (RED)</description>
+    </valueHelp>
+    <constraint>
+      <regex>(fq-codel|fair-queue|drop-tail|random-detect)</regex>
+    </constraint>
+  </properties>
+  <defaultValue>drop-tail</defaultValue>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/set-dscp.xml.i b/interface-definitions/include/qos/set-dscp.xml.i
new file mode 100644
index 000000000..55c0ea44d
--- /dev/null
+++ b/interface-definitions/include/qos/set-dscp.xml.i
@@ -0,0 +1,63 @@
+<!-- include start from qos/set-dscp.xml.i -->
+<leafNode name="set-dscp">
+  <properties>
+    <help>Change the Differentiated Services (DiffServ) field in the IP header</help>
+    <completionHelp>
+      <list>default reliability throughput lowdelay priority immediate flash flash-override critical internet network</list>
+    </completionHelp>
+    <valueHelp>
+      <format>u32:0-63</format>
+      <description>Priority order for bandwidth pool</description>
+    </valueHelp>
+    <valueHelp>
+      <format>default</format>
+      <description>match DSCP (000000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>reliability</format>
+      <description>match DSCP (000001)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>throughput</format>
+      <description>match DSCP (000010)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>lowdelay</format>
+      <description>match DSCP (000100)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>priority</format>
+      <description>match DSCP (001000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>immediate</format>
+      <description>match DSCP (010000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>flash</format>
+      <description>match DSCP (011000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>flash-override</format>
+      <description>match DSCP (100000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>critical</format>
+      <description>match DSCP (101000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>internet</format>
+      <description>match DSCP (110000)</description>
+    </valueHelp>
+    <valueHelp>
+      <format>network</format>
+      <description>match DSCP (111000)</description>
+    </valueHelp>
+    <constraint>
+      <validator name="numeric" argument="--range 0-63"/>
+      <regex>(default|reliability|throughput|lowdelay|priority|immediate|flash|flash-override|critical|internet|network)</regex>
+    </constraint>
+    <constraintErrorMessage>Priority must be between 0 and 63</constraintErrorMessage>
+  </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/target.xml.i b/interface-definitions/include/qos/target.xml.i
new file mode 100644
index 000000000..bf6342ac9
--- /dev/null
+++ b/interface-definitions/include/qos/target.xml.i
@@ -0,0 +1,16 @@
+<!-- include start from qos/target.xml.i -->
+<leafNode name="target">
+  <properties>
+    <help>Acceptable minimum standing/persistent queue delay</help>
+    <valueHelp>
+      <format>u32</format>
+      <description>Queue delay in milliseconds</description>
+    </valueHelp>
+    <constraint>
+      <validator name="numeric" argument="--range 0-4294967295"/>
+    </constraint>
+    <constraintErrorMessage>Delay must be in range 0 to 4294967295</constraintErrorMessage>
+  </properties>
+  <defaultValue>5</defaultValue>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/qos/tcp-flags.xml.i b/interface-definitions/include/qos/tcp-flags.xml.i
new file mode 100644
index 000000000..81d70d1f3
--- /dev/null
+++ b/interface-definitions/include/qos/tcp-flags.xml.i
@@ -0,0 +1,21 @@
+<!-- include start from qos/tcp-flags.xml.i -->
+<node name="tcp">
+  <properties>
+    <help>TCP Flags matching</help>
+  </properties>
+  <children>
+    <leafNode name="ack">
+      <properties>
+        <help>Match TCP ACK</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+    <leafNode name="syn">
+      <properties>
+        <help>Match TCP SYN</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in
index b98f4b960..20ece5137 100644
--- a/interface-definitions/interfaces-bonding.xml.in
+++ b/interface-definitions/interfaces-bonding.xml.in
@@ -207,6 +207,8 @@
               </constraint>
             </properties>
           </leafNode>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
           #include <include/interface/vif-s.xml.i>
           #include <include/interface/vif.xml.i>
           #include <include/interface/xdp.xml.i>
diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in
index fabfb917a..6957067cd 100644
--- a/interface-definitions/interfaces-bridge.xml.in
+++ b/interface-definitions/interfaces-bridge.xml.in
@@ -210,6 +210,8 @@
               <valueless/>
             </properties>
           </leafNode>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
           #include <include/interface/vif.xml.i>
         </children>
       </tagNode>
diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in
index 3bca8b950..109ed1b50 100644
--- a/interface-definitions/interfaces-dummy.xml.in
+++ b/interface-definitions/interfaces-dummy.xml.in
@@ -30,6 +30,8 @@
             </children>
           </node>
           #include <include/interface/netns.xml.i>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
           #include <include/interface/vrf.xml.i>
         </children>
       </tagNode>
diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in
index be7bddfa4..7d28912c0 100644
--- a/interface-definitions/interfaces-ethernet.xml.in
+++ b/interface-definitions/interfaces-ethernet.xml.in
@@ -196,6 +196,8 @@
               </leafNode>
             </children>
           </node>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
           #include <include/interface/vif-s.xml.i>
           #include <include/interface/vif.xml.i>
           #include <include/interface/vrf.xml.i>
diff --git a/interface-definitions/interfaces-geneve.xml.in b/interface-definitions/interfaces-geneve.xml.in
index dd4d324d4..aa5809e60 100644
--- a/interface-definitions/interfaces-geneve.xml.in
+++ b/interface-definitions/interfaces-geneve.xml.in
@@ -50,6 +50,8 @@
               </node>
             </children>
           </node>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
           #include <include/interface/tunnel-remote.xml.i>
           #include <include/vni.xml.i>
         </children>
diff --git a/interface-definitions/interfaces-input.xml.in b/interface-definitions/interfaces-input.xml.in
new file mode 100644
index 000000000..f2eb01c58
--- /dev/null
+++ b/interface-definitions/interfaces-input.xml.in
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="interfaces">
+    <children>
+      <tagNode name="input" owner="${vyos_conf_scripts_dir}/interfaces-input.py">
+        <properties>
+          <help>Input Functional Block (IFB) interface name</help>
+          <!-- before real devices that redirect -->
+          <priority>310</priority>
+          <constraint>
+            <regex>ifb[0-9]+</regex>
+          </constraint>
+          <constraintErrorMessage>Input interface must be named ifbN</constraintErrorMessage>
+          <valueHelp>
+            <format>ifbN</format>
+            <description>Input interface name</description>
+          </valueHelp>
+        </properties>
+        <children>
+          #include <include/interface/description.xml.i>
+          #include <include/interface/disable.xml.i>
+          #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
+        </children>
+      </tagNode>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-l2tpv3.xml.in b/interface-definitions/interfaces-l2tpv3.xml.in
index ba9bcb0a2..124863653 100644
--- a/interface-definitions/interfaces-l2tpv3.xml.in
+++ b/interface-definitions/interfaces-l2tpv3.xml.in
@@ -125,6 +125,7 @@
               </constraint>
             </properties>
           </leafNode>
+          #include <include/interface/traffic-policy.xml.i>
           #include <include/interface/vrf.xml.i>
         </children>
       </tagNode>
diff --git a/interface-definitions/interfaces-loopback.xml.in b/interface-definitions/interfaces-loopback.xml.in
index 7be15ab89..ffffc0220 100644
--- a/interface-definitions/interfaces-loopback.xml.in
+++ b/interface-definitions/interfaces-loopback.xml.in
@@ -26,6 +26,8 @@
               #include <include/interface/source-validation.xml.i>
             </children>
           </node>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
         </children>
       </tagNode>
     </children>
diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in
index 7206e57b1..311e95c2f 100644
--- a/interface-definitions/interfaces-macsec.xml.in
+++ b/interface-definitions/interfaces-macsec.xml.in
@@ -122,6 +122,8 @@
             <defaultValue>1460</defaultValue>
           </leafNode>
           #include <include/source-interface-ethernet.xml.i>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
           #include <include/interface/vrf.xml.i>
         </children>
       </tagNode>
diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in
index eb574eb52..73e30e590 100644
--- a/interface-definitions/interfaces-openvpn.xml.in
+++ b/interface-definitions/interfaces-openvpn.xml.in
@@ -816,6 +816,8 @@
               <valueless/>
             </properties>
           </leafNode>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
           #include <include/interface/vrf.xml.i>
         </children>
       </tagNode>
diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in
index ed0e45840..1d888236e 100644
--- a/interface-definitions/interfaces-pppoe.xml.in
+++ b/interface-definitions/interfaces-pppoe.xml.in
@@ -49,7 +49,6 @@
           #include <include/interface/dhcpv6-options.xml.i>
           #include <include/interface/description.xml.i>
           #include <include/interface/disable.xml.i>
-          #include <include/interface/vrf.xml.i>
           <leafNode name="idle-timeout">
             <properties>
               <help>Delay before disconnecting idle session (in seconds)</help>
@@ -134,6 +133,9 @@
               <constraintErrorMessage>Service name must be alphanumeric only</constraintErrorMessage>
             </properties>
           </leafNode>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
+          #include <include/interface/vrf.xml.i>
         </children>
       </tagNode>
     </children>
diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in
index bf7055f8d..7baeac537 100644
--- a/interface-definitions/interfaces-pseudo-ethernet.xml.in
+++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in
@@ -59,6 +59,8 @@
             <defaultValue>private</defaultValue>
           </leafNode>
           #include <include/interface/mtu-68-16000.xml.i>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
           #include <include/interface/vif-s.xml.i>
           #include <include/interface/vif.xml.i>
         </children>
diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in
index eb1708aaa..bc9297c86 100644
--- a/interface-definitions/interfaces-tunnel.xml.in
+++ b/interface-definitions/interfaces-tunnel.xml.in
@@ -20,7 +20,6 @@
           #include <include/interface/address-ipv4-ipv6.xml.i>
           #include <include/interface/disable.xml.i>
           #include <include/interface/disable-link-detect.xml.i>
-          #include <include/interface/vrf.xml.i>
           #include <include/interface/mtu-64-8024.xml.i>
           <leafNode name="mtu">
             <defaultValue>1476</defaultValue>
@@ -288,6 +287,9 @@
               </node>
             </children>
           </node>
+          #include <include/interface/vrf.xml.i>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
         </children>
       </tagNode>
     </children>
diff --git a/interface-definitions/interfaces-vti.xml.in b/interface-definitions/interfaces-vti.xml.in
index f03c7476d..538194c2b 100644
--- a/interface-definitions/interfaces-vti.xml.in
+++ b/interface-definitions/interfaces-vti.xml.in
@@ -34,6 +34,8 @@
           #include <include/interface/ipv4-options.xml.i>
           #include <include/interface/ipv6-options.xml.i>
           #include <include/interface/mtu-68-16000.xml.i>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
           #include <include/interface/vrf.xml.i>
           #include <include/interface/interface-firewall.xml.i>
           #include <include/interface/interface-policy.xml.i>
diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in
index 0546b4199..18abf9f20 100644
--- a/interface-definitions/interfaces-vxlan.xml.in
+++ b/interface-definitions/interfaces-vxlan.xml.in
@@ -99,6 +99,8 @@
           #include <include/source-address-ipv4-ipv6.xml.i>
           #include <include/source-interface.xml.i>
           #include <include/interface/tunnel-remote-multi.xml.i>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
           #include <include/interface/vrf.xml.i>
           #include <include/vni.xml.i>
         </children>
diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in
index 1b4b4a816..2f130c6f2 100644
--- a/interface-definitions/interfaces-wireguard.xml.in
+++ b/interface-definitions/interfaces-wireguard.xml.in
@@ -19,7 +19,6 @@
           #include <include/interface/address-ipv4-ipv6.xml.i>
           #include <include/interface/description.xml.i>
           #include <include/interface/disable.xml.i>
-          #include <include/interface/vrf.xml.i>
           #include <include/port-number.xml.i>
           #include <include/interface/mtu-68-16000.xml.i>
           #include <include/interface/interface-firewall.xml.i>
@@ -120,6 +119,9 @@
               </leafNode>
             </children>
           </tagNode>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
+          #include <include/interface/vrf.xml.i>
         </children>
       </tagNode>
     </children>
diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in
index 9db9fd757..eebe8f841 100644
--- a/interface-definitions/interfaces-wireless.xml.in
+++ b/interface-definitions/interfaces-wireless.xml.in
@@ -778,6 +778,8 @@
             </properties>
             <defaultValue>monitor</defaultValue>
           </leafNode>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
           #include <include/interface/vif.xml.i>
           #include <include/interface/vif-s.xml.i>
         </children>
diff --git a/interface-definitions/interfaces-wwan.xml.in b/interface-definitions/interfaces-wwan.xml.in
index 03554feed..7007a67ae 100644
--- a/interface-definitions/interfaces-wwan.xml.in
+++ b/interface-definitions/interfaces-wwan.xml.in
@@ -30,7 +30,6 @@
           #include <include/interface/authentication.xml.i>
           #include <include/interface/description.xml.i>
           #include <include/interface/disable.xml.i>
-          #include <include/interface/vrf.xml.i>
           #include <include/interface/disable-link-detect.xml.i>
           #include <include/interface/mtu-68-1500.xml.i>
           <leafNode name="mtu">
@@ -41,6 +40,9 @@
           #include <include/interface/dial-on-demand.xml.i>
           #include <include/interface/interface-firewall.xml.i>
           #include <include/interface/interface-policy.xml.i>
+          #include <include/interface/redirect.xml.i>
+          #include <include/interface/traffic-policy.xml.i>
+          #include <include/interface/vrf.xml.i>
         </children>
       </tagNode>
     </children>
diff --git a/interface-definitions/qos.xml.in b/interface-definitions/qos.xml.in
new file mode 100644
index 000000000..d4468543c
--- /dev/null
+++ b/interface-definitions/qos.xml.in
@@ -0,0 +1,721 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="traffic-policy" owner="${vyos_conf_scripts_dir}/qos.py">
+    <properties>
+      <help>Quality of Service (QOS) policy type</help>
+      <priority>900</priority>
+    </properties>
+    <children>
+      <tagNode name="drop-tail">
+        <properties>
+          <help>Packet limited First In, First Out queue</help>
+          <valueHelp>
+            <format>txt</format>
+            <description>Policy name</description>
+          </valueHelp>
+          <constraint>
+            <regex>[[:alnum:]][-_[:alnum:]]*</regex>
+          </constraint>
+          <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>
+        </properties>
+        <children>
+          #include <include/generic-description.xml.i>
+          #include <include/qos/queue-limit-1-4294967295.xml.i>
+        </children>
+      </tagNode>
+      <tagNode name="fair-queue">
+        <properties>
+          <help>Stochastic Fairness Queueing</help>
+          <valueHelp>
+            <format>txt</format>
+            <description>Policy name</description>
+          </valueHelp>
+          <constraint>
+            <regex>[[:alnum:]][-_[:alnum:]]*</regex>
+          </constraint>
+          <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>
+        </properties>
+        <children>
+          #include <include/generic-description.xml.i>
+          <leafNode name="hash-interval">
+            <properties>
+              <help>Interval in seconds for queue algorithm perturbation</help>
+              <valueHelp>
+                <format>u32:0</format>
+                <description>No perturbation</description>
+              </valueHelp>
+              <valueHelp>
+                <format>u32:1-127</format>
+                <description>Interval in seconds for queue algorithm perturbation (advised: 10)</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 0-127"/>
+              </constraint>
+              <constraintErrorMessage>Interval must be in range 0 to 127</constraintErrorMessage>
+            </properties>
+            <defaultValue>0</defaultValue>
+          </leafNode>
+          <leafNode name="queue-limit">
+            <properties>
+              <help>Upper limit of the SFQ</help>
+              <valueHelp>
+                <format>u32:2-127</format>
+                <description>Queue size in packets</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 2-127"/>
+              </constraint>
+              <constraintErrorMessage>Queue limit must greater than 1 and less than 128</constraintErrorMessage>
+            </properties>
+            <defaultValue>127</defaultValue>
+          </leafNode>
+        </children>
+      </tagNode>
+      <tagNode name="fq-codel">
+        <properties>
+          <help>Fair Queuing Controlled Delay</help>
+          <valueHelp>
+            <format>txt</format>
+            <description>Policy name</description>
+          </valueHelp>
+          <constraint>
+            <regex>[[:alnum:]][-_[:alnum:]]*</regex>
+          </constraint>
+          <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>
+        </properties>
+        <children>
+          #include <include/generic-description.xml.i>
+          #include <include/qos/codel-quantum.xml.i>
+          #include <include/qos/flows.xml.i>
+          #include <include/qos/interval.xml.i>
+          #include <include/qos/queue-limit-2-10999.xml.i>
+          #include <include/qos/target.xml.i>
+        </children>
+      </tagNode>
+      <tagNode name="limiter">
+        <properties>
+          <help>Traffic input limiting policy</help>
+          <valueHelp>
+            <format>txt</format>
+            <description>Policy name</description>
+          </valueHelp>
+          <constraint>
+            <regex>[[:alnum:]][-_[:alnum:]]*</regex>
+          </constraint>
+          <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>
+        </properties>
+        <children>
+          <tagNode name="class">
+            <properties>
+              <help>Class ID</help>
+              <valueHelp>
+                <format>u32:1-4090</format>
+                <description>Class Identifier</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 1-4090"/>
+              </constraint>
+              <constraintErrorMessage>Class identifier must be between 1 and 4090</constraintErrorMessage>
+            </properties>
+            <children>
+              #include <include/qos/bandwidth.xml.i>
+              #include <include/qos/burst.xml.i>
+              #include <include/generic-description.xml.i>
+              #include <include/qos/match.xml.i>
+              <leafNode name="priority">
+                <properties>
+                  <help>Priority for rule evaluation</help>
+                  <valueHelp>
+                    <format>u32:0-20</format>
+                    <description>Priority for match rule evaluation</description>
+                  </valueHelp>
+                  <constraint>
+                    <validator name="numeric" argument="--range 0-20"/>
+                  </constraint>
+                  <constraintErrorMessage>Priority must be between 0 and 20</constraintErrorMessage>
+                </properties>
+                <defaultValue>20</defaultValue>
+              </leafNode>
+            </children>
+          </tagNode>
+          <node name="default">
+            <properties>
+              <help>Default policy</help>
+            </properties>
+            <children>
+              #include <include/qos/bandwidth.xml.i>
+              #include <include/qos/burst.xml.i>
+            </children>
+          </node>
+          #include <include/generic-description.xml.i>
+        </children>
+      </tagNode>
+      <tagNode name="network-emulator">
+        <properties>
+          <help>Network emulator policy</help>
+          <valueHelp>
+            <format>txt</format>
+            <description>Policy name</description>
+          </valueHelp>
+          <constraint>
+            <regex>[[:alnum:]][-_[:alnum:]]*</regex>
+          </constraint>
+          <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>
+        </properties>
+        <children>
+          #include <include/qos/bandwidth.xml.i>
+          #include <include/qos/burst.xml.i>
+          #include <include/generic-description.xml.i>
+          <leafNode name="network-delay">
+            <properties>
+              <help>Adds delay to packets outgoing to chosen network interface</help>
+              <valueHelp>
+                <format>&lt;number&gt;</format>
+                <description>Time in milliseconds</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 0-65535"/>
+              </constraint>
+              <constraintErrorMessage>Priority must be between 0 and 65535</constraintErrorMessage>
+            </properties>
+          </leafNode>
+          <leafNode name="packet-corruption">
+            <properties>
+              <help>Introducing error in a random position for chosen percent of packets</help>
+              <valueHelp>
+                <format>&lt;number&gt;</format>
+                <description>Percentage of packets affected</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 0-100"/>
+              </constraint>
+              <constraintErrorMessage>Priority must be between 0 and 100</constraintErrorMessage>
+            </properties>
+          </leafNode>
+          <leafNode name="packet-loss">
+            <properties>
+              <help>Add independent loss probability to the packets outgoing to chosen network interface</help>
+              <valueHelp>
+                <format>&lt;number&gt;</format>
+                <description>Percentage of packets affected</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 0-100"/>
+              </constraint>
+              <constraintErrorMessage>Must be between 0 and 100</constraintErrorMessage>
+            </properties>
+          </leafNode>
+          <leafNode name="packet-loss">
+            <properties>
+              <help>Add independent loss probability to the packets outgoing to chosen network interface</help>
+              <valueHelp>
+                <format>&lt;number&gt;</format>
+                <description>Percentage of packets affected</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 0-100"/>
+              </constraint>
+              <constraintErrorMessage>Must be between 0 and 100</constraintErrorMessage>
+            </properties>
+          </leafNode>
+          <leafNode name="packet-loss">
+            <properties>
+              <help>Packet reordering percentage</help>
+              <valueHelp>
+                <format>&lt;number&gt;</format>
+                <description>Percentage of packets affected</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 0-100"/>
+              </constraint>
+              <constraintErrorMessage>Must be between 0 and 100</constraintErrorMessage>
+            </properties>
+          </leafNode>
+          #include <include/qos/queue-limit-1-4294967295.xml.i>
+        </children>
+      </tagNode>
+      <tagNode name="priority-queue">
+        <properties>
+          <help>Priority queuing based policy</help>
+          <valueHelp>
+            <format>txt</format>
+            <description>Policy name</description>
+          </valueHelp>
+          <constraint>
+            <regex>[[:alnum:]][-_[:alnum:]]*</regex>
+          </constraint>
+          <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>
+        </properties>
+        <children>
+          <tagNode name="class">
+            <properties>
+              <help>Class Handle</help>
+              <valueHelp>
+                <format>u32:1-7</format>
+                <description>Priority</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 1-7"/>
+              </constraint>
+              <constraintErrorMessage>Class handle must be between 1 and 7</constraintErrorMessage>
+            </properties>
+            <children>
+              #include <include/generic-description.xml.i>
+              #include <include/qos/codel-quantum.xml.i>
+              #include <include/qos/flows.xml.i>
+              #include <include/qos/interval.xml.i>
+              #include <include/qos/match.xml.i>
+              #include <include/qos/queue-limit-2-10999.xml.i>
+              #include <include/qos/target.xml.i>
+              #include <include/qos/queue-type.xml.i>
+            </children>
+          </tagNode>
+          <node name="default">
+            <properties>
+              <help>Default policy</help>
+            </properties>
+            <children>
+              #include <include/generic-description.xml.i>
+              #include <include/qos/codel-quantum.xml.i>
+              #include <include/qos/flows.xml.i>
+              #include <include/qos/interval.xml.i>
+              #include <include/qos/queue-limit-2-10999.xml.i>
+              #include <include/qos/target.xml.i>
+              #include <include/qos/queue-type.xml.i>
+            </children>
+          </node>
+          #include <include/generic-description.xml.i>
+        </children>
+      </tagNode>
+      <tagNode name="random-detect">
+        <properties>
+          <help>Priority queuing based policy</help>
+          <valueHelp>
+            <format>txt</format>
+            <description>Policy name</description>
+          </valueHelp>
+          <constraint>
+            <regex>[[:alnum:]][-_[:alnum:]]*</regex>
+          </constraint>
+          <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>
+        </properties>
+        <children>
+          #include <include/qos/bandwidth.xml.i>
+          <leafNode name="bandwidth">
+            <defaultValue>auto</defaultValue>
+          </leafNode>
+          #include <include/generic-description.xml.i>
+          <tagNode name="precedence">
+            <properties>
+              <help>IP precedence</help>
+              <valueHelp>
+                <format>u32:0-7</format>
+                <description>IP precedence value</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 0-7"/>
+              </constraint>
+              <constraintErrorMessage>IP precedence value must be between 0 and 7</constraintErrorMessage>
+            </properties>
+            <children>
+              #include <include/qos/queue-limit-1-4294967295.xml.i>
+              <leafNode name="average-packet">
+                <properties>
+                  <help>Average packet size (bytes)</help>
+                  <valueHelp>
+                    <format>u32:16-10240</format>
+                    <description>Average packet size in bytes</description>
+                  </valueHelp>
+                  <constraint>
+                    <validator name="numeric" argument="--range 0-100"/>
+                  </constraint>
+                  <constraintErrorMessage>Average packet size must be between 16 and 10240</constraintErrorMessage>
+                </properties>
+                <defaultValue>1024</defaultValue>
+              </leafNode>
+              <leafNode name="mark-probability">
+                <properties>
+                  <help>Mark probability for this precedence</help>
+                  <valueHelp>
+                    <format>&lt;number&gt;</format>
+                    <description>Numeric value (1/N)</description>
+                  </valueHelp>
+                  <constraint>
+                    <validator name="numeric" argument="--positive"/>
+                  </constraint>
+                  <constraintErrorMessage>Mark probability must be greater than 0</constraintErrorMessage>
+                </properties>
+              </leafNode>
+              <leafNode name="maximum-threshold">
+                <properties>
+                  <help>Maximum threshold for random detection</help>
+                  <valueHelp>
+                    <format>u32:0-4096</format>
+                    <description>Maximum Threshold in packets</description>
+                  </valueHelp>
+                  <constraint>
+                    <validator name="numeric" argument="--range 0-4096"/>
+                  </constraint>
+                  <constraintErrorMessage>Threshold must be between 0 and 4096</constraintErrorMessage>
+                </properties>
+              </leafNode>
+              <leafNode name="minimum-threshold">
+                <properties>
+                  <help>Minimum  threshold for random detection</help>
+                  <valueHelp>
+                    <format>u32:0-4096</format>
+                    <description>Maximum Threshold in packets</description>
+                  </valueHelp>
+                  <constraint>
+                    <validator name="numeric" argument="--range 0-4096"/>
+                  </constraint>
+                  <constraintErrorMessage>Threshold must be between 0 and 4096</constraintErrorMessage>
+                </properties>
+              </leafNode>
+            </children>
+          </tagNode>
+        </children>
+      </tagNode>
+      <tagNode name="rate-control">
+        <properties>
+          <help>Rate limiting policy (Token Bucket Filter)</help>
+          <valueHelp>
+            <format>txt</format>
+            <description>Policy name</description>
+          </valueHelp>
+          <constraint>
+            <regex>[[:alnum:]][-_[:alnum:]]*</regex>
+          </constraint>
+          <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>
+        </properties>
+        <children>
+          #include <include/qos/bandwidth.xml.i>
+          #include <include/generic-description.xml.i>
+          #include <include/qos/burst.xml.i>
+          <leafNode name="latency">
+            <properties>
+              <help>Maximum latency</help>
+              <valueHelp>
+                <format>&lt;number&gt;</format>
+                <description>Time in milliseconds</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 0-4096"/>
+              </constraint>
+              <constraintErrorMessage>Threshold must be between 0 and 4096</constraintErrorMessage>
+            </properties>
+            <defaultValue>50</defaultValue>
+          </leafNode>
+        </children>
+      </tagNode>
+      <tagNode name="round-robin">
+        <properties>
+          <help>Round-Robin based policy</help>
+          <valueHelp>
+            <format>txt</format>
+            <description>Policy name</description>
+          </valueHelp>
+          <constraint>
+            <regex>[[:alnum:]][-_[:alnum:]]*</regex>
+          </constraint>
+          <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>
+        </properties>
+        <children>
+          #include <include/generic-description.xml.i>
+          <tagNode name="class">
+            <properties>
+              <help>Class ID</help>
+              <valueHelp>
+                <format>u32:1-4095</format>
+                <description>Class Identifier</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 1-4095"/>
+              </constraint>
+              <constraintErrorMessage>Class identifier must be between 1 and 4095</constraintErrorMessage>
+            </properties>
+            <children>
+              #include <include/qos/codel-quantum.xml.i>
+              #include <include/generic-description.xml.i>
+              #include <include/qos/flows.xml.i>
+              #include <include/qos/interval.xml.i>
+              #include <include/qos/match.xml.i>
+              <leafNode name="quantum">
+                <properties>
+                  <help>Packet scheduling quantum</help>
+                  <valueHelp>
+                    <format>u32:1-4294967295</format>
+                    <description>Packet scheduling quantum (bytes)</description>
+                  </valueHelp>
+                  <constraint>
+                    <validator name="numeric" argument="--range 1-4294967295"/>
+                  </constraint>
+                  <constraintErrorMessage>Quantum must be in range 1 to 4294967295</constraintErrorMessage>
+                </properties>
+              </leafNode>
+              #include <include/qos/queue-limit-1-4294967295.xml.i>
+              #include <include/qos/queue-type.xml.i>
+              #include <include/qos/target.xml.i>
+            </children>
+          </tagNode>
+        </children>
+      </tagNode>
+      <tagNode name="shaper-hfsc">
+        <properties>
+          <help>Hierarchical Fair Service Curve's policy</help>
+          <valueHelp>
+            <format>txt</format>
+            <description>Policy name</description>
+          </valueHelp>
+          <constraint>
+            <regex>[[:alnum:]][-_[:alnum:]]*</regex>
+          </constraint>
+          <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>
+        </properties>
+        <children>
+          #include <include/qos/bandwidth.xml.i>
+          <leafNode name="bandwidth">
+            <defaultValue>auto</defaultValue>
+          </leafNode>
+          #include <include/generic-description.xml.i>
+          <tagNode name="class">
+            <properties>
+              <help>Class ID</help>
+              <valueHelp>
+                <format>u32:1-4095</format>
+                <description>Class Identifier</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 1-4095"/>
+              </constraint>
+              <constraintErrorMessage>Class identifier must be between 1 and 4095</constraintErrorMessage>
+            </properties>
+            <children>
+              #include <include/generic-description.xml.i>
+              <node name="linkshare">
+                <properties>
+                  <help>Linkshare class settings</help>
+                </properties>
+                <children>
+                  #include <include/qos/hfsc-d.xml.i>
+                  #include <include/qos/hfsc-m1.xml.i>
+                  #include <include/qos/hfsc-m2.xml.i>
+                </children>
+              </node>
+              #include <include/qos/match.xml.i>
+              <node name="realtime">
+                <properties>
+                  <help>Realtime class settings</help>
+                </properties>
+                <children>
+                  #include <include/qos/hfsc-d.xml.i>
+                  #include <include/qos/hfsc-m1.xml.i>
+                  #include <include/qos/hfsc-m2.xml.i>
+                </children>
+              </node>
+              <node name="upperlimit">
+                <properties>
+                  <help>Upperlimit class settings</help>
+                </properties>
+                <children>
+                  #include <include/qos/hfsc-d.xml.i>
+                  #include <include/qos/hfsc-m1.xml.i>
+                  #include <include/qos/hfsc-m2.xml.i>
+                </children>
+              </node>
+            </children>
+          </tagNode>
+          <node name="default">
+            <properties>
+              <help>Default policy</help>
+            </properties>
+            <children>
+              <node name="linkshare">
+                <properties>
+                  <help>Linkshare class settings</help>
+                </properties>
+                <children>
+                  #include <include/qos/hfsc-d.xml.i>
+                  #include <include/qos/hfsc-m1.xml.i>
+                  #include <include/qos/hfsc-m2.xml.i>
+                </children>
+              </node>
+              <node name="realtime">
+                <properties>
+                  <help>Realtime class settings</help>
+                </properties>
+                <children>
+                  #include <include/qos/hfsc-d.xml.i>
+                  #include <include/qos/hfsc-m1.xml.i>
+                  #include <include/qos/hfsc-m2.xml.i>
+                </children>
+              </node>
+              <node name="upperlimit">
+                <properties>
+                  <help>Upperlimit class settings</help>
+                </properties>
+                <children>
+                  #include <include/qos/hfsc-d.xml.i>
+                  #include <include/qos/hfsc-m1.xml.i>
+                  #include <include/qos/hfsc-m2.xml.i>
+                </children>
+              </node>
+            </children>
+          </node>
+        </children>
+      </tagNode>
+      <tagNode name="shaper">
+        <properties>
+          <help>Traffic shaping based policy (Hierarchy Token Bucket)</help>
+          <valueHelp>
+            <format>txt</format>
+            <description>Policy name</description>
+          </valueHelp>
+          <constraint>
+            <regex>[[:alnum:]][-_[:alnum:]]*</regex>
+          </constraint>
+          <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>
+        </properties>
+        <children>
+          #include <include/qos/bandwidth.xml.i>
+          <leafNode name="bandwidth">
+            <defaultValue>auto</defaultValue>
+          </leafNode>
+          <tagNode name="class">
+            <properties>
+              <help>Class ID</help>
+              <valueHelp>
+                <format>u32:2-4095</format>
+                <description>Class Identifier</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 2-4095"/>
+              </constraint>
+              <constraintErrorMessage>Class identifier must be between 2 and 4095</constraintErrorMessage>
+            </properties>
+            <children>
+              #include <include/qos/bandwidth.xml.i>
+              <leafNode name="bandwidth">
+                <defaultValue>100%</defaultValue>
+              </leafNode>
+              #include <include/qos/burst.xml.i>
+              <leafNode name="ceiling">
+                <properties>
+                  <help>Bandwidth limit for this class</help>
+                  <valueHelp>
+                    <format>&lt;number&gt;</format>
+                    <description>Rate in kbit (kilobit per second)</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>&lt;number&gt;%%</format>
+                    <description>Percentage of overall rate</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>&lt;number&gt;bit</format>
+                    <description>bit(1), kbit(10^3), mbit(10^6), gbit, tbit</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>&lt;number&gt;ibit</format>
+                    <description>kibit(1024), mibit(1024^2), gibit(1024^3), tbit(1024^4)</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>&lt;number&gt;ibps</format>
+                    <description>kibps(1024*8), mibps(1024^2*8), gibps, tibps - Byte/sec</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>&lt;number&gt;bps</format>
+                    <description>bps(8),kbps(8*10^3),mbps(8*10^6), gbps, tbps - Byte/sec</description>
+                  </valueHelp>
+                </properties>
+              </leafNode>
+              #include <include/qos/codel-quantum.xml.i>
+              #include <include/generic-description.xml.i>
+              #include <include/qos/flows.xml.i>
+              #include <include/qos/interval.xml.i>
+              #include <include/qos/match.xml.i>
+              <leafNode name="priority">
+                <properties>
+                  <help>Priority for usage of excess bandwidth</help>
+                  <valueHelp>
+                    <format>u32:0-7</format>
+                    <description>Priority order for bandwidth pool</description>
+                  </valueHelp>
+                  <constraint>
+                    <validator name="numeric" argument="--range 0-7"/>
+                  </constraint>
+                  <constraintErrorMessage>Priority must be between 0 and 7</constraintErrorMessage>
+                </properties>
+                <defaultValue>20</defaultValue>
+              </leafNode>
+              #include <include/qos/queue-limit-1-4294967295.xml.i>
+              #include <include/qos/queue-type.xml.i>
+              #include <include/qos/set-dscp.xml.i>
+              #include <include/qos/target.xml.i>
+            </children>
+          </tagNode>
+          #include <include/generic-description.xml.i>
+          <node name="default">
+            <properties>
+              <help>Default policy</help>
+            </properties>
+            <children>
+              #include <include/qos/bandwidth.xml.i>
+              #include <include/qos/burst.xml.i>
+              <leafNode name="ceiling">
+                <properties>
+                  <help>Bandwidth limit for this class</help>
+                  <valueHelp>
+                    <format>&lt;number&gt;</format>
+                    <description>Rate in kbit (kilobit per second)</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>&lt;number&gt;%%</format>
+                    <description>Percentage of overall rate</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>&lt;number&gt;bit</format>
+                    <description>bit(1), kbit(10^3), mbit(10^6), gbit, tbit</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>&lt;number&gt;ibit</format>
+                    <description>kibit(1024), mibit(1024^2), gibit(1024^3), tbit(1024^4)</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>&lt;number&gt;ibps</format>
+                    <description>kibps(1024*8), mibps(1024^2*8), gibps, tibps - Byte/sec</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>&lt;number&gt;bps</format>
+                    <description>bps(8),kbps(8*10^3),mbps(8*10^6), gbps, tbps - Byte/sec</description>
+                  </valueHelp>
+                </properties>
+              </leafNode>
+              #include <include/qos/codel-quantum.xml.i>
+              #include <include/generic-description.xml.i>
+              #include <include/qos/flows.xml.i>
+              #include <include/qos/interval.xml.i>
+              <leafNode name="priority">
+                <properties>
+                  <help>Priority for usage of excess bandwidth</help>
+                  <valueHelp>
+                    <format>u32:0-7</format>
+                    <description>Priority order for bandwidth pool</description>
+                  </valueHelp>
+                  <constraint>
+                    <validator name="numeric" argument="--range 0-7"/>
+                  </constraint>
+                  <constraintErrorMessage>Priority must be between 0 and 7</constraintErrorMessage>
+                </properties>
+                <defaultValue>20</defaultValue>
+              </leafNode>
+              #include <include/qos/queue-limit-1-4294967295.xml.i>
+              #include <include/qos/queue-type.xml.i>
+              #include <include/qos/set-dscp.xml.i>
+              #include <include/qos/target.xml.i>
+            </children>
+          </node>
+        </children>
+      </tagNode>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index fab88bc72..7f1258575 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -191,6 +191,19 @@ def verify_mirror(config):
                 raise ConfigError(f'Can not mirror "{direction}" traffic back ' \
                                    'the originating interface!')
 
+def verify_redirect(config):
+    """
+    Common helper function used by interface implementations to perform
+    recurring validation of the redirect interface configuration.
+
+    It makes no sense to mirror and redirect traffic at the same time!
+    """
+    if {'mirror', 'redirect'} <= set(config):
+        raise ConfigError('Can not do both redirect and mirror')
+
+    if dict_search('traffic_policy.in', config) != None:
+        raise ConfigError('Can not use ingress policy and redirect')
+
 def verify_authentication(config):
     """
     Common helper function used by interface implementations to perform
@@ -315,6 +328,7 @@ def verify_vlan_config(config):
         verify_dhcpv6(vlan)
         verify_address(vlan)
         verify_vrf(vlan)
+        verify_redirect(vlan)
         verify_mtu_parent(vlan, config)
 
     # 802.1ad (Q-in-Q) VLANs
@@ -323,6 +337,7 @@ def verify_vlan_config(config):
         verify_dhcpv6(s_vlan)
         verify_address(s_vlan)
         verify_vrf(s_vlan)
+        verify_redirect(s_vlan)
         verify_mtu_parent(s_vlan, config)
 
         for c_vlan in s_vlan.get('vif_c', {}):
@@ -330,6 +345,7 @@ def verify_vlan_config(config):
             verify_dhcpv6(c_vlan)
             verify_address(c_vlan)
             verify_vrf(c_vlan)
+            verify_redirect(c_vlan)
             verify_mtu_parent(c_vlan, config)
             verify_mtu_parent(c_vlan, s_vlan)
 
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index bb53cd6c2..661dc2298 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -27,9 +27,10 @@ from vyos.configdict import is_source_interface
 from vyos.configverify import verify_address
 from vyos.configverify import verify_bridge_delete
 from vyos.configverify import verify_dhcpv6
-from vyos.configverify import verify_source_interface
 from vyos.configverify import verify_mirror
 from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_redirect
+from vyos.configverify import verify_source_interface
 from vyos.configverify import verify_vlan_config
 from vyos.configverify import verify_vrf
 from vyos.ifconfig import BondIf
@@ -151,6 +152,7 @@ def verify(bond):
     verify_dhcpv6(bond)
     verify_vrf(bond)
     verify_mirror(bond)
+    verify_redirect(bond)
 
     # use common function to verify VLAN configuration
     verify_vlan_config(bond)
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 9f840cb58..e16c0e9f4 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -28,6 +28,7 @@ from vyos.configdict import has_vlan_subinterface_configured
 from vyos.configdict import dict_merge
 from vyos.configverify import verify_dhcpv6
 from vyos.configverify import verify_mirror
+from vyos.configverify import verify_redirect
 from vyos.configverify import verify_vrf
 from vyos.ifconfig import BridgeIf
 from vyos.validate import has_address_configured
@@ -107,6 +108,7 @@ def verify(bridge):
     verify_dhcpv6(bridge)
     verify_vrf(bridge)
     verify_mirror(bridge)
+    verify_redirect(bridge)
 
     ifname = bridge['ifname']
 
diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
index 55c783f38..4072c4452 100755
--- a/src/conf_mode/interfaces-dummy.py
+++ b/src/conf_mode/interfaces-dummy.py
@@ -21,6 +21,7 @@ from vyos.configdict import get_interface_dict
 from vyos.configverify import verify_vrf
 from vyos.configverify import verify_address
 from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_redirect
 from vyos.ifconfig import DummyIf
 from vyos import ConfigError
 from vyos import airbag
@@ -46,6 +47,7 @@ def verify(dummy):
 
     verify_vrf(dummy)
     verify_address(dummy)
+    verify_redirect(dummy)
 
     return None
 
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 2a8a126f2..3eeddf190 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -28,6 +28,7 @@ from vyos.configverify import verify_interface_exists
 from vyos.configverify import verify_mirror
 from vyos.configverify import verify_mtu
 from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_redirect
 from vyos.configverify import verify_vlan_config
 from vyos.configverify import verify_vrf
 from vyos.ethtool import Ethtool
@@ -84,6 +85,7 @@ def verify(ethernet):
     verify_vrf(ethernet)
     verify_eapol(ethernet)
     verify_mirror(ethernet)
+    verify_redirect(ethernet)
 
     ethtool = Ethtool(ifname)
     # No need to check speed and duplex keys as both have default values.
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
index 2a63b60aa..a94b5e1f7 100755
--- a/src/conf_mode/interfaces-geneve.py
+++ b/src/conf_mode/interfaces-geneve.py
@@ -24,6 +24,7 @@ from vyos.configdict import get_interface_dict
 from vyos.configverify import verify_address
 from vyos.configverify import verify_mtu_ipv6
 from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_redirect
 from vyos.ifconfig import GeneveIf
 from vyos import ConfigError
 
@@ -50,6 +51,7 @@ def verify(geneve):
 
     verify_mtu_ipv6(geneve)
     verify_address(geneve)
+    verify_redirect(geneve)
 
     if 'remote' not in geneve:
         raise ConfigError('Remote side must be configured')
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 9b6ddd5aa..5ea7159dc 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -25,6 +25,7 @@ from vyos.configdict import leaf_node_changed
 from vyos.configverify import verify_address
 from vyos.configverify import verify_bridge_delete
 from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_redirect
 from vyos.ifconfig import L2TPv3If
 from vyos.util import check_kmod
 from vyos.validate import is_addr_assigned
@@ -76,6 +77,7 @@ def verify(l2tpv3):
 
     verify_mtu_ipv6(l2tpv3)
     verify_address(l2tpv3)
+    verify_redirect(l2tpv3)
     return None
 
 def generate(l2tpv3):
diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py
index 193334443..e6a851113 100755
--- a/src/conf_mode/interfaces-loopback.py
+++ b/src/conf_mode/interfaces-loopback.py
@@ -20,6 +20,7 @@ from sys import exit
 
 from vyos.config import Config
 from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_redirect
 from vyos.ifconfig import LoopbackIf
 from vyos import ConfigError
 from vyos import airbag
@@ -39,6 +40,7 @@ def get_config(config=None):
     return loopback
 
 def verify(loopback):
+    verify_redirect(loopback)
     return None
 
 def generate(loopback):
diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py
index eab69f36e..6a29fdb11 100755
--- a/src/conf_mode/interfaces-macsec.py
+++ b/src/conf_mode/interfaces-macsec.py
@@ -29,6 +29,7 @@ from vyos.configverify import verify_vrf
 from vyos.configverify import verify_address
 from vyos.configverify import verify_bridge_delete
 from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_redirect
 from vyos.configverify import verify_source_interface
 from vyos import ConfigError
 from vyos import airbag
@@ -66,6 +67,7 @@ def verify(macsec):
     verify_vrf(macsec)
     verify_mtu_ipv6(macsec)
     verify_address(macsec)
+    verify_redirect(macsec)
 
     if not (('security' in macsec) and
             ('cipher' in macsec['security'])):
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index 584adc75e..9962e0a08 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -28,6 +28,7 @@ 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_redirect
 from vyos.ifconfig import PPPoEIf
 from vyos.template import render
 from vyos.util import call
@@ -85,6 +86,7 @@ def verify(pppoe):
     verify_authentication(pppoe)
     verify_vrf(pppoe)
     verify_mtu_ipv6(pppoe)
+    verify_redirect(pppoe)
 
     if {'connect_on_demand', 'vrf'} <= set(pppoe):
         raise ConfigError('On-demand dialing and VRF can not be used at the same time')
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index 945a2ea9c..f57e41cc4 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -25,6 +25,7 @@ from vyos.configverify import verify_bridge_delete
 from vyos.configverify import verify_source_interface
 from vyos.configverify import verify_vlan_config
 from vyos.configverify import verify_mtu_parent
+from vyos.configverify import verify_redirect
 from vyos.ifconfig import MACVLANIf
 from vyos import ConfigError
 
@@ -60,6 +61,7 @@ def verify(peth):
     verify_vrf(peth)
     verify_address(peth)
     verify_mtu_parent(peth, peth['parent'])
+    verify_redirect(peth)
     # use common function to verify VLAN configuration
     verify_vlan_config(peth)
 
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index 433764b8a..005fae5eb 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -26,6 +26,7 @@ from vyos.configverify import verify_address
 from vyos.configverify import verify_bridge_delete
 from vyos.configverify import verify_interface_exists
 from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_redirect
 from vyos.configverify import verify_vrf
 from vyos.configverify import verify_tunnel
 from vyos.ifconfig import Interface
@@ -157,6 +158,7 @@ def verify(tunnel):
     verify_mtu_ipv6(tunnel)
     verify_address(tunnel)
     verify_vrf(tunnel)
+    verify_redirect(tunnel)
 
     if 'source_interface' in tunnel:
         verify_interface_exists(tunnel['source_interface'])
diff --git a/src/conf_mode/interfaces-vti.py b/src/conf_mode/interfaces-vti.py
index 57950ffea..30e13536f 100755
--- a/src/conf_mode/interfaces-vti.py
+++ b/src/conf_mode/interfaces-vti.py
@@ -19,6 +19,7 @@ from sys import exit
 
 from vyos.config import Config
 from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_redirect
 from vyos.ifconfig import VTIIf
 from vyos.util import dict_search
 from vyos import ConfigError
@@ -39,6 +40,7 @@ def get_config(config=None):
     return vti
 
 def verify(vti):
+    verify_redirect(vti)
     return None
 
 def generate(vti):
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 29b16af89..a29836efd 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -25,6 +25,7 @@ from vyos.configdict import leaf_node_changed
 from vyos.configverify import verify_address
 from vyos.configverify import verify_bridge_delete
 from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_redirect
 from vyos.configverify import verify_source_interface
 from vyos.ifconfig import Interface
 from vyos.ifconfig import VXLANIf
@@ -140,6 +141,7 @@ def verify(vxlan):
 
     verify_mtu_ipv6(vxlan)
     verify_address(vxlan)
+    verify_redirect(vxlan)
     return None
 
 def generate(vxlan):
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index da64dd076..dc0fe7b9c 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -28,6 +28,7 @@ from vyos.configverify import verify_vrf
 from vyos.configverify import verify_address
 from vyos.configverify import verify_bridge_delete
 from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_redirect
 from vyos.ifconfig import WireGuardIf
 from vyos.util import check_kmod
 from vyos.util import check_port_availability
@@ -70,6 +71,7 @@ def verify(wireguard):
     verify_mtu_ipv6(wireguard)
     verify_address(wireguard)
     verify_vrf(wireguard)
+    verify_redirect(wireguard)
 
     if 'private_key' not in wireguard:
         raise ConfigError('Wireguard private-key not defined')
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index af35b5f03..fdf9e3988 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -27,6 +27,7 @@ from vyos.configverify import verify_address
 from vyos.configverify import verify_bridge_delete
 from vyos.configverify import verify_dhcpv6
 from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_redirect
 from vyos.configverify import verify_vlan_config
 from vyos.configverify import verify_vrf
 from vyos.ifconfig import WiFiIf
@@ -189,6 +190,7 @@ def verify(wifi):
 
     verify_address(wifi)
     verify_vrf(wifi)
+    verify_redirect(wifi)
 
     # use common function to verify VLAN configuration
     verify_vlan_config(wifi)
diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py
index a4b033374..367a50e82 100755
--- a/src/conf_mode/interfaces-wwan.py
+++ b/src/conf_mode/interfaces-wwan.py
@@ -23,6 +23,7 @@ from vyos.config import Config
 from vyos.configdict import get_interface_dict
 from vyos.configverify import verify_authentication
 from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_redirect
 from vyos.configverify import verify_vrf
 from vyos.ifconfig import WWANIf
 from vyos.util import cmd
@@ -77,6 +78,7 @@ def verify(wwan):
     verify_interface_exists(ifname)
     verify_authentication(wwan)
     verify_vrf(wwan)
+    verify_redirect(wwan)
 
     return None
 
diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py
new file mode 100755
index 000000000..cf447d4b5
--- /dev/null
+++ b/src/conf_mode/qos.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['traffic-policy']
+    if not conf.exists(base):
+        return None
+
+    qos = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+    for traffic_policy in ['drop-tail', 'fair-queue', 'fq-codel', 'limiter',
+                           'network-emulator', 'priority-queue', 'random-detect',
+                           'rate-control', 'round-robin', 'shaper', 'shaper-hfsc']:
+        traffic_policy_us = traffic_policy.replace('-','_')
+        # Individual policy type not present on CLI - no need to blend in
+        # any default values
+        if traffic_policy_us not in qos:
+            continue
+
+        default_values = defaults(base + [traffic_policy_us])
+
+        # class is another tag node which requires individual handling
+        class_default_values = defaults(base + [traffic_policy_us, 'class'])
+        if 'class' in default_values:
+            del default_values['class']
+
+        for policy, policy_config in qos[traffic_policy_us].items():
+            qos[traffic_policy_us][policy] = dict_merge(
+                default_values, qos[traffic_policy_us][policy])
+
+            if 'class' in policy_config:
+                for policy_class in policy_config['class']:
+                    qos[traffic_policy_us][policy]['class'][policy_class] = dict_merge(
+                        class_default_values, qos[traffic_policy_us][policy]['class'][policy_class])
+
+    import pprint
+    pprint.pprint(qos)
+    return qos
+
+def verify(qos):
+    if not qos:
+        return None
+
+    # network policy emulator
+    # reorder rerquires delay to be set
+
+    raise ConfigError('123')
+    return None
+
+def generate(qos):
+    return None
+
+def apply(qos):
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
-- 
cgit v1.2.3


From dae259c5dbb6146f584251fe9a39a1a56cf926be Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Tue, 22 Mar 2022 07:11:00 +0100
Subject: qos: T4284: delete traffic-policy CLI path via Makefile

Implementation is still work in progress, as such the CLI XML definitions are
published but the Python code does not work. In this case we must ensure XML
backed node.def files are deleted after generation so they do not conflict with
the current vyatta-cfg-qos implementation.
---
 Makefile | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Makefile b/Makefile
index 431f3a8c2..54f3892ba 100644
--- a/Makefile
+++ b/Makefile
@@ -33,6 +33,7 @@ interface_definitions: $(config_xml_obj)
 	# T4284 neq QoS implementation is not yet live
 	find $(TMPL_DIR)/interfaces -name traffic-policy -type d -exec rm -rf {} \;
 	find $(TMPL_DIR)/interfaces -name redirect -type d -exec rm -rf {} \;
+	rm -rf $(TMPL_DIR)/traffic-policy
 	rm -rf $(TMPL_DIR)/interfaces/input
 
 	# XXX: test if there are empty node.def files - this is not allowed as these
-- 
cgit v1.2.3


From c37829f1e902b84a5bc3bc5618ee97ae1ba0dd86 Mon Sep 17 00:00:00 2001
From: Daniil Baturin <daniil@vyos.io>
Date: Tue, 22 Mar 2022 04:50:34 -0400
Subject: T4313: handle exceptions in the "generate public-key-command" script

---
 src/op_mode/generate_public_key_command.py | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/op_mode/generate_public_key_command.py b/src/op_mode/generate_public_key_command.py
index 7a7b6c923..f071ae350 100755
--- a/src/op_mode/generate_public_key_command.py
+++ b/src/op_mode/generate_public_key_command.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2021 VyOS maintainers and contributors
+# 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
@@ -29,8 +29,12 @@ def get_key(path):
         key_string = vyos.remote.get_remote_config(path)
     return key_string.split()
 
-username = sys.argv[1]
-algorithm, key, identifier = get_key(sys.argv[2])
+try:
+    username = sys.argv[1]
+    algorithm, key, identifier = get_key(sys.argv[2])
+except Exception as e:
+    print("Failed to retrieve the public key: {}".format(e))
+    sys.exit(1)
 
 print('# To add this key as an embedded key, run the following commands:')
 print('configure')
@@ -39,3 +43,4 @@ print(f'set system login user {username} authentication public-keys {identifier}
 print('commit')
 print('save')
 print('exit')
+
-- 
cgit v1.2.3


From 34bb64b361b7887def8f507eab1c99fff44165cf Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Wed, 23 Mar 2022 09:04:36 +0100
Subject: l2tpv3: T1923: remove duplicate mtu include

---
 interface-definitions/interfaces-l2tpv3.xml.in | 1 -
 1 file changed, 1 deletion(-)

diff --git a/interface-definitions/interfaces-l2tpv3.xml.in b/interface-definitions/interfaces-l2tpv3.xml.in
index 124863653..680170b0f 100644
--- a/interface-definitions/interfaces-l2tpv3.xml.in
+++ b/interface-definitions/interfaces-l2tpv3.xml.in
@@ -86,7 +86,6 @@
               </constraint>
             </properties>
           </leafNode>
-          #include <include/interface/mtu-68-16000.xml.i>
           #include <include/interface/tunnel-remote.xml.i>
           <leafNode name="session-id">
             <properties>
-- 
cgit v1.2.3


From d193e5cb9040bfca5011400acef601e8c7111346 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Wed, 23 Mar 2022 08:42:40 -0500
Subject: bgp: T4314: add missing check to migration script

---
 src/migration-scripts/bgp/0-to-1 | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/migration-scripts/bgp/0-to-1 b/src/migration-scripts/bgp/0-to-1
index b1d5a6514..5e9dffe1f 100755
--- a/src/migration-scripts/bgp/0-to-1
+++ b/src/migration-scripts/bgp/0-to-1
@@ -33,7 +33,7 @@ with open(file_name, 'r') as f:
 base = ['protocols', 'bgp']
 config = ConfigTree(config_file)
 
-if not config.exists(base):
+if not config.exists(base) or not config.is_tag(base):
     # Nothing to do
     exit(0)
 
-- 
cgit v1.2.3


From 78a4676f787e5e37f67afd5c2453ce06e3f0f9e9 Mon Sep 17 00:00:00 2001
From: srividya0208 <a.srividya@vyos.io>
Date: Fri, 18 Mar 2022 08:39:14 -0400
Subject: ike-group: T4288 : close-action is missing in swanctl.conf

close-action parameter is missing in the swanctl.conf file
---
 data/templates/ipsec/swanctl/peer.tmpl |  6 +++--
 interface-definitions/vpn_ipsec.xml.in |  8 ++----
 src/migration-scripts/ipsec/8-to-9     | 49 ++++++++++++++++++++++++++++++++++
 3 files changed, 55 insertions(+), 8 deletions(-)
 create mode 100755 src/migration-scripts/ipsec/8-to-9

diff --git a/data/templates/ipsec/swanctl/peer.tmpl b/data/templates/ipsec/swanctl/peer.tmpl
index 562e8fdd5..a622cbf74 100644
--- a/data/templates/ipsec/swanctl/peer.tmpl
+++ b/data/templates/ipsec/swanctl/peer.tmpl
@@ -87,9 +87,10 @@
                 start_action = none
 {%     endif %}
 {%     if ike.dead_peer_detection is defined %}
-{%       set dpd_translate = {'clear': 'clear', 'hold': 'trap', 'restart': 'start'} %}
+{%       set dpd_translate = {'clear': 'clear', 'hold': 'trap', 'restart': 'restart'} %}
                 dpd_action = {{ dpd_translate[ike.dead_peer_detection.action] }}
 {%     endif %}
+                close_action = {{ {'none': 'none', 'hold': 'trap', 'restart': 'start'}[ike.close_action] }}
             }
 {%   elif peer_conf.tunnel is defined %}
 {%     for tunnel_id, tunnel_conf in peer_conf.tunnel.items() if tunnel_conf.disable is not defined %}
@@ -137,9 +138,10 @@
                 start_action = none
 {%       endif %}
 {%       if ike.dead_peer_detection is defined %}
-{%         set dpd_translate = {'clear': 'clear', 'hold': 'trap', 'restart': 'start'} %}
+{%         set dpd_translate = {'clear': 'clear', 'hold': 'trap', 'restart': 'restart'} %}
                 dpd_action = {{ dpd_translate[ike.dead_peer_detection.action] }}
 {%       endif %}
+                close_action = {{ {'none': 'none', 'hold': 'trap', 'restart': 'start'}[ike.close_action] }}
 {%       if peer_conf.vti is defined and peer_conf.vti.bind is defined %}
                 updown = "/etc/ipsec.d/vti-up-down {{ peer_conf.vti.bind }}"
                 {# The key defaults to 0 and will match any policies which similarly do not have a lookup key configuration. #}
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index d8c06a310..a86951ce8 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -231,7 +231,7 @@
                 <properties>
                   <help>Action to take if a child SA is unexpectedly closed</help>
                   <completionHelp>
-                    <list>none hold clear restart</list>
+                    <list>none hold restart</list>
                   </completionHelp>
                   <valueHelp>
                     <format>none</format>
@@ -241,16 +241,12 @@
                     <format>hold</format>
                     <description>Attempt to re-negotiate when matching traffic is seen</description>
                   </valueHelp>
-                  <valueHelp>
-                    <format>clear</format>
-                    <description>Remove the connection immediately</description>
-                  </valueHelp>
                   <valueHelp>
                     <format>restart</format>
                     <description>Attempt to re-negotiate the connection immediately</description>
                   </valueHelp>
                   <constraint>
-                    <regex>^(none|hold|clear|restart)$</regex>
+                    <regex>^(none|hold|restart)$</regex>
                   </constraint>
                 </properties>
               </leafNode>
diff --git a/src/migration-scripts/ipsec/8-to-9 b/src/migration-scripts/ipsec/8-to-9
new file mode 100755
index 000000000..209cd8ac9
--- /dev/null
+++ b/src/migration-scripts/ipsec/8-to-9
@@ -0,0 +1,49 @@
+
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from sys import argv
+from sys import exit
+
+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 = ['vpn', 'ipsec', 'ike-group']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+    # Nothing to do
+    exit(0)
+else:
+    for ike_group in config.list_nodes(base):
+        base_closeaction = base + [ike_group, 'close-action']
+        if config.exists(base_closeaction) and config.return_value(base_closeaction) == 'clear':
+            config.set(base_closeaction, 'none', replace=True)
+
+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)
-- 
cgit v1.2.3


From 999b1e50dfdea8694174e82d22b2438cb1bf5e28 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 24 Mar 2022 17:42:59 +0100
Subject: openvpn: T4294: force service restart on openvpn-option node change

---
 src/conf_mode/interfaces-openvpn.py | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 29a25eedc..c30c0bdd0 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -32,6 +32,7 @@ from shutil import rmtree
 
 from vyos.config import Config
 from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
 from vyos.configverify import verify_vrf
 from vyos.configverify import verify_bridge_delete
 from vyos.ifconfig import VTunIf
@@ -88,6 +89,9 @@ def get_config(config=None):
     if 'deleted' not in openvpn:
         openvpn['pki'] = tmp_pki
 
+        tmp = leaf_node_changed(conf, ['openvpn-option'])
+        if tmp: openvpn['restart_required'] = ''
+
         # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict'
         # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there.
         tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True)
@@ -651,7 +655,10 @@ def apply(openvpn):
 
     # No matching OpenVPN process running - maybe it got killed or none
     # existed - nevertheless, spawn new OpenVPN process
-    call(f'systemctl reload-or-restart openvpn@{interface}.service')
+    action = 'reload-or-restart'
+    if 'restart_required' in openvpn:
+        action = 'restart'
+    call(f'systemctl {action} openvpn@{interface}.service')
 
     o = VTunIf(**openvpn)
     o.update(openvpn)
-- 
cgit v1.2.3


From cb499ee9d467d31f9a75a76f668c9ca0d8a3484f Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 24 Mar 2022 18:28:49 +0100
Subject: ipsec: T4288: drop leading empty line to detect runtime environment

---
 src/migration-scripts/ipsec/8-to-9 | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/migration-scripts/ipsec/8-to-9 b/src/migration-scripts/ipsec/8-to-9
index 209cd8ac9..eb44b6216 100755
--- a/src/migration-scripts/ipsec/8-to-9
+++ b/src/migration-scripts/ipsec/8-to-9
@@ -1,7 +1,6 @@
-
 #!/usr/bin/env python3
 #
-# Copyright (C) 2021 VyOS maintainers and contributors
+# 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
-- 
cgit v1.2.3


From e700bd3e22e080525e70ce560c0e48d41a80a9d2 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 24 Mar 2022 18:42:40 +0100
Subject: ipsec: T4288: bump config version 8 -> 9

---
 interface-definitions/include/version/ipsec-version.xml.i | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/interface-definitions/include/version/ipsec-version.xml.i b/interface-definitions/include/version/ipsec-version.xml.i
index fcdd6c702..59295cc91 100644
--- a/interface-definitions/include/version/ipsec-version.xml.i
+++ b/interface-definitions/include/version/ipsec-version.xml.i
@@ -1,3 +1,3 @@
 <!-- include start from include/version/ipsec-version.xml.i -->
-<syntaxVersion component='ipsec' version='8'></syntaxVersion>
+<syntaxVersion component='ipsec' version='9'></syntaxVersion>
 <!-- include end -->
-- 
cgit v1.2.3


From 87ccafd06b897b63f847e6b47cf72b951b0ed223 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Mar 2022 18:53:12 +0100
Subject: T4319: "system ip(v6)" must run before any interface operation

---
 interface-definitions/system-ip.xml.in   | 3 ++-
 interface-definitions/system-ipv6.xml.in | 1 +
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/interface-definitions/system-ip.xml.in b/interface-definitions/system-ip.xml.in
index 1fa63d517..b43100418 100644
--- a/interface-definitions/system-ip.xml.in
+++ b/interface-definitions/system-ip.xml.in
@@ -5,7 +5,8 @@
       <node name="ip" owner="${vyos_conf_scripts_dir}/system-ip.py">
         <properties>
           <help>IPv4 Settings</help>
-          <priority>400</priority>
+          <!-- must be before any interface, check /opt/vyatta/sbin/priority.pl -->
+          <priority>290</priority>
         </properties>
         <children>
           <node name="arp">
diff --git a/interface-definitions/system-ipv6.xml.in b/interface-definitions/system-ipv6.xml.in
index 5ee7adf54..ff1080544 100644
--- a/interface-definitions/system-ipv6.xml.in
+++ b/interface-definitions/system-ipv6.xml.in
@@ -5,6 +5,7 @@
       <node name="ipv6" owner="${vyos_conf_scripts_dir}/system-ipv6.py">
         <properties>
           <help>IPv6 Settings</help>
+          <!-- must be before any interface, check /opt/vyatta/sbin/priority.pl -->
           <priority>290</priority>
         </properties>
         <children>
-- 
cgit v1.2.3


From eaf4b60c9e7fa094d17b87b29bebaf81182ee7a1 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Mar 2022 18:53:50 +0100
Subject: xml: T4319: use common building block for table-size CLI option

---
 interface-definitions/include/arp-ndp-table-size.xml.i | 14 ++++++++++++++
 interface-definitions/system-ip.xml.in                 | 13 +------------
 interface-definitions/system-ipv6.xml.in               | 14 ++------------
 3 files changed, 17 insertions(+), 24 deletions(-)
 create mode 100644 interface-definitions/include/arp-ndp-table-size.xml.i

diff --git a/interface-definitions/include/arp-ndp-table-size.xml.i b/interface-definitions/include/arp-ndp-table-size.xml.i
new file mode 100644
index 000000000..dec86e91a
--- /dev/null
+++ b/interface-definitions/include/arp-ndp-table-size.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from arp-ndp-table-size.xml.i -->
+<leafNode name="table-size">
+  <properties>
+    <help>Maximum number of entries to keep in the cache</help>
+    <completionHelp>
+      <list>1024 2048 4096 8192 16384 32768</list>
+    </completionHelp>
+    <constraint>
+      <regex>(1024|2048|4096|8192|16384|32768)</regex>
+    </constraint>
+  </properties>
+  <defaultValue>8192</defaultValue>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/system-ip.xml.in b/interface-definitions/system-ip.xml.in
index b43100418..21d70694b 100644
--- a/interface-definitions/system-ip.xml.in
+++ b/interface-definitions/system-ip.xml.in
@@ -14,18 +14,7 @@
               <help>Parameters for ARP cache</help>
             </properties>
             <children>
-              <leafNode name="table-size">
-                <properties>
-                  <help>Maximum number of entries to keep in the ARP cache</help>
-                  <completionHelp>
-                    <list>1024 2048 4096 8192 16384 32768</list>
-                  </completionHelp>
-                  <constraint>
-                    <regex>^(1024|2048|4096|8192|16384|32768)$</regex>
-                  </constraint>
-                </properties>
-                <defaultValue>8192</defaultValue>
-              </leafNode>
+              #include <include/arp-ndp-table-size.xml.i>
             </children>
           </node>
           <leafNode name="disable-forwarding">
diff --git a/interface-definitions/system-ipv6.xml.in b/interface-definitions/system-ipv6.xml.in
index ff1080544..af4dcdb0f 100644
--- a/interface-definitions/system-ipv6.xml.in
+++ b/interface-definitions/system-ipv6.xml.in
@@ -36,20 +36,10 @@
           </node>
           <node name="neighbor">
             <properties>
-              <help>Parameters for Neighbor cache</help>
+              <help>Parameters for neighbor discovery cache</help>
             </properties>
             <children>
-              <leafNode name="table-size">
-                <properties>
-                  <help>Maximum number of entries to keep in the Neighbor cache</help>
-                  <completionHelp>
-                    <list>1024 2048 4096 8192 16384 32768</list>
-                  </completionHelp>
-                  <constraint>
-                    <regex>^(1024|2048|4096|8192|16384|32768)$</regex>
-                  </constraint>
-                </properties>
-              </leafNode>
+              #include <include/arp-ndp-table-size.xml.i>
             </children>
           </node>
           <leafNode name="strict-dad">
-- 
cgit v1.2.3


From 52cb6185a4a51ffa92f10e0ded55a943bc21bc60 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Mar 2022 18:54:44 +0100
Subject: vyos.util: T4319: provide generic sysctl_read() helper

---
 python/vyos/util.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/python/vyos/util.py b/python/vyos/util.py
index 4526375df..da39ee8d1 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -1006,11 +1006,16 @@ def boot_configuration_complete() -> bool:
         return True
     return False
 
+def sysctl_read(name):
+    """ Read and return current value of sysctl() option """
+    tmp = cmd(f'sysctl {name}')
+    return tmp.split()[-1]
+
 def sysctl(name, value):
     """ Change value via sysctl() - return True if changed, False otherwise """
     tmp = cmd(f'sysctl {name}')
     # last list index contains the actual value - only write if value differs
-    if tmp.split()[-1] != str(value):
+    if sysctl_read(name) != str(value):
         call(f'sysctl -wq {name}={value}')
         return True
     return False
-- 
cgit v1.2.3


From 1b16a4eab926462c0d2752d698bedf28c995058d Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Mar 2022 18:55:32 +0100
Subject: system: T4319: align ipv6 settings with ipv4 by using
 get_config_dict()

---
 src/conf_mode/system-ip.py   |  43 +++++++++---------
 src/conf_mode/system-ipv6.py | 103 +++++++++++++++++++------------------------
 2 files changed, 66 insertions(+), 80 deletions(-)

diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py
index 32cb2f036..8b97725ac 100755
--- a/src/conf_mode/system-ip.py
+++ b/src/conf_mode/system-ip.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2019-2020 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
@@ -20,14 +20,13 @@ from vyos.config import Config
 from vyos.configdict import dict_merge
 from vyos.util import call
 from vyos.util import dict_search
+from vyos.util import sysctl
+from vyos.util import write_file
 from vyos.xml import defaults
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
-def sysctl(name, value):
-    call(f'sysctl -wq {name}={value}')
-
 def get_config(config=None):
     if config:
         conf = config
@@ -50,29 +49,29 @@ def generate(opt):
     pass
 
 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))
-    if size:
-        # apply ARP threshold values
-        sysctl('net.ipv4.neigh.default.gc_thresh3', str(size))
-        sysctl('net.ipv4.neigh.default.gc_thresh2', str(size // 2))
-        sysctl('net.ipv4.neigh.default.gc_thresh1', str(size // 8))
+    # Amount upon reaching which the records begin to be cleared immediately
+    sysctl('net.ipv4.neigh.default.gc_thresh3', size)
+    # Amount after which the records begin to be cleaned after 5 seconds
+    sysctl('net.ipv4.neigh.default.gc_thresh2', size // 2)
+    # Minimum number of stored records is indicated which is not cleared
+    sysctl('net.ipv4.neigh.default.gc_thresh1', size // 8)
 
     # enable/disable IPv4 forwarding
-    tmp = '1'
-    if 'disable_forwarding' in opt:
-        tmp = '0'
-    sysctl('net.ipv4.conf.all.forwarding', tmp)
+    tmp = dict_search('disable_forwarding', opt)
+    value = '0' if (tmp != None) else '1'
+    write_file('/proc/sys/net/ipv4/conf/all/forwarding', value)
 
-    tmp = '0'
-    # configure multipath - dict_search() returns an empty dict if key was found
-    if isinstance(dict_search('multipath.ignore_unreachable_nexthops', opt), dict):
-        tmp = '1'
-    sysctl('net.ipv4.fib_multipath_use_neigh', tmp)
+    # configure multipath
+    tmp = dict_search('multipath.ignore_unreachable_nexthops', opt)
+    value = '1' if (tmp != None) else '0'
+    sysctl('net.ipv4.fib_multipath_use_neigh', value)
 
-    tmp = '0'
-    if isinstance(dict_search('multipath.layer4_hashing', opt), dict):
-        tmp = '1'
-    sysctl('net.ipv4.fib_multipath_hash_policy', tmp)
+    tmp = dict_search('multipath.layer4_hashing', opt)
+    value = '1' if (tmp != None) else '0'
+    sysctl('net.ipv4.fib_multipath_hash_policy', value)
 
 if __name__ == '__main__':
     try:
diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
index f70ec2631..8195beaa6 100755
--- a/src/conf_mode/system-ipv6.py
+++ b/src/conf_mode/system-ipv6.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2019 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
@@ -15,95 +15,82 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
-import sys
 
 from sys import exit
-from copy import deepcopy
 from vyos.config import Config
-from vyos import ConfigError
+from vyos.configdict import dict_merge
+from vyos.configdict import leaf_node_changed
 from vyos.util import call
-
+from vyos.util import dict_search
+from vyos.util import sysctl
+from vyos.util import write_file
+from vyos.xml import defaults
+from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
-ipv6_disable_file = '/etc/modprobe.d/vyos_disable_ipv6.conf'
-
-default_config_data = {
-    'reboot_message': False,
-    'ipv6_forward': '1',
-    'disable_addr_assignment': False,
-    'mp_layer4_hashing': '0',
-    'neighbor_cache': 8192,
-    'strict_dad': '1'
-
-}
-
-def sysctl(name, value):
-    call('sysctl -wq {}={}'.format(name, value))
-
 def get_config(config=None):
-    ip_opt = deepcopy(default_config_data)
     if config:
         conf = config
     else:
         conf = Config()
-    conf.set_level('system ipv6')
-    if conf.exists(''):
-        ip_opt['disable_addr_assignment'] = conf.exists('disable')
-        if conf.exists_effective('disable') != conf.exists('disable'):
-            ip_opt['reboot_message'] = True
-
-        if conf.exists('disable-forwarding'):
-            ip_opt['ipv6_forward'] = '0'
+    base = ['system', 'ipv6']
 
-        if conf.exists('multipath layer4-hashing'):
-            ip_opt['mp_layer4_hashing'] = '1'
+    opt = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
 
-        if conf.exists('neighbor table-size'):
-            ip_opt['neighbor_cache'] = int(conf.return_value('neighbor table-size'))
+    tmp = leaf_node_changed(conf, base + ['disable'])
+    if tmp: opt['reboot_required'] = {}
 
-        if conf.exists('strict-dad'):
-            ip_opt['strict_dad'] = 2
+    # 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)
+    opt = dict_merge(default_values, opt)
 
-    return ip_opt
+    return opt
 
-def verify(ip_opt):
+def verify(opt):
     pass
 
-def generate(ip_opt):
+def generate(opt):
     pass
 
-def apply(ip_opt):
-    # disable IPv6 address assignment
-    if ip_opt['disable_addr_assignment']:
-        with open(ipv6_disable_file, 'w') as f:
-            f.write('options ipv6 disable_ipv6=1')
-    else:
-        if os.path.exists(ipv6_disable_file):
-            os.unlink(ipv6_disable_file)
+def apply(opt):
+    # disable IPv6 globally
+    tmp = dict_search('disable', opt)
+    value = '1' if (tmp != None) else '0'
+    sysctl('net.ipv6.conf.all.disable_ipv6', value)
 
-    if ip_opt['reboot_message']:
+    if 'reboot_required' in opt:
         print('Changing IPv6 disable parameter will only take affect\n' \
               'when the system is rebooted.')
 
     # configure multipath
-    sysctl('net.ipv6.fib_multipath_hash_policy', ip_opt['mp_layer4_hashing'])
-
-    # apply neighbor table threshold values
-    sysctl('net.ipv6.neigh.default.gc_thresh3', ip_opt['neighbor_cache'])
-    sysctl('net.ipv6.neigh.default.gc_thresh2', ip_opt['neighbor_cache'] // 2)
-    sysctl('net.ipv6.neigh.default.gc_thresh1', ip_opt['neighbor_cache'] // 8)
+    tmp = dict_search('multipath.layer4_hashing', opt)
+    value = '1' if (tmp != None) else '0'
+    sysctl('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('net.ipv6.neigh.default.gc_thresh3', size)
+    # Amount after which the records begin to be cleaned after 5 seconds
+    sysctl('net.ipv6.neigh.default.gc_thresh2', size // 2)
+    # Minimum number of stored records is indicated which is not cleared
+    sysctl('net.ipv6.neigh.default.gc_thresh1', size // 8)
 
     # enable/disable IPv6 forwarding
-    with open('/proc/sys/net/ipv6/conf/all/forwarding', 'w') as f:
-        f.write(ip_opt['ipv6_forward'])
+    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":
-                with open(os.path.join(root, name), 'w') as f:
-                    f.write(str(ip_opt['strict_dad']))
+            if name == 'accept_dad':
+                write_file(os.path.join(root, name), value)
 
 if __name__ == '__main__':
     try:
-- 
cgit v1.2.3


From f8b3d8999cbea988ce8e7d303957558308ddc1bc Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Mar 2022 18:56:06 +0100
Subject: ipv6: T4319: do not configure IPv6 related settings if it's disabled

---
 python/vyos/ifconfig/interface.py | 93 ++++++++++++++++++++-------------------
 1 file changed, 48 insertions(+), 45 deletions(-)

diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 8c5ff576e..8ce851cdb 100755
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -39,6 +39,7 @@ from vyos.util import read_file
 from vyos.util import get_interface_config
 from vyos.util import get_interface_namespace
 from vyos.util import is_systemd_service_active
+from vyos.util import sysctl_read
 from vyos.template import is_ipv4
 from vyos.template import is_ipv6
 from vyos.validate import is_intf_addr_assigned
@@ -1448,11 +1449,6 @@ class Interface(Control):
         value = tmp if (tmp != None) else '0'
         self.set_tcp_ipv4_mss(value)
 
-        # Configure MSS value for IPv6 TCP connections
-        tmp = dict_search('ipv6.adjust_mss', config)
-        value = tmp if (tmp != None) else '0'
-        self.set_tcp_ipv6_mss(value)
-
         # Configure ARP cache timeout in milliseconds - has default value
         tmp = dict_search('ip.arp_cache_timeout', config)
         value = tmp if (tmp != None) else '30'
@@ -1498,47 +1494,54 @@ class Interface(Control):
         value = tmp if (tmp != None) else '0'
         self.set_ipv4_source_validation(value)
 
-        # IPv6 forwarding
-        tmp = dict_search('ipv6.disable_forwarding', config)
-        value = '0' if (tmp != None) else '1'
-        self.set_ipv6_forwarding(value)
-
-        # IPv6 router advertisements
-        tmp = dict_search('ipv6.address.autoconf', config)
-        value = '2' if (tmp != None) else '1'
-        if 'dhcpv6' in new_addr:
-            value = '2'
-        self.set_ipv6_accept_ra(value)
-
-        # IPv6 address autoconfiguration
-        tmp = dict_search('ipv6.address.autoconf', config)
-        value = '1' if (tmp != None) else '0'
-        self.set_ipv6_autoconf(value)
-
-        # IPv6 Duplicate Address Detection (DAD) tries
-        tmp = dict_search('ipv6.dup_addr_detect_transmits', config)
-        value = tmp if (tmp != None) else '1'
-        self.set_ipv6_dad_messages(value)
-
-        # MTU - Maximum Transfer Unit
-        if 'mtu' in config:
-            self.set_mtu(config.get('mtu'))
-
-        # Delete old IPv6 EUI64 addresses before changing MAC
-        for addr in (dict_search('ipv6.address.eui64_old', config) or []):
-            self.del_ipv6_eui64_address(addr)
-
-        # Manage IPv6 link-local addresses
-        if dict_search('ipv6.address.no_default_link_local', config) != None:
-            self.del_ipv6_eui64_address('fe80::/64')
-        else:
-            self.add_ipv6_eui64_address('fe80::/64')
+        # Only change IPv6 parameters if IPv6 was not explicitly disabled
+        if sysctl_read('net.ipv6.conf.all.disable_ipv6') == '0':
+            # Configure MSS value for IPv6 TCP connections
+            tmp = dict_search('ipv6.adjust_mss', config)
+            value = tmp if (tmp != None) else '0'
+            self.set_tcp_ipv6_mss(value)
+
+            # IPv6 forwarding
+            tmp = dict_search('ipv6.disable_forwarding', config)
+            value = '0' if (tmp != None) else '1'
+            self.set_ipv6_forwarding(value)
+
+            # IPv6 router advertisements
+            tmp = dict_search('ipv6.address.autoconf', config)
+            value = '2' if (tmp != None) else '1'
+            if 'dhcpv6' in new_addr:
+                value = '2'
+            self.set_ipv6_accept_ra(value)
+
+            # IPv6 address autoconfiguration
+            tmp = dict_search('ipv6.address.autoconf', config)
+            value = '1' if (tmp != None) else '0'
+            self.set_ipv6_autoconf(value)
+
+            # IPv6 Duplicate Address Detection (DAD) tries
+            tmp = dict_search('ipv6.dup_addr_detect_transmits', config)
+            value = tmp if (tmp != None) else '1'
+            self.set_ipv6_dad_messages(value)
+
+            # MTU - Maximum Transfer Unit
+            if 'mtu' in config:
+                self.set_mtu(config.get('mtu'))
+
+            # Delete old IPv6 EUI64 addresses before changing MAC
+            for addr in (dict_search('ipv6.address.eui64_old', config) or []):
+                self.del_ipv6_eui64_address(addr)
+
+            # Manage IPv6 link-local addresses
+            if dict_search('ipv6.address.no_default_link_local', config) != None:
+                self.del_ipv6_eui64_address('fe80::/64')
+            else:
+                self.add_ipv6_eui64_address('fe80::/64')
 
-        # Add IPv6 EUI-based addresses
-        tmp = dict_search('ipv6.address.eui64', config)
-        if tmp:
-            for addr in tmp:
-                self.add_ipv6_eui64_address(addr)
+            # Add IPv6 EUI-based addresses
+            tmp = dict_search('ipv6.address.eui64', config)
+            if tmp:
+                for addr in tmp:
+                    self.add_ipv6_eui64_address(addr)
 
         # re-add ourselves to any bridge we might have fallen out of
         if 'is_bridge_member' in config:
-- 
cgit v1.2.3


From 364009e4317fb5c6732635726b511613aa2ed519 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Mar 2022 19:00:36 +0100
Subject: vyos.util: T4319: rename sysctl() -> sysctl_write()

---
 python/vyos/util.py          |  2 +-
 src/conf_mode/system-ip.py   | 12 ++++++------
 src/conf_mode/system-ipv6.py | 12 ++++++------
 src/conf_mode/vrf.py         |  6 +++---
 4 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/python/vyos/util.py b/python/vyos/util.py
index da39ee8d1..f46775490 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -1011,7 +1011,7 @@ def sysctl_read(name):
     tmp = cmd(f'sysctl {name}')
     return tmp.split()[-1]
 
-def sysctl(name, value):
+def sysctl_write(name, value):
     """ Change value via sysctl() - return True if changed, False otherwise """
     tmp = cmd(f'sysctl {name}')
     # last list index contains the actual value - only write if value differs
diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py
index 8b97725ac..05fc3a97a 100755
--- a/src/conf_mode/system-ip.py
+++ b/src/conf_mode/system-ip.py
@@ -20,7 +20,7 @@ from vyos.config import Config
 from vyos.configdict import dict_merge
 from vyos.util import call
 from vyos.util import dict_search
-from vyos.util import sysctl
+from vyos.util import sysctl_write
 from vyos.util import write_file
 from vyos.xml import defaults
 from vyos import ConfigError
@@ -53,11 +53,11 @@ def apply(opt):
     # 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('net.ipv4.neigh.default.gc_thresh3', size)
+    sysctl_write('net.ipv4.neigh.default.gc_thresh3', size)
     # Amount after which the records begin to be cleaned after 5 seconds
-    sysctl('net.ipv4.neigh.default.gc_thresh2', size // 2)
+    sysctl_write('net.ipv4.neigh.default.gc_thresh2', size // 2)
     # Minimum number of stored records is indicated which is not cleared
-    sysctl('net.ipv4.neigh.default.gc_thresh1', size // 8)
+    sysctl_write('net.ipv4.neigh.default.gc_thresh1', size // 8)
 
     # enable/disable IPv4 forwarding
     tmp = dict_search('disable_forwarding', opt)
@@ -67,11 +67,11 @@ def apply(opt):
     # configure multipath
     tmp = dict_search('multipath.ignore_unreachable_nexthops', opt)
     value = '1' if (tmp != None) else '0'
-    sysctl('net.ipv4.fib_multipath_use_neigh', value)
+    sysctl_write('net.ipv4.fib_multipath_use_neigh', value)
 
     tmp = dict_search('multipath.layer4_hashing', opt)
     value = '1' if (tmp != None) else '0'
-    sysctl('net.ipv4.fib_multipath_hash_policy', value)
+    sysctl_write('net.ipv4.fib_multipath_hash_policy', value)
 
 if __name__ == '__main__':
     try:
diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
index 8195beaa6..7fb2dd1cf 100755
--- a/src/conf_mode/system-ipv6.py
+++ b/src/conf_mode/system-ipv6.py
@@ -22,7 +22,7 @@ from vyos.configdict import dict_merge
 from vyos.configdict import leaf_node_changed
 from vyos.util import call
 from vyos.util import dict_search
-from vyos.util import sysctl
+from vyos.util import sysctl_write
 from vyos.util import write_file
 from vyos.xml import defaults
 from vyos import ConfigError
@@ -58,7 +58,7 @@ def apply(opt):
     # disable IPv6 globally
     tmp = dict_search('disable', opt)
     value = '1' if (tmp != None) else '0'
-    sysctl('net.ipv6.conf.all.disable_ipv6', value)
+    sysctl_write('net.ipv6.conf.all.disable_ipv6', value)
 
     if 'reboot_required' in opt:
         print('Changing IPv6 disable parameter will only take affect\n' \
@@ -67,17 +67,17 @@ def apply(opt):
     # configure multipath
     tmp = dict_search('multipath.layer4_hashing', opt)
     value = '1' if (tmp != None) else '0'
-    sysctl('net.ipv6.fib_multipath_hash_policy', value)
+    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('net.ipv6.neigh.default.gc_thresh3', size)
+    sysctl_write('net.ipv6.neigh.default.gc_thresh3', size)
     # Amount after which the records begin to be cleaned after 5 seconds
-    sysctl('net.ipv6.neigh.default.gc_thresh2', size // 2)
+    sysctl_write('net.ipv6.neigh.default.gc_thresh2', size // 2)
     # Minimum number of stored records is indicated which is not cleared
-    sysctl('net.ipv6.neigh.default.gc_thresh1', size // 8)
+    sysctl_write('net.ipv6.neigh.default.gc_thresh1', size // 8)
 
     # enable/disable IPv6 forwarding
     tmp = dict_search('disable_forwarding', opt)
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index cfe0f4d8e..6a521a0dd 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -29,7 +29,7 @@ from vyos.util import dict_search
 from vyos.util import get_interface_config
 from vyos.util import popen
 from vyos.util import run
-from vyos.util import sysctl
+from vyos.util import sysctl_write
 from vyos import ConfigError
 from vyos import frr
 from vyos import airbag
@@ -154,8 +154,8 @@ def apply(vrf):
     bind_all = '0'
     if 'bind-to-all' in vrf:
         bind_all = '1'
-    sysctl('net.ipv4.tcp_l3mdev_accept', bind_all)
-    sysctl('net.ipv4.udp_l3mdev_accept', bind_all)
+    sysctl_write('net.ipv4.tcp_l3mdev_accept', bind_all)
+    sysctl_write('net.ipv4.udp_l3mdev_accept', bind_all)
 
     for tmp in (dict_search('vrf_remove', vrf) or []):
         if os.path.isdir(f'/sys/class/net/{tmp}'):
-- 
cgit v1.2.3


From cabe0c06e2312cc872d3e22d91611a3ccecefdb0 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Mar 2022 19:08:56 +0100
Subject: mpls: T915: use vyos.util.sysctl_write() helper function

---
 src/conf_mode/protocols_mpls.py | 21 ++++++++++-----------
 1 file changed, 10 insertions(+), 11 deletions(-)

diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py
index 0b0c7d07b..933e23065 100755
--- a/src/conf_mode/protocols_mpls.py
+++ b/src/conf_mode/protocols_mpls.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
@@ -20,11 +20,10 @@ from sys import exit
 
 from glob import glob
 from vyos.config import Config
-from vyos.configdict import node_changed
 from vyos.template import render_to_string
-from vyos.util import call
 from vyos.util import dict_search
 from vyos.util import read_file
+from vyos.util import sysctl_write
 from vyos import ConfigError
 from vyos import frr
 from vyos import airbag
@@ -89,21 +88,21 @@ def apply(mpls):
     labels = '0'
     if 'interface' in mpls:
         labels = '1048575'
-    call(f'sysctl -wq net.mpls.platform_labels={labels}')
+    sysctl_write('net.mpls.platform_labels', labels)
 
     # Check for changes in global MPLS options
     if 'parameters' in mpls:
             # Choose whether to copy IP TTL to MPLS header TTL
         if 'no_propagate_ttl' in mpls['parameters']:
-            call('sysctl -wq net.mpls.ip_ttl_propagate=0')
+            sysctl_write('net.mpls.ip_ttl_propagate', 0)
             # Choose whether to limit maximum MPLS header TTL
         if 'maximum_ttl' in mpls['parameters']:
             ttl = mpls['parameters']['maximum_ttl']
-            call(f'sysctl -wq net.mpls.default_ttl={ttl}')
+            sysctl_write('net.mpls.default_ttl', ttl)
     else:
         # Set default global MPLS options if not defined.
-        call('sysctl -wq net.mpls.ip_ttl_propagate=1')
-        call('sysctl -wq net.mpls.default_ttl=255')
+        sysctl_write('net.mpls.ip_ttl_propagate', 1)
+        sysctl_write('net.mpls.default_ttl', 255)
 
     # Enable and disable MPLS processing on interfaces per configuration
     if 'interface' in mpls:
@@ -117,11 +116,11 @@ def apply(mpls):
             if '1' in interface_state:
                 if system_interface not in mpls['interface']:
                     system_interface = system_interface.replace('.', '/')
-                    call(f'sysctl -wq net.mpls.conf.{system_interface}.input=0')
+                    sysctl_write(f'net.mpls.conf.{system_interface}.input', 0)
             elif '0' in interface_state:
                 if system_interface in mpls['interface']:
                     system_interface = system_interface.replace('.', '/')
-                    call(f'sysctl -wq net.mpls.conf.{system_interface}.input=1')
+                    sysctl_write(f'net.mpls.conf.{system_interface}.input', 1)
     else:
         system_interfaces = []
         # If MPLS interfaces are not configured, set MPLS processing disabled
@@ -129,7 +128,7 @@ def apply(mpls):
             system_interfaces.append(os.path.basename(interface))
         for system_interface in system_interfaces:
             system_interface = system_interface.replace('.', '/')
-            call(f'sysctl -wq net.mpls.conf.{system_interface}.input=0')
+            sysctl_write(f'net.mpls.conf.{system_interface}.input', 0)
 
     return None
 
-- 
cgit v1.2.3


From 625ea99e69a319c88fa67125438fdc457eaaaed3 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Mar 2022 19:09:08 +0100
Subject: smoketest: mpls: disable debug output

---
 smoketest/scripts/cli/test_protocols_mpls.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/smoketest/scripts/cli/test_protocols_mpls.py b/smoketest/scripts/cli/test_protocols_mpls.py
index b60f0725a..c6751cc42 100755
--- a/smoketest/scripts/cli/test_protocols_mpls.py
+++ b/smoketest/scripts/cli/test_protocols_mpls.py
@@ -81,7 +81,6 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase):
         self.assertTrue(process_named_running(PROCESS_NAME))
 
     def test_mpls_basic(self):
-        self.debug = True
         router_id = '1.2.3.4'
         transport_ipv4_addr = '5.6.7.8'
         interfaces = Section.interfaces('ethernet')
-- 
cgit v1.2.3


From 6249c2abe7e40a6671549d318ee9b0fbd622b991 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 25 Mar 2022 19:38:48 +0100
Subject: smoketest: ipv6: fix testcase after using new sysctl interface

---
 smoketest/scripts/cli/test_system_ipv6.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/smoketest/scripts/cli/test_system_ipv6.py b/smoketest/scripts/cli/test_system_ipv6.py
index 1325d4b39..3112d2e46 100755
--- a/smoketest/scripts/cli/test_system_ipv6.py
+++ b/smoketest/scripts/cli/test_system_ipv6.py
@@ -1,6 +1,6 @@
 #!/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
@@ -22,7 +22,7 @@ from vyos.util import read_file
 base_path = ['system', 'ipv6']
 
 file_forwarding = '/proc/sys/net/ipv6/conf/all/forwarding'
-file_disable = '/etc/modprobe.d/vyos_disable_ipv6.conf'
+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'
 
@@ -48,7 +48,7 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase):
         self.cli_commit()
 
         # Verify configuration file
-        self.assertEqual(read_file(file_disable), 'options ipv6 disable_ipv6=1')
+        self.assertEqual(read_file(file_disable), '1')
 
     def test_system_ipv6_strict_dad(self):
         # This defaults to 1
-- 
cgit v1.2.3


From 1bfe09f90b9eca58f00cfae880e13e88c6dae894 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 26 Mar 2022 09:24:36 +0100
Subject: vyos.validate: T4321: make is_intf_addr_assigned() VRF aware

---
 python/vyos/validate.py | 23 ++++++++++++++---------
 1 file changed, 14 insertions(+), 9 deletions(-)

diff --git a/python/vyos/validate.py b/python/vyos/validate.py
index 0dad2a6cb..6dfa1139f 100644
--- a/python/vyos/validate.py
+++ b/python/vyos/validate.py
@@ -43,19 +43,15 @@ def _are_same_ip(one, two):
     s_two = AF_INET if is_ipv4(two) else AF_INET6
     return inet_pton(f_one, one) == inet_pton(f_one, two)
 
-def is_intf_addr_assigned(intf, addr):
-    if '/' in addr:
-        ip,mask = addr.split('/')
-        return _is_intf_addr_assigned(intf, ip, mask)
-    return _is_intf_addr_assigned(intf, addr)
-
-def _is_intf_addr_assigned(intf, address, netmask=None):
+def is_intf_addr_assigned(intf, address, vrf=None) -> bool:
     """
     Verify if the given IPv4/IPv6 address is assigned to specific interface.
     It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR
     address 192.0.2.1/24.
     """
     from vyos.template import is_ipv4
+    from vyos.util import get_interface_config
+
     from netifaces import ifaddresses
     from netifaces import AF_INET
     from netifaces import AF_INET6
@@ -72,10 +68,19 @@ def _is_intf_addr_assigned(intf, address, netmask=None):
         print(e)
         return False
 
+    # Check if interface belongs to requested VRF. If interfaces does not
+    # belong to requested VRF - bail out early
+    tmp = get_interface_config(intf)
+    if 'master' in tmp and tmp['master'] != vrf:
+        return False
+
     # determine IP version (AF_INET or AF_INET6) depending on passed address
     addr_type = AF_INET if is_ipv4(address) else AF_INET6
 
     # Check every IP address on this interface for a match
+    netmask = None
+    if '/' in address:
+        address, netmask = address.split('/')
     for ip in ifaces.get(addr_type,[]):
         # ip can have the interface name in the 'addr' field, we need to remove it
         # {'addr': 'fe80::a00:27ff:fec5:f821%eth2', 'netmask': 'ffff:ffff:ffff:ffff::'}
@@ -99,13 +104,13 @@ def _is_intf_addr_assigned(intf, address, netmask=None):
 
     return False
 
-def is_addr_assigned(addr):
+def is_addr_assigned(addr, vrf=None) -> bool:
     """
     Verify if the given IPv4/IPv6 address is assigned to any interface
     """
     from netifaces import interfaces
     for intf in interfaces():
-        tmp = is_intf_addr_assigned(intf, addr)
+        tmp = is_intf_addr_assigned(intf, addr, vrf)
         if tmp == True:
             return True
 
-- 
cgit v1.2.3


From 772d05156aaa75c904fe340cfce024da00f187f4 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 26 Mar 2022 09:25:02 +0100
Subject: bgp: T4321: check neighbor IP addresses against VRF context

---
 src/conf_mode/protocols_bgp.py | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index 9e59177a8..64b113873 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -159,8 +159,14 @@ def verify(bgp):
 
                 # Only checks for ipv4 and ipv6 neighbors
                 # Check if neighbor address is assigned as system interface address
-                if is_ip(peer) and is_addr_assigned(peer):
-                    raise ConfigError(f'Can not configure a local address as neighbor "{peer}"')
+                vrf = None
+                vrf_error_msg = f' in default VRF!'
+                if 'vrf' in bgp:
+                    vrf = bgp['vrf']
+                    vrf_error_msg = f' in VRF "{vrf}"!'
+
+                if is_ip(peer) and is_addr_assigned(peer, vrf):
+                    raise ConfigError(f'Can not configure local address as neighbor "{peer}"{vrf_error_msg}')
                 elif is_interface(peer):
                     if 'peer_group' in peer_config:
                         raise ConfigError(f'peer-group must be set under the interface node of "{peer}"')
-- 
cgit v1.2.3


From 2cfbb51731eb24c80bd4d697e370dde4972400a5 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 26 Mar 2022 09:40:44 +0100
Subject: vyos.validate: T4321: make is_addr_assigned() VRF aware

Commit 1bfe09f9 ("vyos.validate: T4321: make is_intf_addr_assigned() VRF aware")
added VRF support for an interface bound function. As an interface can only be
bound to one VRF check makes less sense.

This commit moves the VRF awareness from is_intf_addr_assigned() to
is_addr_assigned() so we check the VRF assignment even prior of calling
is_intf_addr_assigned() and fail fast.
---
 python/vyos/validate.py | 29 ++++++++++++++---------------
 1 file changed, 14 insertions(+), 15 deletions(-)

diff --git a/python/vyos/validate.py b/python/vyos/validate.py
index 6dfa1139f..e005da0e4 100644
--- a/python/vyos/validate.py
+++ b/python/vyos/validate.py
@@ -43,14 +43,13 @@ def _are_same_ip(one, two):
     s_two = AF_INET if is_ipv4(two) else AF_INET6
     return inet_pton(f_one, one) == inet_pton(f_one, two)
 
-def is_intf_addr_assigned(intf, address, vrf=None) -> bool:
+def is_intf_addr_assigned(intf, address) -> bool:
     """
     Verify if the given IPv4/IPv6 address is assigned to specific interface.
     It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR
     address 192.0.2.1/24.
     """
     from vyos.template import is_ipv4
-    from vyos.util import get_interface_config
 
     from netifaces import ifaddresses
     from netifaces import AF_INET
@@ -68,12 +67,6 @@ def is_intf_addr_assigned(intf, address, vrf=None) -> bool:
         print(e)
         return False
 
-    # Check if interface belongs to requested VRF. If interfaces does not
-    # belong to requested VRF - bail out early
-    tmp = get_interface_config(intf)
-    if 'master' in tmp and tmp['master'] != vrf:
-        return False
-
     # determine IP version (AF_INET or AF_INET6) depending on passed address
     addr_type = AF_INET if is_ipv4(address) else AF_INET6
 
@@ -104,14 +97,20 @@ def is_intf_addr_assigned(intf, address, vrf=None) -> bool:
 
     return False
 
-def is_addr_assigned(addr, vrf=None) -> bool:
-    """
-    Verify if the given IPv4/IPv6 address is assigned to any interface
-    """
+def is_addr_assigned(ip_address, vrf=None) -> bool:
+    """ Verify if the given IPv4/IPv6 address is assigned to any interfac """
     from netifaces import interfaces
-    for intf in interfaces():
-        tmp = is_intf_addr_assigned(intf, addr, vrf)
-        if tmp == True:
+    from vyos.util import get_interface_config
+    from vyos.util import dict_search
+    for interface in interfaces():
+        # Check if interface belongs to the requested VRF, if this is not the
+        # case there is no need to proceed with this data set - continue loop
+        # with next element
+        tmp = get_interface_config(interface)
+        if dict_search('master', tmp) != vrf:
+            continue
+
+        if is_intf_addr_assigned(interface, ip_address):
             return True
 
     return False
-- 
cgit v1.2.3


From 9d3acc2b55f2d1c563f1941e59c98c159211dc58 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Sun, 27 Mar 2022 01:30:21 -0500
Subject: graphql: T3993: add unsettable gql option; this is not exposed by CLI

---
 python/vyos/defaults.py           | 1 +
 src/conf_mode/http-api.py         | 9 +++++++++
 src/services/vyos-http-api-server | 4 +++-
 3 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index c77b695bd..fcb6a7fbc 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -48,6 +48,7 @@ api_data = {
     'port' : '8080',
     'socket' : False,
     'strict' : False,
+    'gql' : False,
     'debug' : False,
     'api_keys' : [ {"id": "testapp", "key": "qwerty"} ]
 }
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index b5f5e919f..00f3d4f7f 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -66,6 +66,15 @@ def get_config(config=None):
     if conf.exists('debug'):
         http_api['debug'] = True
 
+    # this node is not available by CLI by default, and is reserved for
+    # the graphql tools. One can enable it for testing, with the warning
+    # that this will open an unauthenticated server. To do so
+    # mkdir /opt/vyatta/share/vyatta-cfg/templates/service/https/api/gql
+    # touch /opt/vyatta/share/vyatta-cfg/templates/service/https/api/gql/node.def
+    # and configure; editing the config alone is insufficient.
+    if conf.exists('gql'):
+        http_api['gql'] = True
+
     if conf.exists('socket'):
         http_api['socket'] = True
 
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index 06871f1d6..1000d8b72 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -648,10 +648,12 @@ if __name__ == '__main__':
     app.state.vyos_keys = server_config['api_keys']
 
     app.state.vyos_debug = server_config['debug']
+    app.state.vyos_gql = server_config['gql']
     app.state.vyos_strict = server_config['strict']
     app.state.vyos_origins = server_config.get('cors', {}).get('origins', [])
 
-    graphql_init(app)
+    if app.state.vyos_gql:
+        graphql_init(app)
 
     try:
         if not server_config['socket']:
-- 
cgit v1.2.3


From 60f093464692f08c1c32c9e31513a6ae98636617 Mon Sep 17 00:00:00 2001
From: Daniil Baturin <daniil@vyos.io>
Date: Mon, 28 Mar 2022 10:46:30 +0300
Subject: Revert "openvpn: T4230: globally enable ip_nonlocal_bind"

This reverts commit 1cbcbf40b7721849f9696c05fac65db010a66b7c.
---
 src/conf_mode/interfaces-openvpn.py         | 7 +++++++
 src/etc/sysctl.d/33-vyos-nonlocal-bind.conf | 8 --------
 2 files changed, 7 insertions(+), 8 deletions(-)
 delete mode 100644 src/etc/sysctl.d/33-vyos-nonlocal-bind.conf

diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index c30c0bdd0..8f9c0b3f1 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -653,6 +653,13 @@ def apply(openvpn):
 
         return None
 
+    # verify specified IP address is present on any interface on this system
+    # Allow to bind service to nonlocal address, if it virtaual-vrrp address
+    # or if address will be assign later
+    if 'local_host' in openvpn:
+        if not is_addr_assigned(openvpn['local_host']):
+            cmd('sysctl -w net.ipv4.ip_nonlocal_bind=1')
+
     # No matching OpenVPN process running - maybe it got killed or none
     # existed - nevertheless, spawn new OpenVPN process
     action = 'reload-or-restart'
diff --git a/src/etc/sysctl.d/33-vyos-nonlocal-bind.conf b/src/etc/sysctl.d/33-vyos-nonlocal-bind.conf
deleted file mode 100644
index aa81b5336..000000000
--- a/src/etc/sysctl.d/33-vyos-nonlocal-bind.conf
+++ /dev/null
@@ -1,8 +0,0 @@
-### Added by vyos-1x ###
-#
-# ip_nonlocal_bind - BOOLEAN
-#     If set, allows processes to bind() to non-local IP addresses,
-#     which can be quite useful - but may break some applications.
-#     Default: 0
-net.ipv4.ip_nonlocal_bind = 1
-net.ipv6.ip_nonlocal_bind = 1
-- 
cgit v1.2.3


From c82db71fa2ce0aa0b1768c51b78e790ee291836f Mon Sep 17 00:00:00 2001
From: Daniil Baturin <daniil@vyos.io>
Date: Wed, 30 Mar 2022 09:16:20 -0400
Subject: T4319: do not try to add ::1/128 to lo if IPv6 is disabled

---
 python/vyos/ifconfig/loopback.py | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py
index 192c12f5c..de554ef44 100644
--- a/python/vyos/ifconfig/loopback.py
+++ b/python/vyos/ifconfig/loopback.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2022 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
@@ -13,6 +13,8 @@
 # You should have received a copy of the GNU Lesser General Public
 # License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
+import vyos.util
+
 from vyos.ifconfig.interface import Interface
 
 @Interface.register
@@ -23,7 +25,6 @@ class LoopbackIf(Interface):
     """
     _persistent_addresses = ['127.0.0.1/8', '::1/128']
     iftype = 'loopback'
-
     definition = {
         **Interface.definition,
         **{
@@ -32,6 +33,9 @@ class LoopbackIf(Interface):
             'bridgeable': True,
         }
     }
+
+    name = 'loopback'
+
     def remove(self):
         """
         Loopback interface can not be deleted from operating system. We can
@@ -59,7 +63,10 @@ class LoopbackIf(Interface):
 
         addr = config.get('address', [])
         # We must ensure that the loopback addresses are never deleted from the system
-        addr += self._persistent_addresses
+        addr += ['127.0.0.1/8']
+
+        if (vyos.util.sysctl_read('net.ipv6.conf.all.disable_ipv6') == '0'):
+            addr += ['::1/128']
 
         # Update IP address entry in our dictionary
         config.update({'address' : addr})
-- 
cgit v1.2.3


From bafb1973d906707cb571385e994a949d0d90b645 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Wed, 30 Mar 2022 19:16:52 +0200
Subject: vyos.ifconfig: make add_addr() method more reader firendly

---
 python/vyos/ifconfig/interface.py | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 8ce851cdb..4fda1c0a9 100755
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -1089,8 +1089,11 @@ class Interface(Control):
         elif addr == 'dhcpv6':
             self.set_dhcpv6(True)
         elif not is_intf_addr_assigned(self.ifname, addr):
-            self._cmd(f'ip addr add "{addr}" '
-                    f'{"brd + " if addr_is_v4 else ""}dev "{self.ifname}"')
+            tmp = f'ip addr add {addr} dev {self.ifname}'
+            # Add broadcast address for IPv4
+            if is_ipv4(addr): tmp += ' brd +'
+
+            self._cmd(tmp)
         else:
             return False
 
-- 
cgit v1.2.3