#!/usr/bin/env python3 # # Copyright (C) 2021-2024 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 json import os from sys import exit from vyos.base import Warning from vyos.config import Config from vyos.configdep import set_dependents, call_dependents 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.file import write_file from vyos.utils.process import cmd, call from vyos.utils.process import rc_cmd 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' vyos_conntrack_logger_config = r'/run/vyos-conntrack-logger.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': ['tcp dport {21} ct helper set "ftp_tcp" return'] }, 'h323': { 'ko': ['nf_nat_h323', 'nf_conntrack_h323'], 'nftables': ['udp dport {1719} ct helper set "ras_udp" return', 'tcp dport {1720} ct helper set "q931_tcp" return'] }, 'nfs': { 'nftables': ['tcp dport {111} ct helper set "rpc_tcp" return', 'udp dport {111} ct helper set "rpc_udp" return'] }, 'pptp': { 'ko': ['nf_nat_pptp', 'nf_conntrack_pptp'], 'nftables': ['tcp dport {1723} ct helper set "pptp_tcp" return'], 'ipv4': True }, 'rtsp': { 'ko': ['nf_nat_rtsp', 'nf_conntrack_rtsp'], 'nftables': ['tcp dport {554} ct helper set "rtsp_tcp" return'], 'ipv4': True }, 'sip': { 'ko': ['nf_nat_sip', 'nf_conntrack_sip'], 'nftables': ['tcp dport {5060,5061} ct helper set "sip_tcp" return', 'udp dport {5060,5061} ct helper set "sip_udp" return'] }, 'sqlnet': { 'nftables': ['tcp dport {1521,1525,1536} ct helper set "tns_tcp" return'] }, 'tftp': { 'ko': ['nf_nat_tftp', 'nf_conntrack_tftp'], 'nftables': ['udp dport {69} ct helper set "tftp_udp" 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) # If conntrack status changes, VRF zone rules need updating if conf.exists(['vrf']): set_dependents('vrf', 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!') Warning(f'It is prefered to define {inet} conntrack ignore rules in section') 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 if 'log' not in conntrack: # Remove old conntrack-logger config and return if os.path.exists(vyos_conntrack_logger_config): os.unlink(vyos_conntrack_logger_config) # Determine if conntrack is needed conntrack['ipv4_firewall_action'] = 'return' conntrack['ipv6_firewall_action'] = 'return' if dict_search_args(conntrack['firewall'], 'global_options', 'state_policy') != None: conntrack['ipv4_firewall_action'] = 'accept' conntrack['ipv6_firewall_action'] = 'accept' else: 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) if 'log' in conntrack: log_conf_json = json.dumps(conntrack['log'], indent=4) write_file(vyos_conntrack_logger_config, log_conf_json) 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 --file {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}') if 'log' in conntrack: call(f'systemctl restart vyos-conntrack-logger.service') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1)