summaryrefslogtreecommitdiff
path: root/src/conf_mode/system_conntrack.py
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2023-12-30 23:25:20 +0100
committerChristian Breunig <christian@breunig.cc>2023-12-31 23:49:48 +0100
commit4ef110fd2c501b718344c72d495ad7e16d2bd465 (patch)
treee98bf08f93c029ec4431a3b6ca078e7562e0cc58 /src/conf_mode/system_conntrack.py
parent2286b8600da6c631b17e1d5b9b341843e50f9abf (diff)
downloadvyos-1x-4ef110fd2c501b718344c72d495ad7e16d2bd465.tar.gz
vyos-1x-4ef110fd2c501b718344c72d495ad7e16d2bd465.zip
T5474: establish common file name pattern for XML conf mode commands
We will use _ as CLI level divider. The XML definition filename and also the Python helper should match the CLI node. Example: set interfaces ethernet -> interfaces_ethernet.xml.in set interfaces bond -> interfaces_bond.xml.in set service dhcp-server -> service_dhcp-server-xml.in
Diffstat (limited to 'src/conf_mode/system_conntrack.py')
-rwxr-xr-xsrc/conf_mode/system_conntrack.py243
1 files changed, 243 insertions, 0 deletions
diff --git a/src/conf_mode/system_conntrack.py b/src/conf_mode/system_conntrack.py
new file mode 100755
index 000000000..7f6c71440
--- /dev/null
+++ b/src/conf_mode/system_conntrack.py
@@ -0,0 +1,243 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2023 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdep import set_dependents, call_dependents
+from vyos.utils.process import process_named_running
+from vyos.utils.dict import dict_search
+from vyos.utils.dict import dict_search_args
+from vyos.utils.dict import dict_search_recursive
+from vyos.utils.process import cmd
+from vyos.utils.process import rc_cmd
+from vyos.utils.process import run
+from vyos.template import render
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+conntrack_config = r'/etc/modprobe.d/vyatta_nf_conntrack.conf'
+sysctl_file = r'/run/sysctl/10-vyos-conntrack.conf'
+nftables_ct_file = r'/run/nftables-ct.conf'
+
+# Every ALG (Application Layer Gateway) consists of either a Kernel Object
+# also called a Kernel Module/Driver or some rules present in iptables
+module_map = {
+ 'ftp': {
+ 'ko': ['nf_nat_ftp', 'nf_conntrack_ftp'],
+ 'nftables': ['ct helper set "ftp_tcp" tcp dport {21} return']
+ },
+ 'h323': {
+ 'ko': ['nf_nat_h323', 'nf_conntrack_h323'],
+ 'nftables': ['ct helper set "ras_udp" udp dport {1719} return',
+ 'ct helper set "q931_tcp" tcp dport {1720} return']
+ },
+ 'nfs': {
+ 'nftables': ['ct helper set "rpc_tcp" tcp dport {111} return',
+ 'ct helper set "rpc_udp" udp dport {111} return']
+ },
+ 'pptp': {
+ 'ko': ['nf_nat_pptp', 'nf_conntrack_pptp'],
+ 'nftables': ['ct helper set "pptp_tcp" tcp dport {1723} return'],
+ 'ipv4': True
+ },
+ 'sip': {
+ 'ko': ['nf_nat_sip', 'nf_conntrack_sip'],
+ 'nftables': ['ct helper set "sip_tcp" tcp dport {5060,5061} return',
+ 'ct helper set "sip_udp" udp dport {5060,5061} return']
+ },
+ 'sqlnet': {
+ 'nftables': ['ct helper set "tns_tcp" tcp dport {1521,1525,1536} return']
+ },
+ 'tftp': {
+ 'ko': ['nf_nat_tftp', 'nf_conntrack_tftp'],
+ 'nftables': ['ct helper set "tftp_udp" udp dport {69} return']
+ },
+}
+
+valid_groups = [
+ 'address_group',
+ 'domain_group',
+ 'network_group',
+ 'port_group'
+]
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['system', 'conntrack']
+
+ conntrack = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
+
+ conntrack['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ conntrack['ipv4_nat_action'] = 'accept' if conf.exists(['nat']) else 'return'
+ conntrack['ipv6_nat_action'] = 'accept' if conf.exists(['nat66']) else 'return'
+ conntrack['wlb_action'] = 'accept' if conf.exists(['load-balancing', 'wan']) else 'return'
+ conntrack['wlb_local_action'] = conf.exists(['load-balancing', 'wan', 'enable-local-traffic'])
+
+ conntrack['module_map'] = module_map
+
+ if conf.exists(['service', 'conntrack-sync']):
+ set_dependents('conntrack_sync', conf)
+
+ return conntrack
+
+def verify(conntrack):
+ for inet in ['ipv4', 'ipv6']:
+ if dict_search_args(conntrack, 'ignore', inet, 'rule') != None:
+ for rule, rule_config in conntrack['ignore'][inet]['rule'].items():
+ if dict_search('destination.port', rule_config) or \
+ dict_search('destination.group.port_group', rule_config) or \
+ dict_search('source.port', rule_config) or \
+ dict_search('source.group.port_group', rule_config):
+ if 'protocol' not in rule_config or rule_config['protocol'] not in ['tcp', 'udp']:
+ raise ConfigError(f'Port requires tcp or udp as protocol in rule {rule}')
+
+ tcp_flags = dict_search_args(rule_config, 'tcp', 'flags')
+ if tcp_flags:
+ if dict_search_args(rule_config, 'protocol') != 'tcp':
+ raise ConfigError('Protocol must be tcp when specifying tcp flags')
+
+ not_flags = dict_search_args(rule_config, 'tcp', 'flags', 'not')
+ if not_flags:
+ duplicates = [flag for flag in tcp_flags if flag in not_flags]
+ if duplicates:
+ raise ConfigError(f'Cannot match a tcp flag as set and not set')
+
+ for side in ['destination', 'source']:
+ if side in rule_config:
+ side_conf = rule_config[side]
+
+ if 'group' in side_conf:
+ if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1:
+ raise ConfigError('Only one address-group, network-group or domain-group can be specified')
+
+ for group in valid_groups:
+ if group in side_conf['group']:
+ group_name = side_conf['group'][group]
+ error_group = group.replace("_", "-")
+
+ if group in ['address_group', 'network_group', 'domain_group']:
+ if 'address' in side_conf:
+ raise ConfigError(f'{error_group} and address cannot both be defined')
+
+ if group_name and group_name[0] == '!':
+ group_name = group_name[1:]
+
+ if inet == 'ipv6':
+ group = f'ipv6_{group}'
+
+ group_obj = dict_search_args(conntrack['firewall'], 'group', group, group_name)
+
+ if group_obj is None:
+ raise ConfigError(f'Invalid {error_group} "{group_name}" on ignore rule')
+
+ if not group_obj:
+ Warning(f'{error_group} "{group_name}" has no members!')
+
+ if dict_search_args(conntrack, 'timeout', 'custom', inet, 'rule') != None:
+ for rule, rule_config in conntrack['timeout']['custom'][inet]['rule'].items():
+ if 'protocol' not in rule_config:
+ raise ConfigError(f'Conntrack custom timeout rule {rule} requires protocol tcp or udp')
+ else:
+ if 'tcp' in rule_config['protocol'] and 'udp' in rule_config['protocol']:
+ raise ConfigError(f'conntrack custom timeout rule {rule} - Cant use both tcp and udp protocol')
+ return None
+
+def generate(conntrack):
+ if not os.path.exists(nftables_ct_file):
+ conntrack['first_install'] = True
+
+ # Determine if conntrack is needed
+ conntrack['ipv4_firewall_action'] = 'return'
+ conntrack['ipv6_firewall_action'] = 'return'
+
+ for rules, path in dict_search_recursive(conntrack['firewall'], 'rule'):
+ if any(('state' in rule_conf or 'connection_status' in rule_conf or 'offload_target' in rule_conf) for rule_conf in rules.values()):
+ if path[0] == 'ipv4':
+ conntrack['ipv4_firewall_action'] = 'accept'
+ elif path[0] == 'ipv6':
+ conntrack['ipv6_firewall_action'] = 'accept'
+
+ render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.j2', conntrack)
+ render(sysctl_file, 'conntrack/sysctl.conf.j2', conntrack)
+ render(nftables_ct_file, 'conntrack/nftables-ct.j2', conntrack)
+ return None
+
+def apply(conntrack):
+ # Depending on the enable/disable state of the ALG (Application Layer Gateway)
+ # modules we need to either insmod or rmmod the helpers.
+
+ add_modules = []
+ rm_modules = []
+
+ for module, module_config in module_map.items():
+ if dict_search_args(conntrack, 'modules', module) is None:
+ if 'ko' in module_config:
+ unloaded = [mod for mod in module_config['ko'] if os.path.exists(f'/sys/module/{mod}')]
+ rm_modules.extend(unloaded)
+ else:
+ if 'ko' in module_config:
+ add_modules.extend(module_config['ko'])
+
+ # Add modules before nftables uses them
+ if add_modules:
+ module_str = ' '.join(add_modules)
+ cmd(f'modprobe -a {module_str}')
+
+ # Load new nftables ruleset
+ install_result, output = rc_cmd(f'nft -f {nftables_ct_file}')
+ if install_result == 1:
+ raise ConfigError(f'Failed to apply configuration: {output}')
+
+ # Remove modules after nftables stops using them
+ if rm_modules:
+ module_str = ' '.join(rm_modules)
+ cmd(f'rmmod {module_str}')
+
+ try:
+ call_dependents()
+ except ConfigError:
+ # Ignore config errors on dependent due to being called too early. Example:
+ # ConfigError("ConfigError('Interface ethN requires an IP address!')")
+ pass
+
+ # We silently ignore all errors
+ # See: https://bugzilla.redhat.com/show_bug.cgi?id=1264080
+ cmd(f'sysctl -f {sysctl_file}')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)