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 --- smoketest/scripts/cli/base_interfaces_test.py | 12 +- smoketest/scripts/cli/test_firewall.py | 155 +++++++++++++++++++++ smoketest/scripts/cli/test_protocols_nhrp.py | 9 ++ smoketest/scripts/cli/test_system_conntrack.py | 24 ++-- .../scripts/cli/test_system_flow-accounting.py | 15 +- 5 files changed, 194 insertions(+), 21 deletions(-) create mode 100755 smoketest/scripts/cli/test_firewall.py (limited to 'smoketest/scripts') diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 340ec4edd..447c1db30 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -572,11 +572,11 @@ class BasicInterfaceTest: self.cli_commit() for interface in self._interfaces: - base_options = f'-A FORWARD -o {interface} -p tcp -m tcp --tcp-flags SYN,RST SYN' - out = cmd('sudo iptables-save -t mangle') + base_options = f'oifname "{interface}"' + out = cmd('sudo nft list chain raw VYOS_TCP_MSS') for line in out.splitlines(): if line.startswith(base_options): - self.assertIn(f'--set-mss {mss}', line) + self.assertIn(f'tcp option maxseg size set {mss}', line) tmp = read_file(f'/proc/sys/net/ipv4/neigh/{interface}/base_reachable_time_ms') self.assertEqual(tmp, str((int(arp_tmo) * 1000))) # tmo value is in milli seconds @@ -627,11 +627,11 @@ class BasicInterfaceTest: self.cli_commit() for interface in self._interfaces: - base_options = f'-A FORWARD -o {interface} -p tcp -m tcp --tcp-flags SYN,RST SYN' - out = cmd('sudo ip6tables-save -t mangle') + base_options = f'oifname "{interface}"' + out = cmd('sudo nft list chain ip6 raw VYOS_TCP_MSS') for line in out.splitlines(): if line.startswith(base_options): - self.assertIn(f'--set-mss {mss}', line) + self.assertIn(f'tcp option maxseg size set {mss}', line) proc_base = f'/proc/sys/net/ipv6/conf/{interface}' diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py new file mode 100755 index 000000000..1520020fd --- /dev/null +++ b/smoketest/scripts/cli/test_firewall.py @@ -0,0 +1,155 @@ +#!/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 glob import glob + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.util import cmd + +sysfs_config = { + 'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'default': '0', 'test_value': 'disable'}, + 'broadcast_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts', 'default': '1', 'test_value': 'enable'}, + 'ip_src_route': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_source_route', 'default': '0', 'test_value': 'enable'}, + 'ipv6_receive_redirects': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_redirects', 'default': '0', 'test_value': 'enable'}, + 'ipv6_src_route': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_source_route', 'default': '-1', 'test_value': 'enable'}, + 'log_martians': {'sysfs': '/proc/sys/net/ipv4/conf/all/log_martians', 'default': '1', 'test_value': 'disable'}, + 'receive_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_redirects', 'default': '0', 'test_value': 'enable'}, + 'send_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/send_redirects', 'default': '1', 'test_value': 'disable'}, + 'syn_cookies': {'sysfs': '/proc/sys/net/ipv4/tcp_syncookies', 'default': '1', 'test_value': 'disable'}, + 'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337', 'default': '0', 'test_value': 'enable'} +} + +class TestFirewall(VyOSUnitTestSHIM.TestCase): + def setUp(self): + self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '172.16.10.1/24']) + + def tearDown(self): + self.cli_delete(['interfaces', 'ethernet', 'eth0']) + self.cli_commit() + self.cli_delete(['firewall']) + self.cli_commit() + + def test_groups(self): + self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24']) + self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '53']) + self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '123']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'destination', 'group', 'port-group', 'smoketest_port']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'protocol', 'tcp']) + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest']) + + self.cli_commit() + + nftables_search = [ + ['iifname "eth0"', 'jump smoketest'], + ['ip saddr { 172.16.99.0/24 }', 'ip daddr 172.16.10.10', 'tcp dport { 53, 123 }', 'return'], + ] + + nftables_output = cmd('sudo nft list table ip filter') + + 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_basic_rules(self): + self.cli_set(['firewall', 'name', 'smoketest', 'default-action', 'drop']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'address', '172.16.20.10']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'action', 'reject']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'protocol', 'tcp_udp']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'destination', 'port', '8888']) + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest']) + + self.cli_commit() + + nftables_search = [ + ['iifname "eth0"', 'jump smoketest'], + ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'return'], + ['meta l4proto { tcp, udp }', 'th dport { 8888 }', 'reject'], + ['smoketest default-action', 'drop'] + ] + + nftables_output = cmd('sudo nft list table ip filter') + + 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_basic_rules_ipv6(self): + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'default-action', 'drop']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '1', 'action', 'accept']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '1', 'source', 'address', '2002::1']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '1', 'destination', 'address', '2002::1:1']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '2', 'action', 'reject']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '2', 'protocol', 'tcp_udp']) + self.cli_set(['firewall', 'ipv6-name', 'v6-smoketest', 'rule', '2', 'destination', 'port', '8888']) + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'ipv6-name', 'v6-smoketest']) + + self.cli_commit() + + nftables_search = [ + ['iifname "eth0"', 'jump v6-smoketest'], + ['saddr 2002::1', 'daddr 2002::1:1', 'return'], + ['meta l4proto { tcp, udp }', 'th dport { 8888 }', 'reject'], + ['smoketest default-action', 'drop'] + ] + + nftables_output = cmd('sudo nft list table ip6 filter') + + 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_sysfs(self): + for name, conf in sysfs_config.items(): + paths = glob(conf['sysfs']) + for path in paths: + with open(path, 'r') as f: + self.assertEqual(f.read().strip(), conf['default'], msg=path) + + self.cli_set(['firewall', name.replace("_", "-"), conf['test_value']]) + + self.cli_commit() + + for name, conf in sysfs_config.items(): + paths = glob(conf['sysfs']) + for path in paths: + with open(path, 'r') as f: + self.assertNotEqual(f.read().strip(), conf['default'], msg=path) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_nhrp.py b/smoketest/scripts/cli/test_protocols_nhrp.py index aa0ac268d..40b19fec7 100755 --- a/smoketest/scripts/cli/test_protocols_nhrp.py +++ b/smoketest/scripts/cli/test_protocols_nhrp.py @@ -18,6 +18,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.firewall import find_nftables_rule from vyos.util import call, process_named_running, read_file tunnel_path = ['interfaces', 'tunnel'] @@ -91,6 +92,14 @@ class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase): for line in opennhrp_lines: self.assertIn(line, tmp_opennhrp_conf) + firewall_matches = [ + 'ip protocol gre', + 'ip saddr 192.0.2.1', + 'ip daddr 224.0.0.0/4', + 'comment "VYOS_NHRP_tun100"' + ] + + self.assertTrue(find_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', firewall_matches) is not None) self.assertTrue(process_named_running('opennhrp')) if __name__ == '__main__': diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py index b2934cf04..95c2a6c55 100755 --- a/smoketest/scripts/cli/test_system_conntrack.py +++ b/smoketest/scripts/cli/test_system_conntrack.py @@ -15,10 +15,12 @@ # along with this program. If not, see . import os +import re import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.firewall import find_nftables_rule from vyos.util import cmd from vyos.util import read_file @@ -156,8 +158,8 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): 'driver' : ['nf_nat_h323', 'nf_conntrack_h323'], }, 'nfs' : { - 'iptables' : ['-A VYATTA_CT_HELPER -p udp -m udp --dport 111 -j CT --helper rpc', - '-A VYATTA_CT_HELPER -p tcp -m tcp --dport 111 -j CT --helper rpc'], + 'nftables' : ['ct helper set "rpc_tcp"', + 'ct helper set "rpc_udp"'] }, 'pptp' : { 'driver' : ['nf_nat_pptp', 'nf_conntrack_pptp'], @@ -166,9 +168,7 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): 'driver' : ['nf_nat_sip', 'nf_conntrack_sip'], }, 'sqlnet' : { - 'iptables' : ['-A VYATTA_CT_HELPER -p tcp -m tcp --dport 1536 -j CT --helper tns', - '-A VYATTA_CT_HELPER -p tcp -m tcp --dport 1525 -j CT --helper tns', - '-A VYATTA_CT_HELPER -p tcp -m tcp --dport 1521 -j CT --helper tns'], + 'nftables' : ['ct helper set "tns_tcp"'] }, 'tftp' : { 'driver' : ['nf_nat_tftp', 'nf_conntrack_tftp'], @@ -187,10 +187,9 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): if 'driver' in module_options: for driver in module_options['driver']: self.assertTrue(os.path.isdir(f'/sys/module/{driver}')) - if 'iptables' in module_options: - rules = cmd('sudo iptables-save -t raw') - for ruleset in module_options['iptables']: - self.assertIn(ruleset, rules) + if 'nftables' in module_options: + for rule in module_options['nftables']: + self.assertTrue(find_nftables_rule('raw', 'VYOS_CT_HELPER', [rule]) != None) # unload modules for module in modules: @@ -204,10 +203,9 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): if 'driver' in module_options: for driver in module_options['driver']: self.assertFalse(os.path.isdir(f'/sys/module/{driver}')) - if 'iptables' in module_options: - rules = cmd('sudo iptables-save -t raw') - for ruleset in module_options['iptables']: - self.assertNotIn(ruleset, rules) + if 'nftables' in module_options: + for rule in module_options['nftables']: + self.assertTrue(find_nftables_rule('raw', 'VYOS_CT_HELPER', [rule]) == None) def test_conntrack_hash_size(self): hash_size = '65536' diff --git a/smoketest/scripts/cli/test_system_flow-accounting.py b/smoketest/scripts/cli/test_system_flow-accounting.py index a2b5b1481..dfbfba94e 100755 --- a/smoketest/scripts/cli/test_system_flow-accounting.py +++ b/smoketest/scripts/cli/test_system_flow-accounting.py @@ -59,9 +59,20 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): self.cli_commit() # verify configuration - tmp = cmd('sudo iptables-save -t raw') + nftables_output = cmd('sudo nft list chain raw VYOS_CT_PREROUTING_HOOK').splitlines() for interface in Section.interfaces('ethernet'): - self.assertIn(f'-A VYATTA_CT_PREROUTING_HOOK -i {interface} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size 128 --nflog-threshold 100', tmp) + rule_found = False + ifname_search = f'iifname "{interface}"' + + for nftables_line in nftables_output: + if 'FLOW_ACCOUNTING_RULE' in nftables_line and ifname_search in nftables_line: + self.assertIn('group 2', nftables_line) + self.assertIn('snaplen 128', nftables_line) + self.assertIn('queue-threshold 100', nftables_line) + rule_found = True + break + + self.assertTrue(rule_found) uacctd = read_file(uacctd_conf) # circular queue size - buffer_size -- cgit v1.2.3 From c7cf7b941445c9280ac1ed7bbbd68159654560a6 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Sat, 31 Jul 2021 19:23:44 +0200 Subject: zone-policy: T2199: Migrate zone-policy to XML/Python --- data/templates/zone_policy/nftables.tmpl | 97 ++++++++++++++++ interface-definitions/zone-policy.xml.in | 94 ++++++++++++++++ smoketest/scripts/cli/test_zone_policy.py | 63 +++++++++++ src/conf_mode/zone_policy.py | 176 ++++++++++++++++++++++++++++++ 4 files changed, 430 insertions(+) create mode 100644 data/templates/zone_policy/nftables.tmpl create mode 100644 interface-definitions/zone-policy.xml.in create mode 100755 smoketest/scripts/cli/test_zone_policy.py create mode 100755 src/conf_mode/zone_policy.py (limited to 'smoketest/scripts') diff --git a/data/templates/zone_policy/nftables.tmpl b/data/templates/zone_policy/nftables.tmpl new file mode 100644 index 000000000..4575a721c --- /dev/null +++ b/data/templates/zone_policy/nftables.tmpl @@ -0,0 +1,97 @@ +#!/usr/sbin/nft -f + +{% if cleanup_commands is defined %} +{% for command in cleanup_commands %} +{{ command }} +{% endfor %} +{% endif %} + +{% if zone is defined %} +table ip filter { +{% for zone_name, zone_conf in zone.items() if zone_conf.ipv4 %} +{% if zone_conf.local_zone is defined %} + chain VZONE_{{ zone_name }}_IN { + iifname lo counter return +{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.name is defined %} + iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }} + iifname { {{ zone[from_zone].interface | join(",") }} } counter return +{% endfor %} + counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }} + } + chain VZONE_{{ zone_name }}_OUT { + oifname lo counter return +{% for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall.name is defined %} + oifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }} + oifname { {{ zone[from_zone].interface | join(",") }} } counter return +{% endfor %} + counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }} + } +{% else %} + chain VZONE_{{ zone_name }} { + iifname { {{ zone_conf.interface | join(",") }} } counter return +{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.name is defined %} +{% if zone[from_zone].local_zone is not defined %} + iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }} + iifname { {{ zone[from_zone].interface | join(",") }} } counter return +{% endif %} +{% endfor %} + counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }} + } +{% endif %} +{% endfor %} +} + +table ip6 filter { +{% for zone_name, zone_conf in zone.items() if zone_conf.ipv6 %} +{% if zone_conf.local_zone is defined %} + chain VZONE6_{{ zone_name }}_IN { + iifname lo counter return +{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.ipv6_name is defined %} + iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }} + iifname { {{ zone[from_zone].interface | join(",") }} } counter return +{% endfor %} + counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }} + } + chain VZONE6_{{ zone_name }}_OUT { + oifname lo counter return +{% for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall.ipv6_name is defined %} + oifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }} + oifname { {{ zone[from_zone].interface | join(",") }} } counter return +{% endfor %} + counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }} + } +{% else %} + chain VZONE6_{{ zone_name }} { + iifname { {{ zone_conf.interface | join(",") }} } counter return +{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.ipv6_name is defined %} +{% if zone[from_zone].local_zone is not defined %} + iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }} + iifname { {{ zone[from_zone].interface | join(",") }} } counter return +{% endif %} +{% endfor %} + counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }} + } +{% endif %} +{% endfor %} +} + +{% for zone_name, zone_conf in zone.items() %} +{% if zone_conf.ipv4 %} +{% if 'local_zone' in zone_conf %} +insert rule ip filter VYOS_FW_LOCAL counter jump VZONE_{{ zone_name }}_IN +insert rule ip filter VYOS_FW_OUTPUT counter jump VZONE_{{ zone_name }}_OUT +{% else %} +insert rule ip filter VYOS_FW_OUT oifname { {{ zone_conf.interface | join(',') }} } counter jump VZONE_{{ zone_name }} +{% endif %} +{% endif %} +{% if zone_conf.ipv6 %} +{% if 'local_zone' in zone_conf %} +insert rule ip6 filter VYOS_FW6_LOCAL counter jump VZONE6_{{ zone_name }}_IN +insert rule ip6 filter VYOS_FW6_OUTPUT counter jump VZONE6_{{ zone_name }}_OUT +{% else %} +insert rule ip6 filter VYOS_FW6_OUT oifname { {{ zone_conf.interface | join(',') }} } counter jump VZONE6_{{ zone_name }} +{% endif %} +{% endif %} +{% endfor %} + +{% endif %} diff --git a/interface-definitions/zone-policy.xml.in b/interface-definitions/zone-policy.xml.in new file mode 100644 index 000000000..52fd73f15 --- /dev/null +++ b/interface-definitions/zone-policy.xml.in @@ -0,0 +1,94 @@ + + + + + Configure zone-policy + 250 + + + + + Zone name + + txt + Zone name + + + + #include + + + Default-action for traffic coming into this zone + + drop reject + + + drop + Drop silently (default) + + + reject + Drop and notify source + + + ^(drop|reject)$ + + + + + + Zone from which to filter traffic + + zone-policy zone + + + + + + Firewall options + + + + + IPv6 firewall ruleset + + firewall ipv6-name + + + + + + IPv4 firewall ruleset + + firewall name + + + + + + + + + + Interface associated with zone + + txt + Interface associated with zone + + + + + + + + + + Zone to be local-zone + + + + + + + + diff --git a/smoketest/scripts/cli/test_zone_policy.py b/smoketest/scripts/cli/test_zone_policy.py new file mode 100755 index 000000000..c0af6164b --- /dev/null +++ b/smoketest/scripts/cli/test_zone_policy.py @@ -0,0 +1,63 @@ +#!/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 + +class TestZonePolicy(VyOSUnitTestSHIM.TestCase): + def setUp(self): + self.cli_set(['firewall', 'name', 'smoketest', 'default-action', 'drop']) + + def tearDown(self): + self.cli_delete(['zone-policy']) + self.cli_delete(['firewall']) + self.cli_commit() + + def test_basic_zone(self): + self.cli_set(['zone-policy', 'zone', 'smoketest-eth0', 'interface', 'eth0']) + self.cli_set(['zone-policy', 'zone', 'smoketest-eth0', 'from', 'smoketest-local', 'firewall', 'name', 'smoketest']) + self.cli_set(['zone-policy', 'zone', 'smoketest-local', 'local-zone']) + self.cli_set(['zone-policy', 'zone', 'smoketest-local', 'from', 'smoketest-eth0', 'firewall', 'name', 'smoketest']) + + self.cli_commit() + + nftables_search = [ + ['chain VZONE_smoketest-eth0'], + ['chain VZONE_smoketest-local_IN'], + ['chain VZONE_smoketest-local_OUT'], + ['oifname { "eth0" }', 'jump VZONE_smoketest-eth0'], + ['jump VZONE_smoketest-local_IN'], + ['jump VZONE_smoketest-local_OUT'], + ['iifname { "eth0" }', 'jump smoketest'], + ['oifname { "eth0" }', 'jump smoketest'] + ] + + nftables_output = cmd('sudo nft list table ip filter') + + 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) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py new file mode 100755 index 000000000..92f5624c2 --- /dev/null +++ b/src/conf_mode/zone_policy.py @@ -0,0 +1,176 @@ +#!/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() + +nftables_conf = '/run/nftables_zone.conf' + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['zone-policy'] + zone_policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + if zone_policy: + zone_policy['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + return zone_policy + +def verify(zone_policy): + # bail out early - looks like removal from running config + if not zone_policy: + return None + + local_zone = False + interfaces = [] + + if 'zone' in zone_policy: + for zone, zone_conf in zone_policy['zone'].items(): + if 'local_zone' not in zone_conf and 'interface' not in zone_conf: + raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone') + + if 'local_zone' in zone_conf: + if local_zone: + raise ConfigError('There cannot be multiple local zones') + if 'interface' in zone_conf: + raise ConfigError('Local zone cannot have interfaces assigned') + local_zone = True + + if 'interface' in zone_conf: + found_duplicates = [intf for intf in zone_conf['interface'] if intf in interfaces] + + if found_duplicates: + raise ConfigError(f'Interfaces cannot be assigned to multiple zones') + + interfaces += zone_conf['interface'] + + if 'from' in zone_conf: + for from_zone, from_conf in zone_conf['from'].items(): + v4_name = dict_search_args(from_conf, 'firewall', 'name') + if v4_name: + if 'name' not in zone_policy['firewall']: + raise ConfigError(f'Firewall name "{v4_name}" does not exist') + + if 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(from_conf, 'firewall', 'v6_name') + if v6_name: + if 'ipv6_name' not in zone_policy['firewall']: + raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') + + if not dict_search_args(zone_policy, 'firewall', 'ipv6_name', v6_name): + raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') + + return None + +def has_ipv4_fw(zone_conf): + if 'from' not in zone_conf: + return False + zone_from = zone_conf['from'] + return any([True for fz in zone_from if dict_search_args(zone_from, fz, 'firewall', 'name')]) + +def has_ipv6_fw(zone_conf): + if 'from' not in zone_conf: + return False + zone_from = zone_conf['from'] + return any([True for fz in zone_from if dict_search_args(zone_from, fz, 'firewall', 'ipv6_name')]) + +def get_local_from(zone_policy, local_zone_name): + # Get all zone firewall names from the local zone + out = {} + for zone, zone_conf in zone_policy['zone'].items(): + if zone == local_zone_name: + continue + if 'from' not in zone_conf: + continue + if local_zone_name in zone_conf['from']: + out[zone] = zone_conf['from'][local_zone_name] + return out + +def cleanup_commands(): + commands = [] + for table in ['ip filter', 'ip6 filter']: + json_str = cmd(f'nft -j list table {table}') + obj = loads(json_str) + if 'nftables' not in obj: + continue + for item in obj['nftables']: + if 'rule' in item: + chain = item['rule']['chain'] + handle = item['rule']['handle'] + if 'expr' not in item['rule']: + continue + for expr in item['rule']['expr']: + target = dict_search_args(expr, 'jump', 'target') + if target and target.startswith("VZONE"): + commands.append(f'delete rule {table} {chain} handle {handle}') + for item in obj['nftables']: + if 'chain' in item: + if item['chain']['name'].startswith("VZONE"): + chain = item['chain']['name'] + commands.append(f'delete chain {table} {chain}') + return commands + +def generate(zone_policy): + data = zone_policy or {} + + if os.path.exists(nftables_conf): # Check to see if we've run before + data['cleanup_commands'] = cleanup_commands() + + if 'zone' in data: + for zone, zone_conf in data['zone'].items(): + zone_conf['ipv4'] = has_ipv4_fw(zone_conf) + zone_conf['ipv6'] = has_ipv6_fw(zone_conf) + + if 'local_zone' in zone_conf: + zone_conf['from_local'] = get_local_from(data, zone) + + render(nftables_conf, 'zone_policy/nftables.tmpl', data) + return None + +def apply(zone_policy): + install_result = run(f'nft -f {nftables_conf}') + if install_result == 1: + raise ConfigError('Failed to apply zone-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 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 'smoketest/scripts') 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