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
---
src/conf_mode/policy-route-interface.py | 120 +++++++++++++++++++++++++
src/conf_mode/policy-route.py | 154 ++++++++++++++++++++++++++++++++
2 files changed, 274 insertions(+)
create mode 100755 src/conf_mode/policy-route-interface.py
create mode 100755 src/conf_mode/policy-route.py
(limited to 'src/conf_mode')
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