diff options
author | Christian Breunig <christian@breunig.cc> | 2024-01-18 22:05:16 +0100 |
---|---|---|
committer | Christian Breunig <christian@breunig.cc> | 2024-01-18 22:09:30 +0100 |
commit | 80068c8ce453a385981999c25e4ff5aeaa6bf030 (patch) | |
tree | 2c3566becc1e93b70df5fb2f61b4f1028a04dd8f /src/conf_mode/system_conntrack.py | |
parent | 4b3ef473c3acfedeb70a023a9ca46df5437fc5a2 (diff) | |
download | vyos-1x-80068c8ce453a385981999c25e4ff5aeaa6bf030.tar.gz vyos-1x-80068c8ce453a385981999c25e4ff5aeaa6bf030.zip |
conntrack: T5376: T5779: backport from current
Backport of the conntrack system from current branch.
(cherry picked from commit fd0bcaf12)
(cherry picked from commit 5acf5aced)
(cherry picked from commit 42ff4d8a7)
(cherry picked from commit 24a1a7059)
Diffstat (limited to 'src/conf_mode/system_conntrack.py')
-rwxr-xr-x | src/conf_mode/system_conntrack.py | 212 |
1 files changed, 147 insertions, 65 deletions
diff --git a/src/conf_mode/system_conntrack.py b/src/conf_mode/system_conntrack.py index 9c43640a9..7f6c71440 100755 --- a/src/conf_mode/system_conntrack.py +++ b/src/conf_mode/system_conntrack.py @@ -20,11 +20,13 @@ import re from sys import exit from vyos.config import Config -from vyos.firewall import find_nftables_rule -from vyos.firewall import remove_nftables_rule +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 @@ -38,34 +40,44 @@ 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'], + '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'], + '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'] + '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'], + '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'], + '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'] + 'sqlnet': { + 'nftables': ['ct helper set "tns_tcp" tcp dport {1521,1525,1536} return'] }, - 'tftp' : { - 'ko' : ['nf_nat_tftp', 'nf_conntrack_tftp'], + 'tftp': { + 'ko': ['nf_nat_tftp', 'nf_conntrack_tftp'], + 'nftables': ['ct helper set "tftp_udp" udp dport {69} return'] }, } -def resync_conntrackd(): - tmp = run('/usr/libexec/vyos/conf_mode/conntrack_sync.py') - if tmp > 0: - print('ERROR: error restarting conntrackd!') +valid_groups = [ + 'address_group', + 'domain_group', + 'network_group', + 'port_group' +] def get_config(config=None): if config: @@ -78,71 +90,141 @@ def get_config(config=None): 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): - if dict_search('ignore.rule', conntrack) != None: - for rule, rule_config in conntrack['ignore']['rule'].items(): - if dict_search('destination.port', rule_config) or \ - dict_search('source.port', 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}') - + 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) - - # dry-run newly generated configuration - tmp = run(f'nft -c -f {nftables_ct_file}') - if tmp > 0: - if os.path.exists(nftables_ct_file): - os.unlink(nftables_ct_file) - raise ConfigError('Configuration file errors encountered!') - return None -def find_nftables_ct_rule(rule): - helper_search = re.search('ct helper set "(\w+)"', rule) - if helper_search: - rule = helper_search[1] - return find_nftables_rule('raw', 'VYOS_CT_HELPER', [rule]) - -def find_remove_rule(rule): - handle = find_nftables_ct_rule(rule) - if handle: - remove_nftables_rule('raw', 'VYOS_CT_HELPER', handle) - 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(f'modules.{module}', conntrack) is None: + if dict_search_args(conntrack, 'modules', module) is None: if 'ko' in module_config: - for mod in module_config['ko']: - # Only remove the module if it's loaded - if os.path.exists(f'/sys/module/{mod}'): - cmd(f'rmmod {mod}') - if 'nftables' in module_config: - for rule in module_config['nftables']: - find_remove_rule(rule) + 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: - for mod in module_config['ko']: - cmd(f'modprobe {mod}') - if 'nftables' in module_config: - for rule in module_config['nftables']: - if not find_nftables_ct_rule(rule): - cmd(f'nft insert rule ip raw VYOS_CT_HELPER {rule}') + 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 - cmd(f'nft -f {nftables_ct_file}') + install_result, output = rc_cmd(f'nft -f {nftables_ct_file}') + if install_result == 1: + raise ConfigError(f'Failed to apply configuration: {output}') - if process_named_running('conntrackd'): - # Reload conntrack-sync daemon to fetch new sysctl values - resync_conntrackd() + # 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 |