From fdeba8da3e99256fe449e331d0b833a941315226 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Wed, 28 Jul 2021 12:03:21 +0200
Subject: firewall: T2199: Migrate firewall to XML/Python

---
 python/vyos/configdiff.py         |  30 ++++++
 python/vyos/firewall.py           | 194 ++++++++++++++++++++++++++++++++++++++
 python/vyos/ifconfig/interface.py |  45 ++++-----
 python/vyos/template.py           |  26 +++++
 4 files changed, 269 insertions(+), 26 deletions(-)
 create mode 100644 python/vyos/firewall.py

(limited to 'python')

diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py
index 0e41fbe27..4ad7443d7 100644
--- a/python/vyos/configdiff.py
+++ b/python/vyos/configdiff.py
@@ -17,7 +17,9 @@ from enum import IntFlag, auto
 
 from vyos.config import Config
 from vyos.configdict import dict_merge
+from vyos.configdict import list_diff
 from vyos.util import get_sub_dict, mangle_dict_keys
+from vyos.util import dict_search_args
 from vyos.xml import defaults
 
 class ConfigDiffError(Exception):
@@ -134,6 +136,34 @@ class ConfigDiff(object):
                                                     self._key_mangling[1])
         return config_dict
 
+    def get_child_nodes_diff_str(self, path=[]):
+        ret = {'add': {}, 'change': {}, 'delete': {}}
+
+        diff = self.get_child_nodes_diff(path,
+                                expand_nodes=Diff.ADD | Diff.DELETE | Diff.MERGE | Diff.STABLE,
+                                no_defaults=True)
+
+        def parse_dict(diff_dict, diff_type, prefix=[]):
+            for k, v in diff_dict.items():
+                if isinstance(v, dict):
+                    parse_dict(v, diff_type, prefix + [k])
+                else:
+                    path_str = ' '.join(prefix + [k])
+                    if diff_type == 'add' or diff_type == 'delete':
+                        if isinstance(v, list):
+                            v = ', '.join(v)
+                        ret[diff_type][path_str] = v
+                    elif diff_type == 'merge':
+                        old_value = dict_search_args(diff['stable'], *prefix, k)
+                        if old_value and old_value != v:
+                            ret['change'][path_str] = [old_value, v]
+
+        parse_dict(diff['merge'], 'merge')
+        parse_dict(diff['add'], 'add')
+        parse_dict(diff['delete'], 'delete')
+
+        return ret
+
     def get_child_nodes_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False):
         """
         Args:
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
new file mode 100644
index 000000000..9b8af7852
--- /dev/null
+++ b/python/vyos/firewall.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import re
+
+from vyos.util import cmd
+from vyos.util import dict_search_args
+
+def find_nftables_rule(table, chain, rule_matches=[]):
+    # Find rule in table/chain that matches all criteria and return the handle
+    results = cmd(f'sudo nft -a list chain {table} {chain}').split("\n")
+    for line in results:
+        if all(rule_match in line for rule_match in rule_matches):
+            handle_search = re.search('handle (\d+)', line)
+            if handle_search:
+                return handle_search[1]
+    return None
+
+def remove_nftables_rule(table, chain, handle):
+    cmd(f'sudo nft delete rule {table} {chain} handle {handle}')
+
+# Functions below used by template generation
+
+def nft_action(vyos_action):
+    if vyos_action == 'accept':
+        return 'return'
+    return vyos_action
+
+def parse_rule(rule_conf, fw_name, rule_id, ip_name):
+    output = []
+    def_suffix = '6' if ip_name == 'ip6' else ''
+
+    if 'state' in rule_conf and rule_conf['state']:
+        states = ",".join([s for s, v in rule_conf['state'].items() if v == 'enable'])
+        output.append(f'ct state {{{states}}}')
+
+    if 'protocol' in rule_conf and rule_conf['protocol'] != 'all':
+        proto = rule_conf['protocol']
+        if proto == 'tcp_udp':
+            proto = '{tcp, udp}'
+        output.append('meta l4proto ' + proto)
+
+    for side in ['destination', 'source']:
+        if side in rule_conf:
+            prefix = side[0]
+            side_conf = rule_conf[side]
+
+            if 'address' in side_conf:
+                output.append(f'{ip_name} {prefix}addr {side_conf["address"]}')
+
+            if 'mac_address' in side_conf:
+                suffix = side_conf["mac_address"]
+                if suffix[0] == '!':
+                    suffix = f'!= {suffix[1:]}'
+                output.append(f'ether {prefix}addr {suffix}')
+
+            if 'port' in side_conf:
+                proto = rule_conf['protocol']
+                port = side_conf["port"]
+
+                if isinstance(port, list):
+                    port = ",".join(port)
+
+                if proto == 'tcp_udp':
+                    proto = 'th'
+
+                output.append(f'{proto} {prefix}port {{{port}}}')
+
+            if 'group' in side_conf:
+                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}')
+                elif 'network_group' in group:
+                    group_name = group['network_group']
+                    output.append(f'{ip_name} {prefix}addr $N{def_suffix}_{group_name}')
+                if 'port_group' in group:
+                    proto = rule_conf['protocol']
+                    group_name = group['port_group']
+
+                    if proto == 'tcp_udp':
+                        proto = 'th'
+
+                    output.append(f'{proto} {prefix}port $P_{group_name}')
+
+    if 'log' in rule_conf and rule_conf['log'] == 'enable':
+        output.append('log')
+
+    if 'hop_limit' in rule_conf:
+        operators = {'eq': '==', 'gt': '>', 'lt': '<'}
+        for op, operator in operators.items():
+            if op in rule_conf['hop_limit']:
+                value = rule_conf['hop_limit'][op]
+                output.append(f'ip6 hoplimit {operator} {value}')
+
+    for icmp in ['icmp', 'icmpv6']:
+        if icmp in rule_conf:
+            if 'type_name' in rule_conf[icmp]:
+                output.append(icmp + ' type ' + rule_conf[icmp]['type_name'])
+            else:
+                if 'code' in rule_conf[icmp]:
+                    output.append(icmp + ' code ' + rule_conf[icmp]['code'])
+                if 'type' in rule_conf[icmp]:
+                    output.append(icmp + ' type ' + rule_conf[icmp]['type'])
+
+    if 'ipsec' in rule_conf:
+        if 'match_ipsec' in rule_conf['ipsec']:
+            output.append('meta ipsec == 1')
+        if 'match_non_ipsec' in rule_conf['ipsec']:
+            output.append('meta ipsec == 0')
+
+    if 'fragment' in rule_conf:
+        # Checking for fragmentation after priority -400 is not possible,
+        # so we use a priority -450 hook to set a mark
+        if 'match_frag' in rule_conf['fragment']:
+            output.append('meta mark 0xffff1')
+        if 'match_non_frag' in rule_conf['fragment']:
+            output.append('meta mark != 0xffff1')
+
+    if 'limit' in rule_conf:
+        if 'rate' in rule_conf['limit']:
+            output.append(f'limit rate {rule_conf["limit"]["rate"]}/second')
+            if 'burst' in rule_conf['limit']:
+                output.append(f'burst {rule_conf["limit"]["burst"]} packets')
+
+    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.
+
+    if 'time' in rule_conf:
+        output.append(parse_time(rule_conf['time']))
+
+    tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
+    if tcp_flags:
+        output.append(parse_tcp_flags(tcp_flags))
+
+    output.append('counter')
+
+    if 'action' in rule_conf:
+        output.append(nft_action(rule_conf['action']))
+    else:
+        output.append('return')
+
+    output.append(f'comment "{fw_name}-{rule_id}"')
+    return " ".join(output)
+
+def parse_tcp_flags(flags):
+    all_flags = []
+    include = []
+    for flag in flags.split(","):
+        if flag[0] == '!':
+            flag = flag[1:]
+        else:
+            include.append(flag)
+        all_flags.append(flag)
+    return f'tcp flags & ({"|".join(all_flags)}) == {"|".join(include)}'
+
+def parse_time(time):
+    out = []
+    if 'startdate' in time:
+        start = time['startdate']
+        if 'T' not in start and 'starttime' in time:
+            start += f' {time["starttime"]}'
+        out.append(f'time >= "{start}"')
+    if 'starttime' in time and 'startdate' not in time:
+        out.append(f'hour >= "{time["starttime"]}"')
+    if 'stopdate' in time:
+        stop = time['stopdate']
+        if 'T' not in stop and 'stoptime' in time:
+            stop += f' {time["stoptime"]}'
+        out.append(f'time < "{stop}"')
+    if 'stoptime' in time and 'stopdate' not in time:
+        out.append(f'hour < "{time["stoptime"]}"')
+    if 'weekdays' in time:
+        days = time['weekdays'].split(",")
+        out_days = [f'"{day}"' for day in days if day[0] != '!']
+        out.append(f'day {{{",".join(out_days)}}}')
+    return " ".join(out)
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 58d130ef6..edc99d6f7 100755
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -544,6 +544,15 @@ class Interface(Control):
             return None
         return self.set_interface('arp_cache_tmo', tmo)
 
+    def _cleanup_mss_rules(self, table, ifname):
+        commands = []
+        results = self._cmd(f'nft -a list chain {table} VYOS_TCP_MSS').split("\n")
+        for line in results:
+            if f'oifname "{ifname}"' in line:
+                handle_search = re.search('handle (\d+)', line)
+                if handle_search:
+                    self._cmd(f'nft delete rule {table} VYOS_TCP_MSS handle {handle_search[1]}')
+
     def set_tcp_ipv4_mss(self, mss):
         """
         Set IPv4 TCP MSS value advertised when TCP SYN packets leave this
@@ -555,22 +564,14 @@ class Interface(Control):
         >>> from vyos.ifconfig import Interface
         >>> Interface('eth0').set_tcp_ipv4_mss(1340)
         """
-        iptables_bin = 'iptables'
-        base_options = f'-A FORWARD -o {self.ifname} -p tcp -m tcp --tcp-flags SYN,RST SYN'
-        out = self._cmd(f'{iptables_bin}-save -t mangle')
-        for line in out.splitlines():
-            if line.startswith(base_options):
-                # remove OLD MSS mangling configuration
-                line = line.replace('-A FORWARD', '-D FORWARD')
-                self._cmd(f'{iptables_bin} -t mangle {line}')
-
-        cmd_mss = f'{iptables_bin} -t mangle {base_options} --jump TCPMSS'
+        self._cleanup_mss_rules('raw', self.ifname)
+        nft_prefix = 'nft add rule raw VYOS_TCP_MSS'
+        base_cmd = f'oifname "{self.ifname}" tcp flags & (syn|rst) == syn'
         if mss == 'clamp-mss-to-pmtu':
-            self._cmd(f'{cmd_mss} --clamp-mss-to-pmtu')
+            self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size set rt mtu'")
         elif int(mss) > 0:
-            # probably add option to clamp only if bigger:
             low_mss = str(int(mss) + 1)
-            self._cmd(f'{cmd_mss} -m tcpmss --mss {low_mss}:65535 --set-mss {mss}')
+            self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size {low_mss}-65535 tcp option maxseg size set {mss}'")
 
     def set_tcp_ipv6_mss(self, mss):
         """
@@ -583,22 +584,14 @@ class Interface(Control):
         >>> from vyos.ifconfig import Interface
         >>> Interface('eth0').set_tcp_mss(1320)
         """
-        iptables_bin = 'ip6tables'
-        base_options = f'-A FORWARD -o {self.ifname} -p tcp -m tcp --tcp-flags SYN,RST SYN'
-        out = self._cmd(f'{iptables_bin}-save -t mangle')
-        for line in out.splitlines():
-            if line.startswith(base_options):
-                # remove OLD MSS mangling configuration
-                line = line.replace('-A FORWARD', '-D FORWARD')
-                self._cmd(f'{iptables_bin} -t mangle {line}')
-
-        cmd_mss = f'{iptables_bin} -t mangle {base_options} --jump TCPMSS'
+        self._cleanup_mss_rules('ip6 raw', self.ifname)
+        nft_prefix = 'nft add rule ip6 raw VYOS_TCP_MSS'
+        base_cmd = f'oifname "{self.ifname}" tcp flags & (syn|rst) == syn'
         if mss == 'clamp-mss-to-pmtu':
-            self._cmd(f'{cmd_mss} --clamp-mss-to-pmtu')
+            self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size set rt mtu'")
         elif int(mss) > 0:
-            # probably add option to clamp only if bigger:
             low_mss = str(int(mss) + 1)
-            self._cmd(f'{cmd_mss} -m tcpmss --mss {low_mss}:65535 --set-mss {mss}')
+            self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size {low_mss}-65535 tcp option maxseg size set {mss}'")
 
     def set_arp_filter(self, arp_filter):
         """
diff --git a/python/vyos/template.py b/python/vyos/template.py
index b32cafe74..55bd04136 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -22,6 +22,7 @@ from jinja2 import FileSystemLoader
 from vyos.defaults import directories
 from vyos.util import chmod
 from vyos.util import chown
+from vyos.util import dict_search_args
 from vyos.util import makedir
 
 # Holds template filters registered via register_filter()
@@ -479,3 +480,28 @@ def get_openvpn_ncp_ciphers(ciphers):
         else:
             out.append(cipher)
     return ':'.join(out).upper()
+
+@register_filter('nft_action')
+def nft_action(vyos_action):
+    if vyos_action == 'accept':
+        return 'return'
+    return vyos_action
+
+@register_filter('nft_rule')
+def nft_rule(rule_conf, fw_name, rule_id, ip_name='ip'):
+    from vyos.firewall import parse_rule
+    return parse_rule(rule_conf, fw_name, rule_id, ip_name)
+
+@register_filter('nft_state_policy')
+def nft_state_policy(conf, state):
+    out = [f'ct state {state}']
+
+    if 'log' in conf and 'enable' in conf['log']:
+        out.append('log')
+
+    out.append('counter')
+
+    if 'action' in conf:
+        out.append(conf['action'])
+
+    return " ".join(out)
-- 
cgit v1.2.3


From f86041de88c3b0e0ce9ecc6d2cbc309bc8cb28e2 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sun, 1 Aug 2021 00:13:47 +0200
Subject: policy: T2199: Migrate policy route to XML/Python

---
 data/templates/firewall/nftables-policy.tmpl       |  53 ++
 .../include/interface/interface-policy-vif-c.xml.i |  26 +
 .../include/interface/interface-policy-vif.xml.i   |  26 +
 .../include/interface/interface-policy.xml.i       |  26 +
 .../include/interface/vif-s.xml.i                  |   2 +
 interface-definitions/include/interface/vif.xml.i  |   1 +
 .../include/policy/route-common-rule-ipv6.xml.i    | 569 +++++++++++++++++++++
 .../include/policy/route-common-rule.xml.i         | 418 +++++++++++++++
 .../include/policy/route-rule-action.xml.i         |  17 +
 interface-definitions/interfaces-bonding.xml.in    |   1 +
 interface-definitions/interfaces-bridge.xml.in     |   1 +
 interface-definitions/interfaces-dummy.xml.in      |   1 +
 interface-definitions/interfaces-ethernet.xml.in   |   1 +
 interface-definitions/interfaces-geneve.xml.in     |   1 +
 interface-definitions/interfaces-l2tpv3.xml.in     |   1 +
 interface-definitions/interfaces-macsec.xml.in     |   1 +
 interface-definitions/interfaces-openvpn.xml.in    |   1 +
 interface-definitions/interfaces-pppoe.xml.in      |   1 +
 .../interfaces-pseudo-ethernet.xml.in              |   1 +
 interface-definitions/interfaces-tunnel.xml.in     |   1 +
 interface-definitions/interfaces-vti.xml.in        |   1 +
 interface-definitions/interfaces-vxlan.xml.in      |   1 +
 interface-definitions/interfaces-wireguard.xml.in  |   1 +
 interface-definitions/interfaces-wireless.xml.in   |   1 +
 interface-definitions/interfaces-wwan.xml.in       |   1 +
 interface-definitions/policy-route.xml.in          |  83 +++
 python/vyos/firewall.py                            |  23 +
 smoketest/scripts/cli/test_policy_route.py         | 106 ++++
 src/conf_mode/policy-route-interface.py            | 120 +++++
 src/conf_mode/policy-route.py                      | 154 ++++++
 30 files changed, 1640 insertions(+)
 create mode 100644 data/templates/firewall/nftables-policy.tmpl
 create mode 100644 interface-definitions/include/interface/interface-policy-vif-c.xml.i
 create mode 100644 interface-definitions/include/interface/interface-policy-vif.xml.i
 create mode 100644 interface-definitions/include/interface/interface-policy.xml.i
 create mode 100644 interface-definitions/include/policy/route-common-rule-ipv6.xml.i
 create mode 100644 interface-definitions/include/policy/route-common-rule.xml.i
 create mode 100644 interface-definitions/include/policy/route-rule-action.xml.i
 create mode 100644 interface-definitions/policy-route.xml.in
 create mode 100755 smoketest/scripts/cli/test_policy_route.py
 create mode 100755 src/conf_mode/policy-route-interface.py
 create mode 100755 src/conf_mode/policy-route.py

(limited to 'python')

diff --git a/data/templates/firewall/nftables-policy.tmpl b/data/templates/firewall/nftables-policy.tmpl
new file mode 100644
index 000000000..aa6bb6fc1
--- /dev/null
+++ b/data/templates/firewall/nftables-policy.tmpl
@@ -0,0 +1,53 @@
+#!/usr/sbin/nft -f
+
+table ip mangle {
+{% if first_install is defined %}
+    chain VYOS_PBR_PREROUTING {
+        type filter hook prerouting priority -150; policy accept;
+    }
+    chain VYOS_PBR_POSTROUTING {
+        type filter hook postrouting priority -150; policy accept;
+    }
+{% endif %}
+{% if route is defined -%}
+{%   for route_text, conf in route.items() %}
+    chain VYOS_PBR_{{ route_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(route_text, rule_id, 'ip') }}
+{%       endfor %}
+{%     endif %}
+{%     if conf.default_action is defined %}
+        counter {{ conf.default_action | nft_action }} comment "{{ name_text }} default-action {{ conf.default_action }}"
+{%     else %}
+        counter return
+{%     endif %}
+    }
+{%   endfor %}
+{%- endif %}
+}
+
+table ip6 mangle {
+{% if first_install is defined %}
+    chain VYOS_PBR6_PREROUTING {
+        type filter hook prerouting priority -150; policy accept;
+    }
+    chain VYOS_PBR6_POSTROUTING {
+        type filter hook postrouting priority -150; policy accept;
+    }
+{% endif %}
+{% if ipv6_route is defined %}
+{%   for route_text, conf in ipv6_route.items() %}
+    chain VYOS_PBR6_{{ route_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(route_text, rule_id, 'ip6') }}
+{%       endfor %}
+{%     endif %}
+{%     if conf.default_action is defined %}
+        counter {{ conf.default_action | nft_action }} comment "{{ name_text }} default-action {{ conf.default_action }}"
+{%     endif %}
+    }
+{%   endfor %}
+{% endif %}
+}
diff --git a/interface-definitions/include/interface/interface-policy-vif-c.xml.i b/interface-definitions/include/interface/interface-policy-vif-c.xml.i
new file mode 100644
index 000000000..5dad6422b
--- /dev/null
+++ b/interface-definitions/include/interface/interface-policy-vif-c.xml.i
@@ -0,0 +1,26 @@
+<!-- include start from interface/interface-policy-vif-c.xml.i -->
+<node name="policy" owner="${vyos_conf_scripts_dir}/policy-route-interface.py $VAR(../../../@).$VAR(../../@).$VAR(../@)">
+  <properties>
+    <priority>620</priority>
+    <help>Policy route options</help>
+  </properties>
+  <children>
+    <leafNode name="route">
+      <properties>
+        <help>IPv4 policy route ruleset for interface</help>
+        <completionHelp>
+          <path>policy route</path>
+        </completionHelp>
+      </properties>
+    </leafNode>
+    <leafNode name="ipv6-route">
+      <properties>
+        <help>IPv6 policy route ruleset for interface</help>
+        <completionHelp>
+          <path>policy ipv6-route</path>
+        </completionHelp>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/interface/interface-policy-vif.xml.i b/interface-definitions/include/interface/interface-policy-vif.xml.i
new file mode 100644
index 000000000..5ee80ae13
--- /dev/null
+++ b/interface-definitions/include/interface/interface-policy-vif.xml.i
@@ -0,0 +1,26 @@
+<!-- include start from interface/interface-policy-vif.xml.i -->
+<node name="policy" owner="${vyos_conf_scripts_dir}/policy-route-interface.py $VAR(../../@).$VAR(../@)">
+  <properties>
+    <priority>620</priority>
+    <help>Policy route options</help>
+  </properties>
+  <children>
+    <leafNode name="route">
+      <properties>
+        <help>IPv4 policy route ruleset for interface</help>
+        <completionHelp>
+          <path>policy route</path>
+        </completionHelp>
+      </properties>
+    </leafNode>
+    <leafNode name="ipv6-route">
+      <properties>
+        <help>IPv6 policy route ruleset for interface</help>
+        <completionHelp>
+          <path>policy ipv6-route</path>
+        </completionHelp>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/interface/interface-policy.xml.i b/interface-definitions/include/interface/interface-policy.xml.i
new file mode 100644
index 000000000..06f025af1
--- /dev/null
+++ b/interface-definitions/include/interface/interface-policy.xml.i
@@ -0,0 +1,26 @@
+<!-- include start from interface/interface-policy.xml.i -->
+<node name="policy" owner="${vyos_conf_scripts_dir}/policy-route-interface.py $VAR(../@)">
+  <properties>
+    <priority>620</priority>
+    <help>Policy route options</help>
+  </properties>
+  <children>
+    <leafNode name="route">
+      <properties>
+        <help>IPv4 policy route ruleset for interface</help>
+        <completionHelp>
+          <path>policy route</path>
+        </completionHelp>
+      </properties>
+    </leafNode>
+    <leafNode name="ipv6-route">
+      <properties>
+        <help>IPv6 policy route ruleset for interface</help>
+        <completionHelp>
+          <path>policy ipv6-route</path>
+        </completionHelp>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/interface/vif-s.xml.i b/interface-definitions/include/interface/vif-s.xml.i
index caa5248ab..f1a61ff64 100644
--- a/interface-definitions/include/interface/vif-s.xml.i
+++ b/interface-definitions/include/interface/vif-s.xml.i
@@ -19,6 +19,7 @@
     #include <include/interface/disable-link-detect.xml.i>
     #include <include/interface/disable.xml.i>
     #include <include/interface/interface-firewall-vif.xml.i>
+    #include <include/interface/interface-policy-vif.xml.i>
     <leafNode name="protocol">
       <properties>
         <help>Protocol used for service VLAN (default: 802.1ad)</help>
@@ -65,6 +66,7 @@
         #include <include/interface/mtu-68-16000.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/vrf.xml.i>
diff --git a/interface-definitions/include/interface/vif.xml.i b/interface-definitions/include/interface/vif.xml.i
index a2382cc1b..11ba7e2f8 100644
--- a/interface-definitions/include/interface/vif.xml.i
+++ b/interface-definitions/include/interface/vif.xml.i
@@ -20,6 +20,7 @@
     #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">
       <properties>
         <help>VLAN egress QoS</help>
diff --git a/interface-definitions/include/policy/route-common-rule-ipv6.xml.i b/interface-definitions/include/policy/route-common-rule-ipv6.xml.i
new file mode 100644
index 000000000..2d6adcd1d
--- /dev/null
+++ b/interface-definitions/include/policy/route-common-rule-ipv6.xml.i
@@ -0,0 +1,569 @@
+<!-- include start from policy/route-common-rule.xml.i -->
+#include <include/policy/route-rule-action.xml.i>
+#include <include/generic-description.xml.i>
+<leafNode name="disable">
+  <properties>
+    <help>Option to disable firewall rule</help>
+    <valueless/>
+  </properties>
+</leafNode>
+<node name="fragment">
+  <properties>
+    <help>IP fragment match</help>
+  </properties>
+  <children>
+    <leafNode name="match-frag">
+      <properties>
+        <help>Second and further fragments of fragmented packets</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+    <leafNode name="match-non-frag">
+      <properties>
+        <help>Head fragments or unfragmented packets</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<node name="ipsec">
+  <properties>
+    <help>Inbound IPsec packets</help>
+  </properties>
+  <children>
+    <leafNode name="match-ipsec">
+      <properties>
+        <help>Inbound IPsec packets</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+    <leafNode name="match-none">
+      <properties>
+        <help>Inbound non-IPsec packets</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<node name="limit">
+  <properties>
+    <help>Rate limit using a token bucket filter</help>
+  </properties>
+  <children>
+    <leafNode name="burst">
+      <properties>
+        <help>Maximum number of packets to allow in excess of rate</help>
+        <valueHelp>
+          <format>u32:0-4294967295</format>
+          <description>Maximum number of packets to allow in excess of rate</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 0-4294967295"/>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="rate">
+      <properties>
+        <help>Maximum average matching rate</help>
+        <valueHelp>
+          <format>u32:0-4294967295</format>
+          <description>Maximum average matching rate</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 0-4294967295"/>
+        </constraint>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<leafNode name="log">
+  <properties>
+    <help>Option to log packets matching rule</help>
+    <completionHelp>
+      <list>enable disable</list>
+    </completionHelp>
+    <valueHelp>
+      <format>enable</format>
+      <description>Enable log</description>
+    </valueHelp>
+    <valueHelp>
+      <format>disable</format>
+      <description>Disable log</description>
+    </valueHelp>
+    <constraint>
+      <regex>^(enable|disable)$</regex>
+    </constraint>
+  </properties>
+</leafNode>
+<leafNode name="protocol">
+  <properties>
+    <help>Protocol to match (protocol name, number, or "all")</help>
+    <completionHelp>
+      <script>cat /etc/protocols | sed -e '/^#.*/d' | awk '{ print $1 }'</script>
+    </completionHelp>
+    <valueHelp>
+      <format>all</format>
+      <description>All IP protocols</description>
+    </valueHelp>
+    <valueHelp>
+      <format>tcp_udp</format>
+      <description>Both TCP and UDP</description>
+    </valueHelp>
+    <valueHelp>
+      <format>0-255</format>
+      <description>IP protocol number</description>
+    </valueHelp>
+    <valueHelp>
+      <format>!&lt;protocol&gt;</format>
+      <description>IP protocol number</description>
+    </valueHelp>
+    <constraint>
+      <validator name="ip-protocol"/>
+    </constraint>
+  </properties>
+  <defaultValue>all</defaultValue>
+</leafNode>
+<node name="recent">
+  <properties>
+    <help>Parameters for matching recently seen sources</help>
+  </properties>
+  <children>
+    <leafNode name="count">
+      <properties>
+        <help>Source addresses seen more than N times</help>
+        <valueHelp>
+          <format>u32:1-255</format>
+          <description>Source addresses seen more than N times</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 1-255"/>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="time">
+      <properties>
+        <help>Source addresses seen in the last N seconds</help>
+        <valueHelp>
+          <format>u32:0-4294967295</format>
+          <description>Source addresses seen in the last N seconds</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 0-4294967295"/>
+        </constraint>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<node name="set">
+  <properties>
+    <help>Packet modifications</help>
+  </properties>
+  <children>
+    <leafNode name="dscp">
+      <properties>
+        <help>Packet Differentiated Services Codepoint (DSCP)</help>
+        <valueHelp>
+          <format>u32:0-63</format>
+          <description>DSCP number</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 0-63"/>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="mark">
+      <properties>
+        <help>Packet marking</help>
+        <valueHelp>
+          <format>u32:1-2147483647</format>
+          <description>Packet marking</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 1-2147483647"/>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="table">
+      <properties>
+        <help>Routing table to forward packet with</help>
+        <valueHelp>
+          <format>u32:1-200</format>
+          <description>Table number</description>
+        </valueHelp>
+        <valueHelp>
+          <format>main</format>
+          <description>Main table</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 1-200"/>
+          <regex>^(main)$</regex>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="tcp-mss">
+      <properties>
+        <help>TCP Maximum Segment Size</help>
+        <valueHelp>
+          <format>u32:500-1460</format>
+          <description>Explicitly set TCP MSS value</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 500-1460"/>
+        </constraint>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<node name="source">
+  <properties>
+    <help>Source parameters</help>
+  </properties>
+  <children>
+    #include <include/firewall/address-ipv6.xml.i>
+    #include <include/firewall/source-destination-group.xml.i>
+    <leafNode name="mac-address">
+      <properties>
+        <help>Source MAC address</help>
+        <valueHelp>
+          <format>&lt;MAC address&gt;</format>
+          <description>MAC address to match</description>
+        </valueHelp>
+        <valueHelp>
+          <format>!&lt;MAC address&gt;</format>
+          <description>Match everything except the specified MAC address</description>
+        </valueHelp>
+      </properties>
+    </leafNode>
+    #include <include/firewall/port.xml.i>
+  </children>
+</node>
+<node name="state">
+  <properties>
+    <help>Session state</help>
+  </properties>
+  <children>
+    <leafNode name="established">
+      <properties>
+        <help>Established state</help>
+        <completionHelp>
+          <list>enable disable</list>
+        </completionHelp>
+        <valueHelp>
+          <format>enable</format>
+          <description>Enable</description>
+        </valueHelp>
+        <valueHelp>
+          <format>disable</format>
+          <description>Disable</description>
+        </valueHelp>
+        <constraint>
+          <regex>^(enable|disable)$</regex>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="invalid">
+      <properties>
+        <help>Invalid state</help>
+        <completionHelp>
+          <list>enable disable</list>
+        </completionHelp>
+        <valueHelp>
+          <format>enable</format>
+          <description>Enable</description>
+        </valueHelp>
+        <valueHelp>
+          <format>disable</format>
+          <description>Disable</description>
+        </valueHelp>
+        <constraint>
+          <regex>^(enable|disable)$</regex>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="new">
+      <properties>
+        <help>New state</help>
+        <completionHelp>
+          <list>enable disable</list>
+        </completionHelp>
+        <valueHelp>
+          <format>enable</format>
+          <description>Enable</description>
+        </valueHelp>
+        <valueHelp>
+          <format>disable</format>
+          <description>Disable</description>
+        </valueHelp>
+        <constraint>
+          <regex>^(enable|disable)$</regex>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="related">
+      <properties>
+        <help>Related state</help>
+        <completionHelp>
+          <list>enable disable</list>
+        </completionHelp>
+        <valueHelp>
+          <format>enable</format>
+          <description>Enable</description>
+        </valueHelp>
+        <valueHelp>
+          <format>disable</format>
+          <description>Disable</description>
+        </valueHelp>
+        <constraint>
+          <regex>^(enable|disable)$</regex>
+        </constraint>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<node name="tcp">
+  <properties>
+    <help>TCP flags to match</help>
+  </properties>
+  <children>
+    <leafNode name="flags">
+      <properties>
+        <help>TCP flags to match</help>
+        <valueHelp>
+          <format>txt</format>
+          <description>TCP flags to match</description>
+        </valueHelp>
+        <valueHelp>
+          <format> </format>
+          <description>\n\n  Allowed values for TCP flags : SYN ACK FIN RST URG PSH ALL\n  When specifying more than one flag, flags should be comma-separated.\n For example : value of 'SYN,!ACK,!FIN,!RST' will only match packets with\n  the SYN flag set, and the ACK, FIN and RST flags unset</description>
+        </valueHelp>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<node name="time">
+  <properties>
+    <help>Time to match rule</help>
+  </properties>
+  <children>
+    <leafNode name="monthdays">
+      <properties>
+        <help>Monthdays to match rule on</help>
+      </properties>
+    </leafNode>
+    <leafNode name="startdate">
+      <properties>
+        <help>Date to start matching rule</help>
+      </properties>
+    </leafNode>
+    <leafNode name="starttime">
+      <properties>
+        <help>Time of day to start matching rule</help>
+      </properties>
+    </leafNode>
+    <leafNode name="stopdate">
+      <properties>
+        <help>Date to stop matching rule</help>
+      </properties>
+    </leafNode>
+    <leafNode name="stoptime">
+      <properties>
+        <help>Time of day to stop matching rule</help>
+      </properties>
+    </leafNode>
+    <leafNode name="utc">
+      <properties>
+        <help>Interpret times for startdate, stopdate, starttime and stoptime to be UTC</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+    <leafNode name="weekdays">
+      <properties>
+        <help>Weekdays to match rule on</help>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<node name="icmpv6">
+  <properties>
+    <help>ICMPv6 type and code information</help>
+  </properties>
+  <children>
+    <leafNode name="type">
+      <properties>
+        <help>ICMP type-name</help>
+        <completionHelp>
+          <list>any echo-reply pong destination-unreachable network-unreachable host-unreachable protocol-unreachable port-unreachable fragmentation-needed source-route-failed network-unknown host-unknown network-prohibited host-prohibited TOS-network-unreachable TOS-host-unreachable communication-prohibited host-precedence-violation precedence-cutoff source-quench redirect network-redirect host-redirect TOS-network-redirect TOS host-redirect echo-request ping router-advertisement router-solicitation time-exceeded ttl-exceeded ttl-zero-during-transit ttl-zero-during-reassembly parameter-problem ip-header-bad required-option-missing timestamp-request timestamp-reply address-mask-request address-mask-reply packet-too-big</list>
+        </completionHelp>
+        <valueHelp>
+          <format>any</format>
+          <description>Any ICMP type/code</description>
+        </valueHelp>
+        <valueHelp>
+          <format>echo-reply</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>pong</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>destination-unreachable</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>network-unreachable</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>host-unreachable</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>protocol-unreachable</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>port-unreachable</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>fragmentation-needed</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>source-route-failed</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>network-unknown</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>host-unknown</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>network-prohibited</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>host-prohibited</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>TOS-network-unreachable</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>TOS-host-unreachable</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>communication-prohibited</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>host-precedence-violation</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>precedence-cutoff</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>source-quench</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>redirect</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>network-redirect</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>host-redirect</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>TOS-network-redirect</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>TOS host-redirect</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>echo-request</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>ping</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>router-advertisement</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>router-solicitation</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>time-exceeded</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>ttl-exceeded</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>ttl-zero-during-transit</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>ttl-zero-during-reassembly</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>parameter-problem</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>ip-header-bad</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>required-option-missing</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>timestamp-request</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>timestamp-reply</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>address-mask-request</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>address-mask-reply</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <valueHelp>
+          <format>packet-too-big</format>
+          <description>ICMP type/code name</description>
+        </valueHelp>
+        <constraint>
+          <regex>^(any|echo-reply|pong|destination-unreachable|network-unreachable|host-unreachable|protocol-unreachable|port-unreachable|fragmentation-needed|source-route-failed|network-unknown|host-unknown|network-prohibited|host-prohibited|TOS-network-unreachable|TOS-host-unreachable|communication-prohibited|host-precedence-violation|precedence-cutoff|source-quench|redirect|network-redirect|host-redirect|TOS-network-redirect|TOS host-redirect|echo-request|ping|router-advertisement|router-solicitation|time-exceeded|ttl-exceeded|ttl-zero-during-transit|ttl-zero-during-reassembly|parameter-problem|ip-header-bad|required-option-missing|timestamp-request|timestamp-reply|address-mask-request|address-mask-reply|packet-too-big)$</regex>
+          <validator name="numeric" argument="--range 0-255"/>
+        </constraint>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/policy/route-common-rule.xml.i b/interface-definitions/include/policy/route-common-rule.xml.i
new file mode 100644
index 000000000..c4deefd2a
--- /dev/null
+++ b/interface-definitions/include/policy/route-common-rule.xml.i
@@ -0,0 +1,418 @@
+<!-- include start from policy/route-common-rule.xml.i -->
+#include <include/policy/route-rule-action.xml.i>
+#include <include/generic-description.xml.i>
+<leafNode name="disable">
+  <properties>
+    <help>Option to disable firewall rule</help>
+    <valueless/>
+  </properties>
+</leafNode>
+<node name="fragment">
+  <properties>
+    <help>IP fragment match</help>
+  </properties>
+  <children>
+    <leafNode name="match-frag">
+      <properties>
+        <help>Second and further fragments of fragmented packets</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+    <leafNode name="match-non-frag">
+      <properties>
+        <help>Head fragments or unfragmented packets</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<node name="ipsec">
+  <properties>
+    <help>Inbound IPsec packets</help>
+  </properties>
+  <children>
+    <leafNode name="match-ipsec">
+      <properties>
+        <help>Inbound IPsec packets</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+    <leafNode name="match-none">
+      <properties>
+        <help>Inbound non-IPsec packets</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<node name="limit">
+  <properties>
+    <help>Rate limit using a token bucket filter</help>
+  </properties>
+  <children>
+    <leafNode name="burst">
+      <properties>
+        <help>Maximum number of packets to allow in excess of rate</help>
+        <valueHelp>
+          <format>u32:0-4294967295</format>
+          <description>Maximum number of packets to allow in excess of rate</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 0-4294967295"/>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="rate">
+      <properties>
+        <help>Maximum average matching rate</help>
+        <valueHelp>
+          <format>u32:0-4294967295</format>
+          <description>Maximum average matching rate</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 0-4294967295"/>
+        </constraint>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<leafNode name="log">
+  <properties>
+    <help>Option to log packets matching rule</help>
+    <completionHelp>
+      <list>enable disable</list>
+    </completionHelp>
+    <valueHelp>
+      <format>enable</format>
+      <description>Enable log</description>
+    </valueHelp>
+    <valueHelp>
+      <format>disable</format>
+      <description>Disable log</description>
+    </valueHelp>
+    <constraint>
+      <regex>^(enable|disable)$</regex>
+    </constraint>
+  </properties>
+</leafNode>
+<leafNode name="protocol">
+  <properties>
+    <help>Protocol to match (protocol name, number, or "all")</help>
+    <completionHelp>
+      <script>cat /etc/protocols | sed -e '/^#.*/d' | awk '{ print $1 }'</script>
+    </completionHelp>
+    <valueHelp>
+      <format>all</format>
+      <description>All IP protocols</description>
+    </valueHelp>
+    <valueHelp>
+      <format>tcp_udp</format>
+      <description>Both TCP and UDP</description>
+    </valueHelp>
+    <valueHelp>
+      <format>0-255</format>
+      <description>IP protocol number</description>
+    </valueHelp>
+    <valueHelp>
+      <format>!&lt;protocol&gt;</format>
+      <description>IP protocol number</description>
+    </valueHelp>
+    <constraint>
+      <validator name="ip-protocol"/>
+    </constraint>
+  </properties>
+  <defaultValue>all</defaultValue>
+</leafNode>
+<node name="recent">
+  <properties>
+    <help>Parameters for matching recently seen sources</help>
+  </properties>
+  <children>
+    <leafNode name="count">
+      <properties>
+        <help>Source addresses seen more than N times</help>
+        <valueHelp>
+          <format>u32:1-255</format>
+          <description>Source addresses seen more than N times</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 1-255"/>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="time">
+      <properties>
+        <help>Source addresses seen in the last N seconds</help>
+        <valueHelp>
+          <format>u32:0-4294967295</format>
+          <description>Source addresses seen in the last N seconds</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 0-4294967295"/>
+        </constraint>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<node name="set">
+  <properties>
+    <help>Packet modifications</help>
+  </properties>
+  <children>
+    <leafNode name="dscp">
+      <properties>
+        <help>Packet Differentiated Services Codepoint (DSCP)</help>
+        <valueHelp>
+          <format>u32:0-63</format>
+          <description>DSCP number</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 0-63"/>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="mark">
+      <properties>
+        <help>Packet marking</help>
+        <valueHelp>
+          <format>u32:1-2147483647</format>
+          <description>Packet marking</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 1-2147483647"/>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="table">
+      <properties>
+        <help>Routing table to forward packet with</help>
+        <valueHelp>
+          <format>u32:1-200</format>
+          <description>Table number</description>
+        </valueHelp>
+        <valueHelp>
+          <format>main</format>
+          <description>Main table</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 1-200"/>
+          <regex>^(main)$</regex>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="tcp-mss">
+      <properties>
+        <help>TCP Maximum Segment Size</help>
+        <valueHelp>
+          <format>u32:500-1460</format>
+          <description>Explicitly set TCP MSS value</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 500-1460"/>
+        </constraint>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<node name="source">
+  <properties>
+    <help>Source parameters</help>
+  </properties>
+  <children>
+    #include <include/firewall/address.xml.i>
+    #include <include/firewall/source-destination-group.xml.i>
+    <leafNode name="mac-address">
+      <properties>
+        <help>Source MAC address</help>
+        <valueHelp>
+          <format>&lt;MAC address&gt;</format>
+          <description>MAC address to match</description>
+        </valueHelp>
+        <valueHelp>
+          <format>!&lt;MAC address&gt;</format>
+          <description>Match everything except the specified MAC address</description>
+        </valueHelp>
+      </properties>
+    </leafNode>
+    #include <include/firewall/port.xml.i>
+  </children>
+</node>
+<node name="state">
+  <properties>
+    <help>Session state</help>
+  </properties>
+  <children>
+    <leafNode name="established">
+      <properties>
+        <help>Established state</help>
+        <completionHelp>
+          <list>enable disable</list>
+        </completionHelp>
+        <valueHelp>
+          <format>enable</format>
+          <description>Enable</description>
+        </valueHelp>
+        <valueHelp>
+          <format>disable</format>
+          <description>Disable</description>
+        </valueHelp>
+        <constraint>
+          <regex>^(enable|disable)$</regex>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="invalid">
+      <properties>
+        <help>Invalid state</help>
+        <completionHelp>
+          <list>enable disable</list>
+        </completionHelp>
+        <valueHelp>
+          <format>enable</format>
+          <description>Enable</description>
+        </valueHelp>
+        <valueHelp>
+          <format>disable</format>
+          <description>Disable</description>
+        </valueHelp>
+        <constraint>
+          <regex>^(enable|disable)$</regex>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="new">
+      <properties>
+        <help>New state</help>
+        <completionHelp>
+          <list>enable disable</list>
+        </completionHelp>
+        <valueHelp>
+          <format>enable</format>
+          <description>Enable</description>
+        </valueHelp>
+        <valueHelp>
+          <format>disable</format>
+          <description>Disable</description>
+        </valueHelp>
+        <constraint>
+          <regex>^(enable|disable)$</regex>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="related">
+      <properties>
+        <help>Related state</help>
+        <completionHelp>
+          <list>enable disable</list>
+        </completionHelp>
+        <valueHelp>
+          <format>enable</format>
+          <description>Enable</description>
+        </valueHelp>
+        <valueHelp>
+          <format>disable</format>
+          <description>Disable</description>
+        </valueHelp>
+        <constraint>
+          <regex>^(enable|disable)$</regex>
+        </constraint>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<node name="tcp">
+  <properties>
+    <help>TCP flags to match</help>
+  </properties>
+  <children>
+    <leafNode name="flags">
+      <properties>
+        <help>TCP flags to match</help>
+        <valueHelp>
+          <format>txt</format>
+          <description>TCP flags to match</description>
+        </valueHelp>
+        <valueHelp>
+          <format> </format>
+          <description>\n\n  Allowed values for TCP flags : SYN ACK FIN RST URG PSH ALL\n  When specifying more than one flag, flags should be comma-separated.\n For example : value of 'SYN,!ACK,!FIN,!RST' will only match packets with\n  the SYN flag set, and the ACK, FIN and RST flags unset</description>
+        </valueHelp>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<node name="time">
+  <properties>
+    <help>Time to match rule</help>
+  </properties>
+  <children>
+    <leafNode name="monthdays">
+      <properties>
+        <help>Monthdays to match rule on</help>
+      </properties>
+    </leafNode>
+    <leafNode name="startdate">
+      <properties>
+        <help>Date to start matching rule</help>
+      </properties>
+    </leafNode>
+    <leafNode name="starttime">
+      <properties>
+        <help>Time of day to start matching rule</help>
+      </properties>
+    </leafNode>
+    <leafNode name="stopdate">
+      <properties>
+        <help>Date to stop matching rule</help>
+      </properties>
+    </leafNode>
+    <leafNode name="stoptime">
+      <properties>
+        <help>Time of day to stop matching rule</help>
+      </properties>
+    </leafNode>
+    <leafNode name="utc">
+      <properties>
+        <help>Interpret times for startdate, stopdate, starttime and stoptime to be UTC</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+    <leafNode name="weekdays">
+      <properties>
+        <help>Weekdays to match rule on</help>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<node name="icmp">
+  <properties>
+    <help>ICMP type and code information</help>
+  </properties>
+  <children>
+    <leafNode name="code">
+      <properties>
+        <help>ICMP code (0-255)</help>
+        <valueHelp>
+          <format>u32:0-255</format>
+          <description>ICMP code (0-255)</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 0-255"/>
+        </constraint>
+      </properties>
+    </leafNode>
+    <leafNode name="type">
+      <properties>
+        <help>ICMP type (0-255)</help>
+        <valueHelp>
+          <format>u32:0-255</format>
+          <description>ICMP type (0-255)</description>
+        </valueHelp>
+        <constraint>
+          <validator name="numeric" argument="--range 0-255"/>
+        </constraint>
+      </properties>
+    </leafNode>
+    #include <include/firewall/icmp-type-name.xml.i>
+  </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/policy/route-rule-action.xml.i b/interface-definitions/include/policy/route-rule-action.xml.i
new file mode 100644
index 000000000..9c880579d
--- /dev/null
+++ b/interface-definitions/include/policy/route-rule-action.xml.i
@@ -0,0 +1,17 @@
+<!-- include start from policy/route-rule-action.xml.i -->
+<leafNode name="action">
+  <properties>
+    <help>Rule action [REQUIRED]</help>
+    <completionHelp>
+      <list>drop</list>
+    </completionHelp>
+    <valueHelp>
+      <format>drop</format>
+      <description>Drop matching entries</description>
+    </valueHelp>
+    <constraint>
+      <regex>^(drop)$</regex>
+    </constraint>
+  </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in
index 03cbb523d..723041ca5 100644
--- a/interface-definitions/interfaces-bonding.xml.in
+++ b/interface-definitions/interfaces-bonding.xml.in
@@ -57,6 +57,7 @@
           #include <include/interface/vrf.xml.i>
           #include <include/interface/mirror.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
           <leafNode name="hash-policy">
             <properties>
               <help>Bonding transmit hash policy</help>
diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in
index ebf6c3631..0856615be 100644
--- a/interface-definitions/interfaces-bridge.xml.in
+++ b/interface-definitions/interfaces-bridge.xml.in
@@ -42,6 +42,7 @@
           #include <include/interface/vrf.xml.i>
           #include <include/interface/mtu-68-16000.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
           <leafNode name="forwarding-delay">
             <properties>
               <help>Forwarding delay</help>
diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in
index c6061b8bb..1231b1492 100644
--- a/interface-definitions/interfaces-dummy.xml.in
+++ b/interface-definitions/interfaces-dummy.xml.in
@@ -20,6 +20,7 @@
           #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>
           <node name="ip">
             <properties>
               <help>IPv4 routing parameters</help>
diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in
index 3868ebbbc..9e113cb71 100644
--- a/interface-definitions/interfaces-ethernet.xml.in
+++ b/interface-definitions/interfaces-ethernet.xml.in
@@ -32,6 +32,7 @@
           #include <include/interface/disable-link-detect.xml.i>
           #include <include/interface/disable.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
           <leafNode name="duplex">
             <properties>
               <help>Duplex mode</help>
diff --git a/interface-definitions/interfaces-geneve.xml.in b/interface-definitions/interfaces-geneve.xml.in
index 06ad7c82b..dd4d324d4 100644
--- a/interface-definitions/interfaces-geneve.xml.in
+++ b/interface-definitions/interfaces-geneve.xml.in
@@ -24,6 +24,7 @@
           #include <include/interface/mac.xml.i>
           #include <include/interface/mtu-1450-16000.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
           <node name="parameters">
             <properties>
               <help>GENEVE tunnel parameters</help>
diff --git a/interface-definitions/interfaces-l2tpv3.xml.in b/interface-definitions/interfaces-l2tpv3.xml.in
index c5bca4408..85d4ab992 100644
--- a/interface-definitions/interfaces-l2tpv3.xml.in
+++ b/interface-definitions/interfaces-l2tpv3.xml.in
@@ -33,6 +33,7 @@
           </leafNode>
           #include <include/interface/disable.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
           <leafNode name="encapsulation">
             <properties>
               <help>Encapsulation type (default: UDP)</help>
diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in
index 5713d985b..d69a093af 100644
--- a/interface-definitions/interfaces-macsec.xml.in
+++ b/interface-definitions/interfaces-macsec.xml.in
@@ -20,6 +20,7 @@
           #include <include/interface/ipv4-options.xml.i>
           #include <include/interface/ipv6-options.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
           <node name="security">
             <properties>
               <help>Security/Encryption Settings</help>
diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in
index 1fe8e63f8..16d91145f 100644
--- a/interface-definitions/interfaces-openvpn.xml.in
+++ b/interface-definitions/interfaces-openvpn.xml.in
@@ -35,6 +35,7 @@
           </node>
           #include <include/interface/description.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
           <leafNode name="device-type">
             <properties>
               <help>OpenVPN interface device-type (default: tun)</help>
diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in
index d9c30031e..80a890940 100644
--- a/interface-definitions/interfaces-pppoe.xml.in
+++ b/interface-definitions/interfaces-pppoe.xml.in
@@ -20,6 +20,7 @@
           #include <include/interface/authentication.xml.i>
           #include <include/interface/dial-on-demand.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
           <leafNode name="default-route">
             <properties>
               <help>Default route insertion behaviour (default: auto)</help>
diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in
index 974ba1a50..bf7055f8d 100644
--- a/interface-definitions/interfaces-pseudo-ethernet.xml.in
+++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in
@@ -28,6 +28,7 @@
           #include <include/source-interface-ethernet.xml.i>
           #include <include/interface/mac.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
           <leafNode name="mode">
             <properties>
               <help>Receive mode (default: private)</help>
diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in
index b95f07a4b..e08b2fdab 100644
--- a/interface-definitions/interfaces-tunnel.xml.in
+++ b/interface-definitions/interfaces-tunnel.xml.in
@@ -31,6 +31,7 @@
           #include <include/interface/tunnel-remote.xml.i>
           #include <include/source-interface.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
           <leafNode name="6rd-prefix">
             <properties>
               <help>6rd network prefix</help>
diff --git a/interface-definitions/interfaces-vti.xml.in b/interface-definitions/interfaces-vti.xml.in
index a8a330f32..f03c7476d 100644
--- a/interface-definitions/interfaces-vti.xml.in
+++ b/interface-definitions/interfaces-vti.xml.in
@@ -36,6 +36,7 @@
           #include <include/interface/mtu-68-16000.xml.i>
           #include <include/interface/vrf.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
         </children>
       </tagNode>
     </children>
diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in
index c5bb0c8f2..0c13dd4d3 100644
--- a/interface-definitions/interfaces-vxlan.xml.in
+++ b/interface-definitions/interfaces-vxlan.xml.in
@@ -42,6 +42,7 @@
           #include <include/interface/mac.xml.i>
           #include <include/interface/mtu-1200-16000.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
           <leafNode name="mtu">
             <defaultValue>1450</defaultValue>
           </leafNode>
diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in
index c96b3d46b..7a7c9c1d9 100644
--- a/interface-definitions/interfaces-wireguard.xml.in
+++ b/interface-definitions/interfaces-wireguard.xml.in
@@ -23,6 +23,7 @@
           #include <include/port-number.xml.i>
           #include <include/interface/mtu-68-16000.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
           <leafNode name="mtu">
             <defaultValue>1420</defaultValue>
           </leafNode>
diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in
index da739840b..a2d1439a3 100644
--- a/interface-definitions/interfaces-wireless.xml.in
+++ b/interface-definitions/interfaces-wireless.xml.in
@@ -18,6 +18,7 @@
         <children>
           #include <include/interface/address-ipv4-ipv6-dhcp.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
           <node name="capabilities">
             <properties>
               <help>HT and VHT capabilities for your card</help>
diff --git a/interface-definitions/interfaces-wwan.xml.in b/interface-definitions/interfaces-wwan.xml.in
index 926c48194..03554feed 100644
--- a/interface-definitions/interfaces-wwan.xml.in
+++ b/interface-definitions/interfaces-wwan.xml.in
@@ -40,6 +40,7 @@
           #include <include/interface/ipv6-options.xml.i>
           #include <include/interface/dial-on-demand.xml.i>
           #include <include/interface/interface-firewall.xml.i>
+          #include <include/interface/interface-policy.xml.i>
         </children>
       </tagNode>
     </children>
diff --git a/interface-definitions/policy-route.xml.in b/interface-definitions/policy-route.xml.in
new file mode 100644
index 000000000..ed726d1e4
--- /dev/null
+++ b/interface-definitions/policy-route.xml.in
@@ -0,0 +1,83 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="policy">
+    <children>
+      <tagNode name="ipv6-route" owner="${vyos_conf_scripts_dir}/policy-route.py">
+        <properties>
+          <help>IPv6 policy route rule set name</help>
+          <priority>201</priority>
+        </properties>
+        <children>
+          #include <include/generic-description.xml.i>
+          #include <include/firewall/name-default-log.xml.i>
+          <tagNode name="rule">
+            <properties>
+              <help>Rule number (1-9999)</help>
+            </properties>
+            <children>
+              <node name="destination">
+                <properties>
+                  <help>Destination parameters</help>
+                </properties>
+                <children>
+                  #include <include/firewall/address-ipv6.xml.i>
+                  #include <include/firewall/source-destination-group-ipv6.xml.i>
+                  #include <include/firewall/port.xml.i>
+                </children>
+              </node>
+              <node name="source">
+                <properties>
+                  <help>Source parameters</help>
+                </properties>
+                <children>
+                  #include <include/firewall/address-ipv6.xml.i>
+                  #include <include/firewall/source-destination-group-ipv6.xml.i>
+                  #include <include/firewall/port.xml.i>
+                </children>
+              </node>
+              #include <include/policy/route-common-rule-ipv6.xml.i>
+            </children>
+          </tagNode>
+        </children>
+      </tagNode>
+      <tagNode name="route" owner="${vyos_conf_scripts_dir}/policy-route.py">
+        <properties>
+          <help>Policy route rule set name</help>
+          <priority>201</priority>
+        </properties>
+        <children>
+          #include <include/generic-description.xml.i>
+          #include <include/firewall/name-default-log.xml.i>
+          <tagNode name="rule">
+            <properties>
+              <help>Rule number (1-9999)</help>
+            </properties>
+            <children>
+              <node name="destination">
+                <properties>
+                  <help>Destination parameters</help>
+                </properties>
+                <children>
+                  #include <include/firewall/address.xml.i>
+                  #include <include/firewall/source-destination-group.xml.i>
+                  #include <include/firewall/port.xml.i>
+                </children>
+              </node>
+              <node name="source">
+                <properties>
+                  <help>Source parameters</help>
+                </properties>
+                <children>
+                  #include <include/firewall/address.xml.i>
+                  #include <include/firewall/source-destination-group.xml.i>
+                  #include <include/firewall/port.xml.i>
+                </children>
+              </node>
+              #include <include/policy/route-common-rule.xml.i>
+            </children>
+          </tagNode>
+        </children>
+      </tagNode>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index 9b8af7852..8b7402b7e 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -150,8 +150,12 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):
     if tcp_flags:
         output.append(parse_tcp_flags(tcp_flags))
 
+
     output.append('counter')
 
+    if 'set' in rule_conf:
+        output.append(parse_policy_set(rule_conf['set'], def_suffix))
+
     if 'action' in rule_conf:
         output.append(nft_action(rule_conf['action']))
     else:
@@ -192,3 +196,22 @@ def parse_time(time):
         out_days = [f'"{day}"' for day in days if day[0] != '!']
         out.append(f'day {{{",".join(out_days)}}}')
     return " ".join(out)
+
+def parse_policy_set(set_conf, def_suffix):
+    out = []
+    if 'dscp' in set_conf:
+        dscp = set_conf['dscp']
+        out.append(f'ip{def_suffix} dscp set {dscp}')
+    if 'mark' in set_conf:
+        mark = set_conf['mark']
+        out.append(f'meta mark set {mark}')
+    if 'table' in set_conf:
+        table = set_conf['table']
+        if table == 'main':
+            table = '254'
+        mark = 0x7FFFFFFF - int(set_conf['table'])
+        out.append(f'meta mark set {mark}')
+    if 'tcp_mss' in set_conf:
+        mss = set_conf['tcp_mss']
+        out.append(f'tcp option maxseg size set {mss}')
+    return " ".join(out)
diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py
new file mode 100755
index 000000000..70a234187
--- /dev/null
+++ b/smoketest/scripts/cli/test_policy_route.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.util import cmd
+
+mark = '100'
+table_mark_offset = 0x7fffffff
+table_id = '101'
+
+class TestPolicyRoute(VyOSUnitTestSHIM.TestCase):
+    def setUp(self):
+        self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '172.16.10.1/24'])
+        self.cli_set(['protocols', 'static', 'table', '101', 'route', '0.0.0.0/0', 'interface', 'eth0'])
+
+    def tearDown(self):
+        self.cli_delete(['interfaces', 'ethernet', 'eth0'])
+        self.cli_delete(['policy', 'route'])
+        self.cli_delete(['policy', 'ipv6-route'])
+        self.cli_commit()
+
+    def test_pbr_mark(self):
+        self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'address', '172.16.20.10'])
+        self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10'])
+        self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'mark', mark])
+
+        self.cli_set(['interfaces', 'ethernet', 'eth0', 'policy', 'route', 'smoketest'])
+
+        self.cli_commit()
+
+        mark_hex = "{0:#010x}".format(int(mark))
+
+        nftables_search = [
+            ['iifname "eth0"', 'jump VYOS_PBR_smoketest'],
+            ['ip daddr 172.16.10.10', 'ip saddr 172.16.20.10', 'meta mark set ' + mark_hex],
+        ]
+
+        nftables_output = cmd('sudo nft list table ip mangle')
+
+        for search in nftables_search:
+            matched = False
+            for line in nftables_output.split("\n"):
+                if all(item in line for item in search):
+                    matched = True
+                    break
+            self.assertTrue(matched)
+
+    def test_pbr_table(self):
+        self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'tcp_udp'])
+        self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'port', '8888'])
+        self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'table', table_id])
+
+        self.cli_set(['interfaces', 'ethernet', 'eth0', 'policy', 'route', 'smoketest'])
+
+        self.cli_commit()
+
+        mark_hex = "{0:#010x}".format(table_mark_offset - int(table_id))
+
+        nftables_search = [
+            ['iifname "eth0"', 'jump VYOS_PBR_smoketest'],
+            ['meta l4proto { tcp, udp }', 'th dport { 8888 }', 'meta mark set ' + mark_hex]
+        ]
+
+        nftables_output = cmd('sudo nft list table ip mangle')
+
+        for search in nftables_search:
+            matched = False
+            for line in nftables_output.split("\n"):
+                if all(item in line for item in search):
+                    matched = True
+                    break
+            self.assertTrue(matched)
+
+        ip_rule_search = [
+            ['fwmark ' + hex(table_mark_offset - int(table_id)), 'lookup ' + table_id]
+        ]
+
+        ip_rule_output = cmd('ip rule show')
+
+        for search in ip_rule_search:
+            matched = False
+            for line in ip_rule_output.split("\n"):
+                if all(item in line for item in search):
+                    matched = True
+                    break
+            self.assertTrue(matched)
+
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
diff --git a/src/conf_mode/policy-route-interface.py b/src/conf_mode/policy-route-interface.py
new file mode 100755
index 000000000..e81135a74
--- /dev/null
+++ b/src/conf_mode/policy-route-interface.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+
+from sys import argv
+from sys import exit
+
+from vyos.config import Config
+from vyos.ifconfig import Section
+from vyos.template import render
+from vyos.util import cmd
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    ifname = argv[1]
+    ifpath = Section.get_config_path(ifname)
+    if_policy_path = f'interfaces {ifpath} policy'
+
+    if_policy = conf.get_config_dict(if_policy_path, key_mangling=('-', '_'), get_first_key=True,
+                                    no_tag_node_value_mangle=True)
+
+    if_policy['ifname'] = ifname
+    if_policy['policy'] = conf.get_config_dict(['policy'], key_mangling=('-', '_'), get_first_key=True,
+                                    no_tag_node_value_mangle=True)
+
+    return if_policy
+
+def verify(if_policy):
+    # bail out early - looks like removal from running config
+    if not if_policy:
+        return None
+
+    for route in ['route', 'ipv6_route']:
+        if route in if_policy:
+            if route not in if_policy['policy']:
+                raise ConfigError('Policy route not configured')
+
+            route_name = if_policy[route]
+
+            if route_name not in if_policy['policy'][route]:
+                raise ConfigError(f'Invalid policy route name "{name}"')
+
+    return None
+
+def generate(if_policy):
+    return None
+
+def cleanup_rule(table, chain, ifname, new_name=None):
+    results = cmd(f'nft -a list chain {table} {chain}').split("\n")
+    retval = None
+    for line in results:
+        if f'oifname "{ifname}"' in line:
+            if new_name and f'jump {new_name}' in line:
+                # new_name is used to clear rules for any previously referenced chains
+                # returns true when rule exists and doesn't need to be created
+                retval = True
+                continue
+
+            handle_search = re.search('handle (\d+)', line)
+            if handle_search:
+                cmd(f'nft delete rule {table} {chain} handle {handle_search[1]}')
+    return retval
+
+def apply(if_policy):
+    ifname = if_policy['ifname']
+
+    route_chain = 'VYOS_PBR_PREROUTING'
+    ipv6_route_chain = 'VYOS_PBR6_PREROUTING'
+
+    if 'route' in if_policy:
+        name = 'VYOS_PBR_' + if_policy['route']
+        rule_exists = cleanup_rule('ip mangle', route_chain, ifname, name)
+
+        if not rule_exists:
+            cmd(f'nft insert rule ip mangle {route_chain} iifname {ifname} counter jump {name}')
+    else:
+        cleanup_rule('ip mangle', route_chain, ifname)
+
+    if 'ipv6_route' in if_policy:
+        name = 'VYOS_PBR6_' + if_policy['ipv6_route']
+        rule_exists = cleanup_rule('ip6 mangle', ipv6_route_chain, ifname, name)
+
+        if not rule_exists:
+            cmd(f'nft insert rule ip6 mangle {ipv6_route_chain} iifname {ifname} counter jump {name}')
+    else:
+        cleanup_rule('ip6 mangle', ipv6_route_chain, ifname)
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py
new file mode 100755
index 000000000..d098be68d
--- /dev/null
+++ b/src/conf_mode/policy-route.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from json import loads
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import cmd
+from vyos.util import dict_search_args
+from vyos.util import run
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+mark_offset = 0x7FFFFFFF
+nftables_conf = '/run/nftables_policy.conf'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['policy']
+
+    if not conf.exists(base + ['route']) and not conf.exists(base + ['ipv6-route']):
+        return None
+
+    policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
+                                    no_tag_node_value_mangle=True)
+
+    return policy
+
+def verify(policy):
+    # bail out early - looks like removal from running config
+    if not policy:
+        return None
+
+    for route in ['route', 'ipv6_route']:
+        if route in policy:
+            for name, pol_conf in policy[route].items():
+                if 'rule' in pol_conf:
+                    for rule_id, rule_conf in pol_conf.items():
+                        icmp = 'icmp' if route == 'route' else 'icmpv6'
+                        if icmp in rule_conf:
+                            icmp_defined = False
+                            if 'type_name' in rule_conf[icmp]:
+                                icmp_defined = True
+                                if 'code' in rule_conf[icmp] or 'type' in rule_conf[icmp]:
+                                    raise ConfigError(f'{name} rule {rule_id}: Cannot use ICMP type/code with ICMP type-name')
+                            if 'code' in rule_conf[icmp]:
+                                icmp_defined = True
+                                if 'type' not in rule_conf[icmp]:
+                                    raise ConfigError(f'{name} rule {rule_id}: ICMP code can only be defined if ICMP type is defined')
+                            if 'type' in rule_conf[icmp]:
+                                icmp_defined = True
+
+                            if icmp_defined and 'protocol' not in rule_conf or rule_conf['protocol'] != icmp:
+                                raise ConfigError(f'{name} rule {rule_id}: ICMP type/code or type-name can only be defined if protocol is ICMP')
+                        if 'set' in rule_conf:
+                            if 'tcp_mss' in rule_conf['set']:
+                                tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
+                                if not tcp_flags or 'SYN' not in tcp_flags.split(","):
+                                    raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS')
+                        if 'tcp' in rule_conf:
+                            if 'flags' in rule_conf['tcp']:
+                                if 'protocol' not in rule_conf or rule_conf['protocol'] != 'tcp':
+                                    raise ConfigError(f'{name} rule {rule_id}: TCP flags can only be set if protocol is set to TCP')
+
+
+    return None
+
+def generate(policy):
+    if not policy:
+        if os.path.exists(nftables_conf):
+            os.unlink(nftables_conf)
+        return None
+
+    if not os.path.exists(nftables_conf):
+        policy['first_install'] = True
+
+    render(nftables_conf, 'firewall/nftables-policy.tmpl', policy)
+    return None
+
+def apply_table_marks(policy):
+    for route in ['route', 'ipv6_route']:
+        if route in policy:
+            for name, pol_conf in policy[route].items():
+                if 'rule' in pol_conf:
+                    for rule_id, rule_conf in pol_conf['rule'].items():
+                        set_table = dict_search_args(rule_conf, 'set', 'table')
+                        if set_table:
+                            if set_table == 'main':
+                                set_table = '254'
+                            table_mark = mark_offset - int(set_table)
+                            cmd(f'ip rule add fwmark {table_mark} table {set_table}')
+
+def cleanup_table_marks():
+    json_rules = cmd('ip -j -N rule list')
+    rules = loads(json_rules)
+    for rule in rules:
+        if 'fwmark' not in rule or 'table' not in rule:
+            continue
+        fwmark = rule['fwmark']
+        table = int(rule['table'])
+        if fwmark[:2] == '0x':
+            fwmark = int(fwmark, 16)
+        if (int(fwmark) == (mark_offset - table)):
+            cmd(f'ip rule del fwmark {fwmark} table {table}')
+
+def apply(policy):
+    if not policy or 'first_install' not in policy:
+        run(f'nft flush table ip mangle')
+        run(f'nft flush table ip6 mangle')
+
+    if not policy:
+        cleanup_table_marks()
+        return None
+
+    install_result = run(f'nft -f {nftables_conf}')
+    if install_result == 1:
+        raise ConfigError('Failed to apply policy based routing')
+
+    if 'first_install' not in policy:
+        cleanup_table_marks()
+
+    apply_table_marks(policy)
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
-- 
cgit v1.2.3


From 28b285b4791aece18fe1bbd76f3d555370545006 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sun, 31 Oct 2021 21:24:40 +0100
Subject: zone_policy: T3873: Implement intra-zone-filtering

---
 data/templates/zone_policy/nftables.tmpl |  4 +--
 interface-definitions/zone-policy.xml.in | 49 ++++++++++++++++++++++++++++++++
 python/vyos/template.py                  | 15 ++++++++++
 src/conf_mode/zone_policy.py             | 20 +++++++++++++
 4 files changed, 86 insertions(+), 2 deletions(-)

(limited to 'python')

diff --git a/data/templates/zone_policy/nftables.tmpl b/data/templates/zone_policy/nftables.tmpl
index 4575a721c..21230c688 100644
--- a/data/templates/zone_policy/nftables.tmpl
+++ b/data/templates/zone_policy/nftables.tmpl
@@ -28,7 +28,7 @@ table ip filter {
     }
 {%     else %}
     chain VZONE_{{ zone_name }} {
-        iifname { {{ zone_conf.interface | join(",") }} } counter return
+        iifname { {{ zone_conf.interface | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6=False) }}
 {%       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 }}
@@ -62,7 +62,7 @@ table ip6 filter {
     }
 {%     else %}
     chain VZONE6_{{ zone_name }} {
-        iifname { {{ zone_conf.interface | join(",") }} } counter return
+        iifname { {{ zone_conf.interface | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6=True) }}
 {%       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 }}
diff --git a/interface-definitions/zone-policy.xml.in b/interface-definitions/zone-policy.xml.in
index 52fd73f15..dd64c7c16 100644
--- a/interface-definitions/zone-policy.xml.in
+++ b/interface-definitions/zone-policy.xml.in
@@ -81,6 +81,55 @@
               <multi/>
             </properties>
           </leafNode>
+          <node name="intra-zone-filtering">
+            <properties>
+              <help>Intra-zone filtering</help>
+            </properties>
+            <children>
+              <leafNode name="action">
+                <properties>
+                  <help>Action for intra-zone traffic</help>
+                  <completionHelp>
+                    <list>accept drop</list>
+                  </completionHelp>
+                  <valueHelp>
+                    <format>accept</format>
+                    <description>Accept traffic (default)</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>drop</format>
+                    <description>Drop silently</description>
+                  </valueHelp>
+                  <constraint>
+                    <regex>^(accept|drop)$</regex>
+                  </constraint>
+                </properties>
+              </leafNode>
+              <node name="firewall">
+                <properties>
+                  <help>Use the specified firewall chain</help>
+                </properties>
+                <children>
+                  <leafNode name="ipv6-name">
+                    <properties>
+                      <help>IPv6 firewall ruleset</help>
+                      <completionHelp>
+                        <path>firewall ipv6-name</path>
+                      </completionHelp>
+                    </properties>
+                  </leafNode>
+                  <leafNode name="name">
+                    <properties>
+                      <help>IPv4 firewall ruleset</help>
+                      <completionHelp>
+                        <path>firewall name</path>
+                      </completionHelp>
+                    </properties>
+                  </leafNode>
+                </children>
+              </node>
+            </children>
+          </node>
           <leafNode name="local-zone">
             <properties>
               <help>Zone to be local-zone</help>
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 55bd04136..e20890e25 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -505,3 +505,18 @@ def nft_state_policy(conf, state):
         out.append(conf['action'])
 
     return " ".join(out)
+
+@register_filter('nft_intra_zone_action')
+def nft_intra_zone_action(zone_conf, ipv6=False):
+    if 'intra_zone_filtering' in zone_conf:
+        intra_zone = zone_conf['intra_zone_filtering']
+        fw_name = 'ipv6_name' if ipv6 else 'name'
+
+        if 'action' in intra_zone:
+            if intra_zone['action'] == 'accept':
+                return 'return'
+            return intra_zone['action']
+        elif dict_search_args(intra_zone, 'firewall', fw_name):
+            name = dict_search_args(intra_zone, 'firewall', fw_name)
+            return f'jump {name}'
+    return 'return'
diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py
index 92f5624c2..2535ea33b 100755
--- a/src/conf_mode/zone_policy.py
+++ b/src/conf_mode/zone_policy.py
@@ -63,6 +63,8 @@ def verify(zone_policy):
                     raise ConfigError('There cannot be multiple local zones')
                 if 'interface' in zone_conf:
                     raise ConfigError('Local zone cannot have interfaces assigned')
+                if 'intra_zone_filtering' in zone_conf:
+                    raise ConfigError('Local zone cannot use intra-zone-filtering')
                 local_zone = True
 
             if 'interface' in zone_conf:
@@ -73,6 +75,24 @@ def verify(zone_policy):
 
                 interfaces += zone_conf['interface']
 
+            if 'intra_zone_filtering' in zone_conf:
+                intra_zone = zone_conf['intra_zone_filtering']
+
+                if len(intra_zone) > 1:
+                    raise ConfigError('Only one intra-zone-filtering action must be specified')
+
+                if 'firewall' in intra_zone:
+                    v4_name = dict_search_args(intra_zone, 'firewall', 'name')
+                    if v4_name and not dict_search_args(zone_policy, 'firewall', 'name', v4_name):
+                        raise ConfigError(f'Firewall name "{v4_name}" does not exist')
+
+                    v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6-name')
+                    if v6_name and not dict_search_args(zone_policy, 'firewall', 'ipv6-name', v6_name):
+                        raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
+
+                    if not v4_name and not v6_name:
+                        raise ConfigError('No firewall names specified for intra-zone-filtering')
+
             if 'from' in zone_conf:
                 for from_zone, from_conf in zone_conf['from'].items():
                     v4_name = dict_search_args(from_conf, 'firewall', 'name')
-- 
cgit v1.2.3