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 .
+
+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 @@
+
+
+
+ 620
+ Policy route options
+
+
+
+
+ IPv4 policy route ruleset for interface
+
+ policy route
+
+
+
+
+
+ IPv6 policy route ruleset for interface
+
+ policy ipv6-route
+
+
+
+
+
+
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 @@
+
+
+
+ 620
+ Policy route options
+
+
+
+
+ IPv4 policy route ruleset for interface
+
+ policy route
+
+
+
+
+
+ IPv6 policy route ruleset for interface
+
+ policy ipv6-route
+
+
+
+
+
+
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 @@
+
+
+
+ 620
+ Policy route options
+
+
+
+
+ IPv4 policy route ruleset for interface
+
+ policy route
+
+
+
+
+
+ IPv6 policy route ruleset for interface
+
+ policy ipv6-route
+
+
+
+
+
+
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
#include
+ #include
Protocol used for service VLAN (default: 802.1ad)
@@ -65,6 +66,7 @@
#include
#include
#include
+ #include
#include
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
#include
+ #include
VLAN egress QoS
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
+#include
+
+
+ Option to disable firewall rule
+
+
+
+
+
+ IP fragment match
+
+
+
+
+ Second and further fragments of fragmented packets
+
+
+
+
+
+ Head fragments or unfragmented packets
+
+
+
+
+
+
+
+ Inbound IPsec packets
+
+
+
+
+ Inbound IPsec packets
+
+
+
+
+
+ Inbound non-IPsec packets
+
+
+
+
+
+
+
+ Rate limit using a token bucket filter
+
+
+
+
+ Maximum number of packets to allow in excess of rate
+
+ u32:0-4294967295
+ Maximum number of packets to allow in excess of rate
+
+
+
+
+
+
+
+
+ Maximum average matching rate
+
+ u32:0-4294967295
+ Maximum average matching rate
+
+
+
+
+
+
+
+
+
+
+ Option to log packets matching rule
+
+ enable disable
+
+
+ enable
+ Enable log
+
+
+ disable
+ Disable log
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ Protocol to match (protocol name, number, or "all")
+
+
+
+
+ all
+ All IP protocols
+
+
+ tcp_udp
+ Both TCP and UDP
+
+
+ 0-255
+ IP protocol number
+
+
+ !<protocol>
+ IP protocol number
+
+
+
+
+
+ all
+
+
+
+ Parameters for matching recently seen sources
+
+
+
+
+ Source addresses seen more than N times
+
+ u32:1-255
+ Source addresses seen more than N times
+
+
+
+
+
+
+
+
+ Source addresses seen in the last N seconds
+
+ u32:0-4294967295
+ Source addresses seen in the last N seconds
+
+
+
+
+
+
+
+
+
+
+ Packet modifications
+
+
+
+
+ Packet Differentiated Services Codepoint (DSCP)
+
+ u32:0-63
+ DSCP number
+
+
+
+
+
+
+
+
+ Packet marking
+
+ u32:1-2147483647
+ Packet marking
+
+
+
+
+
+
+
+
+ Routing table to forward packet with
+
+ u32:1-200
+ Table number
+
+
+ main
+ Main table
+
+
+
+ ^(main)$
+
+
+
+
+
+ TCP Maximum Segment Size
+
+ u32:500-1460
+ Explicitly set TCP MSS value
+
+
+
+
+
+
+
+
+
+
+ Source parameters
+
+
+ #include
+ #include
+
+
+ Source MAC address
+
+ <MAC address>
+ MAC address to match
+
+
+ !<MAC address>
+ Match everything except the specified MAC address
+
+
+
+ #include
+
+
+
+
+ Session state
+
+
+
+
+ Established state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ Invalid state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ New state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ Related state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+
+
+ TCP flags to match
+
+
+
+
+ TCP flags to match
+
+ txt
+ TCP flags to match
+
+
+
+ \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
+
+
+
+
+
+
+
+ Time to match rule
+
+
+
+
+ Monthdays to match rule on
+
+
+
+
+ Date to start matching rule
+
+
+
+
+ Time of day to start matching rule
+
+
+
+
+ Date to stop matching rule
+
+
+
+
+ Time of day to stop matching rule
+
+
+
+
+ Interpret times for startdate, stopdate, starttime and stoptime to be UTC
+
+
+
+
+
+ Weekdays to match rule on
+
+
+
+
+
+
+ ICMPv6 type and code information
+
+
+
+
+ ICMP type-name
+
+ 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
+
+
+ any
+ Any ICMP type/code
+
+
+ echo-reply
+ ICMP type/code name
+
+
+ pong
+ ICMP type/code name
+
+
+ destination-unreachable
+ ICMP type/code name
+
+
+ network-unreachable
+ ICMP type/code name
+
+
+ host-unreachable
+ ICMP type/code name
+
+
+ protocol-unreachable
+ ICMP type/code name
+
+
+ port-unreachable
+ ICMP type/code name
+
+
+ fragmentation-needed
+ ICMP type/code name
+
+
+ source-route-failed
+ ICMP type/code name
+
+
+ network-unknown
+ ICMP type/code name
+
+
+ host-unknown
+ ICMP type/code name
+
+
+ network-prohibited
+ ICMP type/code name
+
+
+ host-prohibited
+ ICMP type/code name
+
+
+ TOS-network-unreachable
+ ICMP type/code name
+
+
+ TOS-host-unreachable
+ ICMP type/code name
+
+
+ communication-prohibited
+ ICMP type/code name
+
+
+ host-precedence-violation
+ ICMP type/code name
+
+
+ precedence-cutoff
+ ICMP type/code name
+
+
+ source-quench
+ ICMP type/code name
+
+
+ redirect
+ ICMP type/code name
+
+
+ network-redirect
+ ICMP type/code name
+
+
+ host-redirect
+ ICMP type/code name
+
+
+ TOS-network-redirect
+ ICMP type/code name
+
+
+ TOS host-redirect
+ ICMP type/code name
+
+
+ echo-request
+ ICMP type/code name
+
+
+ ping
+ ICMP type/code name
+
+
+ router-advertisement
+ ICMP type/code name
+
+
+ router-solicitation
+ ICMP type/code name
+
+
+ time-exceeded
+ ICMP type/code name
+
+
+ ttl-exceeded
+ ICMP type/code name
+
+
+ ttl-zero-during-transit
+ ICMP type/code name
+
+
+ ttl-zero-during-reassembly
+ ICMP type/code name
+
+
+ parameter-problem
+ ICMP type/code name
+
+
+ ip-header-bad
+ ICMP type/code name
+
+
+ required-option-missing
+ ICMP type/code name
+
+
+ timestamp-request
+ ICMP type/code name
+
+
+ timestamp-reply
+ ICMP type/code name
+
+
+ address-mask-request
+ ICMP type/code name
+
+
+ address-mask-reply
+ ICMP type/code name
+
+
+ packet-too-big
+ ICMP type/code name
+
+
+ ^(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)$
+
+
+
+
+
+
+
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
+#include
+
+
+ Option to disable firewall rule
+
+
+
+
+
+ IP fragment match
+
+
+
+
+ Second and further fragments of fragmented packets
+
+
+
+
+
+ Head fragments or unfragmented packets
+
+
+
+
+
+
+
+ Inbound IPsec packets
+
+
+
+
+ Inbound IPsec packets
+
+
+
+
+
+ Inbound non-IPsec packets
+
+
+
+
+
+
+
+ Rate limit using a token bucket filter
+
+
+
+
+ Maximum number of packets to allow in excess of rate
+
+ u32:0-4294967295
+ Maximum number of packets to allow in excess of rate
+
+
+
+
+
+
+
+
+ Maximum average matching rate
+
+ u32:0-4294967295
+ Maximum average matching rate
+
+
+
+
+
+
+
+
+
+
+ Option to log packets matching rule
+
+ enable disable
+
+
+ enable
+ Enable log
+
+
+ disable
+ Disable log
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ Protocol to match (protocol name, number, or "all")
+
+
+
+
+ all
+ All IP protocols
+
+
+ tcp_udp
+ Both TCP and UDP
+
+
+ 0-255
+ IP protocol number
+
+
+ !<protocol>
+ IP protocol number
+
+
+
+
+
+ all
+
+
+
+ Parameters for matching recently seen sources
+
+
+
+
+ Source addresses seen more than N times
+
+ u32:1-255
+ Source addresses seen more than N times
+
+
+
+
+
+
+
+
+ Source addresses seen in the last N seconds
+
+ u32:0-4294967295
+ Source addresses seen in the last N seconds
+
+
+
+
+
+
+
+
+
+
+ Packet modifications
+
+
+
+
+ Packet Differentiated Services Codepoint (DSCP)
+
+ u32:0-63
+ DSCP number
+
+
+
+
+
+
+
+
+ Packet marking
+
+ u32:1-2147483647
+ Packet marking
+
+
+
+
+
+
+
+
+ Routing table to forward packet with
+
+ u32:1-200
+ Table number
+
+
+ main
+ Main table
+
+
+
+ ^(main)$
+
+
+
+
+
+ TCP Maximum Segment Size
+
+ u32:500-1460
+ Explicitly set TCP MSS value
+
+
+
+
+
+
+
+
+
+
+ Source parameters
+
+
+ #include
+ #include
+
+
+ Source MAC address
+
+ <MAC address>
+ MAC address to match
+
+
+ !<MAC address>
+ Match everything except the specified MAC address
+
+
+
+ #include
+
+
+
+
+ Session state
+
+
+
+
+ Established state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ Invalid state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ New state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ Related state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+
+
+ TCP flags to match
+
+
+
+
+ TCP flags to match
+
+ txt
+ TCP flags to match
+
+
+
+ \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
+
+
+
+
+
+
+
+ Time to match rule
+
+
+
+
+ Monthdays to match rule on
+
+
+
+
+ Date to start matching rule
+
+
+
+
+ Time of day to start matching rule
+
+
+
+
+ Date to stop matching rule
+
+
+
+
+ Time of day to stop matching rule
+
+
+
+
+ Interpret times for startdate, stopdate, starttime and stoptime to be UTC
+
+
+
+
+
+ Weekdays to match rule on
+
+
+
+
+
+
+ ICMP type and code information
+
+
+
+
+ ICMP code (0-255)
+
+ u32:0-255
+ ICMP code (0-255)
+
+
+
+
+
+
+
+
+ ICMP type (0-255)
+
+ u32:0-255
+ ICMP type (0-255)
+
+
+
+
+
+
+ #include
+
+
+
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 @@
+
+
+
+ Rule action [REQUIRED]
+
+ drop
+
+
+ drop
+ Drop matching entries
+
+
+ ^(drop)$
+
+
+
+
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
#include
+ #include
Bonding transmit hash policy
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
#include
+ #include
Forwarding delay
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
#include
+ #include
IPv4 routing parameters
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
#include
+ #include
Duplex mode
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
#include
+ #include
GENEVE tunnel parameters
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 @@
#include
#include
+ #include
Encapsulation type (default: UDP)
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
#include
+ #include
Security/Encryption Settings
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 @@
#include
#include
+ #include
OpenVPN interface device-type (default: tun)
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
#include
+ #include
Default route insertion behaviour (default: auto)
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
#include
+ #include
Receive mode (default: private)
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
#include
+ #include
6rd network prefix
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
#include
+ #include
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
#include
+ #include
1450
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
#include
+ #include
1420
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 @@
#include
#include
+ #include
HT and VHT capabilities for your card
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
#include
+ #include
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 @@
+
+
+
+
+
+
+ IPv6 policy route rule set name
+ 201
+
+
+ #include
+ #include
+
+
+ Rule number (1-9999)
+
+
+
+
+ Destination parameters
+
+
+ #include
+ #include
+ #include
+
+
+
+
+ Source parameters
+
+
+ #include
+ #include
+ #include
+
+
+ #include
+
+
+
+
+
+
+ Policy route rule set name
+ 201
+
+
+ #include
+ #include
+
+
+ Rule number (1-9999)
+
+
+
+
+ Destination parameters
+
+
+ #include
+ #include
+ #include
+
+
+
+
+ Source parameters
+
+
+ #include
+ #include
+ #include
+
+
+ #include
+
+
+
+
+
+
+
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 .
+
+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 .
+
+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 .
+
+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 @@
+
+
+ Intra-zone filtering
+
+
+
+
+ Action for intra-zone traffic
+
+ accept drop
+
+
+ accept
+ Accept traffic (default)
+
+
+ drop
+ Drop silently
+
+
+ ^(accept|drop)$
+
+
+
+
+
+ Use the specified firewall chain
+
+
+
+
+ IPv6 firewall ruleset
+
+ firewall ipv6-name
+
+
+
+
+
+ IPv4 firewall ruleset
+
+ firewall name
+
+
+
+
+
+
+
Zone to be local-zone
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