diff options
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-x | src/conf_mode/firewall.py | 163 | ||||
-rwxr-xr-x | src/conf_mode/interfaces_l2tpv3.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/interfaces_wireguard.py | 6 | ||||
-rwxr-xr-x | src/conf_mode/interfaces_wireless.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/nat.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/nat64.py | 18 | ||||
-rwxr-xr-x | src/conf_mode/nat66.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/policy_route.py | 29 | ||||
-rwxr-xr-x | src/conf_mode/protocols_static_multicast.py | 31 | ||||
-rwxr-xr-x | src/conf_mode/system_acceleration.py | 3 | ||||
-rwxr-xr-x | src/conf_mode/system_console.py | 14 | ||||
-rwxr-xr-x | src/conf_mode/vpn_ipsec.py | 129 | ||||
-rwxr-xr-x | src/conf_mode/vrf.py | 17 |
13 files changed, 308 insertions, 114 deletions
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 352d5cbb1..02bf00bcc 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -36,6 +36,7 @@ from vyos.utils.process import cmd from vyos.utils.process import rc_cmd from vyos import ConfigError from vyos import airbag +from subprocess import run as subp_run airbag.enable() @@ -47,7 +48,12 @@ valid_groups = [ 'domain_group', 'network_group', 'port_group', - 'interface_group' + 'interface_group', + ## Added for group ussage in bridge firewall + 'ipv4_address_group', + 'ipv6_address_group', + 'ipv4_network_group', + 'ipv6_network_group' ] nested_group_types = [ @@ -128,41 +134,32 @@ def get_config(config=None): return firewall -def verify_jump_target(firewall, root_chain, jump_target, ipv6, recursive=False): +def verify_jump_target(firewall, hook, jump_target, family, 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') + if 'name' not in firewall[family]: + raise ConfigError(f'Invalid jump-target. Firewall {family} name {target} does not exist on the system') + elif target not in dict_search_args(firewall, family, 'name'): + raise ConfigError(f'Invalid jump-target. Firewall {family} name {target} does not exist on the system') - no_ipsec_in = root_chain in ('output', ) + target_rules = dict_search_args(firewall, family, 'name', target, 'rule') + no_ipsec_in = hook 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') + raise ConfigError(f'Invalid jump-target for {hook}. Firewall {family} 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}') + raise ConfigError(f'Loop detected in jump-targets, firewall {family} name {target} refers to previously traversed {family} name {child_target}') targets_pending.append(child_target) if len(targets_seen) == 7: path_txt = ' -> '.join(targets_seen) @@ -170,7 +167,7 @@ def verify_jump_target(firewall, root_chain, jump_target, ipv6, recursive=False) targets_seen.append(target) -def verify_rule(firewall, chain_name, rule_conf, ipv6): +def verify_rule(firewall, family, hook, priority, rule_id, rule_conf): if 'action' not in rule_conf: raise ConfigError('Rule action must be defined') @@ -181,10 +178,10 @@ def verify_rule(firewall, chain_name, 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 chain_name != 'name': # This is a bit clumsy, but consolidates a chunk of code. - verify_jump_target(firewall, chain_name, target, ipv6, recursive=True) + if hook != 'name': # This is a bit clumsy, but consolidates a chunk of code. + verify_jump_target(firewall, hook, target, family, recursive=True) else: - verify_jump_target(firewall, chain_name, target, ipv6, recursive=False) + verify_jump_target(firewall, hook, target, family, recursive=False) if rule_conf['action'] == 'offload': if 'offload_target' not in rule_conf: @@ -246,9 +243,9 @@ def verify_rule(firewall, chain_name, rule_conf, ipv6): raise ConfigError(f'Cannot match a tcp flag as set and not set') if 'protocol' in rule_conf: - if rule_conf['protocol'] == 'icmp' and ipv6: + if rule_conf['protocol'] == 'icmp' and family == 'ipv6': raise ConfigError(f'Cannot match IPv4 ICMP protocol on IPv6, use ipv6-icmp') - if rule_conf['protocol'] == 'ipv6-icmp' and not ipv6: + if rule_conf['protocol'] == 'ipv6-icmp' and family == 'ipv4': raise ConfigError(f'Cannot match IPv6 ICMP protocol on IPv4, use icmp') for side in ['destination', 'source']: @@ -266,7 +263,18 @@ def verify_rule(firewall, chain_name, rule_conf, ipv6): if group in side_conf['group']: group_name = side_conf['group'][group] - fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group + if family == 'ipv6' and group in ['address_group', 'network_group']: + fw_group = f'ipv6_{group}' + elif family == 'bridge': + if group =='ipv4_address_group': + fw_group = 'address_group' + elif group == 'ipv4_network_group': + fw_group = 'network_group' + else: + fw_group = group + else: + fw_group = group + error_group = fw_group.replace("_", "-") if group in ['address_group', 'network_group', 'domain_group']: @@ -302,7 +310,7 @@ def verify_rule(firewall, chain_name, rule_conf, ipv6): raise ConfigError(f'Dynamic address group must be defined.') else: target = rule_conf['add_address_to_group'][type]['address_group'] - fwall_group = 'ipv6_address_group' if ipv6 else 'address_group' + fwall_group = 'ipv6_address_group' if family == 'ipv6' else 'address_group' group_obj = dict_search_args(firewall, 'group', 'dynamic_group', fwall_group, target) if group_obj is None: raise ConfigError(f'Invalid dynamic address group on firewall rule') @@ -379,43 +387,25 @@ def verify(firewall): for group_name, group in groups.items(): verify_nested_group(group_name, group, groups, []) - if 'ipv4' in firewall: - for name in ['name','forward','input','output', 'prerouting']: - if name in firewall['ipv4']: - for name_id, name_conf in firewall['ipv4'][name].items(): - if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf: - raise ConfigError('default-action set to jump, but no default-jump-target specified') - if 'default_jump_target' in name_conf: - target = name_conf['default_jump_target'] - if 'jump' not in name_conf['default_action']: - 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.') - 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, name, rule_conf, False) - - if 'ipv6' in firewall: - for name in ['name','forward','input','output', 'prerouting']: - if name in firewall['ipv6']: - for name_id, name_conf in firewall['ipv6'][name].items(): - if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf: - raise ConfigError('default-action set to jump, but no default-jump-target specified') - if 'default_jump_target' in name_conf: - target = name_conf['default_jump_target'] - if 'jump' not in name_conf['default_action']: - 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.') - 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, name, rule_conf, True) - - #### ZONESSSS + for family in ['ipv4', 'ipv6', 'bridge']: + if family in firewall: + for chain in ['name','forward','input','output', 'prerouting']: + if chain in firewall[family]: + for priority, priority_conf in firewall[family][chain].items(): + if 'jump' in priority_conf['default_action'] and 'default_jump_target' not in priority_conf: + raise ConfigError('default-action set to jump, but no default-jump-target specified') + if 'default_jump_target' in priority_conf: + target = priority_conf['default_jump_target'] + if 'jump' not in priority_conf['default_action']: + raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined') + if priority_conf['default_jump_target'] == priority: + raise ConfigError(f'Loop detected on default-jump-target.') + if target not in dict_search_args(firewall[family], 'name'): + raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') + if 'rule' in priority_conf: + for rule_id, rule_conf in priority_conf['rule'].items(): + verify_rule(firewall, family, chain, priority, rule_id, rule_conf) + local_zone = False zone_interfaces = [] @@ -495,8 +485,53 @@ def generate(firewall): render(sysctl_file, 'firewall/sysctl-firewall.conf.j2', firewall) return None +def parse_firewall_error(output): + # Define the regex patterns to extract the error message and the comment + error_pattern = re.compile(r'Error:\s*(.*?)\n') + comment_pattern = re.compile(r'comment\s+"([^"]+)"') + error_output = [] + + # Find all error messages in the output + error_matches = error_pattern.findall(output) + # Find all comment matches in the output + comment_matches = comment_pattern.findall(output) + + if not error_matches or not comment_matches: + raise ConfigError(f'Unknown firewall error detected: {output}') + + error_output.append('Fail to apply firewall') + # Loop over the matches and process them + for error_message, comment in zip(error_matches, comment_matches): + # Parse the comment + parsed_entries = comment.split('-') + family = 'bridge' if parsed_entries[0] == 'bri' else parsed_entries[0] + if parsed_entries[1] == 'NAM': + chain = 'name' + elif parsed_entries[1] == 'FWD': + chain = 'forward' + elif parsed_entries[1] == 'INP': + chain = 'input' + elif parsed_entries[1] == 'OUT': + chain = 'output' + elif parsed_entries[1] == 'PRE': + chain = 'prerouting' + error_output.append(f'Error found on: firewall {family} {chain} {parsed_entries[2]} rule {parsed_entries[3]}') + error_output.append(f'\tError message: {error_message.strip()}') + + raise ConfigError('\n'.join(error_output)) + def apply(firewall): + # Use nft -c option to check current configuration file + completed_process = subp_run(['nft', '-c', '--file', nftables_conf], capture_output=True) + install_result = completed_process.returncode + if install_result == 1: + # We need to handle firewall error + output = completed_process.stderr + parse_firewall_error(output.decode()) + + # No error detected during check, we can apply the new configuration install_result, output = rc_cmd(f'nft --file {nftables_conf}') + # Double check just in case if install_result == 1: raise ConfigError(f'Failed to apply firewall: {output}') diff --git a/src/conf_mode/interfaces_l2tpv3.py b/src/conf_mode/interfaces_l2tpv3.py index b9f827bee..f0a70436e 100755 --- a/src/conf_mode/interfaces_l2tpv3.py +++ b/src/conf_mode/interfaces_l2tpv3.py @@ -86,6 +86,8 @@ def generate(l2tpv3): return None def apply(l2tpv3): + check_kmod(k_mod) + # Check if L2TPv3 interface already exists if interface_exists(l2tpv3['ifname']): # L2TPv3 is picky when changing tunnels/sessions, thus we can simply @@ -102,7 +104,6 @@ def apply(l2tpv3): if __name__ == '__main__': try: - check_kmod(k_mod) c = get_config() verify(c) generate(c) diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py index 0e0b77877..7abdfdbfa 100755 --- a/src/conf_mode/interfaces_wireguard.py +++ b/src/conf_mode/interfaces_wireguard.py @@ -103,7 +103,12 @@ def verify(wireguard): public_keys.append(peer['public_key']) +def generate(wireguard): + return None + def apply(wireguard): + check_kmod('wireguard') + if 'rebuild_required' in wireguard or 'deleted' in wireguard: wg = WireGuardIf(**wireguard) # WireGuard only supports peer removal based on the configured public-key, @@ -123,7 +128,6 @@ def apply(wireguard): if __name__ == '__main__': try: - check_kmod('wireguard') c = get_config() verify(c) apply(c) diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py index f35a250cb..d24675ee6 100755 --- a/src/conf_mode/interfaces_wireless.py +++ b/src/conf_mode/interfaces_wireless.py @@ -239,6 +239,8 @@ def verify(wifi): return None def generate(wifi): + check_kmod('mac80211') + interface = wifi['ifname'] # Delete config files if interface is removed @@ -333,7 +335,6 @@ def apply(wifi): if __name__ == '__main__': try: - check_kmod('mac80211') c = get_config() verify(c) generate(c) diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index f74bb217e..39803fa02 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -240,6 +240,8 @@ def generate(nat): return None def apply(nat): + check_kmod(k_mod) + cmd(f'nft --file {nftables_nat_config}') cmd(f'nft --file {nftables_static_nat_conf}') @@ -253,7 +255,6 @@ def apply(nat): if __name__ == '__main__': try: - check_kmod(k_mod) c = get_config() verify(c) generate(c) diff --git a/src/conf_mode/nat64.py b/src/conf_mode/nat64.py index 32a1c47d1..df501ce7f 100755 --- a/src/conf_mode/nat64.py +++ b/src/conf_mode/nat64.py @@ -46,7 +46,12 @@ def get_config(config: Config | None = None) -> None: base = ["nat64"] nat64 = config.get_config_dict(base, key_mangling=("-", "_"), get_first_key=True) - base_src = base + ["source", "rule"] + return nat64 + + +def verify(nat64) -> None: + check_kmod(["jool"]) + base_src = ["nat64", "source", "rule"] # Load in existing instances so we can destroy any unknown lines = cmd("jool instance display --csv").splitlines() @@ -76,12 +81,8 @@ def get_config(config: Config | None = None) -> None: ): rules[num]["recreate"] = True - return nat64 - - -def verify(nat64) -> None: if not nat64: - # no need to verify the CLI as nat64 is going to be deactivated + # nothing left to do return if dict_search("source.rule", nat64): @@ -128,6 +129,9 @@ def verify(nat64) -> None: def generate(nat64) -> None: + if not nat64: + return + os.makedirs(JOOL_CONFIG_DIR, exist_ok=True) if dict_search("source.rule", nat64): @@ -184,6 +188,7 @@ def generate(nat64) -> None: def apply(nat64) -> None: if not nat64: + unload_kmod(['jool']) return if dict_search("source.rule", nat64): @@ -211,7 +216,6 @@ def apply(nat64) -> None: if __name__ == "__main__": try: - check_kmod(["jool"]) c = get_config() verify(c) generate(c) diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index 075738dad..c44320f36 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -112,6 +112,8 @@ def apply(nat): if not nat: return None + check_kmod(k_mod) + cmd(f'nft --file {nftables_nat66_config}') call_dependents() @@ -119,7 +121,6 @@ def apply(nat): if __name__ == '__main__': try: - check_kmod(k_mod) c = get_config() verify(c) generate(c) diff --git a/src/conf_mode/policy_route.py b/src/conf_mode/policy_route.py index c58fe1bce..223175b8a 100755 --- a/src/conf_mode/policy_route.py +++ b/src/conf_mode/policy_route.py @@ -25,6 +25,9 @@ from vyos.template import render from vyos.utils.dict import dict_search_args from vyos.utils.process import cmd from vyos.utils.process import run +from vyos.utils.network import get_vrf_tableid +from vyos.defaults import rt_global_table +from vyos.defaults import rt_global_vrf from vyos import ConfigError from vyos import airbag airbag.enable() @@ -83,6 +86,9 @@ def verify_rule(policy, name, rule_conf, ipv6, rule_id): if not tcp_flags or 'syn' not in tcp_flags: raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS') + if 'vrf' in rule_conf['set'] and 'table' in rule_conf['set']: + raise ConfigError(f'{name} rule {rule_id}: Cannot set both forwarding route table and VRF') + tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') if tcp_flags: if dict_search_args(rule_conf, 'protocol') != 'tcp': @@ -152,15 +158,26 @@ def apply_table_marks(policy): for name, pol_conf in policy[route].items(): if 'rule' in pol_conf: for rule_id, rule_conf in pol_conf['rule'].items(): + vrf_table_id = None set_table = dict_search_args(rule_conf, 'set', 'table') - if set_table: + set_vrf = dict_search_args(rule_conf, 'set', 'vrf') + if set_vrf: + if set_vrf == 'default': + vrf_table_id = rt_global_vrf + else: + vrf_table_id = get_vrf_tableid(set_vrf) + elif set_table: if set_table == 'main': - set_table = '254' - if set_table in tables: + vrf_table_id = rt_global_table + else: + vrf_table_id = set_table + if vrf_table_id is not None: + vrf_table_id = int(vrf_table_id) + if vrf_table_id in tables: continue - tables.append(set_table) - table_mark = mark_offset - int(set_table) - cmd(f'{cmd_str} rule add pref {set_table} fwmark {table_mark} table {set_table}') + tables.append(vrf_table_id) + table_mark = mark_offset - vrf_table_id + cmd(f'{cmd_str} rule add pref {vrf_table_id} fwmark {table_mark} table {vrf_table_id}') def cleanup_table_marks(): for cmd_str in ['ip', 'ip -6']: diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py index 7f6ae3680..d323ceb4f 100755 --- a/src/conf_mode/protocols_static_multicast.py +++ b/src/conf_mode/protocols_static_multicast.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-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 @@ -14,15 +14,14 @@ # 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 from ipaddress import IPv4Address from sys import exit from vyos import ConfigError +from vyos import frr from vyos.config import Config -from vyos.utils.process import call -from vyos.template import render +from vyos.template import render_to_string from vyos import airbag airbag.enable() @@ -92,23 +91,39 @@ def verify(mroute): if IPv4Address(route[0]) < IPv4Address('224.0.0.0'): raise ConfigError(route + " not a multicast network") + def generate(mroute): if mroute is None: return None - render(config_file, 'frr/static_mcast.frr.j2', mroute) + mroute['new_frr_config'] = render_to_string('frr/static_mcast.frr.j2', mroute) return None + def apply(mroute): if mroute is None: return None + static_daemon = 'staticd' + + frr_cfg = frr.FRRConfig() + frr_cfg.load_configuration(static_daemon) - if os.path.exists(config_file): - call(f'vtysh -d staticd -f {config_file}') - os.remove(config_file) + if 'old_mroute' in mroute: + for route_gr in mroute['old_mroute']: + for nh in mroute['old_mroute'][route_gr]: + if mroute['old_mroute'][route_gr][nh]: + frr_cfg.modify_section(f'^ip mroute {route_gr} {nh} {mroute["old_mroute"][route_gr][nh]}') + else: + frr_cfg.modify_section(f'^ip mroute {route_gr} {nh}') + + if 'new_frr_config' in mroute: + frr_cfg.add_before(frr.default_add_before, mroute['new_frr_config']) + + frr_cfg.commit_configuration(static_daemon) return None + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/system_acceleration.py b/src/conf_mode/system_acceleration.py index e4b248675..d2cf44ff0 100755 --- a/src/conf_mode/system_acceleration.py +++ b/src/conf_mode/system_acceleration.py @@ -79,6 +79,9 @@ def verify(qat): if not data: raise ConfigError('No QAT acceleration device found') +def generate(qat): + return + def apply(qat): # Shutdown VPN service which can use QAT if 'ipsec' in qat: diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py index 19bbb8875..b380e0521 100755 --- a/src/conf_mode/system_console.py +++ b/src/conf_mode/system_console.py @@ -19,6 +19,7 @@ from pathlib import Path from vyos.config import Config from vyos.utils.process import call +from vyos.utils.serial import restart_login_consoles from vyos.system import grub_util from vyos.template import render from vyos import ConfigError @@ -74,7 +75,6 @@ def generate(console): for root, dirs, files in os.walk(base_dir): for basename in files: if 'serial-getty' in basename: - call(f'systemctl stop {basename}') os.unlink(os.path.join(root, basename)) if not console or 'device' not in console: @@ -122,6 +122,11 @@ def apply(console): # Reload systemd manager configuration call('systemctl daemon-reload') + # Service control moved to vyos.utils.serial to unify checks and prompts. + # If users are connected, we want to show an informational message on completing + # the process, but not halt configuration processing with an interactive prompt. + restart_login_consoles(prompt_user=False, quiet=False) + if not console: return None @@ -129,13 +134,6 @@ def apply(console): # Configure screen blank powersaving on VGA console call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux </dev/tty1 >/dev/tty1 2>&1') - # Start getty process on configured serial interfaces - for device in console['device']: - # Only start console if it exists on the running system. If a user - # detaches a USB serial console and reboots - it should not fail! - if os.path.exists(f'/dev/{device}'): - call(f'systemctl restart serial-getty@{device}.service') - return None if __name__ == '__main__': diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index cf82b767f..b3e05a814 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -21,12 +21,15 @@ import jmespath from sys import exit from time import sleep +from ipaddress import ip_address +from netaddr import IPNetwork +from netaddr import IPRange -from vyos.base import Warning from vyos.config import Config from vyos.config import config_dict_merge from vyos.configdep import set_dependents from vyos.configdep import call_dependents +from vyos.configdict import get_interface_dict from vyos.configdict import leaf_node_changed from vyos.configverify import verify_interface_exists from vyos.configverify import dynamic_interface_pattern @@ -47,6 +50,9 @@ from vyos.utils.network import interface_exists from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.process import call +from vyos.utils.vti_updown_db import vti_updown_db_exists +from vyos.utils.vti_updown_db import open_vti_updown_db_for_create_or_update +from vyos.utils.vti_updown_db import remove_vti_updown_db from vyos import ConfigError from vyos import airbag airbag.enable() @@ -104,6 +110,8 @@ def get_config(config=None): ipsec = config_dict_merge(default_values, ipsec) ipsec['dhcp_interfaces'] = set() + ipsec['enabled_vti_interfaces'] = set() + ipsec['persistent_vti_interfaces'] = set() ipsec['dhcp_no_address'] = {} ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface']) @@ -121,6 +129,28 @@ def get_config(config=None): ipsec['l2tp_ike_default'] = 'aes256-sha1-modp1024,3des-sha1-modp1024' ipsec['l2tp_esp_default'] = 'aes256-sha1,3des-sha1' + # Collect the interface dicts for any refernced VTI interfaces in + # case we need to bring the interface up + ipsec['vti_interface_dicts'] = {} + + if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']: + for peer, peer_conf in ipsec['site_to_site']['peer'].items(): + if 'vti' in peer_conf: + if 'bind' in peer_conf['vti']: + vti_interface = peer_conf['vti']['bind'] + if vti_interface not in ipsec['vti_interface_dicts']: + _, vti = get_interface_dict(conf, ['interfaces', 'vti'], vti_interface) + ipsec['vti_interface_dicts'][vti_interface] = vti + + if 'remote_access' in ipsec: + if 'connection' in ipsec['remote_access']: + for name, ra_conf in ipsec['remote_access']['connection'].items(): + if 'bind' in ra_conf: + vti_interface = ra_conf['bind'] + if vti_interface not in ipsec['vti_interface_dicts']: + _, vti = get_interface_dict(conf, ['interfaces', 'vti'], vti_interface) + ipsec['vti_interface_dicts'][vti_interface] = vti + return ipsec def get_dhcp_address(iface): @@ -249,7 +279,8 @@ def verify(ipsec): if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'): raise ConfigError(f"Invalid dhcp-interface on remote-access connection {name}") - ipsec['dhcp_interfaces'].add(dhcp_interface) + if 'disable' not in ra_conf: + ipsec['dhcp_interfaces'].add(dhcp_interface) address = get_dhcp_address(dhcp_interface) count = 0 @@ -304,6 +335,16 @@ def verify(ipsec): if dict_search('remote_access.radius.server', ipsec) == None: raise ConfigError('RADIUS authentication requires at least one server') + if 'bind' in ra_conf: + vti_interface = ra_conf['bind'] + if not interface_exists(vti_interface): + raise ConfigError(f'VTI interface {vti_interface} for remote-access connection {name} does not exist!') + + if 'disable' not in ra_conf: + ipsec['enabled_vti_interfaces'].add(vti_interface) + # remote access VPN interfaces are always up regardless of whether clients are connected + ipsec['persistent_vti_interfaces'].add(vti_interface) + if 'pool' in ra_conf: if {'dhcp', 'radius'} <= set(ra_conf['pool']): raise ConfigError(f'Can not use both DHCP and RADIUS for address allocation '\ @@ -330,26 +371,73 @@ def verify(ipsec): raise ConfigError(f'Requested pool "{pool}" does not exist!') if 'pool' in ipsec['remote_access']: + pool_networks = [] for pool, pool_config in ipsec['remote_access']['pool'].items(): - if 'prefix' not in pool_config: - raise ConfigError(f'Missing madatory prefix option for pool "{pool}"!') + if 'prefix' not in pool_config and 'range' not in pool_config: + raise ConfigError(f'Mandatory prefix or range must be specified for pool "{pool}"!') + + if 'prefix' in pool_config and 'range' in pool_config: + raise ConfigError(f'Only one of prefix or range can be specified for pool "{pool}"!') + + if 'prefix' in pool_config: + range_is_ipv4 = is_ipv4(pool_config['prefix']) + range_is_ipv6 = is_ipv6(pool_config['prefix']) + + net = IPNetwork(pool_config['prefix']) + start = net.first + stop = net.last + for network in pool_networks: + if start in network or stop in network: + raise ConfigError(f'Prefix for pool "{pool}" is already part of another pool\'s range!') + + tmp = IPRange(start, stop) + pool_networks.append(tmp) + + if 'range' in pool_config: + range_config = pool_config['range'] + if not {'start', 'stop'} <= set(range_config.keys()): + raise ConfigError(f'Range start and stop address must be defined for pool "{pool}"!') + + range_both_ipv4 = is_ipv4(range_config['start']) and is_ipv4(range_config['stop']) + range_both_ipv6 = is_ipv6(range_config['start']) and is_ipv6(range_config['stop']) + + if not (range_both_ipv4 or range_both_ipv6): + raise ConfigError(f'Range start and stop must be of the same address family for pool "{pool}"!') + + if ip_address(range_config['stop']) < ip_address(range_config['start']): + raise ConfigError(f'Range stop address must be greater or equal\n' \ + 'to the range\'s start address for pool "{pool}"!') + + range_is_ipv4 = is_ipv4(range_config['start']) + range_is_ipv6 = is_ipv6(range_config['start']) + + start = range_config['start'] + stop = range_config['stop'] + for network in pool_networks: + if start in network: + raise ConfigError(f'Range "{range}" start address "{start}" already part of another pool\'s range!') + if stop in network: + raise ConfigError(f'Range "{range}" stop address "{stop}" already part of another pool\'s range!') + + tmp = IPRange(start, stop) + pool_networks.append(tmp) if 'name_server' in pool_config: if len(pool_config['name_server']) > 2: raise ConfigError(f'Only two name-servers are supported for remote-access pool "{pool}"!') for ns in pool_config['name_server']: - v4_addr_and_ns = is_ipv4(ns) and not is_ipv4(pool_config['prefix']) - v6_addr_and_ns = is_ipv6(ns) and not is_ipv6(pool_config['prefix']) + v4_addr_and_ns = is_ipv4(ns) and not range_is_ipv4 + v6_addr_and_ns = is_ipv6(ns) and not range_is_ipv6 if v4_addr_and_ns or v6_addr_and_ns: - raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and name-server adresses!') + raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix/range and name-server addresses!') if 'exclude' in pool_config: for exclude in pool_config['exclude']: - v4_addr_and_exclude = is_ipv4(exclude) and not is_ipv4(pool_config['prefix']) - v6_addr_and_exclude = is_ipv6(exclude) and not is_ipv6(pool_config['prefix']) + v4_addr_and_exclude = is_ipv4(exclude) and not range_is_ipv4 + v6_addr_and_exclude = is_ipv6(exclude) and not range_is_ipv6 if v4_addr_and_exclude or v6_addr_and_exclude: - raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and exclude prefixes!') + raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix/range and exclude prefixes!') if 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']: for server, server_config in ipsec['remote_access']['radius']['server'].items(): @@ -420,7 +508,8 @@ def verify(ipsec): if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'): raise ConfigError(f"Invalid dhcp-interface on site-to-site peer {peer}") - ipsec['dhcp_interfaces'].add(dhcp_interface) + if 'disable' not in peer_conf: + ipsec['dhcp_interfaces'].add(dhcp_interface) address = get_dhcp_address(dhcp_interface) count = 0 @@ -438,14 +527,12 @@ def verify(ipsec): if 'local_address' in peer_conf and 'dhcp_interface' in peer_conf: raise ConfigError(f"A single local-address or dhcp-interface is required when using VTI on site-to-site peer {peer}") - if dict_search('options.disable_route_autoinstall', - ipsec) == None: - Warning('It\'s recommended to use ipsec vti with the next command\n[set vpn ipsec option disable-route-autoinstall]') - if 'bind' in peer_conf['vti']: vti_interface = peer_conf['vti']['bind'] if not interface_exists(vti_interface): raise ConfigError(f'VTI interface {vti_interface} for site-to-site peer {peer} does not exist!') + if 'disable' not in peer_conf: + ipsec['enabled_vti_interfaces'].add(vti_interface) if 'vti' not in peer_conf and 'tunnel' not in peer_conf: raise ConfigError(f"No VTI or tunnel specified on site-to-site peer {peer}") @@ -623,9 +710,21 @@ def apply(ipsec): systemd_service = 'strongswan.service' if not ipsec: call(f'systemctl stop {systemd_service}') + + if vti_updown_db_exists(): + remove_vti_updown_db() + else: call(f'systemctl reload-or-restart {systemd_service}') + if ipsec['enabled_vti_interfaces']: + with open_vti_updown_db_for_create_or_update() as db: + db.removeAllOtherInterfaces(ipsec['enabled_vti_interfaces']) + db.setPersistentInterfaces(ipsec['persistent_vti_interfaces']) + db.commit(lambda interface: ipsec['vti_interface_dicts'][interface]) + elif vti_updown_db_exists(): + remove_vti_updown_db() + if ipsec.get('nhrp_exists', False): try: call_dependents() diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 184725573..72b178c89 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from sys import exit +from jmespath import search from json import loads from vyos.config import Config @@ -70,6 +71,14 @@ def has_rule(af : str, priority : int, table : str=None): return True return False +def is_nft_vrf_zone_rule_setup() -> bool: + """ + Check if an nftables connection tracking rule already exists + """ + tmp = loads(cmd('sudo nft -j list table inet vrf_zones')) + num_rules = len(search("nftables[].rule[].chain", tmp)) + return bool(num_rules) + def vrf_interfaces(c, match): matched = [] old_level = c.get_level() @@ -264,6 +273,7 @@ def apply(vrf): if not has_rule(afi, 2000, 'l3mdev'): call(f'ip {afi} rule add pref 2000 l3mdev unreachable') + nft_vrf_zone_rule_setup = False for name, config in vrf['name'].items(): table = config['table'] if not interface_exists(name): @@ -302,7 +312,12 @@ def apply(vrf): nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}' cmd(f'nft {nft_add_element}') - if vrf['conntrack']: + # Only call into nftables as long as there is nothing setup to avoid wasting + # CPU time and thus lenghten the commit process + if not nft_vrf_zone_rule_setup: + nft_vrf_zone_rule_setup = is_nft_vrf_zone_rule_setup() + # Install nftables conntrack rules only once + if vrf['conntrack'] and not nft_vrf_zone_rule_setup: for chain, rule in nftables_rules.items(): cmd(f'nft add rule inet vrf_zones {chain} {rule}') |