diff options
Diffstat (limited to 'src/conf_mode')
118 files changed, 2717 insertions, 2314 deletions
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 14387cbbf..4ec9b8849 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -22,6 +22,7 @@ from ipaddress import ip_address from ipaddress import ip_network from json import dumps as json_write +import psutil from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge @@ -148,6 +149,9 @@ def verify(container): if network_name not in container.get('network', {}): raise ConfigError(f'Container network "{network_name}" does not exist!') + if 'name_server' in container_config and 'no_name_server' not in container['network'][network_name]: + raise ConfigError(f'Setting name server has no effect when attached container network has DNS enabled!') + if 'address' in container_config['network'][network_name]: cnt_ipv4 = 0 cnt_ipv6 = 0 @@ -220,6 +224,21 @@ def verify(container): if not os.path.exists(source): raise ConfigError(f'Volume "{volume}" source path "{source}" does not exist!') + if 'tmpfs' in container_config: + for tmpfs, tmpfs_config in container_config['tmpfs'].items(): + if 'destination' not in tmpfs_config: + raise ConfigError(f'tmpfs "{tmpfs}" has no destination path configured!') + if 'size' in tmpfs_config: + free_mem_mb: int = psutil.virtual_memory().available / 1024 / 1024 + if int(tmpfs_config['size']) > free_mem_mb: + Warning(f'tmpfs "{tmpfs}" size is greater than the current free memory!') + + total_mem_mb: int = (psutil.virtual_memory().total / 1024 / 1024) / 2 + if int(tmpfs_config['size']) > total_mem_mb: + raise ConfigError(f'tmpfs "{tmpfs}" size should not be more than 50% of total system memory!') + else: + raise ConfigError(f'tmpfs "{tmpfs}" has no size configured!') + if 'port' in container_config: for tmp in container_config['port']: if not {'source', 'destination'} <= set(container_config['port'][tmp]): @@ -270,6 +289,13 @@ def verify(container): if 'registry' in container: for registry, registry_config in container['registry'].items(): + if 'mirror' in registry_config: + if 'host_name' in registry_config['mirror'] and 'address' in registry_config['mirror']: + raise ConfigError(f'Container registry mirror address/host-name are mutually exclusive!') + + if 'path' in registry_config['mirror'] and not registry_config['mirror']['path'].startswith('/'): + raise ConfigError('Container registry mirror path must start with "/"!') + if 'authentication' not in registry_config: continue if not {'username', 'password'} <= set(registry_config['authentication']): @@ -284,12 +310,13 @@ def generate_run_arguments(name, container_config): memory = container_config['memory'] shared_memory = container_config['shared_memory'] restart = container_config['restart'] + log_driver = container_config['log_driver'] # Add sysctl options sysctl_opt = '' if 'sysctl' in container_config and 'parameter' in container_config['sysctl']: for k, v in container_config['sysctl']['parameter'].items(): - sysctl_opt += f" --sysctl {k}={v['value']}" + sysctl_opt += f" --sysctl \"{k}={v['value']}\"" # Add capability options. Should be in uppercase capabilities = '' @@ -298,6 +325,11 @@ def generate_run_arguments(name, container_config): cap = cap.upper().replace('-', '_') capabilities += f' --cap-add={cap}' + # Grant root capabilities to the container + privileged = '' + if 'privileged' in container_config: + privileged = '--privileged' + # Add a host device to the container /dev/x:/dev/x device = '' if 'device' in container_config: @@ -359,13 +391,26 @@ def generate_run_arguments(name, container_config): prop = vol_config['propagation'] volume += f' --volume {svol}:{dvol}:{mode},{prop}' + # Mount tmpfs + tmpfs = '' + if 'tmpfs' in container_config: + for tmpfs_config in container_config['tmpfs'].values(): + dest = tmpfs_config['destination'] + size = tmpfs_config['size'] + tmpfs += f' --mount=type=tmpfs,tmpfs-size={size}M,destination={dest}' + host_pid = '' if 'allow_host_pid' in container_config: host_pid = '--pid host' - container_base_cmd = f'--detach --interactive --tty --replace {capabilities} --cpus {cpu_quota} {sysctl_opt} ' \ - f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \ - f'--name {name} {hostname} {device} {port} {volume} {env_opt} {label} {uid} {host_pid}' + name_server = '' + if 'name_server' in container_config: + for ns in container_config['name_server']: + name_server += f'--dns {ns}' + + container_base_cmd = f'--detach --interactive --tty --replace {capabilities} {privileged} --cpus {cpu_quota} {sysctl_opt} ' \ + f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} --log-driver={log_driver} ' \ + f'--name {name} {hostname} {device} {port} {name_server} {volume} {tmpfs} {env_opt} {label} {uid} {host_pid}' entrypoint = '' if 'entrypoint' in container_config: @@ -419,12 +464,18 @@ def generate(container): 'dns_enabled': True, 'ipam_options': { 'driver': 'host-local' + }, + 'options': { + 'mtu': '1500' } } if 'no_name_server' in network_config: tmp['dns_enabled'] = False + if 'mtu' in network_config: + tmp['options']['mtu'] = network_config['mtu'] + for prefix in network_config['prefix']: net = {'subnet': prefix, 'gateway': inc_ip(prefix, 1)} tmp['subnets'].append(net) diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 5638a9668..90fdded99 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -17,8 +17,9 @@ import os import re -from sys import exit +from glob import glob +from sys import exit from vyos.base import Warning from vyos.config import Config from vyos.configdict import is_node_changed @@ -31,9 +32,12 @@ from vyos.firewall import geoip_update from vyos.template import render 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 call from vyos.utils.process import cmd from vyos.utils.process import rc_cmd +from vyos.utils.network import get_vrf_members +from vyos.utils.network import get_interface_vrf from vyos import ConfigError from vyos import airbag from subprocess import run as subp_run @@ -41,6 +45,9 @@ from subprocess import run as subp_run airbag.enable() nftables_conf = '/run/nftables.conf' +domain_resolver_usage = '/run/use-vyos-domain-resolver-firewall' +firewall_config_dir = "/config/firewall" + sysctl_file = r'/run/sysctl/10-vyos-firewall.conf' valid_groups = [ @@ -49,7 +56,8 @@ valid_groups = [ 'network_group', 'port_group', 'interface_group', - ## Added for group ussage in bridge firewall + 'remote_group', + ## Added for group usage in bridge firewall 'ipv4_address_group', 'ipv6_address_group', 'ipv4_network_group', @@ -128,7 +136,28 @@ def get_config(config=None): firewall['geoip_updated'] = geoip_updated(conf, firewall) - fqdn_config_parse(firewall) + fqdn_config_parse(firewall, 'firewall') + + if not os.path.exists(nftables_conf): + firewall['first_install'] = True + + if 'zone' in firewall: + for local_zone, local_zone_conf in firewall['zone'].items(): + if 'local_zone' not in local_zone_conf: + # Get physical interfaces assigned to the zone if vrf is used: + if 'vrf' in local_zone_conf['member']: + local_zone_conf['vrf_interfaces'] = {} + for vrf_name in local_zone_conf['member']['vrf']: + local_zone_conf['vrf_interfaces'][vrf_name] = ','.join(get_vrf_members(vrf_name)) + continue + + local_zone_conf['from_local'] = {} + + for zone, zone_conf in firewall['zone'].items(): + if zone == local_zone or 'from' not in zone_conf: + continue + if local_zone in zone_conf['from']: + local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone] set_dependents('conntrack', conf) @@ -178,7 +207,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf): 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 hook != 'name': # This is a bit clumsy, but consolidates a chunk of code. + 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, hook, target, family, recursive=False) @@ -241,12 +270,12 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf): if dict_search_args(rule_conf, 'gre', 'flags', 'checksum') is None: # There is no builtin match in nftables for the GRE key, so we need to do a raw lookup. - # The offset of the key within the packet shifts depending on the C-flag. - # 99% of the time, nobody will have checksums enabled - it's usually a manual config option. - # We can either assume it is unset unless otherwise directed + # The offset of the key within the packet shifts depending on the C-flag. + # 99% of the time, nobody will have checksums enabled - it's usually a manual config option. + # We can either assume it is unset unless otherwise directed # (confusing, requires doco to explain why it doesn't work sometimes) - # or, demand an explicit selection to be made for this specific match rule. - # This check enforces the latter. The user is free to create rules for both cases. + # or, demand an explicit selection to be made for this specific match rule. + # This check enforces the latter. The user is free to create rules for both cases. raise ConfigError('Matching GRE tunnel key requires an explicit checksum flag match. For most cases, use "gre flags checksum unset"') if dict_search_args(rule_conf, 'gre', 'flags', 'key', 'unset') is not None: @@ -259,7 +288,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf): if gre_inner_value < 0 or gre_inner_value > 65535: raise ConfigError('inner-proto outside valid ethertype range 0-65535') except ValueError: - pass # Symbolic constant, pre-validated before reaching here. + pass # Symbolic constant, pre-validated before reaching here. tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') if tcp_flags: @@ -286,8 +315,8 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf): raise ConfigError('Only one of address, fqdn or geoip can be specified') 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') + if len({'address_group', 'network_group', 'domain_group', 'remote_group'} & set(side_conf['group'])) > 1: + raise ConfigError('Only one address-group, network-group, remote-group or domain-group can be specified') for group in valid_groups: if group in side_conf['group']: @@ -307,7 +336,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf): error_group = fw_group.replace("_", "-") - if group in ['address_group', 'network_group', 'domain_group']: + if group in ['address_group', 'network_group', 'domain_group', 'remote_group']: types = [t for t in ['address', 'fqdn', 'geoip'] if t in side_conf] if types: raise ConfigError(f'{error_group} and {types[0]} cannot both be defined') @@ -410,6 +439,16 @@ def verify(firewall): for ifname in interfaces: verify_hardware_offload(ifname) + if 'offload' in firewall.get('global_options', {}).get('state_policy', {}): + offload_path = firewall['global_options']['state_policy']['offload'] + if 'offload_target' not in offload_path: + raise ConfigError('offload-target must be specified') + + offload_target = offload_path['offload_target'] + + if not dict_search_args(firewall, 'flowtable', offload_target): + raise ConfigError(f'Invalid offload-target. Flowtable "{offload_target}" does not exist on the system') + if 'group' in firewall: for group_type in nested_group_types: if group_type in firewall['group']: @@ -417,6 +456,11 @@ def verify(firewall): for group_name, group in groups.items(): verify_nested_group(group_name, group, groups, []) + if 'remote_group' in firewall['group']: + for group_name, group in firewall['group']['remote_group'].items(): + if 'url' not in group: + raise ConfigError(f'remote-group {group_name} must have a url configured') + for family in ['ipv4', 'ipv6', 'bridge']: if family in firewall: for chain in ['name','forward','input','output', 'prerouting']: @@ -438,28 +482,45 @@ def verify(firewall): local_zone = False zone_interfaces = [] + zone_vrf = [] if 'zone' in firewall: for zone, zone_conf in firewall['zone'].items(): - if 'local_zone' not in zone_conf and 'interface' not in zone_conf: + if 'local_zone' not in zone_conf and 'member' not in zone_conf: raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone') if 'local_zone' in zone_conf: if local_zone: raise ConfigError('There cannot be multiple local zones') - if 'interface' in zone_conf: + if 'member' in zone_conf: raise ConfigError('Local zone cannot have interfaces assigned') if 'intra_zone_filtering' in zone_conf: raise ConfigError('Local zone cannot use intra-zone-filtering') local_zone = True - if 'interface' in zone_conf: - found_duplicates = [intf for intf in zone_conf['interface'] if intf in zone_interfaces] + if 'member' in zone_conf: + if 'interface' in zone_conf['member']: + for iface in zone_conf['member']['interface']: + + if iface in zone_interfaces: + raise ConfigError(f'Interfaces cannot be assigned to multiple zones') - if found_duplicates: - raise ConfigError(f'Interfaces cannot be assigned to multiple zones') + iface_vrf = get_interface_vrf(iface) + if iface_vrf != 'default': + Warning(f"Interface {iface} assigned to zone {zone} is in VRF {iface_vrf}. This might not work as expected.") + zone_interfaces.append(iface) - zone_interfaces += zone_conf['interface'] + if 'vrf' in zone_conf['member']: + for vrf in zone_conf['member']['vrf']: + if vrf in zone_vrf: + raise ConfigError(f'VRF cannot be assigned to multiple zones') + zone_vrf.append(vrf) + + if 'vrf_interfaces' in zone_conf: + for vrf_name, vrf_interfaces in zone_conf['vrf_interfaces'].items(): + if not vrf_interfaces: + raise ConfigError( + f'VRF "{vrf_name}" cannot be a member of any zone. It does not contain any interfaces.') if 'intra_zone_filtering' in zone_conf: intra_zone = zone_conf['intra_zone_filtering'] @@ -495,24 +556,17 @@ def verify(firewall): return None def generate(firewall): - if not os.path.exists(nftables_conf): - firewall['first_install'] = True - - if 'zone' in firewall: - for local_zone, local_zone_conf in firewall['zone'].items(): - if 'local_zone' not in local_zone_conf: - continue - - local_zone_conf['from_local'] = {} - - for zone, zone_conf in firewall['zone'].items(): - if zone == local_zone or 'from' not in zone_conf: - continue - if local_zone in zone_conf['from']: - local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone] - render(nftables_conf, 'firewall/nftables.j2', firewall) render(sysctl_file, 'firewall/sysctl-firewall.conf.j2', firewall) + + # Cleanup remote-group cache files + if os.path.exists(firewall_config_dir): + for fw_file in os.listdir(firewall_config_dir): + # Delete matching files in 'config/firewall' that no longer exist as a remote-group in config + if fw_file.startswith("R_") and fw_file.endswith(".txt"): + if 'group' not in firewall or 'remote_group' not in firewall['group'] or fw_file[2:-4] not in firewall['group']['remote_group'].keys(): + os.unlink(os.path.join(firewall_config_dir, fw_file)) + return None def parse_firewall_error(output): @@ -570,19 +624,23 @@ def apply(firewall): call_dependents() - # T970 Enable a resolver (systemd daemon) that checks - # domain-group/fqdn addresses and update entries for domains by timeout - # If router loaded without internet connection or for synchronization - domain_action = 'stop' - if dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'] or firewall['ip6_fqdn']: - domain_action = 'restart' + ## DOMAIN RESOLVER + domain_action = 'restart' + if dict_search_args(firewall, 'group', 'remote_group') or dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'].items() or firewall['ip6_fqdn'].items(): + text = f'# Automatically generated by firewall.py\nThis file indicates that vyos-domain-resolver service is used by the firewall.\n' + write_file(domain_resolver_usage, text) + else: + if os.path.exists(domain_resolver_usage): + os.unlink(domain_resolver_usage) + if not glob('/run/use-vyos-domain-resolver*'): + domain_action = 'stop' call(f'systemctl {domain_action} vyos-domain-resolver.service') if firewall['geoip_updated']: # Call helper script to Update set contents if 'name' in firewall['geoip_updated'] or 'ipv6_name' in firewall['geoip_updated']: print('Updating GeoIP. Please wait...') - geoip_update(firewall) + geoip_update(firewall=firewall) return None diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py index c726db8b2..005bb6cce 100755 --- a/src/conf_mode/high-availability.py +++ b/src/conf_mode/high-availability.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py index bbbfb0385..4a2317f85 100755 --- a/src/conf_mode/interfaces_bonding.py +++ b/src/conf_mode/interfaces_bonding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -30,19 +30,21 @@ from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.ifconfig import BondIf from vyos.ifconfig.ethernet import EthernetIf from vyos.ifconfig import Section -from vyos.template import render_to_string from vyos.utils.assertion import assert_mac from vyos.utils.dict import dict_search from vyos.utils.dict import dict_to_paths_values from vyos.utils.network import interface_exists +from vyos.utils.process import is_systemd_service_running from vyos.configdict import has_address_configured from vyos.configdict import has_vrf_configured -from vyos.configdep import set_dependents, call_dependents +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -87,10 +89,13 @@ def get_config(config=None): bond['mode'] = get_bond_mode(bond['mode']) tmp = is_node_changed(conf, base + [ifname, 'mode']) - if tmp: bond['shutdown_required'] = {} + if tmp: bond.update({'shutdown_required' : {}}) tmp = is_node_changed(conf, base + [ifname, 'lacp-rate']) - if tmp: bond['shutdown_required'] = {} + if tmp: bond.update({'shutdown_required' : {}}) + + tmp = is_node_changed(conf, base + [ifname, 'evpn']) + if tmp: bond.update({'frr_dict' : get_frrender_dict(conf)}) # determine which members have been removed interfaces_removed = leaf_node_changed(conf, base + [ifname, 'member', 'interface']) @@ -121,9 +126,8 @@ def get_config(config=None): # Restore existing config level conf.set_level(old_level) - if dict_search('member.interface', bond): - for interface, interface_config in bond['member']['interface'].items(): - + if dict_search('member.interface', bond) is not None: + for interface in bond['member']['interface']: interface_ethernet_config = conf.get_config_dict( ['interfaces', 'ethernet', interface], key_mangling=('-', '_'), @@ -132,44 +136,45 @@ def get_config(config=None): with_defaults=False, with_recursive_defaults=False) - interface_config['config_paths'] = dict_to_paths_values(interface_ethernet_config) + bond['member']['interface'][interface].update({'config_paths' : + dict_to_paths_values(interface_ethernet_config)}) # Check if member interface is a new member if not conf.exists_effective(base + [ifname, 'member', 'interface', interface]): bond['shutdown_required'] = {} - interface_config['new_added'] = {} + bond['member']['interface'][interface].update({'new_added' : {}}) # Check if member interface is disabled conf.set_level(['interfaces']) section = Section.section(interface) # this will be 'ethernet' for 'eth0' if conf.exists([section, interface, 'disable']): - interface_config['disable'] = '' + if tmp: bond['member']['interface'][interface].update({'disable': ''}) conf.set_level(old_level) # Check if member interface is already member of another bridge tmp = is_member(conf, interface, 'bridge') - if tmp: interface_config['is_bridge_member'] = tmp + if tmp: bond['member']['interface'][interface].update({'is_bridge_member' : tmp}) # Check if member interface is already member of a bond tmp = is_member(conf, interface, 'bonding') - for tmp in is_member(conf, interface, 'bonding'): - if bond['ifname'] == tmp: - continue - interface_config['is_bond_member'] = tmp + if ifname in tmp: + del tmp[ifname] + if tmp: bond['member']['interface'][interface].update({'is_bond_member' : tmp}) # Check if member interface is used as source-interface on another interface tmp = is_source_interface(conf, interface) - if tmp: interface_config['is_source_interface'] = tmp + if tmp: bond['member']['interface'][interface].update({'is_source_interface' : tmp}) # bond members must not have an assigned address tmp = has_address_configured(conf, interface) - if tmp: interface_config['has_address'] = {} + if tmp: bond['member']['interface'][interface].update({'has_address' : ''}) # bond members must not have a VRF attached tmp = has_vrf_configured(conf, interface) - if tmp: interface_config['has_vrf'] = {} + if tmp: bond['member']['interface'][interface].update({'has_vrf' : ''}) + return bond @@ -260,16 +265,16 @@ def verify(bond): return None def generate(bond): - bond['frr_zebra_config'] = '' - if 'deleted' not in bond: - bond['frr_zebra_config'] = render_to_string('frr/evpn.mh.frr.j2', bond) + if 'frr_dict' in bond and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(bond['frr_dict']) return None def apply(bond): - ifname = bond['ifname'] - b = BondIf(ifname) + if 'frr_dict' in bond and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() + + b = BondIf(bond['ifname']) if 'deleted' in bond: - # delete interface b.remove() else: b.update(bond) @@ -281,17 +286,6 @@ def apply(bond): raise ConfigError('Error in updating ethernet interface ' 'after deleting it from bond') - zebra_daemon = 'zebra' - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(f'^interface {ifname}', stop_pattern='^exit', remove_stop_mark=True) - if 'frr_zebra_config' in bond: - frr_cfg.add_before(frr.default_add_before, bond['frr_zebra_config']) - frr_cfg.commit_configuration(zebra_daemon) - return None if __name__ == '__main__': diff --git a/src/conf_mode/interfaces_bridge.py b/src/conf_mode/interfaces_bridge.py index 7b2c1ee0b..8cb0c515a 100755 --- a/src/conf_mode/interfaces_bridge.py +++ b/src/conf_mode/interfaces_bridge.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -25,6 +25,7 @@ from vyos.configdict import has_vlan_subinterface_configured from vyos.configverify import verify_dhcpv6 from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_vrf +from vyos.configverify import verify_mtu_ipv6 from vyos.ifconfig import BridgeIf from vyos.configdict import has_address_configured from vyos.configdict import has_vrf_configured @@ -53,27 +54,30 @@ def get_config(config=None): tmp = node_changed(conf, base + [ifname, 'member', 'interface']) if tmp: if 'member' in bridge: - bridge['member'].update({'interface_remove' : tmp }) + bridge['member'].update({'interface_remove': {t: {} for t in tmp}}) else: - bridge.update({'member' : {'interface_remove' : tmp }}) - for interface in tmp: - # When using VXLAN member interfaces that are configured for Single - # VXLAN Device (SVD) we need to call the VXLAN conf-mode script to - # re-create VLAN to VNI mappings if required, but only if the interface - # is already live on the system - this must not be done on first commit - if interface.startswith('vxlan') and interface_exists(interface): - set_dependents('vxlan', conf, interface) - # When using Wireless member interfaces we need to inform hostapd - # to properly set-up the bridge - elif interface.startswith('wlan') and interface_exists(interface): - set_dependents('wlan', conf, interface) + bridge.update({'member': {'interface_remove': {t: {} for t in tmp}}}) + for interface in tmp: + # When using VXLAN member interfaces that are configured for Single + # VXLAN Device (SVD) we need to call the VXLAN conf-mode script to + # re-create VLAN to VNI mappings if required, but only if the interface + # is already live on the system - this must not be done on first commit + if interface.startswith('vxlan') and interface_exists(interface): + set_dependents('vxlan', conf, interface) + _, vxlan = get_interface_dict(conf, ['interfaces', 'vxlan'], ifname=interface) + bridge['member']['interface_remove'].update({interface: vxlan}) + # When using Wireless member interfaces we need to inform hostapd + # to properly set-up the bridge + elif interface.startswith('wlan') and interface_exists(interface): + set_dependents('wlan', conf, interface) if dict_search('member.interface', bridge) is not None: for interface in list(bridge['member']['interface']): # Check if member interface is already member of another bridge tmp = is_member(conf, interface, 'bridge') - if tmp and bridge['ifname'] not in tmp: - bridge['member']['interface'][interface].update({'is_bridge_member' : tmp}) + if ifname in tmp: + del tmp[ifname] + if tmp: bridge['member']['interface'][interface].update({'is_bridge_member' : tmp}) # Check if member interface is already member of a bond tmp = is_member(conf, interface, 'bonding') @@ -107,6 +111,11 @@ def get_config(config=None): elif interface.startswith('wlan') and interface_exists(interface): set_dependents('wlan', conf, interface) + if interface.startswith('vtun'): + _, tmp_config = get_interface_dict(conf, ['interfaces', 'openvpn'], interface) + tmp = tmp_config.get('device_type') == 'tap' + bridge['member']['interface'][interface].update({'valid_ovpn' : tmp}) + # delete empty dictionary keys - no need to run code paths if nothing is there to do if 'member' in bridge: if 'interface' in bridge['member'] and len(bridge['member']['interface']) == 0: @@ -118,11 +127,22 @@ def get_config(config=None): return bridge def verify(bridge): + # to delete interface or remove a member interface VXLAN first need to check if + # VXLAN does not require to be a member of a bridge interface + if dict_search('member.interface_remove', bridge): + for iface, iface_config in bridge['member']['interface_remove'].items(): + if iface.startswith('vxlan') and dict_search('parameters.neighbor_suppress', iface_config) != None: + raise ConfigError( + f'To detach interface {iface} from bridge you must first ' + f'disable "neighbor-suppress" parameter in the VXLAN interface {iface}' + ) + if 'deleted' in bridge: return None verify_dhcpv6(bridge) verify_vrf(bridge) + verify_mtu_ipv6(bridge) verify_mirror_redirect(bridge) ifname = bridge['ifname'] @@ -152,6 +172,9 @@ def verify(bridge): if 'has_vrf' in interface_config: raise ConfigError(error_msg + 'it has a VRF assigned!') + if 'bpdu_guard' in interface_config and 'root_guard' in interface_config: + raise ConfigError(error_msg + 'bpdu-guard and root-guard cannot be configured at the same time!') + if 'enable_vlan' in bridge: if 'has_vlan' in interface_config: raise ConfigError(error_msg + 'it has VLAN subinterface(s) assigned!') @@ -160,6 +183,9 @@ def verify(bridge): if option in interface_config: raise ConfigError('Can not use VLAN options on non VLAN aware bridge') + if interface.startswith('vtun') and not interface_config['valid_ovpn']: + raise ConfigError(error_msg + 'OpenVPN device-type must be set to "tap"') + if 'enable_vlan' in bridge: if dict_search('vif.1', bridge): raise ConfigError(f'VLAN 1 sub interface cannot be set for VLAN aware bridge {ifname}, and VLAN 1 is always the parent interface') @@ -192,7 +218,7 @@ def apply(bridge): try: call_dependents() except ConfigError: - raise ConfigError('Error updating member interface configuration after changing bridge!') + raise ConfigError(f'Error updating member interface {interface} configuration after changing bridge!') return None diff --git a/src/conf_mode/interfaces_dummy.py b/src/conf_mode/interfaces_dummy.py index db768b94d..0a83eb23b 100755 --- a/src/conf_mode/interfaces_dummy.py +++ b/src/conf_mode/interfaces_dummy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py index 34ce7bc47..b112b244e 100755 --- a/src/conf_mode/interfaces_ethernet.py +++ b/src/conf_mode/interfaces_ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -22,6 +22,7 @@ from vyos.base import Warning from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed +from vyos.configdict import get_flowtable_interfaces from vyos.configverify import verify_address from vyos.configverify import verify_dhcpv6 from vyos.configverify import verify_interface_exists @@ -33,15 +34,16 @@ from vyos.configverify import verify_vrf from vyos.configverify import verify_bond_bridge_member from vyos.configverify import verify_eapol from vyos.ethtool import Ethtool +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.ifconfig import EthernetIf from vyos.ifconfig import BondIf -from vyos.template import render_to_string from vyos.utils.dict import dict_search from vyos.utils.dict import dict_to_paths_values from vyos.utils.dict import dict_set from vyos.utils.dict import dict_delete +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -164,6 +166,11 @@ def get_config(config=None): tmp = is_node_changed(conf, base + [ifname, 'duplex']) if tmp: ethernet.update({'speed_duplex_changed': {}}) + tmp = is_node_changed(conf, base + [ifname, 'evpn']) + if tmp: ethernet.update({'frr_dict' : get_frrender_dict(conf)}) + + ethernet['flowtable_interfaces'] = get_flowtable_interfaces(conf) + return ethernet def verify_speed_duplex(ethernet: dict, ethtool: Ethtool): @@ -265,7 +272,38 @@ def verify_allowedbond_changes(ethernet: dict): f' on interface "{ethernet["ifname"]}".' \ f' Interface is a bond member') +def verify_flowtable(ethernet: dict): + ifname = ethernet['ifname'] + + if 'deleted' in ethernet and ifname in ethernet['flowtable_interfaces']: + raise ConfigError(f'Cannot delete interface "{ifname}", still referenced on a flowtable') + + if 'vif_remove' in ethernet: + for vif in ethernet['vif_remove']: + vifname = f'{ifname}.{vif}' + + if vifname in ethernet['flowtable_interfaces']: + raise ConfigError(f'Cannot delete interface "{vifname}", still referenced on a flowtable') + + if 'vif_s_remove' in ethernet: + for vifs in ethernet['vif_s_remove']: + vifsname = f'{ifname}.{vifs}' + + if vifsname in ethernet['flowtable_interfaces']: + raise ConfigError(f'Cannot delete interface "{vifsname}", still referenced on a flowtable') + + if 'vif_s' in ethernet: + for vifs, vifs_conf in ethernet['vif_s'].items(): + if 'vif_c_delete' in vifs_conf: + for vifc in vifs_conf['vif_c_delete']: + vifcname = f'{ifname}.{vifs}.{vifc}' + + if vifcname in ethernet['flowtable_interfaces']: + raise ConfigError(f'Cannot delete interface "{vifcname}", still referenced on a flowtable') + def verify(ethernet): + verify_flowtable(ethernet) + if 'deleted' in ethernet: return None if 'is_bond_member' in ethernet: @@ -318,42 +356,25 @@ def verify_ethernet(ethernet): return None def generate(ethernet): - if 'deleted' in ethernet: - return None - - ethernet['frr_zebra_config'] = '' - if 'deleted' not in ethernet: - ethernet['frr_zebra_config'] = render_to_string('frr/evpn.mh.frr.j2', ethernet) - + if 'frr_dict' in ethernet and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(ethernet['frr_dict']) return None def apply(ethernet): - ifname = ethernet['ifname'] - - e = EthernetIf(ifname) + if 'frr_dict' in ethernet and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() + e = EthernetIf(ethernet['ifname']) if 'deleted' in ethernet: - # delete interface e.remove() else: e.update(ethernet) - - zebra_daemon = 'zebra' - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(f'^interface {ifname}', stop_pattern='^exit', remove_stop_mark=True) - if 'frr_zebra_config' in ethernet: - frr_cfg.add_before(frr.default_add_before, ethernet['frr_zebra_config']) - frr_cfg.commit_configuration(zebra_daemon) + return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) - apply(c) except ConfigError as e: print(e) diff --git a/src/conf_mode/interfaces_geneve.py b/src/conf_mode/interfaces_geneve.py index 007708d4a..11b349002 100755 --- a/src/conf_mode/interfaces_geneve.py +++ b/src/conf_mode/interfaces_geneve.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -47,7 +47,7 @@ def get_config(config=None): # GENEVE interfaces are picky and require recreation if certain parameters # change. But a GENEVE interface should - of course - not be re-created if # it's description or IP address is adjusted. Feels somehow logic doesn't it? - for cli_option in ['remote', 'vni', 'parameters']: + for cli_option in ['remote', 'vni', 'parameters', 'port']: if is_node_changed(conf, base + [ifname, cli_option]): geneve.update({'rebuild_required': {}}) diff --git a/src/conf_mode/interfaces_input.py b/src/conf_mode/interfaces_input.py index ad248843d..6ed0bd2cf 100755 --- a/src/conf_mode/interfaces_input.py +++ b/src/conf_mode/interfaces_input.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/interfaces_l2tpv3.py b/src/conf_mode/interfaces_l2tpv3.py index f0a70436e..8eca1cedc 100755 --- a/src/conf_mode/interfaces_l2tpv3.py +++ b/src/conf_mode/interfaces_l2tpv3.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/interfaces_loopback.py b/src/conf_mode/interfaces_loopback.py index a784e9ec2..bac36b7f0 100755 --- a/src/conf_mode/interfaces_loopback.py +++ b/src/conf_mode/interfaces_loopback.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/interfaces_macsec.py b/src/conf_mode/interfaces_macsec.py index 3ede4377a..683e12ec3 100755 --- a/src/conf_mode/interfaces_macsec.py +++ b/src/conf_mode/interfaces_macsec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py index 8c1213e2b..5cc09da89 100755 --- a/src/conf_mode/interfaces_openvpn.py +++ b/src/conf_mode/interfaces_openvpn.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -32,6 +32,7 @@ from vyos.base import DeprecationWarning from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed +from vyos.configdiff import get_config_diff from vyos.configverify import verify_vrf from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mirror_redirect @@ -94,6 +95,23 @@ def get_config(config=None): if 'deleted' in openvpn: return openvpn + if not is_node_changed(conf, base) and dict_search_args(openvpn, 'tls'): + diff = get_config_diff(conf) + if diff.get_child_nodes_diff(['pki'], recursive=True).get('add') == ['ca', 'certificate']: + crl_path = os.path.join(cfg_dir, f'{ifname}_crl.pem') + if os.path.exists(crl_path): + # do not restart service when changed only CRL and crl file already exist + openvpn.update({'no_restart_crl': True}) + for rec in diff.get_child_nodes_diff(['pki', 'ca'], recursive=True).get('add'): + if diff.get_child_nodes_diff(['pki', 'ca', rec], recursive=True).get('add') != ['crl']: + openvpn.update({'no_restart_crl': False}) + break + if openvpn.get('no_restart_crl'): + for rec in diff.get_child_nodes_diff(['pki', 'certificate'], recursive=True).get('add'): + if diff.get_child_nodes_diff(['pki', 'certificate', rec], recursive=True).get('add') != ['revoke']: + openvpn.update({'no_restart_crl': False}) + break + if is_node_changed(conf, base + [ifname, 'openvpn-option']): openvpn.update({'restart_required': {}}) if is_node_changed(conf, base + [ifname, 'enable-dco']): @@ -786,10 +804,12 @@ def apply(openvpn): # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process - action = 'reload-or-restart' - if 'restart_required' in openvpn: - action = 'restart' - call(f'systemctl {action} openvpn@{interface}.service') + + if not openvpn.get('no_restart_crl'): + action = 'reload-or-restart' + if 'restart_required' in openvpn: + action = 'restart' + call(f'systemctl {action} openvpn@{interface}.service') o = VTunIf(**openvpn) o.update(openvpn) diff --git a/src/conf_mode/interfaces_pppoe.py b/src/conf_mode/interfaces_pppoe.py index 412676c7d..976702aba 100755 --- a/src/conf_mode/interfaces_pppoe.py +++ b/src/conf_mode/interfaces_pppoe.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/interfaces_pseudo-ethernet.py b/src/conf_mode/interfaces_pseudo-ethernet.py index 446beffd3..6edd3f248 100755 --- a/src/conf_mode/interfaces_pseudo-ethernet.py +++ b/src/conf_mode/interfaces_pseudo-ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -27,6 +27,7 @@ from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_source_interface from vyos.configverify import verify_vlan_config from vyos.configverify import verify_mtu_parent +from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_mirror_redirect from vyos.ifconfig import MACVLANIf from vyos.utils.network import interface_exists @@ -71,6 +72,7 @@ def verify(peth): verify_vrf(peth) verify_address(peth) verify_mtu_parent(peth, peth['parent']) + verify_mtu_ipv6(peth) verify_mirror_redirect(peth) # use common function to verify VLAN configuration verify_vlan_config(peth) diff --git a/src/conf_mode/interfaces_sstpc.py b/src/conf_mode/interfaces_sstpc.py index b9d7a74fb..276fab4f9 100755 --- a/src/conf_mode/interfaces_sstpc.py +++ b/src/conf_mode/interfaces_sstpc.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/interfaces_tunnel.py b/src/conf_mode/interfaces_tunnel.py index 98ef98d12..4cf5ef115 100755 --- a/src/conf_mode/interfaces_tunnel.py +++ b/src/conf_mode/interfaces_tunnel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 yOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -13,9 +13,8 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - from sys import exit - +import ipaddress from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed @@ -89,6 +88,13 @@ def verify(tunnel): raise ConfigError('Tunnel used for NHRP, it can not be deleted!') return None + if 'nhrp' in tunnel: + if 'address' in tunnel: + address_list = dict_search('address', tunnel) + for tunip in address_list: + if ipaddress.ip_network(tunip, strict=False).prefixlen != 32: + raise ConfigError( + 'Tunnel is used for NHRP, Netmask should be /32!') verify_tunnel(tunnel) diff --git a/src/conf_mode/interfaces_virtual-ethernet.py b/src/conf_mode/interfaces_virtual-ethernet.py index cb6104f59..f85c3d1de 100755 --- a/src/conf_mode/interfaces_virtual-ethernet.py +++ b/src/conf_mode/interfaces_virtual-ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -23,6 +23,7 @@ from vyos.configdict import get_interface_dict from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_vrf +from vyos.configverify import verify_mtu_ipv6 from vyos.ifconfig import VethIf from vyos.utils.network import interface_exists airbag.enable() @@ -62,6 +63,7 @@ def verify(veth): return None verify_vrf(veth) + verify_mtu_ipv6(veth) verify_address(veth) if 'peer_name' not in veth: diff --git a/src/conf_mode/interfaces_vti.py b/src/conf_mode/interfaces_vti.py index 20629c6c1..0461c4f68 100755 --- a/src/conf_mode/interfaces_vti.py +++ b/src/conf_mode/interfaces_vti.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -20,6 +20,7 @@ from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_vrf +from vyos.configverify import verify_mtu_ipv6 from vyos.ifconfig import VTIIf from vyos import ConfigError from vyos import airbag @@ -40,6 +41,7 @@ def get_config(config=None): def verify(vti): verify_vrf(vti) + verify_mtu_ipv6(vti) verify_mirror_redirect(vti) return None diff --git a/src/conf_mode/interfaces_vxlan.py b/src/conf_mode/interfaces_vxlan.py index 68646e8ff..34eb3ef5c 100755 --- a/src/conf_mode/interfaces_vxlan.py +++ b/src/conf_mode/interfaces_vxlan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -95,6 +95,8 @@ def verify(vxlan): if 'group' in vxlan: if 'source_interface' not in vxlan: raise ConfigError('Multicast VXLAN requires an underlaying interface') + if 'remote' in vxlan: + raise ConfigError('Both group and remote cannot be specified') verify_source_interface(vxlan) if not any(tmp in ['group', 'remote', 'source_address', 'source_interface'] for tmp in vxlan): diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py index 7abdfdbfa..33d782ec6 100755 --- a/src/conf_mode/interfaces_wireguard.py +++ b/src/conf_mode/interfaces_wireguard.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,11 +14,17 @@ # 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 glob import glob from sys import exit from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed +from vyos.configdict import is_source_interface +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete @@ -29,6 +35,7 @@ from vyos.ifconfig import WireGuardIf from vyos.utils.kernel import check_kmod from vyos.utils.network import check_port_availability from vyos.utils.network import is_wireguard_key_pair +from vyos.utils.process import call from vyos import ConfigError from vyos import airbag airbag.enable() @@ -54,11 +61,31 @@ def get_config(config=None): if is_node_changed(conf, base + [ifname, 'peer']): wireguard.update({'rebuild_required': {}}) + wireguard['peers_need_resolve'] = [] + if 'peer' in wireguard: + for peer, peer_config in wireguard['peer'].items(): + if 'disable' not in peer_config and 'host_name' in peer_config: + wireguard['peers_need_resolve'].append(peer) + + # Check if interface is used as source-interface on VXLAN interface + tmp = is_source_interface(conf, ifname, 'vxlan') + if tmp: + if 'deleted' not in wireguard: + set_dependents('vxlan', conf, tmp) + else: + wireguard['is_source_interface'] = tmp + return wireguard + def verify(wireguard): if 'deleted' in wireguard: verify_bridge_delete(wireguard) + if 'is_source_interface' in wireguard: + raise ConfigError( + f'Interface "{wireguard["ifname"]}" cannot be deleted as it is used ' + f'as source interface for "{wireguard["is_source_interface"]}"!' + ) return None verify_mtu_ipv6(wireguard) @@ -70,62 +97,85 @@ def verify(wireguard): if 'private_key' not in wireguard: raise ConfigError('Wireguard private-key not defined') - if 'peer' not in wireguard: - raise ConfigError('At least one Wireguard peer is required!') - if 'port' in wireguard and 'port_changed' in wireguard: listen_port = int(wireguard['port']) - if check_port_availability('0.0.0.0', listen_port, 'udp') is not True: + if check_port_availability(None, listen_port, protocol='udp') is not True: raise ConfigError(f'UDP port {listen_port} is busy or unavailable and ' 'cannot be used for the interface!') # run checks on individual configured WireGuard peer - public_keys = [] - for tmp in wireguard['peer']: - peer = wireguard['peer'][tmp] + if 'peer' in wireguard: + public_keys = [] + for tmp in wireguard['peer']: + peer = wireguard['peer'][tmp] - if 'allowed_ips' not in peer: - raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!') + base_error = f'WireGuard peer "{tmp}":' - if 'public_key' not in peer: - raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!') + if 'host_name' in peer and 'address' in peer: + raise ConfigError(f'{base_error} address/host-name are mutually exclusive!') - if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer): - raise ConfigError('Both Wireguard port and address must be defined ' - f'for peer "{tmp}" if either one of them is set!') + if 'allowed_ips' not in peer: + raise ConfigError(f'{base_error} missing mandatory allowed-ips!') - if peer['public_key'] in public_keys: - raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"') + if 'public_key' not in peer: + raise ConfigError(f'{base_error} missing mandatory public-key!') - if 'disable' not in peer: - if is_wireguard_key_pair(wireguard['private_key'], peer['public_key']): - raise ConfigError(f'Peer "{tmp}" has the same public key as the interface "{wireguard["ifname"]}"') + if peer['public_key'] in public_keys: + raise ConfigError(f'{base_error} duplicate public-key!') + + if 'disable' not in peer: + if is_wireguard_key_pair(wireguard['private_key'], peer['public_key']): + tmp = wireguard["ifname"] + raise ConfigError(f'{base_error} identical public key as interface "{tmp}"!') + + port_addr_error = f'{base_error} both port and address/host-name must '\ + 'be defined if either one of them is set!' + if 'port' not in peer: + if 'host_name' in peer or 'address' in peer: + raise ConfigError(port_addr_error) + else: + if 'host_name' not in peer and 'address' not in peer: + raise ConfigError(port_addr_error) + + public_keys.append(peer['public_key']) - 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, - # by deleting the entire interface this is the shortcut instead of parsing - # out all peers and removing them one by one. - # - # Peer reconfiguration will always come with a short downtime while the - # WireGuard interface is recreated (see below) - wg.remove() + wg = WireGuardIf(**wireguard) - # Create the new interface if required - if 'deleted' not in wireguard: - wg = WireGuardIf(**wireguard) + if 'deleted' in wireguard: + wg.remove() + else: wg.update(wireguard) + domain_resolver_usage = '/run/use-vyos-domain-resolver-interfaces-wireguard-' + wireguard['ifname'] + + ## DOMAIN RESOLVER + domain_action = 'restart' + if 'peers_need_resolve' in wireguard and len(wireguard['peers_need_resolve']) > 0 and 'disable' not in wireguard: + from vyos.utils.file import write_file + + text = f'# Automatically generated by interfaces_wireguard.py\nThis file indicates that vyos-domain-resolver service is used by the interfaces_wireguard.\n' + text += "interfaces:\n" + "".join([f" - {peer}\n" for peer in wireguard['peers_need_resolve']]) + write_file(domain_resolver_usage, text) + else: + if os.path.exists(domain_resolver_usage): + os.unlink(domain_resolver_usage) + if not glob('/run/use-vyos-domain-resolver*'): + domain_action = 'stop' + call(f'systemctl {domain_action} vyos-domain-resolver.service') + + call_dependents() + return None + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py index d24675ee6..b3b909046 100755 --- a/src/conf_mode/interfaces_wireless.py +++ b/src/conf_mode/interfaces_wireless.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/interfaces_wwan.py b/src/conf_mode/interfaces_wwan.py index 230eb14d6..ddddbe5e8 100755 --- a/src/conf_mode/interfaces_wwan.py +++ b/src/conf_mode/interfaces_wwan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -26,8 +26,10 @@ from vyos.configverify import verify_authentication from vyos.configverify import verify_interface_exists from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_vrf +from vyos.configverify import verify_mtu_ipv6 from vyos.ifconfig import WWANIf from vyos.utils.dict import dict_search +from vyos.utils.network import is_wwan_connected from vyos.utils.process import cmd from vyos.utils.process import call from vyos.utils.process import DEVNULL @@ -98,6 +100,7 @@ def verify(wwan): verify_interface_exists(wwan, ifname) verify_authentication(wwan) verify_vrf(wwan) + verify_mtu_ipv6(wwan) verify_mirror_redirect(wwan) return None @@ -135,7 +138,7 @@ def apply(wwan): break sleep(0.250) - if 'shutdown_required' in wwan: + if 'shutdown_required' in wwan or (not is_wwan_connected(wwan['ifname'])): # we only need the modem number. wwan0 -> 0, wwan1 -> 1 modem = wwan['ifname'].lstrip('wwan') base_cmd = f'mmcli --modem {modem}' @@ -157,7 +160,7 @@ def apply(wwan): return None - if 'shutdown_required' in wwan: + if 'shutdown_required' in wwan or (not is_wwan_connected(wwan['ifname'])): ip_type = 'ipv4' slaac = dict_search('ipv6.address.autoconf', wwan) != None if 'address' in wwan: diff --git a/src/conf_mode/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_haproxy.py index 17226efe9..4c26ed0b4 100755..100644 --- a/src/conf_mode/load-balancing_reverse-proxy.py +++ b/src/conf_mode/load-balancing_haproxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -19,6 +19,7 @@ import os from sys import exit from shutil import rmtree +from vyos.defaults import systemd_services from vyos.config import Config from vyos.configverify import verify_pki_certificate from vyos.configverify import verify_pki_ca_certificate @@ -39,7 +40,6 @@ airbag.enable() load_balancing_dir = '/run/haproxy' load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg' -systemd_service = 'haproxy.service' systemd_override = '/run/systemd/system/haproxy.service.d/10-override.conf' def get_config(config=None): @@ -48,7 +48,7 @@ def get_config(config=None): else: conf = Config() - base = ['load-balancing', 'reverse-proxy'] + base = ['load-balancing', 'haproxy'] if not conf.exists(base): return None lb = conf.get_config_dict(base, @@ -65,29 +65,39 @@ def verify(lb): return None if 'backend' not in lb or 'service' not in lb: - raise ConfigError(f'"service" and "backend" must be configured!') + raise ConfigError('Both "service" and "backend" must be configured!') for front, front_config in lb['service'].items(): if 'port' not in front_config: raise ConfigError(f'"{front} service port" must be configured!') # Check if bind address:port are used by another service - tmp_address = front_config.get('address', '0.0.0.0') + tmp_address = front_config.get('address', None) tmp_port = front_config['port'] if check_port_availability(tmp_address, int(tmp_port), 'tcp') is not True and \ not is_listen_port_bind_service(int(tmp_port), 'haproxy'): - raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service') + raise ConfigError(f'TCP port "{tmp_port}" is used by another service') + + if 'http_compression' in front_config: + if front_config['mode'] != 'http': + raise ConfigError(f'service {front} must be set to http mode to use http-compression!') + if len(front_config['http_compression']['mime_type']) == 0: + raise ConfigError(f'service {front} must have at least one mime-type configured to use' + f'http_compression!') + + for cert in dict_search('ssl.certificate', front_config) or []: + verify_pki_certificate(lb, cert) for back, back_config in lb['backend'].items(): if 'http_check' in back_config: http_check = back_config['http_check'] if 'expect' in http_check and 'status' in http_check['expect'] and 'string' in http_check['expect']: - raise ConfigError(f'"expect status" and "expect string" can not be configured together!') + raise ConfigError('"expect status" and "expect string" can not be configured together!') if 'health_check' in back_config: if back_config['mode'] != 'tcp': raise ConfigError(f'backend "{back}" can only be configured with {back_config["health_check"]} ' + - f'health-check whilst in TCP mode!') + 'health-check whilst in TCP mode!') if 'http_check' in back_config: raise ConfigError(f'backend "{back}" cannot be configured with both http-check and health-check!') @@ -105,20 +115,15 @@ def verify(lb): if {'no_verify', 'ca_certificate'} <= set(back_config['ssl']): raise ConfigError(f'backend {back} cannot have both ssl options no-verify and ca-certificate set!') + tmp = dict_search('ssl.ca_certificate', back_config) + if tmp: verify_pki_ca_certificate(lb, tmp) + # Check if http-response-headers are configured in any frontend/backend where mode != http for group in ['service', 'backend']: for config_name, config in lb[group].items(): if 'http_response_headers' in config and config['mode'] != 'http': raise ConfigError(f'{group} {config_name} must be set to http mode to use http_response_headers!') - for front, front_config in lb['service'].items(): - for cert in dict_search('ssl.certificate', front_config) or []: - verify_pki_certificate(lb, cert) - - for back, back_config in lb['backend'].items(): - tmp = dict_search('ssl.ca_certificate', back_config) - if tmp: verify_pki_ca_certificate(lb, tmp) - def generate(lb): if not lb: @@ -186,12 +191,11 @@ def generate(lb): return None def apply(lb): + action = 'stop' + if lb: + action = 'reload-or-restart' call('systemctl daemon-reload') - if not lb: - call(f'systemctl stop {systemd_service}') - else: - call(f'systemctl reload-or-restart {systemd_service}') - + call(f'systemctl {action} {systemd_services["haproxy"]}') return None diff --git a/src/conf_mode/load-balancing_wan.py b/src/conf_mode/load-balancing_wan.py index 5da0b906b..dc7f1c838 100755 --- a/src/conf_mode/load-balancing_wan.py +++ b/src/conf_mode/load-balancing_wan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,24 +14,16 @@ # 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 sys import exit -from shutil import rmtree -from vyos.base import Warning from vyos.config import Config from vyos.configdep import set_dependents, call_dependents from vyos.utils.process import cmd -from vyos.template import render from vyos import ConfigError from vyos import airbag airbag.enable() -load_balancing_dir = '/run/load-balance' -load_balancing_conf_file = f'{load_balancing_dir}/wlb.conf' -systemd_service = 'vyos-wan-load-balance.service' - +service = 'vyos-wan-load-balance.service' def get_config(config=None): if config: @@ -40,6 +32,7 @@ def get_config(config=None): conf = Config() base = ['load-balancing', 'wan'] + lb = conf.get_config_dict(base, key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True, @@ -59,87 +52,61 @@ def verify(lb): if not lb: return None - if 'interface_health' not in lb: - raise ConfigError( - 'A valid WAN load-balance configuration requires an interface with a nexthop!' - ) - - for interface, interface_config in lb['interface_health'].items(): - if 'nexthop' not in interface_config: - raise ConfigError( - f'interface-health {interface} nexthop must be specified!') - - if 'test' in interface_config: - for test_rule, test_config in interface_config['test'].items(): - if 'type' in test_config: - if test_config['type'] == 'user-defined' and 'test_script' not in test_config: - raise ConfigError( - f'test {test_rule} script must be defined for test-script!' - ) - - if 'rule' not in lb: - Warning( - 'At least one rule with an (outbound) interface must be defined for WAN load balancing to be active!' - ) + if 'interface_health' in lb: + for ifname, health_conf in lb['interface_health'].items(): + if 'nexthop' not in health_conf: + raise ConfigError(f'Nexthop must be configured for interface {ifname}') + + if 'test' not in health_conf: + continue + + for test_id, test_conf in health_conf['test'].items(): + if 'type' not in test_conf: + raise ConfigError(f'No type configured for health test on interface {ifname}') + + if test_conf['type'] == 'user-defined' and 'test_script' not in test_conf: + raise ConfigError(f'Missing user-defined script for health test on interface {ifname}') else: - for rule, rule_config in lb['rule'].items(): - if 'inbound_interface' not in rule_config: - raise ConfigError(f'rule {rule} inbound-interface must be specified!') - if {'failover', 'exclude'} <= set(rule_config): - raise ConfigError(f'rule {rule} failover cannot be configured with exclude!') - if {'limit', 'exclude'} <= set(rule_config): - raise ConfigError(f'rule {rule} limit cannot be used with exclude!') - if 'interface' not in rule_config: - if 'exclude' not in rule_config: - Warning( - f'rule {rule} will be inactive because no (outbound) interfaces have been defined for this rule' - ) - for direction in {'source', 'destination'}: - if direction in rule_config: - if 'protocol' in rule_config and 'port' in rule_config[ - direction]: - if rule_config['protocol'] not in {'tcp', 'udp'}: - raise ConfigError('ports can only be specified when protocol is "tcp" or "udp"') + raise ConfigError('Interface health tests must be configured') + if 'rule' in lb: + for rule_id, rule_conf in lb['rule'].items(): + if 'interface' not in rule_conf and 'exclude' not in rule_conf: + raise ConfigError(f'Interface or exclude not specified on load-balancing wan rule {rule_id}') -def generate(lb): - if not lb: - # Delete /run/load-balance/wlb.conf - if os.path.isfile(load_balancing_conf_file): - os.unlink(load_balancing_conf_file) - # Delete old directories - if os.path.isdir(load_balancing_dir): - rmtree(load_balancing_dir, ignore_errors=True) - if os.path.exists('/var/run/load-balance/wlb.out'): - os.unlink('/var/run/load-balance/wlb.out') + if 'failover' in rule_conf and 'exclude' in rule_conf: + raise ConfigError(f'Failover cannot be configured with exclude on load-balancing wan rule {rule_id}') - return None + if 'limit' in rule_conf: + if 'exclude' in rule_conf: + raise ConfigError(f'Limit cannot be configured with exclude on load-balancing wan rule {rule_id}') - # Create load-balance dir - if not os.path.isdir(load_balancing_dir): - os.mkdir(load_balancing_dir) + if 'rate' in rule_conf['limit'] and 'period' not in rule_conf['limit']: + raise ConfigError(f'Missing "limit period" on load-balancing wan rule {rule_id}') - render(load_balancing_conf_file, 'load-balancing/wlb.conf.j2', lb) + if 'period' in rule_conf['limit'] and 'rate' not in rule_conf['limit']: + raise ConfigError(f'Missing "limit rate" on load-balancing wan rule {rule_id}') - return None + for direction in ['source', 'destination']: + if direction in rule_conf: + if 'port' in rule_conf[direction]: + if 'protocol' not in rule_conf: + raise ConfigError(f'Protocol required to specify port on load-balancing wan rule {rule_id}') + + if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']: + raise ConfigError(f'Protocol must be tcp, udp or tcp_udp to specify port on load-balancing wan rule {rule_id}') +def generate(lb): + return None def apply(lb): if not lb: - try: - cmd(f'systemctl stop {systemd_service}') - except Exception as e: - print(f"Error message: {e}") - + cmd(f'sudo systemctl stop {service}') else: - cmd('sudo sysctl -w net.netfilter.nf_conntrack_acct=1') - cmd(f'systemctl restart {systemd_service}') + cmd(f'sudo systemctl restart {service}') call_dependents() - return None - - if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 39803fa02..8763da886 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -16,20 +16,22 @@ import os +from glob import glob from sys import exit from vyos.base import Warning from vyos.config import Config from vyos.configdep import set_dependents, call_dependents from vyos.template import render -from vyos.template import is_ip_network from vyos.utils.kernel import check_kmod from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args +from vyos.utils.file import write_file from vyos.utils.process import cmd from vyos.utils.process import run -from vyos.utils.network import is_addr_assigned +from vyos.utils.process import call from vyos.utils.network import interface_exists +from vyos.firewall import fqdn_config_parse from vyos import ConfigError from vyos import airbag @@ -39,6 +41,7 @@ k_mod = ['nft_nat', 'nft_chain_nat'] nftables_nat_config = '/run/nftables_nat.conf' nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft' +domain_resolver_usage = '/run/use-vyos-domain-resolver-nat' valid_groups = [ 'address_group', @@ -71,6 +74,8 @@ def get_config(config=None): if 'dynamic_group' in nat['firewall_group']: del nat['firewall_group']['dynamic_group'] + fqdn_config_parse(nat, 'nat') + return nat def verify_rule(config, err_msg, groups_dict): @@ -169,12 +174,6 @@ def verify(nat): if 'exclude' not in config and 'backend' not in config['load_balance']: raise ConfigError(f'{err_msg} translation requires address and/or port') - addr = dict_search('translation.address', config) - if addr != None and addr != 'masquerade' and not is_ip_network(addr): - for ip in addr.split('-'): - if not is_addr_assigned(ip): - Warning(f'IP address {ip} does not exist on the system!') - # common rule verification verify_rule(config, err_msg, nat['firewall_group']) @@ -251,6 +250,19 @@ def apply(nat): call_dependents() + # DOMAIN RESOLVER + if nat and 'deleted' not in nat: + domain_action = 'restart' + if nat['ip_fqdn'].items(): + text = f'# Automatically generated by nat.py\nThis file indicates that vyos-domain-resolver service is used by nat.\n' + write_file(domain_resolver_usage, text) + elif os.path.exists(domain_resolver_usage): + os.unlink(domain_resolver_usage) + + if not glob('/run/use-vyos-domain-resolver*'): + domain_action = 'stop' + call(f'systemctl {domain_action} vyos-domain-resolver.service') + return None if __name__ == '__main__': diff --git a/src/conf_mode/nat64.py b/src/conf_mode/nat64.py index df501ce7f..4f8853661 100755 --- a/src/conf_mode/nat64.py +++ b/src/conf_mode/nat64.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index 95dfae3a5..aea187d18 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -92,6 +92,10 @@ def verify(nat): if prefix != None: if not is_ipv6(prefix): raise ConfigError(f'{err_msg} source-prefix not specified') + + if 'destination' in config and 'group' in config['destination']: + if len({'address_group', 'network_group', 'domain_group'} & set(config['destination']['group'])) > 1: + raise ConfigError('Only one address-group, network-group or domain-group can be specified') if dict_search('destination.rule', nat): for rule, config in dict_search('destination.rule', nat).items(): diff --git a/src/conf_mode/nat_cgnat.py b/src/conf_mode/nat_cgnat.py index 3484e5873..312688b53 100755 --- a/src/conf_mode/nat_cgnat.py +++ b/src/conf_mode/nat_cgnat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/netns.py b/src/conf_mode/netns.py index b57e46a0d..5a3c4e7fa 100755 --- a/src/conf_mode/netns.py +++ b/src/conf_mode/netns.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index 215b22b37..fa6958130 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -19,6 +19,7 @@ import os from sys import argv from sys import exit +from vyos.base import Message from vyos.config import Config from vyos.config import config_dict_merge from vyos.configdep import set_dependents @@ -27,6 +28,9 @@ from vyos.configdict import node_changed from vyos.configdiff import Diff from vyos.configdiff import get_config_diff from vyos.defaults import directories +from vyos.defaults import internal_ports +from vyos.defaults import systemd_services +from vyos.pki import encode_certificate from vyos.pki import is_ca_certificate from vyos.pki import load_certificate from vyos.pki import load_public_key @@ -36,17 +40,22 @@ from vyos.pki import load_private_key from vyos.pki import load_crl from vyos.pki import load_dh_parameters from vyos.utils.boot import boot_configuration_complete +from vyos.utils.configfs import add_cli_node 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 read_file +from vyos.utils.network import check_port_availability from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.process import is_systemd_service_active +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError from vyos import airbag airbag.enable() vyos_certbot_dir = directories['certbot'] +vyos_ca_certificates_dir = directories['ca_certificates'] # keys to recursively search for under specified path sync_search = [ @@ -55,6 +64,10 @@ sync_search = [ 'path': ['service', 'https'], }, { + 'keys': ['key'], + 'path': ['service', 'ssh'], + }, + { 'keys': ['certificate', 'ca_certificate'], 'path': ['interfaces', 'ethernet'], }, @@ -68,7 +81,7 @@ sync_search = [ }, { 'keys': ['certificate', 'ca_certificate'], - 'path': ['load_balancing', 'reverse_proxy'], + 'path': ['load_balancing', 'haproxy'], }, { 'keys': ['key'], @@ -124,8 +137,20 @@ def certbot_request(name: str, config: dict, dry_run: bool=True): f'--standalone --agree-tos --no-eff-email --expand --server {config["url"]} '\ f'--email {config["email"]} --key-type rsa --rsa-key-size {config["rsa_key_size"]} '\ f'{domains}' + + listen_address = None if 'listen_address' in config: - tmp += f' --http-01-address {config["listen_address"]}' + listen_address = config['listen_address'] + + # When ACME is used behind a reverse proxy, we always bind to localhost + # whatever the CLI listen-address is configured for. + if ('used_by' in config and 'haproxy' in config['used_by'] and + is_systemd_service_running(systemd_services['haproxy']) and + not check_port_availability(listen_address, 80)): + tmp += f' --http-01-address 127.0.0.1 --http-01-port {internal_ports["certbot_haproxy"]}' + elif listen_address: + tmp += f' --http-01-address {listen_address}' + # verify() does not need to actually request a cert but only test for plausability if dry_run: tmp += ' --dry-run' @@ -146,35 +171,19 @@ def get_config(config=None): if len(argv) > 1 and argv[1] == 'certbot_renew': pki['certbot_renew'] = {} - tmp = node_changed(conf, base + ['ca'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) - if tmp: - if 'changed' not in pki: pki.update({'changed':{}}) - pki['changed'].update({'ca' : tmp}) - - tmp = node_changed(conf, base + ['certificate'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) - if tmp: - if 'changed' not in pki: pki.update({'changed':{}}) - pki['changed'].update({'certificate' : tmp}) - - tmp = node_changed(conf, base + ['dh'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) - if tmp: - if 'changed' not in pki: pki.update({'changed':{}}) - pki['changed'].update({'dh' : tmp}) - - tmp = node_changed(conf, base + ['key-pair'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) - if tmp: - if 'changed' not in pki: pki.update({'changed':{}}) - pki['changed'].update({'key_pair' : tmp}) - - tmp = node_changed(conf, base + ['openssh'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) - if tmp: - if 'changed' not in pki: pki.update({'changed':{}}) - pki['changed'].update({'openssh' : tmp}) - tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) - if tmp: - if 'changed' not in pki: pki.update({'changed':{}}) - pki['changed'].update({'openvpn' : tmp}) + # Walk through the list of sync_translate mapping and build a list + # which is later used to check if the node was changed in the CLI config + changed_keys = [] + for value in sync_translate.values(): + if value not in changed_keys: + changed_keys.append(value) + # Check for changes to said given keys in the CLI config + for key in changed_keys: + tmp = node_changed(conf, base + [key], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) + if 'changed' not in pki: + pki.update({'changed':{}}) + pki['changed'].update({key.replace('-', '_') : tmp}) # We only merge on the defaults of there is a configuration at all if conf.exists(base): @@ -235,8 +244,8 @@ def get_config(config=None): continue path = search['path'] - path_str = ' '.join(path + found_path) - #print(f'PKI: Updating config: {path_str} {item_name}') + path_str = ' '.join(path + found_path).replace('_','-') + Message(f'Updating configuration: "{path_str} {item_name}"') if path[0] == 'interfaces': ifname = found_path[0] @@ -246,6 +255,24 @@ def get_config(config=None): if not D.node_changed_presence(path): set_dependents(path[1], conf) + # Check PKI certificates if they are auto-generated by ACME. If they are, + # traverse the current configuration and determine the service where the + # certificate is used by. + # Required to check if we might need to run certbot behing a reverse proxy. + if 'certificate' in pki: + for name, cert_config in pki['certificate'].items(): + if 'acme' not in cert_config: + continue + if not dict_search('system.load_balancing.haproxy', pki): + continue + used_by = [] + for cert_list, _ in dict_search_recursive( + pki['system']['load_balancing']['haproxy'], 'certificate'): + if name in cert_list: + used_by.append('haproxy') + if used_by: + pki['certificate'][name]['acme'].update({'used_by': used_by}) + return pki def is_valid_certificate(raw_data): @@ -337,6 +364,15 @@ def verify(pki): raise ConfigError(f'An email address is required to request '\ f'certificate for "{name}" via ACME!') + listen_address = None + if 'listen_address' in cert_conf['acme']: + listen_address = cert_conf['acme']['listen_address'] + + if 'used_by' not in cert_conf['acme']: + if not check_port_availability(listen_address, 80): + raise ConfigError('Port 80 is already in use and not available '\ + f'to provide ACME challenge for "{name}"!') + if 'certbot_renew' not in pki: # Only run the ACME command if something on this entity changed, # as this is time intensive @@ -382,7 +418,8 @@ def verify(pki): if 'country' in default_values: country = default_values['country'] if len(country) != 2 or not country.isalpha(): - raise ConfigError(f'Invalid default country value. Value must be 2 alpha characters.') + raise ConfigError('Invalid default country value. '\ + 'Value must be 2 alpha characters.') if 'changed' in pki: # if the list is getting longer, we can move to a dict() and also embed the @@ -390,34 +427,65 @@ def verify(pki): for search in sync_search: for key in search['keys']: changed_key = sync_translate[key] - if changed_key not in pki['changed']: continue - for item_name in pki['changed'][changed_key]: node_present = False if changed_key == 'openvpn': node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) else: node_present = dict_search_args(pki, changed_key, item_name) + # If the node is still present, we can skip the check + # as we are not deleting it + if node_present: + continue - if not node_present: - search_dict = dict_search_args(pki['system'], *search['path']) - - if not search_dict: - continue + search_dict = dict_search_args(pki['system'], *search['path']) + if not search_dict: + continue - for found_name, found_path in dict_search_recursive(search_dict, key): - if found_name == item_name: - path_str = " ".join(search['path'] + found_path) - raise ConfigError(f'PKI object "{item_name}" still in use by "{path_str}"') + for found_name, found_path in dict_search_recursive(search_dict, key): + # Check if the name matches either by string compare, or beeing + # part of a list + if ((isinstance(found_name, str) and found_name == item_name) or + (isinstance(found_name, list) and item_name in found_name)): + # We do not support _ in CLI paths - this is only a convenience + # as we mangle all - to _, now it's time to reverse this! + path_str = ' '.join(search['path'] + found_path).replace('_','-') + object = changed_key.replace('_','-') + tmp = f'Embedded PKI {object} with name "{item_name}" is still '\ + f'in use by CLI path "{path_str}"' + raise ConfigError(tmp) return None +def cleanup_system_ca(): + if not os.path.exists(vyos_ca_certificates_dir): + os.mkdir(vyos_ca_certificates_dir) + else: + for filename in os.listdir(vyos_ca_certificates_dir): + full_path = os.path.join(vyos_ca_certificates_dir, filename) + if os.path.isfile(full_path): + os.unlink(full_path) + def generate(pki): if not pki: + cleanup_system_ca() return None + # Create or cleanup CA install directory + if 'changed' in pki and 'ca' in pki['changed']: + cleanup_system_ca() + + if 'ca' in pki: + for ca, ca_conf in pki['ca'].items(): + if 'system_install' in ca_conf: + ca_obj = load_certificate(ca_conf['certificate']) + ca_path = os.path.join(vyos_ca_certificates_dir, f'{ca}.crt') + + with open(ca_path, 'w') as f: + f.write(encode_certificate(ca_obj)) + # Certbot renewal only needs to re-trigger the services to load up the # new PEM file if 'certbot_renew' in pki: @@ -433,22 +501,58 @@ def generate(pki): for name, cert_conf in pki['certificate'].items(): if 'acme' in cert_conf: certbot_list.append(name) - # generate certificate if not found on disk + # There is no ACME/certbot managed certificate presend on the + # system, generate it if name not in certbot_list_on_disk: certbot_request(name, cert_conf['acme'], dry_run=False) + # Now that the certificate was properly generated we have + # the PEM files on disk. We need to add the certificate to + # certbot_list_on_disk to automatically import the CA chain + certbot_list_on_disk.append(name) + # We alredy had an ACME managed certificate on the system, but + # something changed in the configuration elif changed_certificates != None and name in changed_certificates: - # when something for the certificate changed, we should delete it + # Delete old ACME certificate first if name in certbot_list_on_disk: certbot_delete(name) + # Request new certificate via certbot certbot_request(name, cert_conf['acme'], dry_run=False) # Cleanup certbot configuration and certificates if no longer in use by CLI # Get foldernames under vyos_certbot_dir which each represent a certbot cert if os.path.exists(f'{vyos_certbot_dir}/live'): for cert in certbot_list_on_disk: + # ACME certificate is no longer in use by CLI remove it if cert not in certbot_list: - # certificate is no longer active on the CLI - remove it certbot_delete(cert) + continue + # ACME not enabled for individual certificate - bail out early + if 'acme' not in pki['certificate'][cert]: + continue + + # Read in ACME certificate chain information + tmp = read_file(f'{vyos_certbot_dir}/live/{cert}/chain.pem') + tmp = load_certificate(tmp, wrap_tags=False) + cert_chain_base64 = "".join(encode_certificate(tmp).strip().split("\n")[1:-1]) + + # Check if CA chain certificate is already present on CLI to avoid adding + # a duplicate. This only checks for manual added CA certificates and not + # auto added ones with the AUTOCHAIN_ prefix + autochain_prefix = 'AUTOCHAIN_' + ca_cert_present = False + if 'ca' in pki: + for ca_base64, cli_path in dict_search_recursive(pki['ca'], 'certificate'): + # Ignore automatic added CA certificates + if any(item.startswith(autochain_prefix) for item in cli_path): + continue + if cert_chain_base64 == ca_base64: + ca_cert_present = True + + if not ca_cert_present: + tmp = dict_search_args(pki, 'ca', f'{autochain_prefix}{cert}', 'certificate') + if not bool(tmp) or tmp != cert_chain_base64: + Message(f'Add/replace automatically imported CA certificate for "{cert}" ...') + add_cli_node(['pki', 'ca', f'{autochain_prefix}{cert}', 'certificate'], value=cert_chain_base64) return None @@ -456,6 +560,7 @@ def apply(pki): systemd_certbot_name = 'certbot.timer' if not pki: call(f'systemctl stop {systemd_certbot_name}') + call('update-ca-certificates') return None has_certbot = False @@ -473,6 +578,10 @@ def apply(pki): if 'changed' in pki: call_dependents() + # Rebuild ca-certificates bundle + if 'ca' in pki['changed']: + call('update-ca-certificates') + return None if __name__ == '__main__': diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py index a5963e72c..84962c807 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,18 +14,30 @@ # 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 re from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.template import render_to_string +from vyos.configverify import has_frr_protocol_in_dict +from vyos.frrender import FRRender +from vyos.frrender import frr_protocols +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError -from vyos import frr +from vyos.base import Warning from vyos import airbag - airbag.enable() +# Sanity checks for large-community-list regex: +# * Require complete 3-tuples, no blank members. Catch missed & doubled colons. +# * Permit appropriate community separators (whitespace, underscore) +# * Permit common regex between tuples while requiring at least one separator +# (eg, "1:1:1_.*_4:4:4", matching "1:1:1 4:4:4" and "1:1:1 2:2:2 4:4:4", +# but not "1:1:13 24:4:4") +# Best practice: stick with basic patterns, mind your wildcards and whitespace. +# Regex that doesn't match this pattern will be allowed with a warning. +large_community_regex_pattern = r'([^: _]+):([^: _]+):([^: _]+)([ _]([^:]+):([^: _]+):([^: _]+))*' def community_action_compatibility(actions: dict) -> bool: """ @@ -87,31 +99,27 @@ def get_config(config=None): else: conf = Config() - base = ['policy'] - policy = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['protocols'], key_mangling=('-', '_'), - no_tag_node_value_mangle=True) - # Merge policy dict into "regular" config dict - policy = dict_merge(tmp, policy) - return policy - - -def verify(policy): - if not policy: + return get_frrender_dict(conf) + + +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'policy'): return None - for policy_type in ['access_list', 'access_list6', 'as_path_list', - 'community_list', 'extcommunity_list', - 'large_community_list', - 'prefix_list', 'prefix_list6', 'route_map']: + policy_types = ['access_list', 'access_list6', 'as_path_list', + 'community_list', 'extcommunity_list', + 'large_community_list', 'prefix_list', + 'prefix_list6', 'route_map'] + + policy = config_dict['policy'] + for protocol in frr_protocols: + if protocol not in config_dict: + continue + if 'protocol' not in policy: + policy.update({'protocol': {}}) + policy['protocol'].update({protocol : config_dict[protocol]}) + + for policy_type in policy_types: # Bail out early and continue with next policy type if policy_type not in policy: continue @@ -151,6 +159,10 @@ def verify(policy): if 'regex' not in rule_config: raise ConfigError(f'A regex {mandatory_error}') + if policy_type == 'large_community_list': + if not re.fullmatch(large_community_regex_pattern, rule_config['regex']): + Warning(f'"policy large-community-list {instance} rule {rule} regex" does not follow expected form and may not match as expected.') + if policy_type in ['prefix_list', 'prefix_list6']: if 'prefix' not in rule_config: raise ConfigError(f'A prefix {mandatory_error}') @@ -246,72 +258,36 @@ def verify(policy): # When the "routing policy" changes and policies, route-maps etc. are deleted, # it is our responsibility to verify that the policy can not be deleted if it # is used by any routing protocol - if 'protocols' in policy: - for policy_type in ['access_list', 'access_list6', 'as_path_list', - 'community_list', - 'extcommunity_list', 'large_community_list', - 'prefix_list', 'route_map']: - if policy_type in policy: - for policy_name in list(set(routing_policy_find(policy_type, - policy[ - 'protocols']))): - found = False - if policy_name in policy[policy_type]: - found = True - # BGP uses prefix-list for selecting both an IPv4 or IPv6 AFI related - # list - we need to go the extra mile here and check both prefix-lists - if policy_type == 'prefix_list' and 'prefix_list6' in policy and policy_name in \ - policy['prefix_list6']: - found = True - if not found: - tmp = policy_type.replace('_', '-') - raise ConfigError( - f'Can not delete {tmp} "{policy_name}", still in use!') + # Check if any routing protocol is activated + if 'protocol' in policy: + for policy_type in policy_types: + for policy_name in list(set(routing_policy_find(policy_type, policy['protocol']))): + found = False + if policy_type in policy and policy_name in policy[policy_type]: + found = True + # BGP uses prefix-list for selecting both an IPv4 or IPv6 AFI related + # list - we need to go the extra mile here and check both prefix-lists + if policy_type == 'prefix_list' and 'prefix_list6' in policy and policy_name in \ + policy['prefix_list6']: + found = True + if not found: + tmp = policy_type.replace('_', '-') + raise ConfigError( + f'Can not delete {tmp} "{policy_name}", still in use!') return None -def generate(policy): - if not policy: - return None - policy['new_frr_config'] = render_to_string('frr/policy.frr.j2', policy) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None - -def apply(policy): - bgp_daemon = 'bgpd' - zebra_daemon = 'zebra' - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(bgp_daemon) - frr_cfg.modify_section(r'^bgp as-path access-list .*') - frr_cfg.modify_section(r'^bgp community-list .*') - frr_cfg.modify_section(r'^bgp extcommunity-list .*') - frr_cfg.modify_section(r'^bgp large-community-list .*') - frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit', - remove_stop_mark=True) - if 'new_frr_config' in policy: - frr_cfg.add_before(frr.default_add_before, policy['new_frr_config']) - frr_cfg.commit_configuration(bgp_daemon) - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'^access-list .*') - frr_cfg.modify_section(r'^ipv6 access-list .*') - frr_cfg.modify_section(r'^ip prefix-list .*') - frr_cfg.modify_section(r'^ipv6 prefix-list .*') - frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit', - remove_stop_mark=True) - if 'new_frr_config' in policy: - frr_cfg.add_before(frr.default_add_before, policy['new_frr_config']) - frr_cfg.commit_configuration(zebra_daemon) - +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None - if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/policy_local-route.py b/src/conf_mode/policy_local-route.py index 331fd972d..77b39e59f 100755 --- a/src/conf_mode/policy_local-route.py +++ b/src/conf_mode/policy_local-route.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -54,6 +54,7 @@ def get_config(config=None): dst = leaf_node_changed(conf, base_rule + [rule, 'destination', 'address']) dst_port = leaf_node_changed(conf, base_rule + [rule, 'destination', 'port']) table = leaf_node_changed(conf, base_rule + [rule, 'set', 'table']) + vrf = leaf_node_changed(conf, base_rule + [rule, 'set', 'vrf']) proto = leaf_node_changed(conf, base_rule + [rule, 'protocol']) rule_def = {} if src: @@ -70,6 +71,8 @@ def get_config(config=None): rule_def = dict_merge({'destination': {'port': dst_port}}, rule_def) if table: rule_def = dict_merge({'table' : table}, rule_def) + if vrf: + rule_def = dict_merge({'vrf' : vrf}, rule_def) if proto: rule_def = dict_merge({'protocol' : proto}, rule_def) dict = dict_merge({dict_id : {rule : rule_def}}, dict) @@ -90,6 +93,7 @@ def get_config(config=None): dst = leaf_node_changed(conf, base_rule + [rule, 'destination', 'address']) dst_port = leaf_node_changed(conf, base_rule + [rule, 'destination', 'port']) table = leaf_node_changed(conf, base_rule + [rule, 'set', 'table']) + vrf = leaf_node_changed(conf, base_rule + [rule, 'set', 'vrf']) proto = leaf_node_changed(conf, base_rule + [rule, 'protocol']) # keep track of changes in configuration # otherwise we might remove an existing node although nothing else has changed @@ -179,6 +183,15 @@ def get_config(config=None): if len(table) > 0: rule_def = dict_merge({'table' : table}, rule_def) + # vrf + if vrf is None: + if 'set' in rule_config and 'vrf' in rule_config['set']: + rule_def = dict_merge({'vrf': [rule_config['set']['vrf']]}, rule_def) + else: + changed = True + if len(vrf) > 0: + rule_def = dict_merge({'vrf' : vrf}, rule_def) + # protocol if proto is None: if 'protocol' in rule_config: @@ -218,8 +231,15 @@ def verify(pbr): ): raise ConfigError('Source or destination address or fwmark or inbound-interface or protocol is required!') - if 'set' not in pbr_route['rule'][rule] or 'table' not in pbr_route['rule'][rule]['set']: - raise ConfigError('Table set is required!') + if 'set' not in pbr_route['rule'][rule]: + raise ConfigError('Either set table or set vrf is required!') + + set_tgts = pbr_route['rule'][rule]['set'] + if 'table' not in set_tgts and 'vrf' not in set_tgts: + raise ConfigError('Either set table or set vrf is required!') + + if 'table' in set_tgts and 'vrf' in set_tgts: + raise ConfigError('set table and set vrf cannot both be set!') if 'inbound_interface' in pbr_route['rule'][rule]: interface = pbr_route['rule'][rule]['inbound_interface'] @@ -250,11 +270,14 @@ def apply(pbr): fwmark = rule_config.get('fwmark', ['']) inbound_interface = rule_config.get('inbound_interface', ['']) protocol = rule_config.get('protocol', ['']) - table = rule_config.get('table', ['']) + # VRF 'default' is actually table 'main' for RIB rules + vrf = [ 'main' if x == 'default' else x for x in rule_config.get('vrf', ['']) ] + # See generate section below for table/vrf overlap explanation + table_or_vrf = rule_config.get('table', vrf) - for src, dst, src_port, dst_port, fwmk, iif, proto, table in product( + for src, dst, src_port, dst_port, fwmk, iif, proto, table_or_vrf in product( source, destination, source_port, destination_port, - fwmark, inbound_interface, protocol, table): + fwmark, inbound_interface, protocol, table_or_vrf): f_src = '' if src == '' else f' from {src} ' f_src_port = '' if src_port == '' else f' sport {src_port} ' f_dst = '' if dst == '' else f' to {dst} ' @@ -262,7 +285,7 @@ def apply(pbr): f_fwmk = '' if fwmk == '' else f' fwmark {fwmk} ' f_iif = '' if iif == '' else f' iif {iif} ' f_proto = '' if proto == '' else f' ipproto {proto} ' - f_table = '' if table == '' else f' lookup {table} ' + f_table = '' if table_or_vrf == '' else f' lookup {table_or_vrf} ' call(f'ip{v6} rule del prio {rule} {f_src}{f_dst}{f_proto}{f_src_port}{f_dst_port}{f_fwmk}{f_iif}{f_table}') @@ -276,7 +299,13 @@ def apply(pbr): if 'rule' in pbr_route: for rule, rule_config in pbr_route['rule'].items(): - table = rule_config['set'].get('table', '') + # VRFs get configred as route table alias names for iproute2 and only + # one 'set' can get past validation. Either can be fed to lookup. + vrf = rule_config['set'].get('vrf', '') + if vrf == 'default': + table_or_vrf = 'main' + else: + table_or_vrf = rule_config['set'].get('table', vrf) source = rule_config.get('source', {}).get('address', ['all']) source_port = rule_config.get('source', {}).get('port', '') destination = rule_config.get('destination', {}).get('address', ['all']) @@ -295,7 +324,7 @@ def apply(pbr): f_iif = f' iif {inbound_interface} ' if inbound_interface else '' f_proto = f' ipproto {protocol} ' if protocol else '' - call(f'ip{v6} rule add prio {rule}{f_src}{f_dst}{f_proto}{f_src_port}{f_dst_port}{f_fwmk}{f_iif} lookup {table}') + call(f'ip{v6} rule add prio {rule}{f_src}{f_dst}{f_proto}{f_src_port}{f_dst_port}{f_fwmk}{f_iif} lookup {table_or_vrf}') return None diff --git a/src/conf_mode/policy_route.py b/src/conf_mode/policy_route.py index 223175b8a..15d735f75 100755 --- a/src/conf_mode/policy_route.py +++ b/src/conf_mode/policy_route.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -21,13 +21,16 @@ from sys import exit from vyos.base import Warning from vyos.config import Config +from vyos.configdiff import get_config_diff, Diff from vyos.template import render 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 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.firewall import geoip_update from vyos import ConfigError from vyos import airbag airbag.enable() @@ -43,6 +46,43 @@ valid_groups = [ 'interface_group' ] +def geoip_updated(conf, policy): + diff = get_config_diff(conf) + node_diff = diff.get_child_nodes_diff(['policy'], expand_nodes=Diff.DELETE, recursive=True) + + out = { + 'name': [], + 'ipv6_name': [], + 'deleted_name': [], + 'deleted_ipv6_name': [] + } + updated = False + + for key, path in dict_search_recursive(policy, 'geoip'): + set_name = f'GEOIP_CC_{path[0]}_{path[1]}_{path[3]}' + if (path[0] == 'route'): + out['name'].append(set_name) + elif (path[0] == 'route6'): + set_name = f'GEOIP_CC6_{path[0]}_{path[1]}_{path[3]}' + out['ipv6_name'].append(set_name) + + updated = True + + if 'delete' in node_diff: + for key, path in dict_search_recursive(node_diff['delete'], 'geoip'): + set_name = f'GEOIP_CC_{path[0]}_{path[1]}_{path[3]}' + if (path[0] == 'route'): + out['deleted_name'].append(set_name) + elif (path[0] == 'route6'): + set_name = f'GEOIP_CC6_{path[0]}_{path[1]}_{path[3]}' + out['deleted_ipv6_name'].append(set_name) + updated = True + + if updated: + return out + + return False + def get_config(config=None): if config: conf = config @@ -60,6 +100,7 @@ def get_config(config=None): if 'dynamic_group' in policy['firewall_group']: del policy['firewall_group']['dynamic_group'] + policy['geoip_updated'] = geoip_updated(conf, policy) return policy def verify_rule(policy, name, rule_conf, ipv6, rule_id): @@ -203,6 +244,12 @@ def apply(policy): apply_table_marks(policy) + if policy['geoip_updated']: + # Call helper script to Update set contents + if 'name' in policy['geoip_updated'] or 'ipv6_name' in policy['geoip_updated']: + print('Updating GeoIP. Please wait...') + geoip_update(policy=policy) + return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_babel.py b/src/conf_mode/protocols_babel.py index 90b6e4a31..a683031bd 100755 --- a/src/conf_mode/protocols_babel.py +++ b/src/conf_mode/protocols_babel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -17,15 +17,14 @@ from sys import exit from vyos.config import Config -from vyos.config import config_dict_merge -from vyos.configdict import dict_merge -from vyos.configdict import node_changed +from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_access_list from vyos.configverify import verify_prefix_list +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search -from vyos.template import render_to_string +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -34,46 +33,16 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['protocols', 'babel'] - babel = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True) - # FRR has VRF support for different routing daemons. As interfaces belong - # to VRFs - or the global VRF, we need to check for changed interfaces so - # that they will be properly rendered for the FRR config. Also this eases - # removal of interfaces from the running configuration. - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - babel['interface_removed'] = list(interfaces_removed) + return get_frrender_dict(conf) - # Bail out early if configuration tree does not exist - if not conf.exists(base): - babel.update({'deleted' : ''}) - return babel - - # We have gathered the dict representation of the CLI, but there are default - # values which we need to update into the dictionary retrieved. - default_values = conf.get_config_defaults(base, key_mangling=('-', '_'), - get_first_key=True, - recursive=True) - - # merge in default values - babel = config_dict_merge(default_values, babel) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - babel = dict_merge(tmp, babel) - return babel - -def verify(babel): - if not babel: +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'babel'): return None + babel = config_dict['babel'] + babel['policy'] = config_dict['policy'] + # verify distribute_list if "distribute_list" in babel: acl_keys = { @@ -120,32 +89,14 @@ def verify(babel): verify_prefix_list(prefix_list, babel, version='6' if address_family == 'ipv6' else '') -def generate(babel): - if not babel or 'deleted' in babel: - return None - - babel['new_frr_config'] = render_to_string('frr/babeld.frr.j2', babel) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None -def apply(babel): - babel_daemon = 'babeld' - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - frr_cfg.load_configuration(babel_daemon) - frr_cfg.modify_section('^router babel', stop_pattern='^exit', remove_stop_mark=True) - - for key in ['interface', 'interface_removed']: - if key not in babel: - continue - for interface in babel[key]: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - if 'new_frr_config' in babel: - frr_cfg.add_before(frr.default_add_before, babel['new_frr_config']) - frr_cfg.commit_configuration(babel_daemon) - +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 1361bb1a9..953611f24 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -16,11 +16,13 @@ from vyos.config import Config from vyos.configverify import verify_vrf +from vyos.configverify import has_frr_protocol_in_dict +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.template import is_ipv6 -from vyos.template import render_to_string from vyos.utils.network import is_ipv6_link_local +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -29,22 +31,14 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['protocols', 'bfd'] - bfd = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - # Bail out early if configuration tree does not exist - if not conf.exists(base): - return bfd - bfd = conf.merge_defaults(bfd, recursive=True) + return get_frrender_dict(conf) - return bfd - -def verify(bfd): - if not bfd: +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'bfd'): return None + bfd = config_dict['bfd'] if 'peer' in bfd: for peer, peer_config in bfd['peer'].items(): # IPv6 link local peers require an explicit local address/interface @@ -83,22 +77,13 @@ def verify(bfd): return None -def generate(bfd): - if not bfd: - return None - bfd['new_frr_config'] = render_to_string('frr/bfdd.frr.j2', bfd) - -def apply(bfd): - bfd_daemon = 'bfdd' - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(bfd_daemon) - frr_cfg.modify_section('^bfd', stop_pattern='^exit', remove_stop_mark=True) - if 'new_frr_config' in bfd: - frr_cfg.add_before(frr.default_add_before, bfd['new_frr_config']) - frr_cfg.commit_configuration(bfd_daemon) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 22f020099..bc7925d28 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -19,21 +19,20 @@ from sys import argv from vyos.base import Warning from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.configdict import node_changed +from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_prefix_list from vyos.configverify import verify_route_map from vyos.configverify import verify_vrf +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.template import is_ip from vyos.template import is_interface -from vyos.template import render_to_string from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_vrf from vyos.utils.network import is_addr_assigned +from vyos.utils.process import is_systemd_service_running from vyos.utils.process import process_named_running -from vyos.utils.process import call from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -43,68 +42,7 @@ def get_config(config=None): else: conf = Config() - vrf = None - if len(argv) > 1: - vrf = argv[1] - - base_path = ['protocols', 'bgp'] - - # eqivalent of the C foo ? 'a' : 'b' statement - base = vrf and ['vrf', 'name', vrf, 'protocols', 'bgp'] or base_path - bgp = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - - bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'], - key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - - # Remove per interface MPLS configuration - get a list if changed - # nodes under the interface tagNode - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - bgp['interface_removed'] = list(interfaces_removed) - - # Assign the name of our VRF context. This MUST be done before the return - # statement below, else on deletion we will delete the default instance - # instead of the VRF instance. - if vrf: - bgp.update({'vrf' : vrf}) - # We can not delete the BGP VRF instance if there is a L3VNI configured - # FRR L3VNI must be deleted first otherwise we will see error: - # "FRR error: Please unconfigure l3vni 3000" - tmp = ['vrf', 'name', vrf, 'vni'] - if conf.exists_effective(tmp): - bgp.update({'vni' : conf.return_effective_value(tmp)}) - # We can safely delete ourself from the dependent vrf list - if vrf in bgp['dependent_vrfs']: - del bgp['dependent_vrfs'][vrf] - - bgp['dependent_vrfs'].update({'default': {'protocols': { - 'bgp': conf.get_config_dict(base_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True)}}}) - - if not conf.exists(base): - # If bgp instance is deleted then mark it - bgp.update({'deleted' : ''}) - return bgp - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - bgp = conf.merge_defaults(bgp, recursive=True) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - bgp = dict_merge(tmp, bgp) - - return bgp - + return get_frrender_dict(conf, argv) def verify_vrf_as_import(search_vrf_name: str, afi_name: str, vrfs_config: dict) -> bool: """ @@ -237,13 +175,24 @@ def verify_afi(peer_config, bgp_config): if tmp: return True return False -def verify(bgp): +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'bgp'): + return None + + vrf = None + if 'vrf_context' in config_dict: + vrf = config_dict['vrf_context'] + + # eqivalent of the C foo ? 'a' : 'b' statement + bgp = vrf and config_dict['vrf']['name'][vrf]['protocols']['bgp'] or config_dict['bgp'] + bgp['policy'] = config_dict['policy'] + if 'deleted' in bgp: - if 'vrf' in bgp: + if vrf: # Cannot delete vrf if it exists in import vrf list in other vrfs for tmp_afi in ['ipv4_unicast', 'ipv6_unicast']: - if verify_vrf_as_import(bgp['vrf'], tmp_afi, bgp['dependent_vrfs']): - raise ConfigError(f'Cannot delete VRF instance "{bgp["vrf"]}", ' \ + if verify_vrf_as_import(vrf, tmp_afi, bgp['dependent_vrfs']): + raise ConfigError(f'Cannot delete VRF instance "{vrf}", ' \ 'unconfigure "import vrf" commands!') else: # We are running in the default VRF context, thus we can not delete @@ -252,8 +201,9 @@ def verify(bgp): for vrf, vrf_options in bgp['dependent_vrfs'].items(): if vrf != 'default': if dict_search('protocols.bgp', vrf_options): - raise ConfigError('Cannot delete default BGP instance, ' \ - 'dependent VRF instance(s) exist(s)!') + dependent_vrfs = ', '.join(bgp['dependent_vrfs'].keys()) + raise ConfigError(f'Cannot delete default BGP instance, ' \ + f'dependent VRF instance(s): {dependent_vrfs}') if 'vni' in vrf_options: raise ConfigError('Cannot delete default BGP instance, ' \ 'dependent L3VNI exists!') @@ -281,9 +231,8 @@ def verify(bgp): for interface in bgp['interface']: error_msg = f'Interface "{interface}" belongs to different VRF instance' tmp = get_interface_vrf(interface) - if 'vrf' in bgp: - if bgp['vrf'] != tmp: - vrf = bgp['vrf'] + if vrf: + if vrf != tmp: raise ConfigError(f'{error_msg} "{vrf}"!') elif tmp != 'default': raise ConfigError(f'{error_msg} "{tmp}"!') @@ -384,10 +333,8 @@ def verify(bgp): # Only checks for ipv4 and ipv6 neighbors # Check if neighbor address is assigned as system interface address - vrf = None vrf_error_msg = f' in default VRF!' - if 'vrf' in bgp: - vrf = bgp['vrf'] + if vrf: vrf_error_msg = f' in VRF "{vrf}"!' if is_ip(peer) and is_addr_assigned(peer, vrf): @@ -466,15 +413,19 @@ def verify(bgp): verify_route_map(afi_config['route_map'][tmp], bgp) if 'route_reflector_client' in afi_config: - peer_group_as = peer_config.get('remote_as') + peer_as = peer_config.get('remote_as') - if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']): + if peer_as is not None and (peer_as != 'internal' and peer_as != bgp['system_as']): raise ConfigError('route-reflector-client only supported for iBGP peers') else: + # Check into the peer group for the remote as, if we are in a peer group, check in peer itself if 'peer_group' in peer_config: peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', bgp) - if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']): - raise ConfigError('route-reflector-client only supported for iBGP peers') + elif neighbor == 'peer_group': + peer_group_as = peer_config.get('remote_as') + + if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']): + raise ConfigError('route-reflector-client only supported for iBGP peers') # T5833 not all AFIs are supported for VRF if 'vrf' in bgp and 'address_family' in peer_config: @@ -529,7 +480,7 @@ def verify(bgp): f'{afi} administrative distance {key}!') if afi in ['ipv4_unicast', 'ipv6_unicast']: - vrf_name = bgp['vrf'] if dict_search('vrf', bgp) else 'default' + vrf_name = vrf if vrf else 'default' # Verify if currant VRF contains rd and route-target options # and does not exist in import list in other VRFs if dict_search(f'rd.vpn.export', afi_config): @@ -576,12 +527,24 @@ def verify(bgp): raise ConfigError( 'Please unconfigure import vrf commands before using vpn commands in dependent VRFs!') + # Verify if the route-map exists + if dict_search('route_map.vrf.import', afi_config) is not None: + verify_route_map(afi_config['route_map']['vrf']['import'], bgp) + + if (dict_search('route_map.vrf.import', afi_config) is not None + or dict_search('import.vrf', afi_config) is not None): # FRR error: please unconfigure vpn to vrf commands before # using import vrf commands - if 'vpn' in afi_config['import'] or dict_search('export.vpn', afi_config) != None: + if (dict_search('import.vpn', afi_config) is not None + or dict_search('export.vpn', afi_config) is not None): raise ConfigError('Please unconfigure VPN to VRF commands before '\ 'using "import vrf" commands!') + if (dict_search('route_map.vpn.import', afi_config) is not None + or dict_search('route_map.vpn.export', afi_config) is not None) : + raise ConfigError('Please unconfigure route-map VPN to VRF commands before '\ + 'using "import vrf" commands!') + # Verify that the export/import route-maps do exist for export_import in ['export', 'import']: tmp = dict_search(f'route_map.vpn.{export_import}', afi_config) @@ -602,46 +565,14 @@ def verify(bgp): return None -def generate(bgp): - if not bgp or 'deleted' in bgp: - return None - - bgp['frr_bgpd_config'] = render_to_string('frr/bgpd.frr.j2', bgp) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None -def apply(bgp): - if 'deleted' in bgp: - # We need to ensure that the L3VNI is deleted first. - # This is not possible with old config backend - # priority bug - if {'vrf', 'vni'} <= set(bgp): - call('vtysh -c "conf t" -c "vrf {vrf}" -c "no vni {vni}"'.format(**bgp)) - - bgp_daemon = 'bgpd' - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # Generate empty helper string which can be ammended to FRR commands, it - # will be either empty (default VRF) or contain the "vrf <name" statement - vrf = '' - if 'vrf' in bgp: - vrf = ' vrf ' + bgp['vrf'] - - frr_cfg.load_configuration(bgp_daemon) - - # Remove interface specific config - for key in ['interface', 'interface_removed']: - if key not in bgp: - continue - for interface in bgp[key]: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - frr_cfg.modify_section(f'^router bgp \d+{vrf}', stop_pattern='^exit', remove_stop_mark=True) - if 'frr_bgpd_config' in bgp: - frr_cfg.add_before(frr.default_add_before, bgp['frr_bgpd_config']) - frr_cfg.commit_configuration(bgp_daemon) - +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_eigrp.py b/src/conf_mode/protocols_eigrp.py index c13e52a3d..e127a3a07 100755 --- a/src/conf_mode/protocols_eigrp.py +++ b/src/conf_mode/protocols_eigrp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -18,94 +18,49 @@ from sys import exit from sys import argv from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_vrf -from vyos.template import render_to_string +from vyos.utils.process import is_systemd_service_running +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() - def get_config(config=None): if config: conf = config else: conf = Config() - vrf = None - if len(argv) > 1: - vrf = argv[1] - - base_path = ['protocols', 'eigrp'] - - # eqivalent of the C foo ? 'a' : 'b' statement - base = vrf and ['vrf', 'name', vrf, 'protocols', 'eigrp'] or base_path - eigrp = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) + return get_frrender_dict(conf, argv) - # Assign the name of our VRF context. This MUST be done before the return - # statement below, else on deletion we will delete the default instance - # instead of the VRF instance. - if vrf: eigrp.update({'vrf' : vrf}) - - if not conf.exists(base): - eigrp.update({'deleted' : ''}) - if not vrf: - # We are running in the default VRF context, thus we can not delete - # our main EIGRP instance if there are dependent EIGRP VRF instances. - eigrp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'], - key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - - return eigrp - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - eigrp = dict_merge(tmp, eigrp) +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'eigrp'): + return None - return eigrp + vrf = None + if 'vrf_context' in config_dict: + vrf = config_dict['vrf_context'] -def verify(eigrp): - if not eigrp or 'deleted' in eigrp: - return + # eqivalent of the C foo ? 'a' : 'b' statement + eigrp = vrf and config_dict['vrf']['name'][vrf]['protocols']['eigrp'] or config_dict['eigrp'] + eigrp['policy'] = config_dict['policy'] if 'system_as' not in eigrp: raise ConfigError('EIGRP system-as must be defined!') - if 'vrf' in eigrp: - verify_vrf(eigrp) - -def generate(eigrp): - if not eigrp or 'deleted' in eigrp: - return None - - eigrp['frr_eigrpd_config'] = render_to_string('frr/eigrpd.frr.j2', eigrp) + if vrf: + verify_vrf({'vrf': vrf}) -def apply(eigrp): - eigrp_daemon = 'eigrpd' - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # Generate empty helper string which can be ammended to FRR commands, it - # will be either empty (default VRF) or contain the "vrf <name" statement - vrf = '' - if 'vrf' in eigrp: - vrf = ' vrf ' + eigrp['vrf'] - - frr_cfg.load_configuration(eigrp_daemon) - frr_cfg.modify_section(f'^router eigrp \d+{vrf}', stop_pattern='^exit', remove_stop_mark=True) - if 'frr_eigrpd_config' in eigrp: - frr_cfg.add_before(frr.default_add_before, eigrp['frr_eigrpd_config']) - frr_cfg.commit_configuration(eigrp_daemon) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) + return None +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_failover.py b/src/conf_mode/protocols_failover.py index e7e44db84..9b50cc1ed 100755 --- a/src/conf_mode/protocols_failover.py +++ b/src/conf_mode/protocols_failover.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/protocols_igmp-proxy.py b/src/conf_mode/protocols_igmp-proxy.py index 9a07adf05..c2d2fea5b 100755 --- a/src/conf_mode/protocols_igmp-proxy.py +++ b/src/conf_mode/protocols_igmp-proxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index ba2f3cf0d..8b1408f57 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -18,16 +18,16 @@ from sys import exit from sys import argv from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.configdict import node_changed +from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_interface_exists +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.ifconfig import Interface from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config -from vyos.template import render_to_string +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -37,54 +37,21 @@ def get_config(config=None): else: conf = Config() - vrf = None - if len(argv) > 1: - vrf = argv[1] + return get_frrender_dict(conf, argv) + +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'isis'): + return None - base_path = ['protocols', 'isis'] + vrf = None + if 'vrf_context' in config_dict: + vrf = config_dict['vrf_context'] # eqivalent of the C foo ? 'a' : 'b' statement - base = vrf and ['vrf', 'name', vrf, 'protocols', 'isis'] or base_path - isis = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - - # Assign the name of our VRF context. This MUST be done before the return - # statement below, else on deletion we will delete the default instance - # instead of the VRF instance. - if vrf: isis['vrf'] = vrf - - # FRR has VRF support for different routing daemons. As interfaces belong - # to VRFs - or the global VRF, we need to check for changed interfaces so - # that they will be properly rendered for the FRR config. Also this eases - # removal of interfaces from the running configuration. - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - isis['interface_removed'] = list(interfaces_removed) - - # Bail out early if configuration tree does no longer exist. this must - # be done after retrieving the list of interfaces to be removed. - if not conf.exists(base): - isis.update({'deleted' : ''}) - return isis - - # merge in default values - isis = conf.merge_defaults(isis, recursive=True) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - isis = dict_merge(tmp, isis) - - return isis - -def verify(isis): - # bail out early - looks like removal from running config - if not isis or 'deleted' in isis: + isis = vrf and config_dict['vrf']['name'][vrf]['protocols']['isis'] or config_dict['isis'] + isis['policy'] = config_dict['policy'] + + if 'deleted' in isis: return None if 'net' not in isis: @@ -114,12 +81,11 @@ def verify(isis): f'Recommended area lsp-mtu {recom_area_mtu} or less ' \ '(calculated on MTU size).') - if 'vrf' in isis: + if vrf: # If interface specific options are set, we must ensure that the # interface is bound to our requesting VRF. Due to the VyOS # priorities the interface is bound to the VRF after creation of # the VRF itself, and before any routing protocol is configured. - vrf = isis['vrf'] tmp = get_interface_config(interface) if 'master' not in tmp or tmp['master'] != vrf: raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!') @@ -266,39 +232,14 @@ def verify(isis): return None -def generate(isis): - if not isis or 'deleted' in isis: - return None - - isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.j2', isis) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None -def apply(isis): - isis_daemon = 'isisd' - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # Generate empty helper string which can be ammended to FRR commands, it - # will be either empty (default VRF) or contain the "vrf <name" statement - vrf = '' - if 'vrf' in isis: - vrf = ' vrf ' + isis['vrf'] - - frr_cfg.load_configuration(isis_daemon) - frr_cfg.modify_section(f'^router isis VyOS{vrf}', stop_pattern='^exit', remove_stop_mark=True) - - for key in ['interface', 'interface_removed']: - if key not in isis: - continue - for interface in isis[key]: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - if 'frr_isisd_config' in isis: - frr_cfg.add_before(frr.default_add_before, isis['frr_isisd_config']) - - frr_cfg.commit_configuration(isis_daemon) - +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index ad164db9f..d534ad2e3 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -20,33 +20,32 @@ from sys import exit from glob import glob from vyos.config import Config -from vyos.template import render_to_string +from vyos.configverify import has_frr_protocol_in_dict +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search from vyos.utils.file import read_file +from vyos.utils.process import is_systemd_service_running from vyos.utils.system import sysctl_write from vyos.configverify import verify_interface_exists from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() -config_file = r'/tmp/ldpd.frr' - def get_config(config=None): if config: conf = config else: conf = Config() - base = ['protocols', 'mpls'] - mpls = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - return mpls + return get_frrender_dict(conf) -def verify(mpls): - # If no config, then just bail out early. - if not mpls: +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'mpls'): return None + mpls = config_dict['mpls'] + if 'interface' in mpls: for interface in mpls['interface']: verify_interface_exists(mpls, interface) @@ -68,26 +67,19 @@ def verify(mpls): return None -def generate(mpls): - # If there's no MPLS config generated, create dictionary key with no value. - if not mpls or 'deleted' in mpls: - return None - - mpls['frr_ldpd_config'] = render_to_string('frr/ldpd.frr.j2', mpls) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None -def apply(mpls): - ldpd_damon = 'ldpd' - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() - frr_cfg.load_configuration(ldpd_damon) - frr_cfg.modify_section(f'^mpls ldp', stop_pattern='^exit', remove_stop_mark=True) + if not has_frr_protocol_in_dict(config_dict, 'mpls'): + return None - if 'frr_ldpd_config' in mpls: - frr_cfg.add_before(frr.default_add_before, mpls['frr_ldpd_config']) - frr_cfg.commit_configuration(ldpd_damon) + mpls = config_dict['mpls'] # Set number of entries in the platform label tables labels = '0' diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py index 0bd68b7d8..be08080b1 100755 --- a/src/conf_mode/protocols_nhrp.py +++ b/src/conf_mode/protocols_nhrp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,95 +14,112 @@ # 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 sys import exit +from sys import argv +import ipaddress from vyos.config import Config -from vyos.configdict import node_changed from vyos.template import render +from vyos.configverify import has_frr_protocol_in_dict from vyos.utils.process import run +from vyos.utils.dict import dict_search from vyos import ConfigError from vyos import airbag +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict +from vyos.utils.process import is_systemd_service_running + airbag.enable() -opennhrp_conf = '/run/opennhrp/opennhrp.conf' +nflog_redirect = 1 +nflog_multicast = 2 nhrp_nftables_conf = '/run/nftables_nhrp.conf' + def get_config(config=None): if config: conf = config else: conf = Config() - base = ['protocols', 'nhrp'] - - nhrp = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - nhrp['del_tunnels'] = node_changed(conf, base + ['tunnel']) - - if not conf.exists(base): - return nhrp - nhrp['if_tunnel'] = conf.get_config_dict(['interfaces', 'tunnel'], key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) + return get_frrender_dict(conf, argv) - nhrp['profile_map'] = {} - profile = conf.get_config_dict(['vpn', 'ipsec', 'profile'], key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - for name, profile_conf in profile.items(): - if 'bind' in profile_conf and 'tunnel' in profile_conf['bind']: - interfaces = profile_conf['bind']['tunnel'] - if isinstance(interfaces, str): - interfaces = [interfaces] - for interface in interfaces: - nhrp['profile_map'][interface] = name - - return nhrp - -def verify(nhrp): - if 'tunnel' in nhrp: - for name, nhrp_conf in nhrp['tunnel'].items(): - if not nhrp['if_tunnel'] or name not in nhrp['if_tunnel']: +def verify(config_dict): + if not config_dict or 'deleted' in config_dict: + return None + if 'tunnel' in config_dict: + for name, nhrp_conf in config_dict['tunnel'].items(): + if not config_dict['if_tunnel'] or name not in config_dict['if_tunnel']: raise ConfigError(f'Tunnel interface "{name}" does not exist') - tunnel_conf = nhrp['if_tunnel'][name] + tunnel_conf = config_dict['if_tunnel'][name] + if 'address' in tunnel_conf: + address_list = dict_search('address', tunnel_conf) + for tunip in address_list: + if ipaddress.ip_network(tunip, + strict=False).prefixlen != 32: + raise ConfigError( + f'Tunnel {name} is used for NHRP, Netmask should be /32!') if 'encapsulation' not in tunnel_conf or tunnel_conf['encapsulation'] != 'gre': raise ConfigError(f'Tunnel "{name}" is not an mGRE tunnel') + if 'network_id' not in nhrp_conf: + raise ConfigError(f'network-id is not specified in tunnel "{name}"') + if 'remote' in tunnel_conf: raise ConfigError(f'Tunnel "{name}" cannot have a remote address defined') - if 'map' in nhrp_conf: - for map_name, map_conf in nhrp_conf['map'].items(): - if 'nbma_address' not in map_conf: + map_tunnelip = dict_search('map.tunnel_ip', nhrp_conf) + if map_tunnelip: + for map_name, map_conf in map_tunnelip.items(): + if 'nbma' not in map_conf: raise ConfigError(f'nbma-address missing on map {map_name} on tunnel {name}') - if 'dynamic_map' in nhrp_conf: - for map_name, map_conf in nhrp_conf['dynamic_map'].items(): - if 'nbma_domain_name' not in map_conf: - raise ConfigError(f'nbma-domain-name missing on dynamic-map {map_name} on tunnel {name}') + nhs_tunnelip = dict_search('nhs.tunnel_ip', nhrp_conf) + nbma_list = [] + if nhs_tunnelip: + for nhs_name, nhs_conf in nhs_tunnelip.items(): + if 'nbma' not in nhs_conf: + raise ConfigError(f'nbma-address missing on map nhs {nhs_name} on tunnel {name}') + if nhs_name != 'dynamic': + if len(list(dict_search('nbma', nhs_conf))) > 1: + raise ConfigError( + f'Static nhs tunnel-ip {nhs_name} cannot contain multiple nbma-addresses') + for nbma_ip in dict_search('nbma', nhs_conf): + if nbma_ip not in nbma_list: + nbma_list.append(nbma_ip) + else: + raise ConfigError( + f'Nbma address {nbma_ip} cannot be maped to several tunnel-ip') return None -def generate(nhrp): - if not os.path.exists(nhrp_nftables_conf): - nhrp['first_install'] = True - render(opennhrp_conf, 'nhrp/opennhrp.conf.j2', nhrp) - render(nhrp_nftables_conf, 'nhrp/nftables.conf.j2', nhrp) +def generate(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'nhrp'): + return None + + if 'deleted' in config_dict['nhrp']: + return None + render(nhrp_nftables_conf, 'frr/nhrpd_nftables.conf.j2', config_dict['nhrp']) + + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None -def apply(nhrp): + +def apply(config_dict): + nft_rc = run(f'nft --file {nhrp_nftables_conf}') if nft_rc != 0: raise ConfigError('Failed to apply NHRP tunnel firewall rules') - action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop' - service_rc = run(f'systemctl {action} opennhrp.service') - if service_rc != 0: - raise ConfigError(f'Failed to {action} the NHRP service') - + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None + if __name__ == '__main__': try: c = get_config() @@ -112,3 +129,4 @@ if __name__ == '__main__': except ConfigError as e: print(e) exit(1) + diff --git a/src/conf_mode/protocols_openfabric.py b/src/conf_mode/protocols_openfabric.py index 8e8c50c06..f490d28bf 100644 --- a/src/conf_mode/protocols_openfabric.py +++ b/src/conf_mode/protocols_openfabric.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -18,13 +18,13 @@ from sys import exit from vyos.base import Warning from vyos.config import Config -from vyos.configdict import node_changed from vyos.configverify import verify_interface_exists -from vyos.template import render_to_string +from vyos.configverify import has_frr_protocol_in_dict +from vyos.utils.process import is_systemd_service_running +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos import ConfigError -from vyos import frr from vyos import airbag - airbag.enable() def get_config(config=None): @@ -33,32 +33,14 @@ def get_config(config=None): else: conf = Config() - base_path = ['protocols', 'openfabric'] - - openfabric = conf.get_config_dict(base_path, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - - # Remove per domain MPLS configuration - get a list of all changed Openfabric domains - # (removed and added) so that they will be properly rendered for the FRR config. - openfabric['domains_all'] = list(conf.list_nodes(' '.join(base_path) + f' domain') + - node_changed(conf, base_path + ['domain'])) - - # Get a list of all interfaces - openfabric['interfaces_all'] = [] - for domain in openfabric['domains_all']: - interfaces_modified = list(node_changed(conf, base_path + ['domain', domain, 'interface']) + - conf.list_nodes(' '.join(base_path) + f' domain {domain} interface')) - openfabric['interfaces_all'].extend(interfaces_modified) + return get_frrender_dict(conf) - if not conf.exists(base_path): - openfabric.update({'deleted': ''}) - - return openfabric +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'openfabric'): + return None -def verify(openfabric): - # bail out early - looks like removal from running config - if not openfabric or 'deleted' in openfabric: + openfabric = config_dict['openfabric'] + if 'deleted' in openfabric: return None if 'net' not in openfabric: @@ -107,31 +89,14 @@ def verify(openfabric): return None -def generate(openfabric): - if not openfabric or 'deleted' in openfabric: - return None - - openfabric['frr_fabricd_config'] = render_to_string('frr/fabricd.frr.j2', openfabric) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None -def apply(openfabric): - openfabric_daemon = 'fabricd' - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - frr_cfg.load_configuration(openfabric_daemon) - for domain in openfabric['domains_all']: - frr_cfg.modify_section(f'^router openfabric {domain}', stop_pattern='^exit', remove_stop_mark=True) - - for interface in openfabric['interfaces_all']: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - if 'frr_fabricd_config' in openfabric: - frr_cfg.add_before(frr.default_add_before, openfabric['frr_fabricd_config']) - - frr_cfg.commit_configuration(openfabric_daemon) - +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index 7347c4faa..9421765a1 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -17,19 +17,19 @@ from sys import exit from sys import argv +from vyos.base import Warning from vyos.config import Config -from vyos.config import config_dict_merge -from vyos.configdict import dict_merge -from vyos.configdict import node_changed from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_route_map from vyos.configverify import verify_interface_exists from vyos.configverify import verify_access_list -from vyos.template import render_to_string +from vyos.configverify import has_frr_protocol_in_dict +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -39,85 +39,19 @@ def get_config(config=None): else: conf = Config() - vrf = None - if len(argv) > 1: - vrf = argv[1] + return get_frrender_dict(conf, argv) + +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'ospf'): + return None - base_path = ['protocols', 'ospf'] + vrf = None + if 'vrf_context' in config_dict: + vrf = config_dict['vrf_context'] # eqivalent of the C foo ? 'a' : 'b' statement - base = vrf and ['vrf', 'name', vrf, 'protocols', 'ospf'] or base_path - ospf = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True) - - # Assign the name of our VRF context. This MUST be done before the return - # statement below, else on deletion we will delete the default instance - # instead of the VRF instance. - if vrf: ospf['vrf'] = vrf - - # FRR has VRF support for different routing daemons. As interfaces belong - # to VRFs - or the global VRF, we need to check for changed interfaces so - # that they will be properly rendered for the FRR config. Also this eases - # removal of interfaces from the running configuration. - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - ospf['interface_removed'] = list(interfaces_removed) - - # Bail out early if configuration tree does no longer exist. this must - # be done after retrieving the list of interfaces to be removed. - if not conf.exists(base): - ospf.update({'deleted' : ''}) - return ospf - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = conf.get_config_defaults(**ospf.kwargs, recursive=True) - - # We have to cleanup the default dict, as default values could enable features - # which are not explicitly enabled on the CLI. Example: default-information - # originate comes with a default metric-type of 2, which will enable the - # entire default-information originate tree, even when not set via CLI so we - # need to check this first and probably drop that key. - if dict_search('default_information.originate', ospf) is None: - del default_values['default_information'] - if 'mpls_te' not in ospf: - del default_values['mpls_te'] - if 'graceful_restart' not in ospf: - del default_values['graceful_restart'] - for area_num in default_values.get('area', []): - if dict_search(f'area.{area_num}.area_type.nssa', ospf) is None: - del default_values['area'][area_num]['area_type']['nssa'] - - for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']: - if dict_search(f'redistribute.{protocol}', ospf) is None: - del default_values['redistribute'][protocol] - if not bool(default_values['redistribute']): - del default_values['redistribute'] - - for interface in ospf.get('interface', []): - # We need to reload the defaults on every pass b/c of - # hello-multiplier dependency on dead-interval - # If hello-multiplier is set, we need to remove the default from - # dead-interval. - if 'hello_multiplier' in ospf['interface'][interface]: - del default_values['interface'][interface]['dead_interval'] - - ospf = config_dict_merge(default_values, ospf) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - ospf = dict_merge(tmp, ospf) - - return ospf - -def verify(ospf): - if not ospf: - return None + ospf = vrf and config_dict['vrf']['name'][vrf]['protocols']['ospf'] or config_dict['ospf'] + ospf['policy'] = config_dict['policy'] verify_common_route_maps(ospf) @@ -129,6 +63,16 @@ def verify(ospf): if 'area' in ospf: networks = [] for area, area_config in ospf['area'].items(): + # Implemented as warning to not break existing configurations + if area == '0' and dict_search('area_type.nssa', area_config) != None: + Warning('You cannot configure NSSA to backbone!') + # Implemented as warning to not break existing configurations + if area == '0' and dict_search('area_type.stub', area_config) != None: + Warning('You cannot configure STUB to backbone!') + # Implemented as warning to not break existing configurations + if len(area_config['area_type']) > 1: + Warning(f'Only one area-type is supported for area "{area}"!') + if 'import_list' in area_config: acl_import = area_config['import_list'] if acl_import: verify_access_list(acl_import, ospf) @@ -164,8 +108,7 @@ def verify(ospf): # interface is bound to our requesting VRF. Due to the VyOS # priorities the interface is bound to the VRF after creation of # the VRF itself, and before any routing protocol is configured. - if 'vrf' in ospf: - vrf = ospf['vrf'] + if vrf: tmp = get_interface_config(interface) if 'master' not in tmp or tmp['master'] != vrf: raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!') @@ -244,39 +187,14 @@ def verify(ospf): return None -def generate(ospf): - if not ospf or 'deleted' in ospf: - return None - - ospf['frr_ospfd_config'] = render_to_string('frr/ospfd.frr.j2', ospf) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None -def apply(ospf): - ospf_daemon = 'ospfd' - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # Generate empty helper string which can be ammended to FRR commands, it - # will be either empty (default VRF) or contain the "vrf <name" statement - vrf = '' - if 'vrf' in ospf: - vrf = ' vrf ' + ospf['vrf'] - - frr_cfg.load_configuration(ospf_daemon) - frr_cfg.modify_section(f'^router ospf{vrf}', stop_pattern='^exit', remove_stop_mark=True) - - for key in ['interface', 'interface_removed']: - if key not in ospf: - continue - for interface in ospf[key]: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - if 'frr_ospfd_config' in ospf: - frr_cfg.add_before(frr.default_add_before, ospf['frr_ospfd_config']) - - frr_cfg.commit_configuration(ospf_daemon) - +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index 60c2a9b16..1d2eceb8f 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -18,18 +18,17 @@ from sys import exit from sys import argv from vyos.config import Config -from vyos.config import config_dict_merge -from vyos.configdict import dict_merge -from vyos.configdict import node_changed from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_route_map from vyos.configverify import verify_interface_exists -from vyos.template import render_to_string +from vyos.configverify import has_frr_protocol_in_dict +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.ifconfig import Interface from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -39,75 +38,19 @@ def get_config(config=None): else: conf = Config() - vrf = None - if len(argv) > 1: - vrf = argv[1] + return get_frrender_dict(conf, argv) + +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'ospfv3'): + return None - base_path = ['protocols', 'ospfv3'] + vrf = None + if 'vrf_context' in config_dict: + vrf = config_dict['vrf_context'] # eqivalent of the C foo ? 'a' : 'b' statement - base = vrf and ['vrf', 'name', vrf, 'protocols', 'ospfv3'] or base_path - ospfv3 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - - # Assign the name of our VRF context. This MUST be done before the return - # statement below, else on deletion we will delete the default instance - # instead of the VRF instance. - if vrf: ospfv3['vrf'] = vrf - - # FRR has VRF support for different routing daemons. As interfaces belong - # to VRFs - or the global VRF, we need to check for changed interfaces so - # that they will be properly rendered for the FRR config. Also this eases - # removal of interfaces from the running configuration. - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - ospfv3['interface_removed'] = list(interfaces_removed) - - # Bail out early if configuration tree does no longer exist. this must - # be done after retrieving the list of interfaces to be removed. - if not conf.exists(base): - ospfv3.update({'deleted' : ''}) - return ospfv3 - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = conf.get_config_defaults(**ospfv3.kwargs, - recursive=True) - - # We have to cleanup the default dict, as default values could enable features - # which are not explicitly enabled on the CLI. Example: default-information - # originate comes with a default metric-type of 2, which will enable the - # entire default-information originate tree, even when not set via CLI so we - # need to check this first and probably drop that key. - if dict_search('default_information.originate', ospfv3) is None: - del default_values['default_information'] - if 'graceful_restart' not in ospfv3: - del default_values['graceful_restart'] - - for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'ripng', 'static']: - if dict_search(f'redistribute.{protocol}', ospfv3) is None: - del default_values['redistribute'][protocol] - if not bool(default_values['redistribute']): - del default_values['redistribute'] - - default_values.pop('interface', {}) - - # merge in remaining default values - ospfv3 = config_dict_merge(default_values, ospfv3) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - ospfv3 = dict_merge(tmp, ospfv3) - - return ospfv3 - -def verify(ospfv3): - if not ospfv3: - return None + ospfv3 = vrf and config_dict['vrf']['name'][vrf]['protocols']['ospfv3'] or config_dict['ospfv3'] + ospfv3['policy'] = config_dict['policy'] verify_common_route_maps(ospfv3) @@ -137,47 +80,21 @@ def verify(ospfv3): # interface is bound to our requesting VRF. Due to the VyOS # priorities the interface is bound to the VRF after creation of # the VRF itself, and before any routing protocol is configured. - if 'vrf' in ospfv3: - vrf = ospfv3['vrf'] + if vrf: tmp = get_interface_config(interface) if 'master' not in tmp or tmp['master'] != vrf: raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!') return None -def generate(ospfv3): - if not ospfv3 or 'deleted' in ospfv3: - return None - - ospfv3['new_frr_config'] = render_to_string('frr/ospf6d.frr.j2', ospfv3) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None -def apply(ospfv3): - ospf6_daemon = 'ospf6d' - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # Generate empty helper string which can be ammended to FRR commands, it - # will be either empty (default VRF) or contain the "vrf <name" statement - vrf = '' - if 'vrf' in ospfv3: - vrf = ' vrf ' + ospfv3['vrf'] - - frr_cfg.load_configuration(ospf6_daemon) - frr_cfg.modify_section(f'^router ospf6{vrf}', stop_pattern='^exit', remove_stop_mark=True) - - for key in ['interface', 'interface_removed']: - if key not in ospfv3: - continue - for interface in ospfv3[key]: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - if 'new_frr_config' in ospfv3: - frr_cfg.add_before(frr.default_add_before, ospfv3['new_frr_config']) - - frr_cfg.commit_configuration(ospf6_daemon) - +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py index 79294a1f0..bb55aada0 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -22,72 +22,33 @@ from signal import SIGTERM from sys import exit from vyos.config import Config -from vyos.config import config_dict_merge -from vyos.configdict import node_changed from vyos.configverify import verify_interface_exists +from vyos.configverify import has_frr_protocol_in_dict +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict +from vyos.frrender import pim_daemon +from vyos.utils.process import is_systemd_service_running from vyos.utils.process import process_named_running from vyos.utils.process import call -from vyos.template import render_to_string from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() -RESERVED_MC_NET = '224.0.0.0/24' - - def get_config(config=None): if config: conf = config else: conf = Config() - base = ['protocols', 'pim'] - - pim = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - - # We can not run both IGMP proxy and PIM at the same time - get IGMP - # proxy status - if conf.exists(['protocols', 'igmp-proxy']): - pim.update({'igmp_proxy_enabled' : {}}) - - # FRR has VRF support for different routing daemons. As interfaces belong - # to VRFs - or the global VRF, we need to check for changed interfaces so - # that they will be properly rendered for the FRR config. Also this eases - # removal of interfaces from the running configuration. - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - pim['interface_removed'] = list(interfaces_removed) - - # Bail out early if configuration tree does no longer exist. this must - # be done after retrieving the list of interfaces to be removed. - if not conf.exists(base): - pim.update({'deleted' : ''}) - return pim - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = conf.get_config_defaults(**pim.kwargs, recursive=True) - - # We have to cleanup the default dict, as default values could enable features - # which are not explicitly enabled on the CLI. Example: default-information - # originate comes with a default metric-type of 2, which will enable the - # entire default-information originate tree, even when not set via CLI so we - # need to check this first and probably drop that key. - for interface in pim.get('interface', []): - # We need to reload the defaults on every pass b/c of - # hello-multiplier dependency on dead-interval - # If hello-multiplier is set, we need to remove the default from - # dead-interval. - if 'igmp' not in pim['interface'][interface]: - del default_values['interface'][interface]['igmp'] - - pim = config_dict_merge(default_values, pim) - return pim - -def verify(pim): - if not pim or 'deleted' in pim: + return get_frrender_dict(conf) + +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'pim'): + return None + + pim = config_dict['pim'] + + if 'deleted' in pim: return None if 'igmp_proxy_enabled' in pim: @@ -96,6 +57,7 @@ def verify(pim): if 'interface' not in pim: raise ConfigError('PIM require defined interfaces!') + RESERVED_MC_NET = '224.0.0.0/24' for interface, interface_config in pim['interface'].items(): verify_interface_exists(pim, interface) @@ -124,41 +86,26 @@ def verify(pim): raise ConfigError(f'{pim_base_error} must be unique!') unique.append(gr_addr) -def generate(pim): - if not pim or 'deleted' in pim: - return None - pim['frr_pimd_config'] = render_to_string('frr/pimd.frr.j2', pim) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None -def apply(pim): - pim_daemon = 'pimd' - pim_pid = process_named_running(pim_daemon) - - if not pim or 'deleted' in pim: - if 'deleted' in pim: - os.kill(int(pim_pid), SIGTERM) +def apply(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'pim'): + return None + pim_pid = process_named_running(pim_daemon) + pim = config_dict['pim'] + if 'deleted' in pim: + os.kill(int(pim_pid), SIGTERM) return None if not pim_pid: call('/usr/lib/frr/pimd -d -F traditional --daemon -A 127.0.0.1') - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - frr_cfg.load_configuration(pim_daemon) - frr_cfg.modify_section(f'^ip pim') - frr_cfg.modify_section(f'^ip igmp') - - for key in ['interface', 'interface_removed']: - if key not in pim: - continue - for interface in pim[key]: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - if 'frr_pimd_config' in pim: - frr_cfg.add_before(frr.default_add_before, pim['frr_pimd_config']) - frr_cfg.commit_configuration(pim_daemon) + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_pim6.py b/src/conf_mode/protocols_pim6.py index 581ffe238..f7803246a 100755 --- a/src/conf_mode/protocols_pim6.py +++ b/src/conf_mode/protocols_pim6.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -19,12 +19,12 @@ from ipaddress import IPv6Network from sys import exit from vyos.config import Config -from vyos.config import config_dict_merge -from vyos.configdict import node_changed +from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_interface_exists -from vyos.template import render_to_string +from vyos.utils.process import is_systemd_service_running +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -33,34 +33,15 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['protocols', 'pim6'] - pim6 = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, with_recursive_defaults=True) + return get_frrender_dict(conf) - # FRR has VRF support for different routing daemons. As interfaces belong - # to VRFs - or the global VRF, we need to check for changed interfaces so - # that they will be properly rendered for the FRR config. Also this eases - # removal of interfaces from the running configuration. - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - pim6['interface_removed'] = list(interfaces_removed) +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'pim6'): + return None - # Bail out early if configuration tree does no longer exist. this must - # be done after retrieving the list of interfaces to be removed. - if not conf.exists(base): - pim6.update({'deleted' : ''}) - return pim6 - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - default_values = conf.get_config_defaults(**pim6.kwargs, recursive=True) - - pim6 = config_dict_merge(default_values, pim6) - return pim6 - -def verify(pim6): - if not pim6 or 'deleted' in pim6: - return + pim6 = config_dict['pim6'] + if 'deleted' in pim6: + return None for interface, interface_config in pim6.get('interface', {}).items(): verify_interface_exists(pim6, interface) @@ -94,32 +75,14 @@ def verify(pim6): raise ConfigError(f'{pim_base_error} must be unique!') unique.append(gr_addr) -def generate(pim6): - if not pim6 or 'deleted' in pim6: - return - pim6['new_frr_config'] = render_to_string('frr/pim6d.frr.j2', pim6) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None -def apply(pim6): - if pim6 is None: - return - - pim6_daemon = 'pim6d' - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - frr_cfg.load_configuration(pim6_daemon) - - for key in ['interface', 'interface_removed']: - if key not in pim6: - continue - for interface in pim6[key]: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - if 'new_frr_config' in pim6: - frr_cfg.add_before(frr.default_add_before, pim6['new_frr_config']) - frr_cfg.commit_configuration(pim6_daemon) +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py index 9afac544d..c6adcde5b 100755 --- a/src/conf_mode/protocols_rip.py +++ b/src/conf_mode/protocols_rip.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -17,15 +17,15 @@ from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.configdict import node_changed +from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_access_list from vyos.configverify import verify_prefix_list +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search -from vyos.template import render_to_string +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -34,41 +34,16 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['protocols', 'rip'] - rip = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # FRR has VRF support for different routing daemons. As interfaces belong - # to VRFs - or the global VRF, we need to check for changed interfaces so - # that they will be properly rendered for the FRR config. Also this eases - # removal of interfaces from the running configuration. - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - rip['interface_removed'] = list(interfaces_removed) + return get_frrender_dict(conf) - # Bail out early if configuration tree does not exist - if not conf.exists(base): - rip.update({'deleted' : ''}) - return rip - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - rip = conf.merge_defaults(rip, recursive=True) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - rip = dict_merge(tmp, rip) - - return rip - -def verify(rip): - if not rip: +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'rip'): return None + rip = config_dict['rip'] + rip['policy'] = config_dict['policy'] + verify_common_route_maps(rip) acl_in = dict_search('distribute_list.access_list.in', rip) @@ -93,39 +68,14 @@ def verify(rip): raise ConfigError(f'You can not have "split-horizon poison-reverse" enabled ' \ f'with "split-horizon disable" for "{interface}"!') -def generate(rip): - if not rip or 'deleted' in rip: - return None - - rip['new_frr_config'] = render_to_string('frr/ripd.frr.j2', rip) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None -def apply(rip): - rip_daemon = 'ripd' - zebra_daemon = 'zebra' - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section('^ip protocol rip route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - frr_cfg.commit_configuration(zebra_daemon) - - frr_cfg.load_configuration(rip_daemon) - frr_cfg.modify_section('^key chain \S+', stop_pattern='^exit', remove_stop_mark=True) - frr_cfg.modify_section('^router rip', stop_pattern='^exit', remove_stop_mark=True) - - for key in ['interface', 'interface_removed']: - if key not in rip: - continue - for interface in rip[key]: - frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) - - if 'new_frr_config' in rip: - frr_cfg.add_before(frr.default_add_before, rip['new_frr_config']) - frr_cfg.commit_configuration(rip_daemon) - +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py index 23416ff96..e5babf2e8 100755 --- a/src/conf_mode/protocols_ripng.py +++ b/src/conf_mode/protocols_ripng.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -17,14 +17,15 @@ from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_access_list from vyos.configverify import verify_prefix_list +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search -from vyos.template import render_to_string +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -33,32 +34,16 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['protocols', 'ripng'] - ripng = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # Bail out early if configuration tree does not exist - if not conf.exists(base): - return ripng + return get_frrender_dict(conf) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - ripng = conf.merge_defaults(ripng, recursive=True) - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - ripng = dict_merge(tmp, ripng) - - return ripng - -def verify(ripng): - if not ripng: +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'ripng'): return None + ripng = config_dict['ripng'] + ripng['policy'] = config_dict['policy'] + verify_common_route_maps(ripng) acl_in = dict_search('distribute_list.access_list.in', ripng) @@ -83,34 +68,14 @@ def verify(ripng): raise ConfigError(f'You can not have "split-horizon poison-reverse" enabled ' \ f'with "split-horizon disable" for "{interface}"!') -def generate(ripng): - if not ripng: - ripng['new_frr_config'] = '' - return None - - ripng['new_frr_config'] = render_to_string('frr/ripngd.frr.j2', ripng) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None -def apply(ripng): - ripng_daemon = 'ripngd' - zebra_daemon = 'zebra' - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section('^ipv6 protocol ripng route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - frr_cfg.commit_configuration(zebra_daemon) - - frr_cfg.load_configuration(ripng_daemon) - frr_cfg.modify_section('key chain \S+', stop_pattern='^exit', remove_stop_mark=True) - frr_cfg.modify_section('interface \S+', stop_pattern='^exit', remove_stop_mark=True) - frr_cfg.modify_section('^router ripng', stop_pattern='^exit', remove_stop_mark=True) - if 'new_frr_config' in ripng: - frr_cfg.add_before(frr.default_add_before, ripng['new_frr_config']) - frr_cfg.commit_configuration(ripng_daemon) - +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py index a59ecf3e4..5ba23c141 100755 --- a/src/conf_mode/protocols_rpki.py +++ b/src/conf_mode/protocols_rpki.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -18,15 +18,18 @@ import os from glob import glob from sys import exit +from sys import argv from vyos.config import Config +from vyos.configverify import has_frr_protocol_in_dict +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.pki import wrap_openssh_public_key from vyos.pki import wrap_openssh_private_key -from vyos.template import render_to_string from vyos.utils.dict import dict_search_args from vyos.utils.file import write_file +from vyos.utils.process import is_systemd_service_running from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -37,24 +40,18 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['protocols', 'rpki'] + return get_frrender_dict(conf, argv) - rpki = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, with_pki=True) - # Bail out early if configuration tree does not exist - if not conf.exists(base): - rpki.update({'deleted' : ''}) - return rpki - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - rpki = conf.merge_defaults(rpki, recursive=True) +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'rpki'): + return None - return rpki + vrf = None + if 'vrf_context' in config_dict: + vrf = config_dict['vrf_context'] -def verify(rpki): - if not rpki: - return None + # eqivalent of the C foo ? 'a' : 'b' statement + rpki = vrf and config_dict['vrf']['name'][vrf]['protocols']['rpki'] or config_dict['rpki'] if 'cache' in rpki: preferences = [] @@ -81,12 +78,19 @@ def verify(rpki): return None -def generate(rpki): +def generate(config_dict): for key in glob(f'{rpki_ssh_key_base}*'): os.unlink(key) - if not rpki: - return + if not has_frr_protocol_in_dict(config_dict, 'rpki'): + return None + + vrf = None + if 'vrf_context' in config_dict: + vrf = config_dict['vrf_context'] + + # eqivalent of the C foo ? 'a' : 'b' statement + rpki = vrf and config_dict['vrf']['name'][vrf]['protocols']['rpki'] or config_dict['rpki'] if 'cache' in rpki: for cache, cache_config in rpki['cache'].items(): @@ -102,21 +106,13 @@ def generate(rpki): write_file(cache_config['ssh']['public_key_file'], wrap_openssh_public_key(public_key_data, public_key_type)) write_file(cache_config['ssh']['private_key_file'], wrap_openssh_private_key(private_key_data)) - rpki['new_frr_config'] = render_to_string('frr/rpki.frr.j2', rpki) - + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None -def apply(rpki): - bgp_daemon = 'bgpd' - - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(bgp_daemon) - frr_cfg.modify_section('^rpki', stop_pattern='^exit', remove_stop_mark=True) - if 'new_frr_config' in rpki: - frr_cfg.add_before(frr.default_add_before, rpki['new_frr_config']) - - frr_cfg.commit_configuration(bgp_daemon) +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_segment-routing.py b/src/conf_mode/protocols_segment-routing.py index b36c2ca11..4020a0917 100755 --- a/src/conf_mode/protocols_segment-routing.py +++ b/src/conf_mode/protocols_segment-routing.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -17,12 +17,15 @@ from sys import exit from vyos.config import Config -from vyos.configdict import node_changed -from vyos.template import render_to_string +from vyos.configdict import list_diff +from vyos.configverify import has_frr_protocol_in_dict +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict +from vyos.ifconfig import Section from vyos.utils.dict import dict_search +from vyos.utils.process import is_systemd_service_running from vyos.utils.system import sysctl_write from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -32,25 +35,14 @@ def get_config(config=None): else: conf = Config() - base = ['protocols', 'segment-routing'] - sr = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True, - with_recursive_defaults=True) + return get_frrender_dict(conf) - # FRR has VRF support for different routing daemons. As interfaces belong - # to VRFs - or the global VRF, we need to check for changed interfaces so - # that they will be properly rendered for the FRR config. Also this eases - # removal of interfaces from the running configuration. - interfaces_removed = node_changed(conf, base + ['interface']) - if interfaces_removed: - sr['interface_removed'] = list(interfaces_removed) +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'segment_routing'): + return None - import pprint - pprint.pprint(sr) - return sr + sr = config_dict['segment_routing'] -def verify(sr): if 'srv6' in sr: srv6_enable = False if 'interface' in sr: @@ -62,47 +54,43 @@ def verify(sr): raise ConfigError('SRv6 should be enabled on at least one interface!') return None -def generate(sr): - if not sr: - return None - - sr['new_frr_config'] = render_to_string('frr/zebra.segment_routing.frr.j2', sr) +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) return None -def apply(sr): - zebra_daemon = 'zebra' +def apply(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'segment_routing'): + return None - if 'interface_removed' in sr: - for interface in sr['interface_removed']: - # Disable processing of IPv6-SR packets - sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0') + sr = config_dict['segment_routing'] + + current_interfaces = Section.interfaces() + sr_interfaces = list(sr.get('interface', {}).keys()) - if 'interface' in sr: - for interface, interface_config in sr['interface'].items(): - # Accept or drop SR-enabled IPv6 packets on this interface - if 'srv6' in interface_config: - sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '1') - # Define HMAC policy for ingress SR-enabled packets on this interface - # It's a redundant check as HMAC has a default value - but better safe - # then sorry - tmp = dict_search('srv6.hmac', interface_config) - if tmp == 'accept': - sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '0') - elif tmp == 'drop': - sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '1') - elif tmp == 'ignore': - sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '-1') - else: - sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0') + for interface in list_diff(current_interfaces, sr_interfaces): + # Disable processing of IPv6-SR packets + sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0') - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'^segment-routing') - if 'new_frr_config' in sr: - frr_cfg.add_before(frr.default_add_before, sr['new_frr_config']) - frr_cfg.commit_configuration(zebra_daemon) + for interface, interface_config in sr.get('interface', {}).items(): + # Accept or drop SR-enabled IPv6 packets on this interface + if 'srv6' in interface_config: + sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '1') + # Define HMAC policy for ingress SR-enabled packets on this interface + # It's a redundant check as HMAC has a default value - but better safe + # then sorry + tmp = dict_search('srv6.hmac', interface_config) + if tmp == 'accept': + sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '0') + elif tmp == 'drop': + sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '1') + elif tmp == 'ignore': + sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '-1') + else: + sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0') + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index a2373218a..7c730ca81 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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,19 +14,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +from ipaddress import IPv4Network from sys import exit from sys import argv from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.configdict import get_dhcp_interfaces -from vyos.configdict import get_pppoe_interfaces +from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_vrf +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict +from vyos.utils.process import is_systemd_service_running from vyos.template import render -from vyos.template import render_to_string from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -38,36 +38,20 @@ def get_config(config=None): else: conf = Config() + return get_frrender_dict(conf, argv) + +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'static'): + return None + vrf = None - if len(argv) > 1: - vrf = argv[1] + if 'vrf_context' in config_dict: + vrf = config_dict['vrf_context'] - base_path = ['protocols', 'static'] # eqivalent of the C foo ? 'a' : 'b' statement - base = vrf and ['vrf', 'name', vrf, 'protocols', 'static'] or base_path - static = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - - # Assign the name of our VRF context - if vrf: static['vrf'] = vrf - - # We also need some additional information from the config, prefix-lists - # and route-maps for instance. They will be used in verify(). - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = conf.get_config_dict(['policy']) - # Merge policy dict into "regular" config dict - static = dict_merge(tmp, static) - - # T3680 - get a list of all interfaces currently configured to use DHCP - tmp = get_dhcp_interfaces(conf, vrf) - if tmp: static.update({'dhcp' : tmp}) - tmp = get_pppoe_interfaces(conf, vrf) - if tmp: static.update({'pppoe' : tmp}) - - return static - -def verify(static): + static = vrf and config_dict['vrf']['name'][vrf]['protocols']['static'] or config_dict['static'] + static['policy'] = config_dict['policy'] + verify_common_route_maps(static) for route in ['route', 'route6']: @@ -88,37 +72,36 @@ def verify(static): if {'blackhole', 'reject'} <= set(prefix_options): raise ConfigError(f'Can not use both blackhole and reject for '\ - 'prefix "{prefix}"!') + f'prefix "{prefix}"!') + + if 'multicast' in static and 'route' in static['multicast']: + for prefix, prefix_options in static['multicast']['route'].items(): + if not IPv4Network(prefix).is_multicast: + raise ConfigError(f'{prefix} is not a multicast network!') return None -def generate(static): - if not static: +def generate(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'static'): return None - # Put routing table names in /etc/iproute2/rt_tables - render(config_file, 'iproute2/static.conf.j2', static) - static['new_frr_config'] = render_to_string('frr/staticd.frr.j2', static) - return None - -def apply(static): - static_daemon = 'staticd' + vrf = None + if 'vrf_context' in config_dict: + vrf = config_dict['vrf_context'] - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(static_daemon) + # eqivalent of the C foo ? 'a' : 'b' statement + static = vrf and config_dict['vrf']['name'][vrf]['protocols']['static'] or config_dict['static'] - if 'vrf' in static: - vrf = static['vrf'] - frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit-vrf', remove_stop_mark=True) - else: - frr_cfg.modify_section(r'^ip route .*') - frr_cfg.modify_section(r'^ipv6 route .*') + # Put routing table names in /etc/iproute2/rt_tables + render(config_file, 'iproute2/static.conf.j2', static) - if 'new_frr_config' in static: - frr_cfg.add_before(frr.default_add_before, static['new_frr_config']) - frr_cfg.commit_configuration(static_daemon) + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) + return None +def apply(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_static_arp.py b/src/conf_mode/protocols_static_arp.py index b141f1141..87dc5229e 100755 --- a/src/conf_mode/protocols_static_arp.py +++ b/src/conf_mode/protocols_static_arp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py deleted file mode 100755 index d323ceb4f..000000000 --- a/src/conf_mode/protocols_static_multicast.py +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env python3 -# -# 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 -# 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/>. - - -from ipaddress import IPv4Address -from sys import exit - -from vyos import ConfigError -from vyos import frr -from vyos.config import Config -from vyos.template import render_to_string - -from vyos import airbag -airbag.enable() - -config_file = r'/tmp/static_mcast.frr' - -# Get configuration for static multicast route -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - mroute = { - 'old_mroute' : {}, - 'mroute' : {} - } - - base_path = "protocols static multicast" - - if not (conf.exists(base_path) or conf.exists_effective(base_path)): - return None - - conf.set_level(base_path) - - # Get multicast effective routes - for route in conf.list_effective_nodes('route'): - mroute['old_mroute'][route] = {} - for next_hop in conf.list_effective_nodes('route {0} next-hop'.format(route)): - mroute['old_mroute'][route].update({ - next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop)) - }) - - # Get multicast effective interface-routes - for route in conf.list_effective_nodes('interface-route'): - if not route in mroute['old_mroute']: - mroute['old_mroute'][route] = {} - for next_hop in conf.list_effective_nodes('interface-route {0} next-hop-interface'.format(route)): - mroute['old_mroute'][route].update({ - next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop)) - }) - - # Get multicast routes - for route in conf.list_nodes('route'): - mroute['mroute'][route] = {} - for next_hop in conf.list_nodes('route {0} next-hop'.format(route)): - mroute['mroute'][route].update({ - next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop)) - }) - - # Get multicast interface-routes - for route in conf.list_nodes('interface-route'): - if not route in mroute['mroute']: - mroute['mroute'][route] = {} - for next_hop in conf.list_nodes('interface-route {0} next-hop-interface'.format(route)): - mroute['mroute'][route].update({ - next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop)) - }) - - return mroute - -def verify(mroute): - if mroute is None: - return None - - for route in mroute['mroute']: - route = route.split('/') - 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 - - 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 '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() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/protocols_static_neighbor-proxy.py b/src/conf_mode/protocols_static_neighbor-proxy.py index 8a1ea1df9..bda737e75 100755 --- a/src/conf_mode/protocols_static_neighbor-proxy.py +++ b/src/conf_mode/protocols_static_neighbor-proxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index 7dfad3180..326d617a2 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -85,7 +85,13 @@ def _clean_conf_dict(conf): } """ if isinstance(conf, dict): - return {node: _clean_conf_dict(val) for node, val in conf.items() if val != {} and _clean_conf_dict(val) != {}} + preserve_empty_nodes = {'syn', 'ack'} + + return { + node: _clean_conf_dict(val) + for node, val in conf.items() + if (val != {} and _clean_conf_dict(val) != {}) or node in preserve_empty_nodes + } else: return conf @@ -198,10 +204,16 @@ def get_config(config=None): def _verify_match(cls_config: dict) -> None: if 'match' in cls_config: for match, match_config in cls_config['match'].items(): - if {'ip', 'ipv6'} <= set(match_config): + filters = set(match_config) + if {'ip', 'ipv6'} <= filters: raise ConfigError( f'Can not use both IPv6 and IPv4 in one match ({match})!') + if {'interface', 'vif'} & filters: + if {'ip', 'ipv6', 'ether'} & filters: + raise ConfigError( + f'Can not combine protocol and interface or vlan tag match ({match})!') + def _verify_match_group_exist(cls_config, qos): if 'match_group' in cls_config: @@ -210,6 +222,46 @@ def _verify_match_group_exist(cls_config, qos): Warning(f'Match group "{group}" does not exist!') +def _verify_default_policy_exist(policy, policy_config): + if 'default' not in policy_config: + raise ConfigError(f'Policy {policy} misses "default" class!') + + +def _check_shaper_hfsc_rate(cls, cls_conf): + is_m2_exist = False + for crit in TrafficShaperHFSC.criteria: + if cls_conf.get(crit, {}).get('m2') is not None: + is_m2_exist = True + + if cls_conf.get(crit, {}).get('m1') is not None: + for crit_val in ['m2', 'd']: + if cls_conf.get(crit, {}).get(crit_val) is None: + raise ConfigError( + f'{cls} {crit} m1 value is set, but no {crit_val} was found!' + ) + + if not is_m2_exist: + raise ConfigError(f'At least one m2 value needs to be set for class: {cls}') + + if ( + cls_conf.get('upperlimit', {}).get('m2') is not None + and cls_conf.get('linkshare', {}).get('m2') is None + ): + raise ConfigError( + f'Linkshare m2 needs to be defined to use upperlimit m2 for class: {cls}' + ) + + +def _verify_shaper_hfsc(policy, policy_config): + _verify_default_policy_exist(policy, policy_config) + + _check_shaper_hfsc_rate('default', policy_config.get('default')) + + if 'class' in policy_config: + for cls, cls_conf in policy_config['class'].items(): + _check_shaper_hfsc_rate(cls, cls_conf) + + def verify(qos): if not qos or 'interface' not in qos: return None @@ -253,8 +305,13 @@ def verify(qos): if queue_lim < max_tr: raise ConfigError(f'Policy "{policy}" uses queue-limit "{queue_lim}" < max-threshold "{max_tr}"!') if policy_type in ['priority_queue']: - if 'default' not in policy_config: - raise ConfigError(f'Policy {policy} misses "default" class!') + _verify_default_policy_exist(policy, policy_config) + if policy_type in ['rate_control']: + if 'bandwidth' not in policy_config: + raise ConfigError('Bandwidth not defined') + if policy_type in ['shaper_hfsc']: + _verify_shaper_hfsc(policy, policy_config) + if 'default' in policy_config: if 'bandwidth' not in policy_config['default'] and policy_type not in ['priority_queue', 'round_robin', 'shaper_hfsc']: raise ConfigError('Bandwidth not defined for default traffic!') @@ -290,6 +347,7 @@ def generate(qos): return None + def apply(qos): # Always delete "old" shapers first for interface in interfaces(): diff --git a/src/conf_mode/service_aws_glb.py b/src/conf_mode/service_aws_glb.py index d1ed5a07b..aa5ec5ebe 100755 --- a/src/conf_mode/service_aws_glb.py +++ b/src/conf_mode/service_aws_glb.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_broadcast-relay.py b/src/conf_mode/service_broadcast-relay.py index d35954718..f7f2875f5 100755 --- a/src/conf_mode/service_broadcast-relay.py +++ b/src/conf_mode/service_broadcast-relay.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_config-sync.py b/src/conf_mode/service_config-sync.py index 4b8a7f6ee..32001ce57 100755 --- a/src/conf_mode/service_config-sync.py +++ b/src/conf_mode/service_config-sync.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_conntrack-sync.py b/src/conf_mode/service_conntrack-sync.py index 3a233a172..5eb4ca0e5 100755 --- a/src/conf_mode/service_conntrack-sync.py +++ b/src/conf_mode/service_conntrack-sync.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py index b112add3f..82386babf 100755 --- a/src/conf_mode/service_console-server.py +++ b/src/conf_mode/service_console-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2021 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -98,6 +98,12 @@ def generate(proxy): return None def apply(proxy): + if not os.path.exists('/etc/dropbear/dropbear_rsa_host_key'): + call('dropbearkey -t rsa -s 4096 -f /etc/dropbear/dropbear_rsa_host_key') + + if not os.path.exists('/etc/dropbear/dropbear_ecdsa_host_key'): + call('dropbearkey -t ecdsa -f /etc/dropbear/dropbear_ecdsa_host_key') + call('systemctl daemon-reload') call('systemctl stop dropbear@*.service conserver-server.service') diff --git a/src/conf_mode/service_dhcp-relay.py b/src/conf_mode/service_dhcp-relay.py index 37d708847..ebdf5e8ee 100755 --- a/src/conf_mode/service_dhcp-relay.py +++ b/src/conf_mode/service_dhcp-relay.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py index e89448e2d..6bbe72e90 100755 --- a/src/conf_mode/service_dhcp-server.py +++ b/src/conf_mode/service_dhcp-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -38,20 +38,21 @@ from vyos.utils.network import is_subnet_connected from vyos.utils.network import is_addr_assigned from vyos import ConfigError from vyos import airbag + airbag.enable() -ctrl_config_file = '/run/kea/kea-ctrl-agent.conf' ctrl_socket = '/run/kea/dhcp4-ctrl-socket' config_file = '/run/kea/kea-dhcp4.conf' +config_file_d2 = '/run/kea/kea-dhcp-ddns.conf' lease_file = '/config/dhcp/dhcp4-leases.csv' lease_file_glob = '/config/dhcp/dhcp4-leases*' -systemd_override = r'/run/systemd/system/kea-ctrl-agent.service.d/10-override.conf' user_group = '_kea' ca_cert_file = '/run/kea/kea-failover-ca.pem' cert_file = '/run/kea/kea-failover.pem' cert_key_file = '/run/kea/kea-failover-key.pem' + def dhcp_slice_range(exclude_list, range_dict): """ This function is intended to slice a DHCP range. What does it mean? @@ -74,19 +75,21 @@ def dhcp_slice_range(exclude_list, range_dict): range_last_exclude = '' for e in exclude_list: - if (ip_address(e) >= ip_address(range_start)) and \ - (ip_address(e) <= ip_address(range_stop)): + if (ip_address(e) >= ip_address(range_start)) and ( + ip_address(e) <= ip_address(range_stop) + ): range_last_exclude = e for e in exclude_list: - if (ip_address(e) >= ip_address(range_start)) and \ - (ip_address(e) <= ip_address(range_stop)): - + if (ip_address(e) >= ip_address(range_start)) and ( + ip_address(e) <= ip_address(range_stop) + ): # Build new address range ending one address before exclude address - r = { - 'start' : range_start, - 'stop' : str(ip_address(e) -1) - } + r = {'start': range_start, 'stop': str(ip_address(e) - 1)} + + if 'option' in range_dict: + r['option'] = range_dict['option'] + # On the next run our address range will start one address after # the exclude address range_start = str(ip_address(e) + 1) @@ -100,21 +103,23 @@ def dhcp_slice_range(exclude_list, range_dict): # Take care of last IP address range spanning from the last exclude # address (+1) to the end of the initial configured range if ip_address(e) == ip_address(range_last_exclude): - r = { - 'start': str(ip_address(e) + 1), - 'stop': str(range_stop) - } + r = {'start': str(ip_address(e) + 1), 'stop': str(range_stop)} + + if 'option' in range_dict: + r['option'] = range_dict['option'] + if not (ip_address(r['start']) > ip_address(r['stop'])): output.append(r) else: - # if the excluded address was not part of the range, we simply return - # the entire ranga again - if not range_last_exclude: - if range_dict not in output: - output.append(range_dict) + # if the excluded address was not part of the range, we simply return + # the entire ranga again + if not range_last_exclude: + if range_dict not in output: + output.append(range_dict) return output + def get_config(config=None): if config: conf = config @@ -124,10 +129,13 @@ def get_config(config=None): if not conf.exists(base): return None - dhcp = conf.get_config_dict(base, key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - get_first_key=True, - with_recursive_defaults=True) + dhcp = conf.get_config_dict( + base, + key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + get_first_key=True, + with_recursive_defaults=True, + ) if 'shared_network_name' in dhcp: for network, network_config in dhcp['shared_network_name'].items(): @@ -139,22 +147,40 @@ def get_config(config=None): new_range_id = 0 new_range_dict = {} for r, r_config in subnet_config['range'].items(): - for slice in dhcp_slice_range(subnet_config['exclude'], r_config): - new_range_dict.update({new_range_id : slice}) - new_range_id +=1 + for slice in dhcp_slice_range( + subnet_config['exclude'], r_config + ): + new_range_dict.update({new_range_id: slice}) + new_range_id += 1 dhcp['shared_network_name'][network]['subnet'][subnet].update( - {'range' : new_range_dict}) + {'range': new_range_dict} + ) if len(dhcp['high_availability']) == 1: ## only default value for mode is set, need to remove ha node del dhcp['high_availability'] else: if dict_search('high_availability.certificate', dhcp): - dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + dhcp['pki'] = conf.get_config_dict( + ['pki'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + ) return dhcp +def verify_ddns_domain_servers(domain_type, domain): + if 'dns_server' in domain: + invalid_servers = [] + for server_no, server_config in domain['dns_server'].items(): + if 'address' not in server_config: + invalid_servers.append(server_no) + if len(invalid_servers) > 0: + raise ConfigError(f'{domain_type} DNS servers {", ".join(invalid_servers)} in DDNS configuration need to have an IP address') + return None + def verify(dhcp): # bail out early - looks like removal from running config if not dhcp or 'disable' in dhcp: @@ -162,13 +188,15 @@ def verify(dhcp): # If DHCP is enabled we need one share-network if 'shared_network_name' not in dhcp: - raise ConfigError('No DHCP shared networks configured.\n' \ - 'At least one DHCP shared network must be configured.') + raise ConfigError( + 'No DHCP shared networks configured.\n' + 'At least one DHCP shared network must be configured.' + ) # Inspect shared-network/subnet listen_ok = False subnets = [] - shared_networks = len(dhcp['shared_network_name']) + shared_networks = len(dhcp['shared_network_name']) disabled_shared_networks = 0 subnet_ids = [] @@ -179,12 +207,16 @@ def verify(dhcp): disabled_shared_networks += 1 if 'subnet' not in network_config: - raise ConfigError(f'No subnets defined for {network}. At least one\n' \ - 'lease subnet must be configured.') + raise ConfigError( + f'No subnets defined for {network}. At least one\n' + 'lease subnet must be configured.' + ) for subnet, subnet_config in network_config['subnet'].items(): if 'subnet_id' not in subnet_config: - raise ConfigError(f'Unique subnet ID not specified for subnet "{subnet}"') + raise ConfigError( + f'Unique subnet ID not specified for subnet "{subnet}"' + ) if subnet_config['subnet_id'] in subnet_ids: raise ConfigError(f'Subnet ID for subnet "{subnet}" is not unique') @@ -195,32 +227,46 @@ def verify(dhcp): if 'static_route' in subnet_config: for route, route_option in subnet_config['static_route'].items(): if 'next_hop' not in route_option: - raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!') + raise ConfigError( + f'DHCP static-route "{route}" requires router to be defined!' + ) # Check if DHCP address range is inside configured subnet declaration if 'range' in subnet_config: networks = [] for range, range_config in subnet_config['range'].items(): if not {'start', 'stop'} <= set(range_config): - raise ConfigError(f'DHCP range "{range}" start and stop address must be defined!') + raise ConfigError( + f'DHCP range "{range}" start and stop address must be defined!' + ) # Start/Stop address must be inside network for key in ['start', 'stop']: if ip_address(range_config[key]) not in ip_network(subnet): - raise ConfigError(f'DHCP range "{range}" {key} address not within shared-network "{network}, {subnet}"!') + raise ConfigError( + f'DHCP range "{range}" {key} address not within shared-network "{network}, {subnet}"!' + ) # Stop address must be greater or equal to start address - if ip_address(range_config['stop']) < ip_address(range_config['start']): - raise ConfigError(f'DHCP range "{range}" stop address must be greater or equal\n' \ - 'to the ranges start address!') + if ip_address(range_config['stop']) < ip_address( + range_config['start'] + ): + raise ConfigError( + f'DHCP range "{range}" stop address must be greater or equal\n' + 'to the ranges start address!' + ) for network in networks: start = range_config['start'] stop = range_config['stop'] if start in network: - raise ConfigError(f'Range "{range}" start address "{start}" already part of another range!') + raise ConfigError( + f'Range "{range}" start address "{start}" already part of another range!' + ) if stop in network: - raise ConfigError(f'Range "{range}" stop address "{stop}" already part of another range!') + raise ConfigError( + f'Range "{range}" stop address "{stop}" already part of another range!' + ) tmp = IPRange(range_config['start'], range_config['stop']) networks.append(tmp) @@ -229,12 +275,16 @@ def verify(dhcp): if 'exclude' in subnet_config: for exclude in subnet_config['exclude']: if ip_address(exclude) not in ip_network(subnet): - raise ConfigError(f'Excluded IP address "{exclude}" not within shared-network "{network}, {subnet}"!') + raise ConfigError( + f'Excluded IP address "{exclude}" not within shared-network "{network}, {subnet}"!' + ) # At least one DHCP address range or static-mapping required if 'range' not in subnet_config and 'static_mapping' not in subnet_config: - raise ConfigError(f'No DHCP address range or active static-mapping configured\n' \ - f'within shared-network "{network}, {subnet}"!') + raise ConfigError( + f'No DHCP address range or active static-mapping configured\n' + f'within shared-network "{network}, {subnet}"!' + ) if 'static_mapping' in subnet_config: # Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set) @@ -243,29 +293,42 @@ def verify(dhcp): used_duid = [] for mapping, mapping_config in subnet_config['static_mapping'].items(): if 'ip_address' in mapping_config: - if ip_address(mapping_config['ip_address']) not in ip_network(subnet): - raise ConfigError(f'Configured static lease address for mapping "{mapping}" is\n' \ - f'not within shared-network "{network}, {subnet}"!') - - if ('mac' not in mapping_config and 'duid' not in mapping_config) or \ - ('mac' in mapping_config and 'duid' in mapping_config): - raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for ' - f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!') + if ip_address(mapping_config['ip_address']) not in ip_network( + subnet + ): + raise ConfigError( + f'Configured static lease address for mapping "{mapping}" is\n' + f'not within shared-network "{network}, {subnet}"!' + ) + + if ( + 'mac' not in mapping_config and 'duid' not in mapping_config + ) or ('mac' in mapping_config and 'duid' in mapping_config): + raise ConfigError( + f'Either MAC address or Client identifier (DUID) is required for ' + f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!' + ) if 'disable' not in mapping_config: if mapping_config['ip_address'] in used_ips: - raise ConfigError(f'Configured IP address for static mapping "{mapping}" already exists on another static mapping') + raise ConfigError( + f'Configured IP address for static mapping "{mapping}" already exists on another static mapping' + ) used_ips.append(mapping_config['ip_address']) if 'disable' not in mapping_config: if 'mac' in mapping_config: if mapping_config['mac'] in used_mac: - raise ConfigError(f'Configured MAC address for static mapping "{mapping}" already exists on another static mapping') + raise ConfigError( + f'Configured MAC address for static mapping "{mapping}" already exists on another static mapping' + ) used_mac.append(mapping_config['mac']) if 'duid' in mapping_config: if mapping_config['duid'] in used_duid: - raise ConfigError(f'Configured DUID for static mapping "{mapping}" already exists on another static mapping') + raise ConfigError( + f'Configured DUID for static mapping "{mapping}" already exists on another static mapping' + ) used_duid.append(mapping_config['duid']) # There must be one subnet connected to a listen interface. @@ -276,73 +339,118 @@ def verify(dhcp): # Subnets must be non overlapping if subnet in subnets: - raise ConfigError(f'Configured subnets must be unique! Subnet "{subnet}"\n' - 'defined multiple times!') + raise ConfigError( + f'Configured subnets must be unique! Subnet "{subnet}"\n' + 'defined multiple times!' + ) subnets.append(subnet) # Check for overlapping subnets net = ip_network(subnet) for n in subnets: net2 = ip_network(n) - if (net != net2): + if net != net2: if net.overlaps(net2): - raise ConfigError(f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!') + raise ConfigError( + f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!' + ) # Prevent 'disable' for shared-network if only one network is configured if (shared_networks - disabled_shared_networks) < 1: - raise ConfigError(f'At least one shared network must be active!') + raise ConfigError('At least one shared network must be active!') if 'high_availability' in dhcp: for key in ['name', 'remote', 'source_address', 'status']: if key not in dhcp['high_availability']: tmp = key.replace('_', '-') - raise ConfigError(f'DHCP high-availability requires "{tmp}" to be specified!') + raise ConfigError( + f'DHCP high-availability requires "{tmp}" to be specified!' + ) if len({'certificate', 'ca_certificate'} & set(dhcp['high_availability'])) == 1: - raise ConfigError(f'DHCP secured high-availability requires both certificate and CA certificate') + raise ConfigError( + 'DHCP secured high-availability requires both certificate and CA certificate' + ) if 'certificate' in dhcp['high_availability']: cert_name = dhcp['high_availability']['certificate'] if cert_name not in dhcp['pki']['certificate']: - raise ConfigError(f'Invalid certificate specified for DHCP high-availability') - - if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'certificate'): - raise ConfigError(f'Invalid certificate specified for DHCP high-availability') - - if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'private', 'key'): - raise ConfigError(f'Missing private key on certificate specified for DHCP high-availability') + raise ConfigError( + 'Invalid certificate specified for DHCP high-availability' + ) + + if not dict_search_args( + dhcp['pki']['certificate'], cert_name, 'certificate' + ): + raise ConfigError( + 'Invalid certificate specified for DHCP high-availability' + ) + + if not dict_search_args( + dhcp['pki']['certificate'], cert_name, 'private', 'key' + ): + raise ConfigError( + 'Missing private key on certificate specified for DHCP high-availability' + ) if 'ca_certificate' in dhcp['high_availability']: ca_cert_name = dhcp['high_availability']['ca_certificate'] if ca_cert_name not in dhcp['pki']['ca']: - raise ConfigError(f'Invalid CA certificate specified for DHCP high-availability') + raise ConfigError( + 'Invalid CA certificate specified for DHCP high-availability' + ) if not dict_search_args(dhcp['pki']['ca'], ca_cert_name, 'certificate'): - raise ConfigError(f'Invalid CA certificate specified for DHCP high-availability') + raise ConfigError( + 'Invalid CA certificate specified for DHCP high-availability' + ) - for address in (dict_search('listen_address', dhcp) or []): + for address in dict_search('listen_address', dhcp) or []: if is_addr_assigned(address, include_vrf=True): listen_ok = True # no need to probe further networks, we have one that is valid continue else: - raise ConfigError(f'listen-address "{address}" not configured on any interface') + raise ConfigError( + f'listen-address "{address}" not configured on any interface' + ) if not listen_ok: - raise ConfigError('None of the configured subnets have an appropriate primary IP address on any\n' - 'broadcast interface configured, nor was there an explicit listen-address\n' - 'configured for serving DHCP relay packets!') + raise ConfigError( + 'None of the configured subnets have an appropriate primary IP address on any\n' + 'broadcast interface configured, nor was there an explicit listen-address\n' + 'configured for serving DHCP relay packets!' + ) if 'listen_address' in dhcp and 'listen_interface' in dhcp: - raise ConfigError(f'Cannot define listen-address and listen-interface at the same time') + raise ConfigError( + 'Cannot define listen-address and listen-interface at the same time' + ) - for interface in (dict_search('listen_interface', dhcp) or []): + for interface in dict_search('listen_interface', dhcp) or []: if not interface_exists(interface): raise ConfigError(f'listen-interface "{interface}" does not exist') + if 'dynamic_dns_update' in dhcp: + ddns = dhcp['dynamic_dns_update'] + if 'tsig_key' in ddns: + invalid_keys = [] + for tsig_key_name, tsig_key_config in ddns['tsig_key'].items(): + if not ('algorithm' in tsig_key_config and 'secret' in tsig_key_config): + invalid_keys.append(tsig_key_name) + if len(invalid_keys) > 0: + raise ConfigError(f'Both algorithm and secret need to be set for TSIG keys: {", ".join(invalid_keys)}') + + if 'forward_domain' in ddns: + verify_ddns_domain_servers('Forward', ddns['forward_domain']) + + if 'reverse_domain' in ddns: + verify_ddns_domain_servers('Reverse', ddns['reverse_domain']) + return None + def generate(dhcp): # bail out early - looks like removal from running config if not dhcp or 'disable' in dhcp: @@ -374,8 +482,12 @@ def generate(dhcp): cert_name = dhcp['high_availability']['certificate'] cert_data = dhcp['pki']['certificate'][cert_name]['certificate'] key_data = dhcp['pki']['certificate'][cert_name]['private']['key'] - write_file(cert_file, wrap_certificate(cert_data), user=user_group, mode=0o600) - write_file(cert_key_file, wrap_private_key(key_data), user=user_group, mode=0o600) + write_file( + cert_file, wrap_certificate(cert_data), user=user_group, mode=0o600 + ) + write_file( + cert_key_file, wrap_private_key(key_data), user=user_group, mode=0o600 + ) dhcp['high_availability']['cert_file'] = cert_file dhcp['high_availability']['cert_key_file'] = cert_key_file @@ -383,19 +495,36 @@ def generate(dhcp): if 'ca_certificate' in dhcp['high_availability']: ca_cert_name = dhcp['high_availability']['ca_certificate'] ca_cert_data = dhcp['pki']['ca'][ca_cert_name]['certificate'] - write_file(ca_cert_file, wrap_certificate(ca_cert_data), user=user_group, mode=0o600) + write_file( + ca_cert_file, + wrap_certificate(ca_cert_data), + user=user_group, + mode=0o600, + ) dhcp['high_availability']['ca_cert_file'] = ca_cert_file - render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp) - - render(ctrl_config_file, 'dhcp-server/kea-ctrl-agent.conf.j2', dhcp, user=user_group, group=user_group) - render(config_file, 'dhcp-server/kea-dhcp4.conf.j2', dhcp, user=user_group, group=user_group) + render( + config_file, + 'dhcp-server/kea-dhcp4.conf.j2', + dhcp, + user=user_group, + group=user_group, + ) + if 'dynamic_dns_update' in dhcp: + render( + config_file_d2, + 'dhcp-server/kea-dhcp-ddns.conf.j2', + dhcp, + user=user_group, + group=user_group + ) return None + def apply(dhcp): - services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server'] + services = ['kea-dhcp4-server', 'kea-dhcp-ddns-server'] if not dhcp or 'disable' in dhcp: for service in services: @@ -412,13 +541,11 @@ def apply(dhcp): if service == 'kea-dhcp-ddns-server' and 'dynamic_dns_update' not in dhcp: action = 'stop' - if service == 'kea-ctrl-agent' and 'high_availability' not in dhcp: - action = 'stop' - call(f'systemctl {action} {service}.service') return None + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/service_dhcpv6-relay.py b/src/conf_mode/service_dhcpv6-relay.py index 6537ca3c2..4547b608c 100755 --- a/src/conf_mode/service_dhcpv6-relay.py +++ b/src/conf_mode/service_dhcpv6-relay.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_dhcpv6-server.py b/src/conf_mode/service_dhcpv6-server.py index 7af88007c..07dd08337 100755 --- a/src/conf_mode/service_dhcpv6-server.py +++ b/src/conf_mode/service_dhcpv6-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_dns_dynamic.py b/src/conf_mode/service_dns_dynamic.py index 5f5303856..b321d5f51 100755 --- a/src/conf_mode/service_dns_dynamic.py +++ b/src/conf_mode/service_dns_dynamic.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_dns_forwarding.py b/src/conf_mode/service_dns_forwarding.py index e3bdbc9f8..cd0c6a38a 100755 --- a/src/conf_mode/service_dns_forwarding.py +++ b/src/conf_mode/service_dns_forwarding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -366,6 +366,13 @@ def apply(dns): hc.add_name_server_tags_recursor(['dhcp-' + interface, 'dhcpv6-' + interface ]) + # add dhcp interfaces + if 'dhcp' in dns: + for interface in dns['dhcp']: + if interface_exists(interface): + hc.add_name_server_tags_recursor(['dhcp-' + interface, + 'dhcpv6-' + interface ]) + # hostsd will generate the forward-zones file # the list and keys() are required as get returns a dict, not list hc.delete_forward_zones(list(hc.get_forward_zones().keys())) diff --git a/src/conf_mode/service_event-handler.py b/src/conf_mode/service_event-handler.py index 5028ef52f..1b9e7ff53 100755 --- a/src/conf_mode/service_event-handler.py +++ b/src/conf_mode/service_event-handler.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_https.py b/src/conf_mode/service_https.py index 9e58b4c72..38bb485e7 100755 --- a/src/conf_mode/service_https.py +++ b/src/conf_mode/service_https.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -28,6 +28,7 @@ from vyos.configverify import verify_vrf from vyos.configverify import verify_pki_certificate from vyos.configverify import verify_pki_ca_certificate from vyos.configverify import verify_pki_dh_parameters +from vyos.configdiff import get_config_diff from vyos.defaults import api_config_state from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key @@ -79,6 +80,14 @@ def get_config(config=None): # merge CLI and default dictionary https = config_dict_merge(default_values, https) + + # some settings affecting nginx will require a restart: + # for example, a reload will not suffice when binding the listen address + # after nginx has started and dropped privileges; add flag here + diff = get_config_diff(conf) + children_changed = diff.node_changed_children(base) + https['nginx_restart_required'] = bool(set(children_changed) != set(['api'])) + return https def verify(https): @@ -208,7 +217,10 @@ def apply(https): elif is_systemd_service_active(http_api_service_name): call(f'systemctl stop {http_api_service_name}') - call(f'systemctl reload-or-restart {https_service_name}') + if https['nginx_restart_required']: + call(f'systemctl restart {https_service_name}') + else: + call(f'systemctl reload-or-restart {https_service_name}') if __name__ == '__main__': try: diff --git a/src/conf_mode/service_ids_ddos-protection.py b/src/conf_mode/service_ids_ddos-protection.py deleted file mode 100755 index 276a71fcb..000000000 --- a/src/conf_mode/service_ids_ddos-protection.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-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 - -from sys import exit - -from vyos.config import Config -from vyos.template import render -from vyos.utils.process import call -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -config_file = r'/run/fastnetmon/fastnetmon.conf' -networks_list = r'/run/fastnetmon/networks_list' -excluded_networks_list = r'/run/fastnetmon/excluded_networks_list' -attack_dir = '/var/log/fastnetmon_attacks' - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - base = ['service', 'ids', 'ddos-protection'] - if not conf.exists(base): - return None - - fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - with_recursive_defaults=True) - - return fastnetmon - -def verify(fastnetmon): - if not fastnetmon: - return None - - if 'mode' not in fastnetmon: - raise ConfigError('Specify operating mode!') - - if fastnetmon.get('mode') == 'mirror' and 'listen_interface' not in fastnetmon: - raise ConfigError("Incorrect settings for 'mode mirror': must specify interface(s) for traffic mirroring") - - if fastnetmon.get('mode') == 'sflow' and 'listen_address' not in fastnetmon.get('sflow', {}): - raise ConfigError("Incorrect settings for 'mode sflow': must specify sFlow 'listen-address'") - - if 'alert_script' in fastnetmon: - if os.path.isfile(fastnetmon['alert_script']): - # Check script permissions - if not os.access(fastnetmon['alert_script'], os.X_OK): - raise ConfigError('Script "{alert_script}" is not executable!'.format(fastnetmon['alert_script'])) - else: - raise ConfigError('File "{alert_script}" does not exists!'.format(fastnetmon)) - -def generate(fastnetmon): - if not fastnetmon: - for file in [config_file, networks_list]: - if os.path.isfile(file): - os.unlink(file) - - return None - - # Create dir for log attack details - if not os.path.exists(attack_dir): - os.mkdir(attack_dir) - - render(config_file, 'ids/fastnetmon.j2', fastnetmon) - render(networks_list, 'ids/fastnetmon_networks_list.j2', fastnetmon) - render(excluded_networks_list, 'ids/fastnetmon_excluded_networks_list.j2', fastnetmon) - return None - -def apply(fastnetmon): - systemd_service = 'fastnetmon.service' - if not fastnetmon: - # Stop fastnetmon service if removed - call(f'systemctl stop {systemd_service}') - else: - call(f'systemctl reload-or-restart {systemd_service}') - - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index c7e3ef033..50628947e 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -31,6 +31,7 @@ from vyos.accel_ppp_util import verify_accel_ppp_ip_pool from vyos.accel_ppp_util import verify_accel_ppp_authentication from vyos import ConfigError from vyos import airbag + airbag.enable() @@ -52,7 +53,9 @@ def get_config(config=None): if dict_search('client_ip_pool', ipoe): # Multiple named pools require ordered values T5099 - ipoe['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', ipoe)) + ipoe['ordered_named_pools'] = get_pools_in_order( + dict_search('client_ip_pool', ipoe) + ) ipoe['server_type'] = 'ipoe' return ipoe @@ -68,11 +71,29 @@ def verify(ipoe): for interface, iface_config in ipoe['interface'].items(): verify_interface_exists(ipoe, interface, warning_only=True) if 'client_subnet' in iface_config and 'vlan' in iface_config: - raise ConfigError('Option "client-subnet" and "vlan" are mutually exclusive, ' - 'use "client-ip-pool" instead!') - if 'vlan_mon' in iface_config and not 'vlan' in iface_config: + raise ConfigError( + 'Options "client-subnet" and "vlan" are mutually exclusive, ' + 'use "client-ip-pool" instead!' + ) + if 'vlan_mon' in iface_config and 'vlan' not in iface_config: raise ConfigError('Option "vlan-mon" requires "vlan" to be set!') + if 'lua_username' in iface_config: + if 'lua_file' not in ipoe: + raise ConfigError( + 'Option "lua-username" requires "lua-file" to be set!' + ) + if dict_search('authentication.mode', ipoe) != 'radius': + raise ConfigError( + 'Can configure username with Lua script only for RADIUS authentication' + ) + + if dict_search('external_dhcp.dhcp_relay', iface_config): + if not dict_search('external_dhcp.giaddr', iface_config): + raise ConfigError( + f'"external-dhcp dhcp-relay" requires "giaddr" to be set for interface {interface}' + ) + verify_accel_ppp_authentication(ipoe, local_users=False) verify_accel_ppp_ip_pool(ipoe) verify_accel_ppp_name_servers(ipoe) @@ -88,14 +109,15 @@ def generate(ipoe): render(ipoe_conf, 'accel-ppp/ipoe.config.j2', ipoe) if dict_search('authentication.mode', ipoe) == 'local': - render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.j2', - ipoe, permission=0o640) + render( + ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.j2', ipoe, permission=0o640 + ) return None def apply(ipoe): systemd_service = 'accel-ppp@ipoe.service' - if ipoe == None: + if ipoe is None: call(f'systemctl stop {systemd_service}') for file in [ipoe_conf, ipoe_chap_secrets]: if os.path.exists(file): @@ -103,7 +125,10 @@ def apply(ipoe): return None - call(f'systemctl reload-or-restart {systemd_service}') + # Accel-pppd does not do soft-reload correctly. + # Most of the changes require restarting the service + call(f'systemctl restart {systemd_service}') + if __name__ == '__main__': try: diff --git a/src/conf_mode/service_lldp.py b/src/conf_mode/service_lldp.py index 04b1db880..50e9a49e6 100755 --- a/src/conf_mode/service_lldp.py +++ b/src/conf_mode/service_lldp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_mdns_repeater.py b/src/conf_mode/service_mdns_repeater.py index b0ece031c..360b94588 100755 --- a/src/conf_mode/service_mdns_repeater.py +++ b/src/conf_mode/service_mdns_repeater.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_monitoring_network_event.py b/src/conf_mode/service_monitoring_network_event.py new file mode 100644 index 000000000..f43ea23ca --- /dev/null +++ b/src/conf_mode/service_monitoring_network_event.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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 json + +from sys import exit + +from vyos.config import Config +from vyos.utils.file import write_file +from vyos.utils.process import call +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +vyos_network_event_logger_config = r'/run/vyos-network-event-logger.conf' + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['service', 'monitoring', 'network-event'] + if not conf.exists(base): + return None + + monitoring = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + monitoring = conf.merge_defaults(monitoring, recursive=True) + + return monitoring + + +def verify(monitoring): + if not monitoring: + return None + + return None + + +def generate(monitoring): + if not monitoring: + # Delete config + if os.path.exists(vyos_network_event_logger_config): + os.unlink(vyos_network_event_logger_config) + + return None + + # Create config + log_conf_json = json.dumps(monitoring, indent=4) + write_file(vyos_network_event_logger_config, log_conf_json) + + return None + + +def apply(monitoring): + # Reload systemd manager configuration + systemd_service = 'vyos-network-event-logger.service' + + if not monitoring: + call(f'systemctl stop {systemd_service}') + return + + call(f'systemctl restart {systemd_service}') + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_monitoring_prometheus.py b/src/conf_mode/service_monitoring_prometheus.py new file mode 100755 index 000000000..b02f9f154 --- /dev/null +++ b/src/conf_mode/service_monitoring_prometheus.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +# +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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 + +from sys import exit + +from vyos.config import Config +from vyos.configdict import is_node_changed +from vyos.configverify import verify_vrf +from vyos.template import render +from vyos.utils.process import call +from vyos.utils.process import is_systemd_service_active +from vyos import ConfigError +from vyos import airbag + +airbag.enable() + +node_exporter_service_file = '/etc/systemd/system/node_exporter.service' +node_exporter_systemd_service = 'node_exporter.service' +node_exporter_collector_path = '/run/node_exporter/collector' + +frr_exporter_service_file = '/etc/systemd/system/frr_exporter.service' +frr_exporter_systemd_service = 'frr_exporter.service' + +blackbox_exporter_service_file = '/etc/systemd/system/blackbox_exporter.service' +blackbox_exporter_systemd_service = 'blackbox_exporter.service' + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['service', 'monitoring', 'prometheus'] + if not conf.exists(base): + return None + + monitoring = {} + exporters = { + 'node_exporter': base + ['node-exporter'], + 'frr_exporter': base + ['frr-exporter'], + 'blackbox_exporter': base + ['blackbox-exporter'], + } + + for exporter_name, exporter_base in exporters.items(): + if conf.exists(exporter_base): + monitoring[exporter_name] = conf.get_config_dict( + exporter_base, + key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True, + ) + + tmp = is_node_changed(conf, base + ['node-exporter', 'vrf']) + if tmp: + monitoring.update({'node_exporter_restart_required': {}}) + + tmp = is_node_changed(conf, base + ['frr-exporter', 'vrf']) + if tmp: + monitoring.update({'frr_exporter_restart_required': {}}) + + tmp = False + for node in ['vrf', 'config-file']: + tmp = tmp or is_node_changed(conf, base + ['blackbox-exporter', node]) + if tmp: + monitoring.update({'blackbox_exporter_restart_required': {}}) + + return monitoring + + +def verify(monitoring): + if not monitoring: + return None + + if 'node_exporter' in monitoring: + verify_vrf(monitoring['node_exporter']) + + if 'frr_exporter' in monitoring: + verify_vrf(monitoring['frr_exporter']) + + if 'blackbox_exporter' in monitoring: + verify_vrf(monitoring['blackbox_exporter']) + + if ( + 'modules' in monitoring['blackbox_exporter'] + and 'dns' in monitoring['blackbox_exporter']['modules'] + and 'name' in monitoring['blackbox_exporter']['modules']['dns'] + ): + for mod_name, mod_config in monitoring['blackbox_exporter']['modules'][ + 'dns' + ]['name'].items(): + if 'query_name' not in mod_config: + raise ConfigError( + f'query name not specified in dns module {mod_name}' + ) + + return None + + +def generate(monitoring): + if not monitoring or 'node_exporter' not in monitoring: + # Delete systemd files + if os.path.isfile(node_exporter_service_file): + os.unlink(node_exporter_service_file) + + if not monitoring or 'frr_exporter' not in monitoring: + # Delete systemd files + if os.path.isfile(frr_exporter_service_file): + os.unlink(frr_exporter_service_file) + + if not monitoring or 'blackbox_exporter' not in monitoring: + # Delete systemd files + if os.path.isfile(blackbox_exporter_service_file): + os.unlink(blackbox_exporter_service_file) + + if not monitoring: + return None + + if 'node_exporter' in monitoring: + # Render node_exporter node_exporter_service_file + render( + node_exporter_service_file, + 'prometheus/node_exporter.service.j2', + monitoring['node_exporter'], + ) + if ( + 'collectors' in monitoring['node_exporter'] + and 'textfile' in monitoring['node_exporter']['collectors'] + ): + # Create textcollector folder + if not os.path.isdir(node_exporter_collector_path): + os.makedirs(node_exporter_collector_path) + + if 'frr_exporter' in monitoring: + # Render frr_exporter service_file + render( + frr_exporter_service_file, + 'prometheus/frr_exporter.service.j2', + monitoring['frr_exporter'], + ) + + if 'blackbox_exporter' in monitoring: + # Render blackbox_exporter service_file + render( + blackbox_exporter_service_file, + 'prometheus/blackbox_exporter.service.j2', + monitoring['blackbox_exporter'], + ) + # Render blackbox_exporter config file + render( + '/run/blackbox_exporter/config.yml', + 'prometheus/blackbox_exporter.yml.j2', + monitoring['blackbox_exporter'], + ) + + return None + + +def apply(monitoring): + # Reload systemd manager configuration + call('systemctl daemon-reload') + if not monitoring or 'node_exporter' not in monitoring: + if is_systemd_service_active(node_exporter_systemd_service): + call(f'systemctl stop {node_exporter_systemd_service}') + if not monitoring or 'frr_exporter' not in monitoring: + if is_systemd_service_active(frr_exporter_systemd_service): + call(f'systemctl stop {frr_exporter_systemd_service}') + if not monitoring or 'blackbox_exporter' not in monitoring: + if is_systemd_service_active(blackbox_exporter_systemd_service): + call(f'systemctl stop {blackbox_exporter_systemd_service}') + + if not monitoring: + return + + if 'node_exporter' in monitoring: + # we need to restart the service if e.g. the VRF name changed + systemd_action = 'reload-or-restart' + if 'node_exporter_restart_required' in monitoring: + systemd_action = 'restart' + + call(f'systemctl {systemd_action} {node_exporter_systemd_service}') + + if 'frr_exporter' in monitoring: + # we need to restart the service if e.g. the VRF name changed + systemd_action = 'reload-or-restart' + if 'frr_exporter_restart_required' in monitoring: + systemd_action = 'restart' + + call(f'systemctl {systemd_action} {frr_exporter_systemd_service}') + + if 'blackbox_exporter' in monitoring: + # we need to restart the service if e.g. the VRF name changed + systemd_action = 'reload-or-restart' + if 'blackbox_exporter_restart_required' in monitoring: + systemd_action = 'restart' + + call(f'systemctl {systemd_action} {blackbox_exporter_systemd_service}') + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index db870aae5..de8097e11 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_monitoring_zabbix-agent.py b/src/conf_mode/service_monitoring_zabbix-agent.py index 98d8a32ca..5f3a8d4b5 100755 --- a/src/conf_mode/service_monitoring_zabbix-agent.py +++ b/src/conf_mode/service_monitoring_zabbix-agent.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -18,6 +18,8 @@ import os from vyos.config import Config from vyos.template import render +from vyos.utils.dict import dict_search +from vyos.utils.file import write_file from vyos.utils.process import call from vyos import ConfigError from vyos import airbag @@ -26,6 +28,7 @@ airbag.enable() service_name = 'zabbix-agent2' service_conf = f'/run/zabbix/{service_name}.conf' +service_psk_file = f'/run/zabbix/{service_name}.psk' systemd_override = r'/run/systemd/system/zabbix-agent2.service.d/10-override.conf' @@ -49,6 +52,8 @@ def get_config(config=None): if 'directory' in config and config['directory'].endswith('/'): config['directory'] = config['directory'][:-1] + config['service_psk_file'] = service_psk_file + return config @@ -60,18 +65,34 @@ def verify(config): if 'server' not in config: raise ConfigError('Server is required!') + if 'authentication' in config and dict_search("authentication.mode", + config) == 'pre_shared_secret': + if 'id' not in config['authentication']['psk']: + raise ConfigError( + 'PSK identity is required for pre-shared-secret authentication mode') + + if 'secret' not in config['authentication']['psk']: + raise ConfigError( + 'PSK secret is required for pre-shared-secret authentication mode') + def generate(config): # bail out early - looks like removal from running config if config is None: # Remove old config and return - config_files = [service_conf, systemd_override] + config_files = [service_conf, systemd_override, service_psk_file] for file in config_files: if os.path.isfile(file): os.unlink(file) return None + if not dict_search("authentication.psk.secret", config): + if os.path.isfile(service_psk_file): + os.unlink(service_psk_file) + else: + write_file(service_psk_file, config["authentication"]["psk"]["secret"]) + # Write configuration file render(service_conf, 'zabbix-agent/zabbix-agent.conf.j2', config) render(systemd_override, 'zabbix-agent/10-override.conf.j2', config) diff --git a/src/conf_mode/service_ndp-proxy.py b/src/conf_mode/service_ndp-proxy.py index 024ad79f2..f42ee9be8 100755 --- a/src/conf_mode/service_ndp-proxy.py +++ b/src/conf_mode/service_ndp-proxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_ntp.py b/src/conf_mode/service_ntp.py index 83880fd72..fbdcd6b58 100755 --- a/src/conf_mode/service_ntp.py +++ b/src/conf_mode/service_ntp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -17,6 +17,7 @@ import os from vyos.config import Config +from vyos.config import config_dict_merge from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.configverify import verify_interface_exists @@ -42,13 +43,21 @@ def get_config(config=None): if not conf.exists(base): return None - ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, with_defaults=True) + ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) ntp['config_file'] = config_file ntp['user'] = user_group tmp = is_node_changed(conf, base + ['vrf']) if tmp: ntp.update({'restart_required': {}}) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = conf.get_config_defaults(**ntp.kwargs, recursive=True) + # Only defined PTP default port, if PTP feature is in use + if 'ptp' not in ntp: + del default_values['ptp'] + + ntp = config_dict_merge(default_values, ntp) return ntp def verify(ntp): @@ -87,6 +96,15 @@ def verify(ntp): if ipv6_addresses > 1: raise ConfigError(f'NTP Only admits one ipv6 value for listen-address parameter ') + if 'server' in ntp: + for host, server in ntp['server'].items(): + if 'ptp' in server: + if 'ptp' not in ntp: + raise ConfigError('PTP must be enabled for the NTP service '\ + f'before it can be used for server "{host}"') + else: + break + return None def generate(ntp): diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index ac697c509..d910e770d 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -73,7 +73,9 @@ def get_config(config=None): # https://phabricator.accel-ppp.org/T3 conditions = [is_node_changed(conf, base + ['client-ip-pool']), is_node_changed(conf, base + ['client-ipv6-pool']), - is_node_changed(conf, base + ['interface'])] + is_node_changed(conf, base + ['interface']), + is_node_changed(conf, base + ['authentication','radius','dynamic-author']), + is_node_changed(conf, base + ['authentication','mode'])] if any(conditions): pppoe.update({'restart_required': {}}) pppoe['server_type'] = 'pppoe' diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py index 88d767bb8..4345bea1d 100755 --- a/src/conf_mode/service_router-advert.py +++ b/src/conf_mode/service_router-advert.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_salt-minion.py b/src/conf_mode/service_salt-minion.py index edf74b0c0..9e9940175 100755 --- a/src/conf_mode/service_salt-minion.py +++ b/src/conf_mode/service_salt-minion.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_sla.py b/src/conf_mode/service_sla.py index ba5e645f0..0a7b81073 100755 --- a/src/conf_mode/service_sla.py +++ b/src/conf_mode/service_sla.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_snmp.py b/src/conf_mode/service_snmp.py index c9c0ed9a0..c6059c260 100755 --- a/src/conf_mode/service_snmp.py +++ b/src/conf_mode/service_snmp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -22,6 +22,7 @@ from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.configverify import verify_vrf +from vyos.defaults import systemd_services from vyos.snmpv3_hashgen import plaintext_to_md5 from vyos.snmpv3_hashgen import plaintext_to_sha1 from vyos.snmpv3_hashgen import random @@ -43,7 +44,7 @@ config_file_access = r'/usr/share/snmp/snmpd.conf' config_file_user = r'/var/lib/snmp/snmpd.conf' default_script_dir = r'/config/user-data/' systemd_override = r'/run/systemd/system/snmpd.service.d/override.conf' -systemd_service = 'snmpd.service' +systemd_service = systemd_services['snmpd'] def get_config(config=None): if config: @@ -146,6 +147,9 @@ def verify(snmp): return None if 'user' in snmp['v3']: + if 'engineid' not in snmp['v3']: + raise ConfigError(f'EngineID must be configured for SNMPv3!') + for user, user_config in snmp['v3']['user'].items(): if 'group' not in user_config: raise ConfigError(f'Group membership required for user "{user}"!') @@ -260,15 +264,6 @@ def apply(snmp): # start SNMP daemon call(f'systemctl reload-or-restart {systemd_service}') - - # Enable AgentX in FRR - # This should be done for each daemon individually because common command - # works only if all the daemons started with SNMP support - # Following daemons from FRR 9.0/stable have SNMP module compiled in VyOS - frr_daemons_list = ['zebra', 'bgpd', 'ospf6d', 'ospfd', 'ripd', 'isisd', 'ldpd'] - for frr_daemon in frr_daemons_list: - call(f'vtysh -c "configure terminal" -d {frr_daemon} -c "agentx" >/dev/null') - return None if __name__ == '__main__': diff --git a/src/conf_mode/service_ssh.py b/src/conf_mode/service_ssh.py index 9abdd33dc..bf8afe8b7 100755 --- a/src/conf_mode/service_ssh.py +++ b/src/conf_mode/service_ssh.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -23,10 +23,17 @@ from syslog import LOG_INFO from vyos.config import Config from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf +from vyos.configverify import verify_pki_openssh_key +from vyos.defaults import config_files from vyos.utils.process import call from vyos.template import render from vyos import ConfigError from vyos import airbag +from vyos.pki import encode_public_key +from vyos.pki import load_openssh_public_key +from vyos.utils.dict import dict_search_recursive +from vyos.utils.file import write_file + airbag.enable() config_file = r'/run/sshd/sshd_config' @@ -38,6 +45,8 @@ key_rsa = '/etc/ssh/ssh_host_rsa_key' key_dsa = '/etc/ssh/ssh_host_dsa_key' key_ed25519 = '/etc/ssh/ssh_host_ed25519_key' +trusted_user_ca = config_files['sshd_user_ca'] + def get_config(config=None): if config: conf = config @@ -46,36 +55,55 @@ def get_config(config=None): base = ['service', 'ssh'] if not conf.exists(base): return None - - ssh = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + ssh = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, with_pki=True) tmp = is_node_changed(conf, base + ['vrf']) - if tmp: ssh.update({'restart_required': {}}) + if tmp: + ssh.update({'restart_required': {}}) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. ssh = conf.merge_defaults(ssh, recursive=True) - # pass config file path - used in override template - ssh['config_file'] = config_file - # Ignore default XML values if config doesn't exists # Delete key from dict if not conf.exists(base + ['dynamic-protection']): - del ssh['dynamic_protection'] + del ssh['dynamic_protection'] + + # See if any user has specified a list of principal names that are accepted + # for certificate authentication. + tmp = conf.get_config_dict(['system', 'login', 'user'], + key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + get_first_key=True) + + for value, _ in dict_search_recursive(tmp, 'principal'): + # Only enable principal handling if SSH trusted-user-ca is set + if 'trusted_user_ca' in ssh: + ssh['has_principals'] = {} + # We do only need to execute this code path once as we need to know + # if any one of the local users has a principal set or not - this + # accounts for the entire system. + break return ssh + def verify(ssh): if not ssh: return None if 'rekey' in ssh and 'data' not in ssh['rekey']: - raise ConfigError(f'Rekey data is required!') + raise ConfigError('Rekey data is required!') + + if 'trusted_user_ca' in ssh: + verify_pki_openssh_key(ssh, ssh['trusted_user_ca']) verify_vrf(ssh) return None + def generate(ssh): if not ssh: if os.path.isfile(config_file): @@ -95,6 +123,18 @@ def generate(ssh): syslog(LOG_INFO, 'SSH ed25519 host key not found, generating new key!') call(f'ssh-keygen -q -N "" -t ed25519 -f {key_ed25519}') + if 'trusted_user_ca' in ssh: + key_name = ssh['trusted_user_ca'] + openssh_cert = ssh['pki']['openssh'][key_name] + loaded_ca_cert = load_openssh_public_key(openssh_cert['public']['key'], + openssh_cert['public']['type']) + tmp = encode_public_key(loaded_ca_cert, encoding='OpenSSH', + key_format='OpenSSH') + write_file(trusted_user_ca, tmp, trailing_newline=True) + else: + if os.path.exists(trusted_user_ca): + os.unlink(trusted_user_ca) + render(config_file, 'ssh/sshd_config.j2', ssh) if 'dynamic_protection' in ssh: @@ -103,12 +143,12 @@ def generate(ssh): return None + def apply(ssh): - systemd_service_ssh = 'ssh.service' systemd_service_sshguard = 'sshguard.service' if not ssh: # SSH access is removed in the commit - call(f'systemctl stop ssh@*.service') + call('systemctl stop ssh@*.service') call(f'systemctl stop {systemd_service_sshguard}') return None @@ -122,13 +162,14 @@ def apply(ssh): if 'restart_required' in ssh: # this is only true if something for the VRFs changed, thus we # stop all VRF services and only restart then new ones - call(f'systemctl stop ssh@*.service') + call('systemctl stop ssh@*.service') systemd_action = 'restart' for vrf in ssh['vrf']: call(f'systemctl {systemd_action} ssh@{vrf}.service') return None + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/service_stunnel.py b/src/conf_mode/service_stunnel.py index 8ec762548..eb1d4dea2 100644 --- a/src/conf_mode/service_stunnel.py +++ b/src/conf_mode/service_stunnel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_suricata.py b/src/conf_mode/service_suricata.py index 1ce170145..728c5607e 100755 --- a/src/conf_mode/service_suricata.py +++ b/src/conf_mode/service_suricata.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_tftp-server.py b/src/conf_mode/service_tftp-server.py index 5b7303c40..dc5ec5674 100755 --- a/src/conf_mode/service_tftp-server.py +++ b/src/conf_mode/service_tftp-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/service_webproxy.py b/src/conf_mode/service_webproxy.py index 12ae4135e..7a4954de2 100755 --- a/src/conf_mode/service_webproxy.py +++ b/src/conf_mode/service_webproxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/system_acceleration.py b/src/conf_mode/system_acceleration.py index d2cf44ff0..954bf8bba 100755 --- a/src/conf_mode/system_acceleration.py +++ b/src/conf_mode/system_acceleration.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/system_config-management.py b/src/conf_mode/system_config-management.py index c681a8405..c1d83af34 100755 --- a/src/conf_mode/system_config-management.py +++ b/src/conf_mode/system_config-management.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -22,6 +22,7 @@ from vyos.config import Config from vyos.config_mgmt import ConfigMgmt from vyos.config_mgmt import commit_post_hook_dir, commit_hooks + def get_config(config=None): if config: conf = config @@ -36,22 +37,32 @@ def get_config(config=None): return mgmt -def verify(_mgmt): + +def verify(mgmt): + if mgmt is None: + return + + d = mgmt.config_dict + confirm = d.get('commit_confirm', {}) + if confirm.get('action', '') == 'reload' and 'commit_revisions' not in d: + raise ConfigError('commit-confirm reload requires non-zero commit-revisions') + return + def generate(mgmt): if mgmt is None: return mgmt.initialize_revision() + def apply(mgmt): if mgmt is None: return locations = mgmt.locations - archive_target = os.path.join(commit_post_hook_dir, - commit_hooks['commit_archive']) + archive_target = os.path.join(commit_post_hook_dir, commit_hooks['commit_archive']) if locations: try: os.symlink('/usr/bin/config-mgmt', archive_target) @@ -68,8 +79,9 @@ def apply(mgmt): raise ConfigError from exc revisions = mgmt.max_revisions - revision_target = os.path.join(commit_post_hook_dir, - commit_hooks['commit_revision']) + revision_target = os.path.join( + commit_post_hook_dir, commit_hooks['commit_revision'] + ) if revisions > 0: try: os.symlink('/usr/bin/config-mgmt', revision_target) @@ -85,6 +97,7 @@ def apply(mgmt): except OSError as exc: raise ConfigError from exc + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/system_conntrack.py b/src/conf_mode/system_conntrack.py index 2529445bf..eed1a4458 100755 --- a/src/conf_mode/system_conntrack.py +++ b/src/conf_mode/system_conntrack.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -32,7 +32,6 @@ 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' @@ -204,7 +203,6 @@ def generate(conntrack): 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) @@ -258,6 +256,8 @@ def apply(conntrack): if 'log' in conntrack: call(f'systemctl restart vyos-conntrack-logger.service') + else: + call(f'systemctl stop vyos-conntrack-logger.service') return None diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py index b380e0521..90648e2b8 100755 --- a/src/conf_mode/system_console.py +++ b/src/conf_mode/system_console.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/system_flow-accounting.py b/src/conf_mode/system_flow-accounting.py index a12ee363d..618227fc0 100755 --- a/src/conf_mode/system_flow-accounting.py +++ b/src/conf_mode/system_flow-accounting.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -18,7 +18,6 @@ import os import re from sys import exit -from ipaddress import ip_address from vyos.config import Config from vyos.config import config_dict_merge @@ -159,9 +158,9 @@ def get_config(config=None): # delete individual flow type defaults - should only be added if user # sets this feature - for flow_type in ['sflow', 'netflow']: - if flow_type not in flow_accounting and flow_type in default_values: - del default_values[flow_type] + flow_type = 'netflow' + if flow_type not in flow_accounting and flow_type in default_values: + del default_values[flow_type] flow_accounting = config_dict_merge(default_values, flow_accounting) @@ -171,9 +170,9 @@ def verify(flow_config): if not flow_config: return None - # check if at least one collector is enabled - if 'sflow' not in flow_config and 'netflow' not in flow_config and 'disable_imt' in flow_config: - raise ConfigError('You need to configure at least sFlow or NetFlow, ' \ + # check if collector is enabled + if 'netflow' not in flow_config and 'disable_imt' in flow_config: + raise ConfigError('You need to configure NetFlow, ' \ 'or not set "disable-imt" for flow-accounting!') # Check if at least one interface is configured @@ -185,45 +184,7 @@ def verify(flow_config): for interface in flow_config['interface']: verify_interface_exists(flow_config, interface, warning_only=True) - # check sFlow configuration - if 'sflow' in flow_config: - # check if at least one sFlow collector is configured - if 'server' not in flow_config['sflow']: - raise ConfigError('You need to configure at least one sFlow server!') - - # check that all sFlow collectors use the same IP protocol version - sflow_collector_ipver = None - for server in flow_config['sflow']['server']: - if sflow_collector_ipver: - if sflow_collector_ipver != ip_address(server).version: - raise ConfigError("All sFlow servers must use the same IP protocol") - else: - sflow_collector_ipver = ip_address(server).version - - # check if vrf is defined for Sflow - verify_vrf(flow_config) - sflow_vrf = None - if 'vrf' in flow_config: - sflow_vrf = flow_config['vrf'] - - # check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa - for server in flow_config['sflow']['server']: - if 'agent_address' in flow_config['sflow']: - if ip_address(server).version != ip_address(flow_config['sflow']['agent_address']).version: - raise ConfigError('IPv4 and IPv6 addresses can not be mixed in "sflow agent-address" and "sflow '\ - 'server". You need to set the same IP version for both "agent-address" and '\ - 'all sFlow servers') - - if 'agent_address' in flow_config['sflow']: - tmp = flow_config['sflow']['agent_address'] - if not is_addr_assigned(tmp, sflow_vrf): - raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!') - - # Check if configured sflow source-address exist in the system - if 'source_address' in flow_config['sflow']: - if not is_addr_assigned(flow_config['sflow']['source_address'], sflow_vrf): - tmp = flow_config['sflow']['source_address'] - raise ConfigError(f'Configured "sflow source-address {tmp}" does not exist on the system!') + verify_vrf(flow_config) # check NetFlow configuration if 'netflow' in flow_config: diff --git a/src/conf_mode/system_frr.py b/src/conf_mode/system_frr.py index d9ac543d0..06af37ad8 100755 --- a/src/conf_mode/system_frr.py +++ b/src/conf_mode/system_frr.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/system_host-name.py b/src/conf_mode/system_host-name.py index 3f245f166..2a5592484 100755 --- a/src/conf_mode/system_host-name.py +++ b/src/conf_mode/system_host-name.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -23,6 +23,7 @@ import vyos.hostsd_client from vyos.base import Warning from vyos.config import Config from vyos.configdict import leaf_node_changed +from vyos.defaults import systemd_services from vyos.ifconfig import Section from vyos.template import is_ip from vyos.utils.process import cmd @@ -174,11 +175,13 @@ def apply(config): # Restart services that use the hostname if hostname_new != hostname_old: - call("systemctl restart rsyslog.service") + tmp = systemd_services['syslog'] + call(f'systemctl restart {tmp}') # If SNMP is running, restart it too if process_named_running('snmpd') and config['snmpd_restart_reqired']: - call('systemctl restart snmpd.service') + tmp = systemd_services['snmpd'] + call(f'systemctl restart {tmp}') return None diff --git a/src/conf_mode/system_ip.py b/src/conf_mode/system_ip.py index c8a91fd2f..13471eb3b 100755 --- a/src/conf_mode/system_ip.py +++ b/src/conf_mode/system_ip.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -17,17 +17,17 @@ from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents +from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_route_map -from vyos.template import render_to_string +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search -from vyos.utils.file import write_file from vyos.utils.process import is_systemd_service_active +from vyos.utils.process import is_systemd_service_running from vyos.utils.system import sysctl_write -from vyos.configdep import set_dependents -from vyos.configdep import call_dependents from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -36,42 +36,41 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['system', 'ip'] - - opt = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - with_recursive_defaults=True) - - # When working with FRR we need to know the corresponding address-family - opt['afi'] = 'ip' - - # We also need the route-map information from the config - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'], - get_first_key=True)}} - # Merge policy dict into "regular" config dict - opt = dict_merge(tmp, opt) # If IPv4 ARP table size is set here and also manually in sysctl, the more # fine grained value from sysctl must win set_dependents('sysctl', conf) + return get_frrender_dict(conf) - return opt +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'ip'): + return None + + opt = config_dict['ip'] + opt['policy'] = config_dict['policy'] -def verify(opt): if 'protocol' in opt: for protocol, protocol_options in opt['protocol'].items(): if 'route_map' in protocol_options: verify_route_map(protocol_options['route_map'], opt) - return -def generate(opt): - opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt) + if dict_search('import_table', opt): + for table_num, import_config in opt['import_table'].items(): + if dict_search('route_map', import_config): + verify_route_map(import_config['route_map'], opt) return -def apply(opt): +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) + return None + +def apply(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'ip'): + + return None + opt = config_dict['ip'] + # Apply ARP threshold values # table_size has a default value - thus the key always exists size = int(dict_search('arp.table_size', opt)) @@ -82,11 +81,6 @@ def apply(opt): # Minimum number of stored records is indicated which is not cleared sysctl_write('net.ipv4.neigh.default.gc_thresh1', size // 8) - # enable/disable IPv4 forwarding - tmp = dict_search('disable_forwarding', opt) - value = '0' if (tmp != None) else '1' - write_file('/proc/sys/net/ipv4/conf/all/forwarding', value) - # configure multipath tmp = dict_search('multipath.ignore_unreachable_nexthops', opt) value = '1' if (tmp != None) else '0' @@ -121,19 +115,11 @@ def apply(opt): # running when this script is called first. Skip this part and wait for initial # commit of the configuration to trigger this statement if is_systemd_service_active('frr.service'): - zebra_daemon = 'zebra' - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'no ip nht resolve-via-default') - frr_cfg.modify_section(r'ip protocol \w+ route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - if 'frr_zebra_config' in opt: - frr_cfg.add_before(frr.default_add_before, opt['frr_zebra_config']) - frr_cfg.commit_configuration(zebra_daemon) + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() call_dependents() + return None if __name__ == '__main__': try: diff --git a/src/conf_mode/system_ipv6.py b/src/conf_mode/system_ipv6.py index a2442d009..35f343d82 100755 --- a/src/conf_mode/system_ipv6.py +++ b/src/conf_mode/system_ipv6.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -18,17 +18,18 @@ import os from sys import exit from vyos.config import Config -from vyos.configdict import dict_merge +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents +from vyos.configverify import has_frr_protocol_in_dict from vyos.configverify import verify_route_map -from vyos.template import render_to_string +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.utils.dict import dict_search from vyos.utils.file import write_file from vyos.utils.process import is_systemd_service_active +from vyos.utils.process import is_systemd_service_running from vyos.utils.system import sysctl_write -from vyos.configdep import set_dependents -from vyos.configdep import call_dependents from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -37,42 +38,35 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['system', 'ipv6'] - - opt = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - with_recursive_defaults=True) - - # When working with FRR we need to know the corresponding address-family - opt['afi'] = 'ipv6' - - # We also need the route-map information from the config - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'], - get_first_key=True)}} - # Merge policy dict into "regular" config dict - opt = dict_merge(tmp, opt) # If IPv6 neighbor table size is set here and also manually in sysctl, the more # fine grained value from sysctl must win set_dependents('sysctl', conf) + return get_frrender_dict(conf) + +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'ipv6'): + return None - return opt + opt = config_dict['ipv6'] + opt['policy'] = config_dict['policy'] -def verify(opt): if 'protocol' in opt: for protocol, protocol_options in opt['protocol'].items(): if 'route_map' in protocol_options: verify_route_map(protocol_options['route_map'], opt) return -def generate(opt): - opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt) - return +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) + return None + +def apply(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'ipv6'): + return None + opt = config_dict['ipv6'] -def apply(opt): # configure multipath tmp = dict_search('multipath.layer4_hashing', opt) value = '1' if (tmp != None) else '0' @@ -88,11 +82,6 @@ def apply(opt): # Minimum number of stored records is indicated which is not cleared sysctl_write('net.ipv6.neigh.default.gc_thresh1', size // 8) - # enable/disable IPv6 forwarding - tmp = dict_search('disable_forwarding', opt) - value = '0' if (tmp != None) else '1' - write_file('/proc/sys/net/ipv6/conf/all/forwarding', value) - # configure IPv6 strict-dad tmp = dict_search('strict_dad', opt) value = '2' if (tmp != None) else '1' @@ -105,19 +94,11 @@ def apply(opt): # running when this script is called first. Skip this part and wait for initial # commit of the configuration to trigger this statement if is_systemd_service_active('frr.service'): - zebra_daemon = 'zebra' - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'no ipv6 nht resolve-via-default') - frr_cfg.modify_section(r'ipv6 protocol \w+ route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - if 'frr_zebra_config' in opt: - frr_cfg.add_before(frr.default_add_before, opt['frr_zebra_config']) - frr_cfg.commit_configuration(zebra_daemon) + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() call_dependents() + return None if __name__ == '__main__': try: diff --git a/src/conf_mode/system_lcd.py b/src/conf_mode/system_lcd.py index eb88224d1..1e97414dc 100755 --- a/src/conf_mode/system_lcd.py +++ b/src/conf_mode/system_lcd.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2020-2022 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/system_login.py b/src/conf_mode/system_login.py index 439fa645b..00cacccd1 100755 --- a/src/conf_mode/system_login.py +++ b/src/conf_mode/system_login.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -24,10 +24,15 @@ from pwd import getpwuid from sys import exit from time import sleep +from vyos.base import Warning from vyos.config import Config +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents from vyos.configverify import verify_vrf from vyos.template import render from vyos.template import is_ipv4 +from vyos.utils.auth import EPasswdStrength +from vyos.utils.auth import evaluate_strength from vyos.utils.auth import get_current_user from vyos.utils.configfs import delete_cli_node from vyos.utils.configfs import add_cli_node @@ -58,20 +63,21 @@ MAX_RADIUS_TIMEOUT: int = 50 MAX_RADIUS_COUNT: int = 8 # Maximum number of supported TACACS servers MAX_TACACS_COUNT: int = 8 - +# Minimum USER id for TACACS users +MIN_TACACS_UID = 900 # List of local user accounts that must be preserved SYSTEM_USER_SKIP_LIST: list = ['radius_user', 'radius_priv_user', 'tacacs0', 'tacacs1', 'tacacs2', 'tacacs3', 'tacacs4', 'tacacs5', 'tacacs6', 'tacacs7', 'tacacs8', 'tacacs9', 'tacacs10',' tacacs11', 'tacacs12', 'tacacs13', 'tacacs14', 'tacacs15'] -def get_local_users(): +def get_local_users(min_uid=MIN_USER_UID, max_uid=MAX_USER_UID): """Return list of dynamically allocated users (see Debian Policy Manual)""" local_users = [] for s_user in getpwall(): - if getpwnam(s_user.pw_name).pw_uid < MIN_USER_UID: + if getpwnam(s_user.pw_name).pw_uid < min_uid: continue - if getpwnam(s_user.pw_name).pw_uid > MAX_USER_UID: + if getpwnam(s_user.pw_name).pw_uid > max_uid: continue if s_user.pw_name in SYSTEM_USER_SKIP_LIST: continue @@ -119,6 +125,13 @@ def get_config(config=None): rm_users = [tmp for tmp in all_users if tmp not in cli_users] if rm_users: login.update({'rm_users' : rm_users}) + # Build TACACS user mapping + if 'tacacs' in login: + login['exclude_users'] = get_local_users(min_uid=0, + max_uid=MIN_TACACS_UID) + cli_users + login['tacacs_min_uid'] = MIN_TACACS_UID + + set_dependents('ssh', conf) return login def verify(login): @@ -139,6 +152,19 @@ def verify(login): if s_user.pw_name == user and s_user.pw_uid < MIN_USER_UID: raise ConfigError(f'User "{user}" can not be created, conflict with local system account!') + # T6353: Check password for complexity using cracklib. + # A user password should be sufficiently complex + plaintext_password = dict_search( + path='authentication.plaintext_password', + dict_object=user_config + ) or None + + failed_check_status = [EPasswdStrength.WEAK, EPasswdStrength.ERROR] + if plaintext_password is not None: + result = evaluate_strength(plaintext_password) + if result['strength'] in failed_check_status: + Warning(result['error']) + for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items(): if 'type' not in pubkey_options: raise ConfigError(f'Missing type for public-key "{pubkey}"!') @@ -322,6 +348,17 @@ def apply(login): user_config, permission=0o600, formater=lambda _: _.replace(""", '"'), user=user, group='users') + + principals_file = f'{home_dir}/.ssh/authorized_principals' + if dict_search('authentication.principal', user_config): + render(principals_file, 'login/authorized_principals.j2', + user_config, permission=0o600, + formater=lambda _: _.replace(""", '"'), + user=user, group='users') + else: + if os.path.exists(principals_file): + os.unlink(principals_file) + except Exception as e: raise ConfigError(f'Adding user "{user}" raised exception: "{e}"') @@ -338,14 +375,15 @@ def apply(login): chown(home_dir, user=user, recursive=True) # Generate 2FA/MFA One-Time-Pad configuration + google_auth_file = f'{home_dir}/.google_authenticator' if dict_search('authentication.otp.key', user_config): enable_otp = True - render(f'{home_dir}/.google_authenticator', 'login/pam_otp_ga.conf.j2', + render(google_auth_file, 'login/pam_otp_ga.conf.j2', user_config, permission=0o400, user=user, group='users') else: # delete configuration as it's not enabled for the user - if os.path.exists(f'{home_dir}/.google_authenticator'): - os.remove(f'{home_dir}/.google_authenticator') + if os.path.exists(google_auth_file): + os.unlink(google_auth_file) # Lock/Unlock local user account lock_unlock = '--unlock' @@ -359,6 +397,22 @@ def apply(login): # Disable user to prevent re-login call(f'usermod -s /sbin/nologin {user}') + home_dir = getpwnam(user).pw_dir + # Remove SSH authorized keys file + authorized_keys_file = f'{home_dir}/.ssh/authorized_keys' + if os.path.exists(authorized_keys_file): + os.unlink(authorized_keys_file) + + # Remove SSH authorized principals file + principals_file = f'{home_dir}/.ssh/authorized_principals' + if os.path.exists(principals_file): + os.unlink(principals_file) + + # Remove Google Authenticator file + google_auth_file = f'{home_dir}/.google_authenticator' + if os.path.exists(google_auth_file): + os.unlink(google_auth_file) + # Logout user if he is still logged in if user in list(set([tmp[0] for tmp in users()])): print(f'{user} is logged in, forcing logout!') @@ -397,8 +451,9 @@ def apply(login): # Enable/disable Google authenticator cmd('pam-auth-update --disable mfa-google-authenticator') if enable_otp: - cmd(f'pam-auth-update --enable mfa-google-authenticator') + cmd('pam-auth-update --enable mfa-google-authenticator') + call_dependents() return None diff --git a/src/conf_mode/system_login_banner.py b/src/conf_mode/system_login_banner.py index 923e1bf57..9d5fba65f 100755 --- a/src/conf_mode/system_login_banner.py +++ b/src/conf_mode/system_login_banner.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -28,6 +28,7 @@ airbag.enable() PRELOGIN_FILE = r'/etc/issue' PRELOGIN_NET_FILE = r'/etc/issue.net' POSTLOGIN_FILE = r'/etc/motd' +POSTLOGIN_VYOS_FILE = r'/run/motd.d/01-vyos-nonproduction' default_config_data = { 'issue': 'Welcome to VyOS - \\n \\l\n\n', @@ -94,6 +95,13 @@ def apply(banner): render(POSTLOGIN_FILE, 'login/default_motd.j2', banner, permission=0o644, user='root', group='root') + if banner['version_data']['build_type'] != 'release': + render(POSTLOGIN_VYOS_FILE, 'login/motd_vyos_nonproduction.j2', + banner, + permission=0o644, + user='root', + group='root') + return None if __name__ == '__main__': diff --git a/src/conf_mode/system_logs.py b/src/conf_mode/system_logs.py index 8ad4875d4..f0e742d1b 100755 --- a/src/conf_mode/system_logs.py +++ b/src/conf_mode/system_logs.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py index a84572f83..fbe7231df 100755 --- a/src/conf_mode/system_option.py +++ b/src/conf_mode/system_option.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -46,6 +46,13 @@ systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target' usb_autosuspend = r'/etc/udev/rules.d/40-usb-autosuspend.rules' kernel_dynamic_debug = r'/sys/kernel/debug/dynamic_debug/control' time_format_to_locale = {'12-hour': 'en_US.UTF-8', '24-hour': 'en_GB.UTF-8'} +tuned_profiles = { + 'power-save': 'powersave', + 'network-latency': 'network-latency', + 'network-throughput': 'network-throughput', + 'virtual-guest': 'virtual-guest', + 'virtual-host': 'virtual-host', +} def get_config(config=None): @@ -79,7 +86,7 @@ def verify(options): if 'source_address' in config: if not is_addr_assigned(config['source_address']): - raise ConfigError('No interface with give address specified!') + raise ConfigError('No interface with given address specified!') if 'ssh_client' in options: config = options['ssh_client'] @@ -115,7 +122,14 @@ def generate(options): render(ssh_config, 'system/ssh_config.j2', options) render(usb_autosuspend, 'system/40_usb_autosuspend.j2', options) + # XXX: This code path and if statements must be kept in sync with the Kernel + # option handling in image_installer.py:get_cli_kernel_options(). This + # occurance is used for having the appropriate options passed to GRUB + # when re-configuring options on the CLI. cmdline_options = [] + kernel_opts = options.get('kernel', {}) + k_cpu_opts = kernel_opts.get('cpu', {}) + k_memory_opts = kernel_opts.get('memory', {}) if 'kernel' in options: if 'disable_mitigations' in options['kernel']: cmdline_options.append('mitigations=off') @@ -124,8 +138,51 @@ def generate(options): if 'amd_pstate_driver' in options['kernel']: mode = options['kernel']['amd_pstate_driver'] cmdline_options.append( - f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}' - ) + f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}') + if 'quiet' in options['kernel']: + cmdline_options.append('quiet') + + if 'disable_hpet' in kernel_opts: + cmdline_options.append('hpet=disable') + + if 'disable_mce' in kernel_opts: + cmdline_options.append('mce=off') + + if 'disable_softlockup' in kernel_opts: + cmdline_options.append('nosoftlockup') + + # CPU options + isol_cpus = k_cpu_opts.get('isolate_cpus') + if isol_cpus: + cmdline_options.append(f'isolcpus={isol_cpus}') + + nohz_full = k_cpu_opts.get('nohz_full') + if nohz_full: + cmdline_options.append(f'nohz_full={nohz_full}') + + rcu_nocbs = k_cpu_opts.get('rcu_no_cbs') + if rcu_nocbs: + cmdline_options.append(f'rcu_nocbs={rcu_nocbs}') + + if 'disable_nmi_watchdog' in k_cpu_opts: + cmdline_options.append('nmi_watchdog=0') + + # Memory options + if 'disable_numa_balancing' in k_memory_opts: + cmdline_options.append('numa_balancing=disable') + + default_hp_size = k_memory_opts.get('default_hugepage_size') + if default_hp_size: + cmdline_options.append(f'default_hugepagesz={default_hp_size}') + + hp_sizes = k_memory_opts.get('hugepage_size') + if hp_sizes: + for size, settings in hp_sizes.items(): + cmdline_options.append(f'hugepagesz={size}') + count = settings.get('hugepage_count') + if count: + cmdline_options.append(f'hugepages={count}') + grub_util.update_kernel_cmdline_options(' '.join(cmdline_options)) return None @@ -171,7 +228,10 @@ def apply(options): # wait until daemon has started before sending configuration while not is_systemd_service_running('tuned.service'): sleep(0.250) - cmd('tuned-adm profile network-{performance}'.format(**options)) + performance = ' '.join( + list(tuned_profiles[profile] for profile in options['performance']) + ) + cmd(f'tuned-adm profile {performance}') else: cmd('systemctl stop tuned.service') diff --git a/src/conf_mode/system_proxy.py b/src/conf_mode/system_proxy.py index 079c43e7e..3843ad527 100755 --- a/src/conf_mode/system_proxy.py +++ b/src/conf_mode/system_proxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/system_sflow.py b/src/conf_mode/system_sflow.py index 41119b494..86a344ff7 100755 --- a/src/conf_mode/system_sflow.py +++ b/src/conf_mode/system_sflow.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -54,7 +54,7 @@ def verify(sflow): # Check if configured sflow agent-address exist in the system if 'agent_address' in sflow: tmp = sflow['agent_address'] - if not is_addr_assigned(tmp): + if not is_addr_assigned(tmp, include_vrf=True): raise ConfigError( f'Configured "sflow agent-address {tmp}" does not exist in the system!' ) diff --git a/src/conf_mode/system_sysctl.py b/src/conf_mode/system_sysctl.py index f6b02023d..8e018ec0b 100755 --- a/src/conf_mode/system_sysctl.py +++ b/src/conf_mode/system_sysctl.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/system_syslog.py b/src/conf_mode/system_syslog.py index 07fbb0734..c1a8baa1d 100755 --- a/src/conf_mode/system_syslog.py +++ b/src/conf_mode/system_syslog.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -18,18 +18,24 @@ import os from sys import exit +from vyos.base import Warning from vyos.config import Config -from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf +from vyos.defaults import systemd_services +from vyos.utils.network import is_addr_assigned from vyos.utils.process import call from vyos.template import render +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 from vyos import ConfigError from vyos import airbag airbag.enable() -rsyslog_conf = '/etc/rsyslog.d/00-vyos.conf' +rsyslog_conf = '/run/rsyslog/rsyslog.conf' logrotate_conf = '/etc/logrotate.d/vyos-rsyslog' -systemd_override = r'/run/systemd/system/rsyslog.service.d/override.conf' + +systemd_socket = 'syslog.socket' +systemd_service = systemd_services['syslog'] def get_config(config=None): if config: @@ -45,12 +51,17 @@ def get_config(config=None): syslog.update({ 'logrotate' : logrotate_conf }) - tmp = is_node_changed(conf, base + ['vrf']) - if tmp: syslog.update({'restart_required': {}}) - syslog = conf.merge_defaults(syslog, recursive=True) - if syslog.from_defaults(['global']): - del syslog['global'] + if syslog.from_defaults(['local']): + del syslog['local'] + + if 'preserve_fqdn' in syslog: + if conf.exists(['system', 'host-name']): + tmp = conf.return_value(['system', 'host-name']) + syslog['preserve_fqdn']['host_name'] = tmp + if conf.exists(['system', 'domain-name']): + tmp = conf.return_value(['system', 'domain-name']) + syslog['preserve_fqdn']['domain_name'] = tmp return syslog @@ -58,7 +69,33 @@ def verify(syslog): if not syslog: return None - verify_vrf(syslog) + if 'preserve_fqdn' in syslog: + if 'host_name' not in syslog['preserve_fqdn']: + Warning('No "system host-name" defined - cannot set syslog FQDN!') + if 'domain_name' not in syslog['preserve_fqdn']: + Warning('No "system domain-name" defined - cannot set syslog FQDN!') + + if 'remote' in syslog: + for remote, remote_options in syslog['remote'].items(): + if 'protocol' in remote_options and remote_options['protocol'] == 'udp': + if 'format' in remote_options and 'octet_counted' in remote_options['format']: + Warning(f'Syslog UDP transport for "{remote}" should not use octet-counted format!') + + if 'vrf' in remote_options: + verify_vrf(remote_options) + + if 'source_address' in remote_options: + vrf = None + if 'vrf' in remote_options: + vrf = remote_options['vrf'] + if not is_addr_assigned(remote_options['source_address'], vrf): + raise ConfigError('No interface with given address specified!') + + source_address = remote_options['source_address'] + if ((is_ipv4(remote) and is_ipv6(source_address)) or + (is_ipv6(remote) and is_ipv4(source_address))): + raise ConfigError(f'Source-address "{source_address}" does not match '\ + f'address-family of remote "{remote}"!') def generate(syslog): if not syslog: @@ -70,26 +107,15 @@ def generate(syslog): return None render(rsyslog_conf, 'rsyslog/rsyslog.conf.j2', syslog) - render(systemd_override, 'rsyslog/override.conf.j2', syslog) render(logrotate_conf, 'rsyslog/logrotate.j2', syslog) - - # Reload systemd manager configuration - call('systemctl daemon-reload') return None def apply(syslog): - systemd_socket = 'syslog.socket' - systemd_service = 'syslog.service' if not syslog: call(f'systemctl stop {systemd_service} {systemd_socket}') return None - # we need to restart the service if e.g. the VRF name changed - systemd_action = 'reload-or-restart' - if 'restart_required' in syslog: - systemd_action = 'restart' - - call(f'systemctl {systemd_action} {systemd_service}') + call(f'systemctl reload-or-restart {systemd_service}') return None if __name__ == '__main__': diff --git a/src/conf_mode/system_task-scheduler.py b/src/conf_mode/system_task-scheduler.py index 129be5d3c..c0253006a 100755 --- a/src/conf_mode/system_task-scheduler.py +++ b/src/conf_mode/system_task-scheduler.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/system_timezone.py b/src/conf_mode/system_timezone.py index 39770fdb4..54ffb88ee 100755 --- a/src/conf_mode/system_timezone.py +++ b/src/conf_mode/system_timezone.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/system_update-check.py b/src/conf_mode/system_update-check.py index 71ac13e51..7ed736eac 100755 --- a/src/conf_mode/system_update-check.py +++ b/src/conf_mode/system_update-check.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/system_wireless.py b/src/conf_mode/system_wireless.py index e0ca0ab8e..2d377c50a 100644 --- a/src/conf_mode/system_wireless.py +++ b/src/conf_mode/system_wireless.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index ca0c3657f..1871956b2 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -64,6 +64,7 @@ swanctl_dir = '/etc/swanctl' charon_conf = '/etc/strongswan.d/charon.conf' charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf' charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf' +charon_systemd_conf = '/etc/strongswan.d/charon-systemd.conf' interface_conf = '/etc/strongswan.d/interfaces_use.conf' swanctl_conf = f'{swanctl_dir}/swanctl.conf' @@ -86,8 +87,6 @@ def get_config(config=None): conf = Config() base = ['vpn', 'ipsec'] l2tp_base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings'] - if not conf.exists(base): - return None # retrieve common dictionary keys ipsec = conf.get_config_dict(base, key_mangling=('-', '_'), @@ -95,6 +94,14 @@ def get_config(config=None): get_first_key=True, with_pki=True) + ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel']) + if ipsec['nhrp_exists']: + set_dependents('nhrp', conf) + + if not conf.exists(base): + ipsec.update({'deleted' : ''}) + return ipsec + # We have to cleanup the default dict, as default values could # enable features which are not explicitly enabled on the # CLI. E.g. dead-peer-detection defaults should not be injected @@ -115,7 +122,6 @@ def get_config(config=None): 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']) - ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel']) if ipsec['nhrp_exists']: set_dependents('nhrp', conf) @@ -151,6 +157,8 @@ def get_config(config=None): _, vti = get_interface_dict(conf, ['interfaces', 'vti'], vti_interface) ipsec['vti_interface_dicts'][vti_interface] = vti + ipsec['vpp_ipsec_exists'] = conf.exists(['vpp', 'settings', 'ipsec']) + return ipsec def get_dhcp_address(iface): @@ -196,8 +204,8 @@ def verify_pki_rsa(pki, rsa_conf): return True def verify(ipsec): - if not ipsec: - return None + if not ipsec or 'deleted' in ipsec: + return if 'authentication' in ipsec: if 'psk' in ipsec['authentication']: @@ -214,6 +222,19 @@ def verify(ipsec): else: verify_interface_exists(ipsec, interface) + # need to use a pseudo-random function (PRF) with an authenticated encryption algorithm. + # If a hash algorithm is defined then it will be mapped to an equivalent PRF + if 'ike_group' in ipsec: + for _, ike_config in ipsec['ike_group'].items(): + for proposal, proposal_config in ike_config.get('proposal', {}).items(): + if 'encryption' in proposal_config and 'prf' not in proposal_config: + # list of hash algorithms that cannot be mapped to an equivalent PRF + algs = ['aes128gmac', 'aes192gmac', 'aes256gmac', 'sha256_96'] + if 'hash' in proposal_config and proposal_config['hash'] in algs: + raise ConfigError( + f"A PRF algorithm is mandatory in IKE proposal {proposal}" + ) + if 'l2tp' in ipsec: if 'esp_group' in ipsec['l2tp']: if 'esp_group' not in ipsec or ipsec['l2tp']['esp_group'] not in ipsec['esp_group']: @@ -466,6 +487,17 @@ def verify(ipsec): else: raise ConfigError(f"Missing ike-group on site-to-site peer {peer}") + # verify encryption algorithm compatibility for IKE with VPP + if ipsec['vpp_ipsec_exists']: + ike_group = ipsec['ike_group'][peer_conf['ike_group']] + for proposal, proposal_config in ike_group.get('proposal', {}).items(): + algs = ['gmac', 'serpent', 'twofish'] + if any(alg in proposal_config['encryption'] for alg in algs): + raise ConfigError( + f'Encryption algorithm {proposal_config["encryption"]} cannot be used ' + f'for IKE proposal {proposal} for site-to-site peer {peer} with VPP' + ) + if 'authentication' not in peer_conf or 'mode' not in peer_conf['authentication']: raise ConfigError(f"Missing authentication on site-to-site peer {peer}") @@ -544,7 +576,7 @@ def verify(ipsec): esp_group_name = tunnel_conf['esp_group'] if 'esp_group' in tunnel_conf else peer_conf['default_esp_group'] - if esp_group_name not in ipsec['esp_group']: + if esp_group_name not in ipsec.get('esp_group'): raise ConfigError(f"Invalid esp-group on tunnel {tunnel} for site-to-site peer {peer}") esp_group = ipsec['esp_group'][esp_group_name] @@ -556,6 +588,18 @@ def verify(ipsec): if ('local' in tunnel_conf and 'prefix' in tunnel_conf['local']) or ('remote' in tunnel_conf and 'prefix' in tunnel_conf['remote']): raise ConfigError(f"Local/remote prefix cannot be used with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}") + # verify ESP encryption algorithm compatibility with VPP + # because Marvel plugin for VPP doesn't support all algorithms that Strongswan does + if ipsec['vpp_ipsec_exists']: + for proposal, proposal_config in esp_group.get('proposal', {}).items(): + algs = ['aes128', 'aes192', 'aes256', 'aes128gcm128', 'aes192gcm128', 'aes256gcm128'] + if proposal_config['encryption'] not in algs: + raise ConfigError( + f'Encryption algorithm {proposal_config["encryption"]} cannot be used ' + f'for ESP proposal {proposal} on tunnel {tunnel} for site-to-site peer {peer} with VPP' + ) + + def cleanup_pki_files(): for path in [CERT_PATH, CA_PATH, CRL_PATH, KEY_PATH, PUBKEY_PATH]: if not os.path.exists(path): @@ -611,7 +655,7 @@ def generate_pki_files_rsa(pki, rsa_conf): def generate(ipsec): cleanup_pki_files() - if not ipsec: + if not ipsec or 'deleted' in ipsec: for config_file in [charon_dhcp_conf, charon_radius_conf, interface_conf, swanctl_conf]: if os.path.isfile(config_file): os.unlink(config_file) @@ -683,7 +727,7 @@ def generate(ipsec): for remote_prefix in remote_prefixes: local_net = ipaddress.ip_network(local_prefix) remote_net = ipaddress.ip_network(remote_prefix) - if local_net.overlaps(remote_net): + if local_net.subnet_of(remote_net): if passthrough is None: passthrough = [] passthrough.append(local_prefix) @@ -702,21 +746,19 @@ def generate(ipsec): render(charon_conf, 'ipsec/charon.j2', ipsec) render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.j2', ipsec) render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.j2', ipsec) + render(charon_systemd_conf, 'ipsec/charon_systemd.conf.j2', ipsec) render(interface_conf, 'ipsec/interfaces_use.conf.j2', ipsec) render(swanctl_conf, 'ipsec/swanctl.conf.j2', ipsec) def apply(ipsec): systemd_service = 'strongswan.service' - if not ipsec: + if not ipsec or 'deleted' in 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']) @@ -724,7 +766,7 @@ def apply(ipsec): db.commit(lambda interface: ipsec['vti_interface_dicts'][interface]) elif vti_updown_db_exists(): remove_vti_updown_db() - + if ipsec: if ipsec.get('nhrp_exists', False): try: call_dependents() @@ -733,7 +775,6 @@ def apply(ipsec): # ConfigError("ConfigError('Interface ethN requires an IP address!')") pass - if __name__ == '__main__': try: ipsec = get_config() diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 04ccbcec3..d6f5e4c28 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 42785134f..78cdaa179 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -37,19 +37,22 @@ from passlib.hash import sha512_crypt from time import sleep from vyos import airbag + airbag.enable() -cfg_dir = '/run/ocserv' -ocserv_conf = cfg_dir + '/ocserv.conf' -ocserv_passwd = cfg_dir + '/ocpasswd' +cfg_dir = '/run/ocserv' +ocserv_conf = cfg_dir + '/ocserv.conf' +ocserv_passwd = cfg_dir + '/ocpasswd' ocserv_otp_usr = cfg_dir + '/users.oath' -radius_cfg = cfg_dir + '/radiusclient.conf' +radius_cfg = cfg_dir + '/radiusclient.conf' radius_servers = cfg_dir + '/radius_servers' + # Generate hash from user cleartext password def get_hash(password): return sha512_crypt.hash(password) + def get_config(config=None): if config: conf = config @@ -59,78 +62,133 @@ def get_config(config=None): if not conf.exists(base): return None - ocserv = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - with_recursive_defaults=True, - with_pki=True) + ocserv = conf.get_config_dict( + base, + key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True, + with_pki=True, + ) return ocserv + def verify(ocserv): if ocserv is None: return None # Check if listen-ports not binded other services # It can be only listen by 'ocserv-main' for proto, port in ocserv.get('listen_ports').items(): - if check_port_availability(ocserv['listen_address'], int(port), proto) is not True and \ - not is_listen_port_bind_service(int(port), 'ocserv-main'): + if check_port_availability( + ocserv['listen_address'], int(port), proto + ) is not True and not is_listen_port_bind_service(int(port), 'ocserv-main'): raise ConfigError(f'"{proto}" port "{port}" is used by another service') # Check accounting - if "accounting" in ocserv: - if "mode" in ocserv["accounting"] and "radius" in ocserv["accounting"]["mode"]: - if not origin["accounting"]['radius']['server']: - raise ConfigError('OpenConnect accounting mode radius requires at least one RADIUS server') - if "authentication" not in ocserv or "mode" not in ocserv["authentication"]: - raise ConfigError('Accounting depends on OpenConnect authentication configuration') - elif "radius" not in ocserv["authentication"]["mode"]: - raise ConfigError('RADIUS accounting must be used with RADIUS authentication') + if 'accounting' in ocserv: + if 'mode' in ocserv['accounting'] and 'radius' in ocserv['accounting']['mode']: + if not ocserv['accounting']['radius']['server']: + raise ConfigError( + 'OpenConnect accounting mode radius requires at least one RADIUS server' + ) + if 'authentication' not in ocserv or 'mode' not in ocserv['authentication']: + raise ConfigError( + 'Accounting depends on OpenConnect authentication configuration' + ) + elif 'radius' not in ocserv['authentication']['mode']: + raise ConfigError( + 'RADIUS accounting must be used with RADIUS authentication' + ) # Check authentication - if "authentication" in ocserv: - if "mode" in ocserv["authentication"]: - if ("local" in ocserv["authentication"]["mode"] and - "radius" in ocserv["authentication"]["mode"]): - raise ConfigError('OpenConnect authentication modes are mutually-exclusive, remove either local or radius from your configuration') - if "radius" in ocserv["authentication"]["mode"]: - if not ocserv["authentication"]['radius']['server']: - raise ConfigError('OpenConnect authentication mode radius requires at least one RADIUS server') - if "local" in ocserv["authentication"]["mode"]: - if not ocserv.get("authentication", {}).get("local_users"): - raise ConfigError('OpenConnect mode local required at least one user') - if not ocserv["authentication"]["local_users"]["username"]: - raise ConfigError('OpenConnect mode local required at least one user') + if 'authentication' in ocserv: + if 'mode' in ocserv['authentication']: + if ( + 'local' in ocserv['authentication']['mode'] + and 'radius' in ocserv['authentication']['mode'] + ): + raise ConfigError( + 'OpenConnect authentication modes are mutually-exclusive, remove either local or radius from your configuration' + ) + if 'radius' in ocserv['authentication']['mode']: + if 'server' not in ocserv['authentication']['radius']: + raise ConfigError( + 'OpenConnect authentication mode radius requires at least one RADIUS server' + ) + if 'local' in ocserv['authentication']['mode']: + if not ocserv.get('authentication', {}).get('local_users'): + raise ConfigError( + 'OpenConnect mode local required at least one user' + ) + if not ocserv['authentication']['local_users']['username']: + raise ConfigError( + 'OpenConnect mode local required at least one user' + ) else: # For OTP mode: verify that each local user has an OTP key - if "otp" in ocserv["authentication"]["mode"]["local"]: + if 'otp' in ocserv['authentication']['mode']['local']: users_wo_key = [] - for user, user_config in ocserv["authentication"]["local_users"]["username"].items(): + for user, user_config in ocserv['authentication'][ + 'local_users' + ]['username'].items(): # User has no OTP key defined - if dict_search('otp.key', user_config) == None: + if dict_search('otp.key', user_config) is None: users_wo_key.append(user) if users_wo_key: - raise ConfigError(f'OTP enabled, but no OTP key is configured for these users:\n{users_wo_key}') + raise ConfigError( + f'OTP enabled, but no OTP key is configured for these users:\n{users_wo_key}' + ) # For password (and default) mode: verify that each local user has password - if "password" in ocserv["authentication"]["mode"]["local"] or "otp" not in ocserv["authentication"]["mode"]["local"]: + if ( + 'password' in ocserv['authentication']['mode']['local'] + or 'otp' not in ocserv['authentication']['mode']['local'] + ): users_wo_pswd = [] - for user in ocserv["authentication"]["local_users"]["username"]: - if not "password" in ocserv["authentication"]["local_users"]["username"][user]: + for user in ocserv['authentication']['local_users']['username']: + if ( + 'password' + not in ocserv['authentication']['local_users'][ + 'username' + ][user] + ): users_wo_pswd.append(user) if users_wo_pswd: - raise ConfigError(f'password required for users:\n{users_wo_pswd}') + raise ConfigError( + f'password required for users:\n{users_wo_pswd}' + ) # Validate that if identity-based-config is configured all child config nodes are set - if 'identity_based_config' in ocserv["authentication"]: - if 'disabled' not in ocserv["authentication"]["identity_based_config"]: - Warning("Identity based configuration files is a 3rd party addition. Use at your own risk, this might break the ocserv daemon!") - if 'mode' not in ocserv["authentication"]["identity_based_config"]: - raise ConfigError('OpenConnect radius identity-based-config enabled but mode not selected') - elif 'group' in ocserv["authentication"]["identity_based_config"]["mode"] and "radius" not in ocserv["authentication"]["mode"]: - raise ConfigError('OpenConnect config-per-group must be used with radius authentication') - if 'directory' not in ocserv["authentication"]["identity_based_config"]: - raise ConfigError('OpenConnect identity-based-config enabled but directory not set') - if 'default_config' not in ocserv["authentication"]["identity_based_config"]: - raise ConfigError('OpenConnect identity-based-config enabled but default-config not set') + if 'identity_based_config' in ocserv['authentication']: + if 'disabled' not in ocserv['authentication']['identity_based_config']: + Warning( + 'Identity based configuration files is a 3rd party addition. Use at your own risk, this might break the ocserv daemon!' + ) + if 'mode' not in ocserv['authentication']['identity_based_config']: + raise ConfigError( + 'OpenConnect radius identity-based-config enabled but mode not selected' + ) + elif ( + 'group' + in ocserv['authentication']['identity_based_config']['mode'] + and 'radius' not in ocserv['authentication']['mode'] + ): + raise ConfigError( + 'OpenConnect config-per-group must be used with radius authentication' + ) + if ( + 'directory' + not in ocserv['authentication']['identity_based_config'] + ): + raise ConfigError( + 'OpenConnect identity-based-config enabled but directory not set' + ) + if ( + 'default_config' + not in ocserv['authentication']['identity_based_config'] + ): + raise ConfigError( + 'OpenConnect identity-based-config enabled but default-config not set' + ) else: raise ConfigError('OpenConnect authentication mode required') else: @@ -149,94 +207,162 @@ def verify(ocserv): verify_pki_ca_certificate(ocserv, ca_cert) # Check network settings - if "network_settings" in ocserv: - if "push_route" in ocserv["network_settings"]: + if 'network_settings' in ocserv: + if 'push_route' in ocserv['network_settings']: # Replace default route - if "0.0.0.0/0" in ocserv["network_settings"]["push_route"]: - ocserv["network_settings"]["push_route"].remove("0.0.0.0/0") - ocserv["network_settings"]["push_route"].append("default") + if '0.0.0.0/0' in ocserv['network_settings']['push_route']: + ocserv['network_settings']['push_route'].remove('0.0.0.0/0') + ocserv['network_settings']['push_route'].append('default') else: - ocserv["network_settings"]["push_route"] = ["default"] + ocserv['network_settings']['push_route'] = ['default'] else: raise ConfigError('OpenConnect network settings required!') + def generate(ocserv): if not ocserv: return None - if "radius" in ocserv["authentication"]["mode"]: + if 'radius' in ocserv['authentication']['mode']: if dict_search(ocserv, 'accounting.mode.radius'): # Render radius client configuration render(radius_cfg, 'ocserv/radius_conf.j2', ocserv) - merged_servers = ocserv["accounting"]["radius"]["server"] | ocserv["authentication"]["radius"]["server"] + merged_servers = ( + ocserv['accounting']['radius']['server'] + | ocserv['authentication']['radius']['server'] + ) # Render radius servers # Merge the accounting and authentication servers into a single dictionary - render(radius_servers, 'ocserv/radius_servers.j2', {'server': merged_servers}) + render( + radius_servers, 'ocserv/radius_servers.j2', {'server': merged_servers} + ) else: # Render radius client configuration render(radius_cfg, 'ocserv/radius_conf.j2', ocserv) # Render radius servers - render(radius_servers, 'ocserv/radius_servers.j2', ocserv["authentication"]["radius"]) - elif "local" in ocserv["authentication"]["mode"]: + render( + radius_servers, + 'ocserv/radius_servers.j2', + ocserv['authentication']['radius'], + ) + elif 'local' in ocserv['authentication']['mode']: # if mode "OTP", generate OTP users file parameters - if "otp" in ocserv["authentication"]["mode"]["local"]: - if "local_users" in ocserv["authentication"]: - for user in ocserv["authentication"]["local_users"]["username"]: + if 'otp' in ocserv['authentication']['mode']['local']: + if 'local_users' in ocserv['authentication']: + for user in ocserv['authentication']['local_users']['username']: # OTP token type from CLI parameters: - otp_interval = str(ocserv["authentication"]["local_users"]["username"][user]["otp"].get("interval")) - token_type = ocserv["authentication"]["local_users"]["username"][user]["otp"].get("token_type") - otp_length = str(ocserv["authentication"]["local_users"]["username"][user]["otp"].get("otp_length")) - if token_type == "hotp-time": - otp_type = "HOTP/T" + otp_interval - elif token_type == "hotp-event": - otp_type = "HOTP/E" + otp_interval = str( + ocserv['authentication']['local_users']['username'][user][ + 'otp' + ].get('interval') + ) + token_type = ocserv['authentication']['local_users']['username'][ + user + ]['otp'].get('token_type') + otp_length = str( + ocserv['authentication']['local_users']['username'][user][ + 'otp' + ].get('otp_length') + ) + if token_type == 'hotp-time': + otp_type = 'HOTP/T' + otp_interval + elif token_type == 'hotp-event': + otp_type = 'HOTP/E' else: - otp_type = "HOTP/T" + otp_interval - ocserv["authentication"]["local_users"]["username"][user]["otp"]["token_tmpl"] = otp_type + "/" + otp_length + otp_type = 'HOTP/T' + otp_interval + ocserv['authentication']['local_users']['username'][user]['otp'][ + 'token_tmpl' + ] = otp_type + '/' + otp_length # if there is a password, generate hash - if "password" in ocserv["authentication"]["mode"]["local"] or not "otp" in ocserv["authentication"]["mode"]["local"]: - if "local_users" in ocserv["authentication"]: - for user in ocserv["authentication"]["local_users"]["username"]: - ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"]) - - if "password-otp" in ocserv["authentication"]["mode"]["local"]: + if ( + 'password' in ocserv['authentication']['mode']['local'] + or 'otp' not in ocserv['authentication']['mode']['local'] + ): + if 'local_users' in ocserv['authentication']: + for user in ocserv['authentication']['local_users']['username']: + ocserv['authentication']['local_users']['username'][user][ + 'hash' + ] = get_hash( + ocserv['authentication']['local_users']['username'][user][ + 'password' + ] + ) + + if 'password-otp' in ocserv['authentication']['mode']['local']: # Render local users ocpasswd - render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"]) + render( + ocserv_passwd, + 'ocserv/ocserv_passwd.j2', + ocserv['authentication']['local_users'], + ) # Render local users OTP keys - render(ocserv_otp_usr, 'ocserv/ocserv_otp_usr.j2', ocserv["authentication"]["local_users"]) - elif "password" in ocserv["authentication"]["mode"]["local"]: + render( + ocserv_otp_usr, + 'ocserv/ocserv_otp_usr.j2', + ocserv['authentication']['local_users'], + ) + elif 'password' in ocserv['authentication']['mode']['local']: # Render local users ocpasswd - render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"]) - elif "otp" in ocserv["authentication"]["mode"]["local"]: + render( + ocserv_passwd, + 'ocserv/ocserv_passwd.j2', + ocserv['authentication']['local_users'], + ) + elif 'otp' in ocserv['authentication']['mode']['local']: # Render local users OTP keys - render(ocserv_otp_usr, 'ocserv/ocserv_otp_usr.j2', ocserv["authentication"]["local_users"]) + render( + ocserv_otp_usr, + 'ocserv/ocserv_otp_usr.j2', + ocserv['authentication']['local_users'], + ) else: # Render local users ocpasswd - render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"]) + render( + ocserv_passwd, + 'ocserv/ocserv_passwd.j2', + ocserv['authentication']['local_users'], + ) else: - if "local_users" in ocserv["authentication"]: - for user in ocserv["authentication"]["local_users"]["username"]: - ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"]) + if 'local_users' in ocserv['authentication']: + for user in ocserv['authentication']['local_users']['username']: + ocserv['authentication']['local_users']['username'][user]['hash'] = ( + get_hash( + ocserv['authentication']['local_users']['username'][user][ + 'password' + ] + ) + ) # Render local users - render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"]) + render( + ocserv_passwd, + 'ocserv/ocserv_passwd.j2', + ocserv['authentication']['local_users'], + ) - if "ssl" in ocserv: + if 'ssl' in ocserv: cert_file_path = os.path.join(cfg_dir, 'cert.pem') cert_key_path = os.path.join(cfg_dir, 'cert.key') - if 'certificate' in ocserv['ssl']: cert_name = ocserv['ssl']['certificate'] pki_cert = ocserv['pki']['certificate'][cert_name] loaded_pki_cert = load_certificate(pki_cert['certificate']) - loaded_ca_certs = {load_certificate(c['certificate']) - for c in ocserv['pki']['ca'].values()} if 'ca' in ocserv['pki'] else {} + loaded_ca_certs = ( + { + load_certificate(c['certificate']) + for c in ocserv['pki']['ca'].values() + } + if 'ca' in ocserv['pki'] + else {} + ) cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs) - write_file(cert_file_path, - '\n'.join(encode_certificate(c) for c in cert_full_chain)) + write_file( + cert_file_path, + '\n'.join(encode_certificate(c) for c in cert_full_chain), + ) if 'private' in pki_cert and 'key' in pki_cert['private']: write_file(cert_key_path, wrap_private_key(pki_cert['private']['key'])) @@ -250,7 +376,8 @@ def generate(ocserv): loaded_ca_cert = load_certificate(pki_ca_cert['certificate']) ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs) ca_chains.append( - '\n'.join(encode_certificate(c) for c in ca_full_chain)) + '\n'.join(encode_certificate(c) for c in ca_full_chain) + ) write_file(ca_cert_file_path, '\n'.join(ca_chains)) @@ -269,11 +396,13 @@ def apply(ocserv): counter = 0 while True: # exit early when service runs - if is_systemd_service_running("ocserv.service"): + if is_systemd_service_running('ocserv.service'): break sleep(0.250) if counter > 5: - raise ConfigError('OpenConnect failed to start, check the logs for details') + raise ConfigError( + 'OpenConnect failed to start, check the logs for details' + ) break counter += 1 diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py index c0d8330bd..c11619779 100755 --- a/src/conf_mode/vpn_pptp.py +++ b/src/conf_mode/vpn_pptp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 7490fd0e0..5382fc711 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 72b178c89..1eacba112 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -18,24 +18,26 @@ from sys import exit from jmespath import search from json import loads +import vyos.defaults + from vyos.config import Config -from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.configverify import verify_route_map from vyos.firewall import conntrack_required +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict from vyos.ifconfig import Interface from vyos.template import render -from vyos.template import render_to_string from vyos.utils.dict import dict_search from vyos.utils.network import get_vrf_tableid from vyos.utils.network import get_vrf_members from vyos.utils.network import interface_exists from vyos.utils.process import call from vyos.utils.process import cmd +from vyos.utils.process import is_systemd_service_running from vyos.utils.process import popen from vyos.utils.system import sysctl_write from vyos import ConfigError -from vyos import frr from vyos import airbag airbag.enable() @@ -132,15 +134,9 @@ def get_config(config=None): if 'name' in vrf: vrf['conntrack'] = conntrack_required(conf) - # We also need the route-map information from the config - # - # XXX: one MUST always call this without the key_mangling() option! See - # vyos.configverify.verify_common_route_maps() for more information. - tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'], - get_first_key=True)}} - - # Merge policy dict into "regular" config dict - vrf = dict_merge(tmp, vrf) + # We need to merge the FRR rendering dict into the VRF dict + # this is required to get the route-map information to FRR + vrf.update({'frr_dict' : get_frrender_dict(conf)}) return vrf def verify(vrf): @@ -155,9 +151,11 @@ def verify(vrf): f'static routes installed!') if 'name' in vrf: - reserved_names = ["add", "all", "broadcast", "default", "delete", "dev", - "get", "inet", "mtu", "link", "type", "vrf"] + reserved_names = ['add', 'all', 'broadcast', 'default', 'delete', 'dev', + 'down', 'get', 'inet', 'link', 'mtu', 'type', 'up', 'vrf'] + table_ids = [] + vnis = [] for name, vrf_config in vrf['name'].items(): # Reserved VRF names if name in reserved_names: @@ -167,6 +165,11 @@ def verify(vrf): if 'table' not in vrf_config: raise ConfigError(f'VRF "{name}" table id is mandatory!') + if int(vrf_config['table']) == vyos.defaults.rt_global_vrf: + raise ConfigError( + f'VRF "{name}" table id {vrf_config["table"]} cannot be used!' + ) + # routing table id can't be changed - OS restriction if interface_exists(name): tmp = get_vrf_tableid(name) @@ -178,17 +181,24 @@ def verify(vrf): raise ConfigError(f'VRF "{name}" table id is not unique!') table_ids.append(vrf_config['table']) + # VRF VNIs must be unique on the system + if 'vni' in vrf_config: + vni = vrf_config['vni'] + if vni in vnis: + raise ConfigError(f'VRF "{name}" VNI "{vni}" is not unique!') + vnis.append(vni) + tmp = dict_search('ip.protocol', vrf_config) if tmp != None: for protocol, protocol_options in tmp.items(): if 'route_map' in protocol_options: - verify_route_map(protocol_options['route_map'], vrf) + verify_route_map(protocol_options['route_map'], vrf['frr_dict']) tmp = dict_search('ipv6.protocol', vrf_config) if tmp != None: for protocol, protocol_options in tmp.items(): if 'route_map' in protocol_options: - verify_route_map(protocol_options['route_map'], vrf) + verify_route_map(protocol_options['route_map'], vrf['frr_dict']) return None @@ -196,8 +206,9 @@ def verify(vrf): def generate(vrf): # Render iproute2 VR helper names render(config_file, 'iproute2/vrf.conf.j2', vrf) - # Render VRF Kernel/Zebra route-map filters - vrf['frr_zebra_config'] = render_to_string('frr/zebra.vrf.route-map.frr.j2', vrf) + + if 'frr_dict' in vrf and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(vrf['frr_dict']) return None @@ -229,7 +240,7 @@ def apply(vrf): vrf_iface.set_dhcpv6(False) # Remove nftables conntrack zone map item - nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{tmp}" }}' + nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ \'"{tmp}"\' }}' # Check if deleting is possible first to avoid raising errors _, err = popen(f'nft --check {nft_del_element}') if not err: @@ -309,7 +320,7 @@ def apply(vrf): state = 'down' if 'disable' in config else 'up' vrf_if.set_admin_state(state) # Add nftables conntrack zone map item - nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}' + nft_add_element = f'add element inet vrf_zones ct_iface_map {{ \'"{name}"\' : {table} }}' cmd(f'nft {nft_add_element}') # Only call into nftables as long as there is nothing setup to avoid wasting @@ -339,17 +350,8 @@ def apply(vrf): if has_rule(afi, 2000, 'l3mdev'): call(f'ip {afi} rule del pref 2000 l3mdev unreachable') - # Apply FRR filters - zebra_daemon = 'zebra' - # Save original configuration prior to starting any commit actions - frr_cfg = frr.FRRConfig() - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(f'^vrf .+', stop_pattern='^exit-vrf', remove_stop_mark=True) - if 'frr_zebra_config' in vrf: - frr_cfg.add_before(frr.default_add_before, vrf['frr_zebra_config']) - frr_cfg.commit_configuration(zebra_daemon) + if 'frr_dict' in vrf and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() return None |