diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/firewall.py | 70 | ||||
-rwxr-xr-x | src/migration-scripts/firewall/16-to-17 | 60 |
2 files changed, 114 insertions, 16 deletions
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index ec6b86ef2..352d5cbb1 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -128,7 +128,49 @@ def get_config(config=None): return firewall -def verify_rule(firewall, rule_conf, ipv6): +def verify_jump_target(firewall, root_chain, jump_target, ipv6, recursive=False): + targets_seen = [] + targets_pending = [jump_target] + + while targets_pending: + target = targets_pending.pop() + + if not ipv6: + if target not in dict_search_args(firewall, 'ipv4', 'name'): + raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') + target_rules = dict_search_args(firewall, 'ipv4', 'name', target, 'rule') + else: + if target not in dict_search_args(firewall, 'ipv6', 'name'): + raise ConfigError(f'Invalid jump-target. Firewall ipv6 name {target} does not exist on the system') + target_rules = dict_search_args(firewall, 'ipv6', 'name', target, 'rule') + + no_ipsec_in = root_chain in ('output', ) + + if target_rules: + for target_rule_conf in target_rules.values(): + # Output hook types will not tolerate 'meta ipsec exists' matches even in jump targets: + if no_ipsec_in and (dict_search_args(target_rule_conf, 'ipsec', 'match_ipsec_in') is not None \ + or dict_search_args(target_rule_conf, 'ipsec', 'match_none_in') is not None): + if not ipv6: + raise ConfigError(f'Invalid jump-target for {root_chain}. Firewall name {target} rules contain incompatible ipsec inbound matches') + else: + raise ConfigError(f'Invalid jump-target for {root_chain}. Firewall ipv6 name {target} rules contain incompatible ipsec inbound matches') + # Make sure we're not looping back on ourselves somewhere: + if recursive and 'jump_target' in target_rule_conf: + child_target = target_rule_conf['jump_target'] + if child_target in targets_seen: + if not ipv6: + raise ConfigError(f'Loop detected in jump-targets, firewall name {target} refers to previously traversed name {child_target}') + else: + raise ConfigError(f'Loop detected in jump-targets, firewall ipv6 name {target} refers to previously traversed ipv6 name {child_target}') + targets_pending.append(child_target) + if len(targets_seen) == 7: + path_txt = ' -> '.join(targets_seen) + Warning(f'Deep nesting of jump targets has reached 8 levels deep, following the path {path_txt} -> {child_target}!') + + targets_seen.append(target) + +def verify_rule(firewall, chain_name, rule_conf, ipv6): if 'action' not in rule_conf: raise ConfigError('Rule action must be defined') @@ -139,12 +181,10 @@ def verify_rule(firewall, rule_conf, ipv6): if 'jump' not in rule_conf['action']: raise ConfigError('jump-target defined, but action jump needed and it is not defined') target = rule_conf['jump_target'] - if not ipv6: - if target not in dict_search_args(firewall, 'ipv4', 'name'): - raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') + if chain_name != 'name': # This is a bit clumsy, but consolidates a chunk of code. + verify_jump_target(firewall, chain_name, target, ipv6, recursive=True) else: - if target not in dict_search_args(firewall, 'ipv6', 'name'): - raise ConfigError(f'Invalid jump-target. Firewall ipv6 name {target} does not exist on the system') + verify_jump_target(firewall, chain_name, target, ipv6, recursive=False) if rule_conf['action'] == 'offload': if 'offload_target' not in rule_conf: @@ -185,8 +225,10 @@ def verify_rule(firewall, rule_conf, ipv6): raise ConfigError('Limit rate integer cannot be less than 1') if 'ipsec' in rule_conf: - if {'match_ipsec', 'match_non_ipsec'} <= set(rule_conf['ipsec']): - raise ConfigError('Cannot specify both "match-ipsec" and "match-non-ipsec"') + if {'match_ipsec_in', 'match_none_in'} <= set(rule_conf['ipsec']): + raise ConfigError('Cannot specify both "match-ipsec" and "match-none"') + if {'match_ipsec_out', 'match_none_out'} <= set(rule_conf['ipsec']): + raise ConfigError('Cannot specify both "match-ipsec" and "match-none"') if 'recent' in rule_conf: if not {'count', 'time'} <= set(rule_conf['recent']): @@ -349,13 +391,11 @@ def verify(firewall): raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined') if name_conf['default_jump_target'] == name_id: raise ConfigError(f'Loop detected on default-jump-target.') - ## Now need to check that default-jump-target exists (other firewall chain/name) - if target not in dict_search_args(firewall['ipv4'], 'name'): - raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') + verify_jump_target(firewall, name, target, False, recursive=True) if 'rule' in name_conf: for rule_id, rule_conf in name_conf['rule'].items(): - verify_rule(firewall, rule_conf, False) + verify_rule(firewall, name, rule_conf, False) if 'ipv6' in firewall: for name in ['name','forward','input','output', 'prerouting']: @@ -369,13 +409,11 @@ def verify(firewall): raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined') if name_conf['default_jump_target'] == name_id: raise ConfigError(f'Loop detected on default-jump-target.') - ## Now need to check that default-jump-target exists (other firewall chain/name) - if target not in dict_search_args(firewall['ipv6'], 'name'): - raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') + verify_jump_target(firewall, name, target, True, recursive=True) if 'rule' in name_conf: for rule_id, rule_conf in name_conf['rule'].items(): - verify_rule(firewall, rule_conf, True) + verify_rule(firewall, name, rule_conf, True) #### ZONESSSS local_zone = False diff --git a/src/migration-scripts/firewall/16-to-17 b/src/migration-scripts/firewall/16-to-17 new file mode 100755 index 000000000..9ad7a30f8 --- /dev/null +++ b/src/migration-scripts/firewall/16-to-17 @@ -0,0 +1,60 @@ +# Copyright (C) 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 <http://www.gnu.org/licenses/>. + +# +# T4694: Adding rt ipsec exists/missing match to firewall configs. +# This involves a syntax change for IPsec matches, reflecting that different +# nftables expressions are required depending on whether we're matching a +# decrypted packet or a packet that will be encrypted - it's directional. +# The old rules only matched decrypted packets, those matches are now *-in: + # from: set firewall <family> <chainspec> rule <rule#> ipsec match-ipsec|match-none + # to: set firewall <family> <chainspec> rule <rule#> ipsec match-ipsec-in|match-none-in +# +# The <chainspec> positions this match allowed were: +# name (any custom chains), forward filter, input filter, prerouting raw. +# There are positions where it was possible to set, but it would never commit +# (nftables rejects 'meta ipsec' in output hooks), they are not considered here. +# + +import sys + +from vyos.configtree import ConfigTree + +firewall_base = ['firewall'] + +def migrate_chain(config: ConfigTree, path: list[str]) -> None: + for rule_num in config.list_nodes(path + ['rule']): + tmp_path = path + ['rule', rule_num, 'ipsec'] + if config.exists(tmp_path + ['match-ipsec']): + config.delete(tmp_path + ['match-ipsec']) + config.set(tmp_path + ['match-ipsec-in']) + elif config.exists(tmp_path + ['match-none']): + config.delete(tmp_path + ['match-none']) + config.set(tmp_path + ['match-none-in']) + +def migrate(config: ConfigTree) -> None: + if not config.exists(firewall_base): + # Nothing to do + return + + for family in ['ipv4', 'ipv6']: + tmp_path = firewall_base + [family, 'name'] + if config.exists(tmp_path): + for custom_fwname in config.list_nodes(tmp_path): + migrate_chain(config, tmp_path + [custom_fwname]) + + for base_hook in [['forward', 'filter'], ['input', 'filter'], ['prerouting', 'raw']]: + tmp_path = firewall_base + [family] + base_hook + if config.exists(tmp_path): + migrate_chain(config, tmp_path) |