diff options
Diffstat (limited to 'src')
61 files changed, 1288 insertions, 189 deletions
diff --git a/src/completion/list_bgp_neighbors.sh b/src/completion/list_bgp_neighbors.sh index f74f102ef..869a7ab0a 100755 --- a/src/completion/list_bgp_neighbors.sh +++ b/src/completion/list_bgp_neighbors.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 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 @@ -18,19 +18,21 @@ ipv4=0 ipv6=0 +vrf="" while [[ "$#" -gt 0 ]]; do case $1 in -4|--ipv4) ipv4=1 ;; -6|--ipv6) ipv6=1 ;; -b|--both) ipv4=1; ipv6=1 ;; + --vrf) vrf="vrf name $2"; shift ;; *) echo "Unknown parameter passed: $1" ;; esac shift done declare -a vals -eval "vals=($(cli-shell-api listActiveNodes protocols bgp neighbor))" +eval "vals=($(cli-shell-api listActiveNodes $vrf protocols bgp neighbor))" if [ $ipv4 -eq 1 ] && [ $ipv6 -eq 1 ]; then echo -n '<x.x.x.x>' '<h:h:h:h:h:h:h:h>' ${vals[@]} @@ -54,9 +56,10 @@ elif [ $ipv6 -eq 1 ] ; then done else echo "Usage:" - echo "-4|--ipv4 list only IPv4 peers" - echo "-6|--ipv6 list only IPv6 peers" - echo "--both list both IP4 and IPv6 peers" + echo "-4|--ipv4 list only IPv4 peers" + echo "-6|--ipv6 list only IPv6 peers" + echo "--both list both IP4 and IPv6 peers" + echo "--vrf <name> apply command to given VRF (optional)" echo "" exit 1 fi diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 2110fd9e0..ac3dc536b 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -90,10 +90,10 @@ def get_config(config=None): container['name'][name] = dict_merge(default_values, container['name'][name]) # Delete container network, delete containers - tmp = node_changed(conf, base + ['container', 'network']) + tmp = node_changed(conf, base + ['network']) if tmp: container.update({'network_remove' : tmp}) - tmp = node_changed(conf, base + ['container', 'name']) + tmp = node_changed(conf, base + ['name']) if tmp: container.update({'container_remove' : tmp}) return container @@ -132,7 +132,7 @@ def verify(container): # Check if the specified container network exists network_name = list(container_config['network'])[0] - if network_name not in container['network']: + if network_name not in container.get('network', {}): raise ConfigError(f'Container network "{network_name}" does not exist!') if 'address' in container_config['network'][network_name]: @@ -270,12 +270,13 @@ def apply(container): # Option "--force" allows to delete containers with any status if 'container_remove' in container: for name in container['container_remove']: - call(f'podman stop {name}') + call(f'podman stop --time 3 {name}') call(f'podman rm --force {name}') # Delete old networks if needed if 'network_remove' in container: for network in container['network_remove']: + call(f'podman network rm {network}') tmp = f'/etc/cni/net.d/{network}.conflist' if os.path.exists(tmp): os.unlink(tmp) @@ -294,7 +295,7 @@ def apply(container): # check if there is a container by that name running tmp = _cmd('podman ps -a --format "{{.Names}}"') if name in tmp: - _cmd(f'podman stop {name}') + _cmd(f'podman stop --time 3 {name}') _cmd(f'podman rm --force {name}') continue diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index f1c2d1f43..d0d87d73e 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -98,6 +98,9 @@ def get_config(config=None): dns['authoritative_zone_errors'].append('{}.{}: at least one address is required'.format(subnode, node)) continue + if subnode == 'any': + subnode = '*' + for address in rdata['address']: zone['records'].append({ 'name': subnode, @@ -263,6 +266,12 @@ def verify(dns): if 'server' not in dns['domain'][domain]: raise ConfigError(f'No server configured for domain {domain}!') + if 'dns64_prefix' in dns: + dns_prefix = dns['dns64_prefix'].split('/')[1] + # RFC 6147 requires prefix /96 + if int(dns_prefix) != 96: + raise ConfigError('DNS 6to4 prefix must be of length /96') + if ('authoritative_zone_errors' in dns) and dns['authoritative_zone_errors']: for error in dns['authoritative_zone_errors']: print(error) diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py index 9a5d278e9..ab1c69259 100755 --- a/src/conf_mode/firewall-interface.py +++ b/src/conf_mode/firewall-interface.py @@ -64,6 +64,11 @@ def get_config(config=None): return if_firewall +def verify_chain(table, chain): + # Verify firewall applied + code = run(f'nft list chain {table} {chain}') + return code == 0 + def verify(if_firewall): # bail out early - looks like removal from running config if not if_firewall: @@ -80,6 +85,9 @@ def verify(if_firewall): if name not in if_firewall['firewall']['name']: raise ConfigError(f'Invalid firewall name "{name}"') + if not verify_chain('ip filter', f'{NAME_PREFIX}{name}'): + raise ConfigError('Firewall did not apply') + if 'ipv6_name' in if_firewall[direction]: name = if_firewall[direction]['ipv6_name'] @@ -89,6 +97,9 @@ def verify(if_firewall): if name not in if_firewall['firewall']['ipv6_name']: raise ConfigError(f'Invalid firewall ipv6-name "{name}"') + if not verify_chain('ip6 filter', f'{NAME6_PREFIX}{name}'): + raise ConfigError('Firewall did not apply') + return None def generate(if_firewall): diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 6924bf555..07eca722f 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 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 @@ -26,9 +26,17 @@ from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.configdiff import get_config_diff, Diff +from vyos.firewall import geoip_update +from vyos.firewall import get_ips_domains_dict +from vyos.firewall import nft_add_set_elements +from vyos.firewall import nft_flush_set +from vyos.firewall import nft_init_set +from vyos.firewall import nft_update_set_elements from vyos.template import render +from vyos.util import call from vyos.util import cmd from vyos.util import dict_search_args +from vyos.util import dict_search_recursive from vyos.util import process_named_running from vyos.util import run from vyos.xml import defaults @@ -79,10 +87,26 @@ nft6_iface_chains = ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL'] valid_groups = [ 'address_group', + 'domain_group', 'network_group', 'port_group' ] +nested_group_types = [ + 'address_group', 'network_group', 'mac_group', + 'port_group', 'ipv6_address_group', 'ipv6_network_group' +] + +group_set_prefix = { + 'A_': 'address_group', + 'A6_': 'ipv6_address_group', + 'D_': 'domain_group', + 'M_': 'mac_group', + 'N_': 'network_group', + 'N6_': 'ipv6_network_group', + 'P_': 'port_group' +} + snmp_change_type = { 'unknown': 0, 'add': 1, @@ -138,6 +162,40 @@ def get_firewall_zones(conf): return {'name': used_v4, 'ipv6_name': used_v6} +def geoip_updated(conf, firewall): + diff = get_config_diff(conf) + node_diff = diff.get_child_nodes_diff(['firewall'], expand_nodes=Diff.DELETE, recursive=True) + + out = { + 'name': [], + 'ipv6_name': [], + 'deleted_name': [], + 'deleted_ipv6_name': [] + } + updated = False + + for key, path in dict_search_recursive(firewall, 'geoip'): + set_name = f'GEOIP_CC_{path[1]}_{path[3]}' + if path[0] == 'name': + out['name'].append(set_name) + elif path[0] == 'ipv6_name': + 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[1]}_{path[3]}' + if path[0] == 'name': + out['deleted_name'].append(set_name) + elif path[0] == 'ipv6-name': + out['deleted_ipv6_name'].append(set_name) + updated = True + + if updated: + return out + + return False + def get_config(config=None): if config: conf = config @@ -162,6 +220,8 @@ def get_config(config=None): key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + firewall['geoip_updated'] = geoip_updated(conf, firewall) + return firewall def verify_rule(firewall, rule_conf, ipv6): @@ -207,6 +267,16 @@ def verify_rule(firewall, rule_conf, ipv6): if side in rule_conf: side_conf = rule_conf[side] + if dict_search_args(side_conf, 'geoip', 'country_code'): + if 'address' in side_conf: + raise ConfigError('Address and GeoIP cannot both be defined') + + if dict_search_args(side_conf, 'group', 'address_group'): + raise ConfigError('Address-group and GeoIP cannot both be defined') + + if dict_search_args(side_conf, 'group', 'network_group'): + raise ConfigError('Network-group and GeoIP cannot both be defined') + if 'group' in side_conf: if {'address_group', 'network_group'} <= set(side_conf['group']): raise ConfigError('Only one address-group or network-group can be specified') @@ -235,11 +305,34 @@ def verify_rule(firewall, rule_conf, ipv6): if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']: raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group') +def verify_nested_group(group_name, group, groups, seen): + if 'include' not in group: + return + + for g in group['include']: + if g not in groups: + raise ConfigError(f'Nested group "{g}" does not exist') + + if g in seen: + raise ConfigError(f'Group "{group_name}" has a circular reference') + + seen.append(g) + + if 'include' in groups[g]: + verify_nested_group(g, groups[g], groups, seen) + def verify(firewall): if 'config_trap' in firewall and firewall['config_trap'] == 'enable': if not firewall['trap_targets']: raise ConfigError(f'Firewall config-trap enabled but "service snmp trap-target" is not defined') + if 'group' in firewall: + for group_type in nested_group_types: + if group_type in firewall['group']: + groups = firewall['group'][group_type] + for group_name, group in groups.items(): + verify_nested_group(group_name, group, groups, []) + for name in ['name', 'ipv6_name']: if name in firewall: for name_id, name_conf in firewall[name].items(): @@ -271,55 +364,75 @@ def verify(firewall): return None -def cleanup_rule(table, jump_chain): - commands = [] - chains = nft_iface_chains if table == 'ip filter' else nft6_iface_chains - for chain in chains: - results = cmd(f'nft -a list chain {table} {chain}').split("\n") - for line in results: - if f'jump {jump_chain}' in line: - handle_search = re.search('handle (\d+)', line) - if handle_search: - commands.append(f'delete rule {table} {chain} handle {handle_search[1]}') - return commands - def cleanup_commands(firewall): commands = [] - commands_end = [] + commands_chains = [] + commands_sets = [] for table in ['ip filter', 'ip6 filter']: + name_node = 'name' if table == 'ip filter' else 'ipv6_name' + chain_prefix = NAME_PREFIX if table == 'ip filter' else NAME6_PREFIX state_chain = 'VYOS_STATE_POLICY' if table == 'ip filter' else 'VYOS_STATE_POLICY6' - json_str = cmd(f'nft -j list table {table}') + iface_chains = nft_iface_chains if table == 'ip filter' else nft6_iface_chains + + geoip_list = [] + if firewall['geoip_updated']: + geoip_key = 'deleted_ipv6_name' if table == 'ip6 filter' else 'deleted_name' + geoip_list = dict_search_args(firewall, 'geoip_updated', geoip_key) or [] + + json_str = cmd(f'nft -t -j list table {table}') obj = loads(json_str) + if 'nftables' not in obj: continue + for item in obj['nftables']: if 'chain' in item: chain = item['chain']['name'] - if chain in ['VYOS_STATE_POLICY', 'VYOS_STATE_POLICY6']: - if 'state_policy' not in firewall: - commands.append(f'delete chain {table} {chain}') - else: - commands.append(f'flush chain {table} {chain}') - elif chain not in preserve_chains and not chain.startswith("VZONE"): - if table == 'ip filter' and dict_search_args(firewall, 'name', chain.replace(NAME_PREFIX, "", 1)) != None: - commands.append(f'flush chain {table} {chain}') - elif table == 'ip6 filter' and dict_search_args(firewall, 'ipv6_name', chain.replace(NAME6_PREFIX, "", 1)) != None: - commands.append(f'flush chain {table} {chain}') - else: - commands += cleanup_rule(table, chain) - commands.append(f'delete chain {table} {chain}') - elif 'rule' in item: + if chain in preserve_chains or chain.startswith("VZONE"): + continue + + if chain == state_chain: + command = 'delete' if 'state_policy' not in firewall else 'flush' + commands_chains.append(f'{command} chain {table} {chain}') + elif dict_search_args(firewall, name_node, chain.replace(chain_prefix, "", 1)) != None: + commands.append(f'flush chain {table} {chain}') + else: + commands_chains.append(f'delete chain {table} {chain}') + + if 'rule' in item: rule = item['rule'] - if rule['chain'] in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL', 'VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']: - if 'expr' in rule and any([True for expr in rule['expr'] if dict_search_args(expr, 'jump', 'target') == state_chain]): - if 'state_policy' not in firewall: - chain = rule['chain'] - handle = rule['handle'] + chain = rule['chain'] + handle = rule['handle'] + + if chain in iface_chains: + target, _ = next(dict_search_recursive(rule['expr'], 'target')) + + if target == state_chain and 'state_policy' not in firewall: + commands.append(f'delete rule {table} {chain} handle {handle}') + + if target.startswith(chain_prefix): + if dict_search_args(firewall, name_node, target.replace(chain_prefix, "", 1)) == None: commands.append(f'delete rule {table} {chain} handle {handle}') - elif 'set' in item: + + if 'set' in item: set_name = item['set']['name'] - commands_end.append(f'delete set {table} {set_name}') - return commands + commands_end + + if set_name.startswith('GEOIP_CC_') and set_name in geoip_list: + commands_sets.append(f'delete set {table} {set_name}') + continue + + if set_name.startswith("RECENT_"): + commands_sets.append(f'delete set {table} {set_name}') + continue + + for prefix, group_type in group_set_prefix.items(): + if set_name.startswith(prefix): + group_name = set_name.replace(prefix, "", 1) + if dict_search_args(firewall, 'group', group_type, group_name) != None: + commands_sets.append(f'flush set {table} {set_name}') + else: + commands_sets.append(f'delete set {table} {set_name}') + return commands + commands_chains + commands_sets def generate(firewall): if not os.path.exists(nftables_conf): @@ -328,7 +441,6 @@ def generate(firewall): firewall['cleanup_commands'] = cleanup_commands(firewall) render(nftables_conf, 'firewall/nftables.j2', firewall) - render(nftables_defines_conf, 'firewall/nftables-defines.j2', firewall) return None def apply_sysfs(firewall): @@ -408,6 +520,27 @@ def apply(firewall): if install_result == 1: raise ConfigError('Failed to apply firewall') + # set fireall group domain-group xxx + if 'group' in firewall: + if 'domain_group' in firewall['group']: + # T970 Enable a resolver (systemd daemon) that checks + # domain-group addresses and update entries for domains by timeout + # If router loaded without internet connection or for synchronization + call('systemctl restart vyos-domain-group-resolve.service') + for group, group_config in firewall['group']['domain_group'].items(): + domains = [] + if group_config.get('address') is not None: + for address in group_config.get('address'): + domains.append(address) + # Add elements to domain-group, try to resolve domain => ip + # and add elements to nft set + ip_dict = get_ips_domains_dict(domains) + elements = sum(ip_dict.values(), []) + nft_init_set(f'D_{group}') + nft_add_set_elements(f'D_{group}', elements) + else: + call('systemctl stop vyos-domain-group-resolve.service') + if 'state_policy' in firewall and not state_policy_rule_exists(): for chain in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL']: cmd(f'nft insert rule ip filter {chain} jump VYOS_STATE_POLICY') @@ -420,6 +553,12 @@ def apply(firewall): if firewall['policy_resync']: resync_policy_route() + 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) + post_apply_trap(firewall) return None diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index 4167594e3..7e146f446 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -36,6 +36,7 @@ from vyos.ifconfig import BondIf from vyos.ifconfig import Section from vyos.util import dict_search from vyos.validate import has_address_configured +from vyos.validate import has_vrf_configured from vyos import ConfigError from vyos import airbag airbag.enable() @@ -109,20 +110,26 @@ def get_config(config=None): for interface, interface_config in bond['member']['interface'].items(): # Check if member interface is already member of another bridge tmp = is_member(conf, interface, 'bridge') - if tmp: interface_config.update({'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') - if tmp and bond['ifname'] not in tmp: - interface_config.update({'is_bond_member' : tmp}) + for tmp in is_member(conf, interface, 'bonding'): + if bond['ifname'] == tmp: + continue + 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.update({'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.update({'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: bond['member']['interface'][interface].update({'has_vrf' : {}}) return bond @@ -167,11 +174,11 @@ def verify(bond): raise ConfigError(error_msg + 'it does not exist!') if 'is_bridge_member' in interface_config: - tmp = next(iter(interface_config['is_bridge_member'])) + tmp = interface_config['is_bridge_member'] raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!') if 'is_bond_member' in interface_config: - tmp = next(iter(interface_config['is_bond_member'])) + tmp = interface_config['is_bond_member'] raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') if 'is_source_interface' in interface_config: @@ -181,6 +188,8 @@ def verify(bond): if 'has_address' in interface_config: raise ConfigError(error_msg + 'it has an address assigned!') + if 'has_vrf' in interface_config: + raise ConfigError(error_msg + 'it has a VRF assigned!') if 'primary' in bond: if bond['primary'] not in bond['member']['interface']: diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 38ae727c1..cd0d9003b 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -31,6 +31,7 @@ from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_vrf from vyos.ifconfig import BridgeIf from vyos.validate import has_address_configured +from vyos.validate import has_vrf_configured from vyos.xml import defaults from vyos.util import cmd @@ -93,6 +94,10 @@ def get_config(config=None): tmp = has_address_configured(conf, interface) if tmp: bridge['member']['interface'][interface].update({'has_address' : ''}) + # Bridge members must not have a VRF attached + tmp = has_vrf_configured(conf, interface) + if tmp: bridge['member']['interface'][interface].update({'has_vrf' : ''}) + # VLAN-aware bridge members must not have VLAN interface configuration tmp = has_vlan_subinterface_configured(conf,interface) if 'enable_vlan' in bridge and tmp: @@ -118,11 +123,11 @@ def verify(bridge): raise ConfigError('Loopback interface "lo" can not be added to a bridge') if 'is_bridge_member' in interface_config: - tmp = next(iter(interface_config['is_bridge_member'])) + tmp = interface_config['is_bridge_member'] raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!') if 'is_bond_member' in interface_config: - tmp = next(iter(interface_config['is_bond_member'])) + tmp = interface_config['is_bond_member'] raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') if 'is_source_interface' in interface_config: @@ -132,9 +137,12 @@ def verify(bridge): if 'has_address' in interface_config: raise ConfigError(error_msg + 'it has an address assigned!') + if 'has_vrf' in interface_config: + raise ConfigError(error_msg + 'it has a VRF assigned!') + if 'enable_vlan' in bridge: if 'has_vlan' in interface_config: - raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!') + raise ConfigError(error_msg + 'it has VLAN subinterface(s) assigned!') if 'wlan' in interface: raise ConfigError(error_msg + 'VLAN aware cannot be set!') diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index fec4456fb..30e7a2af7 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -31,6 +31,7 @@ from vyos.configverify import verify_mtu from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf +from vyos.configverify import verify_bond_bridge_member from vyos.ethtool import Ethtool from vyos.ifconfig import EthernetIf from vyos.pki import find_chain @@ -83,6 +84,7 @@ def verify(ethernet): verify_dhcpv6(ethernet) verify_address(ethernet) verify_vrf(ethernet) + verify_bond_bridge_member(ethernet) verify_eapol(ethernet) verify_mirror_redirect(ethernet) diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index b9cf2fa3c..08cc3a48d 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -27,6 +27,7 @@ from vyos.configverify import verify_address from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mirror_redirect +from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import GeneveIf from vyos import ConfigError @@ -64,6 +65,7 @@ def verify(geneve): verify_mtu_ipv6(geneve) verify_address(geneve) + verify_bond_bridge_member(geneve) verify_mirror_redirect(geneve) if 'remote' not in geneve: diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 6a486f969..ca321e01d 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -26,6 +26,7 @@ from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_mirror_redirect +from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import L2TPv3If from vyos.util import check_kmod from vyos.validate import is_addr_assigned @@ -77,6 +78,7 @@ def verify(l2tpv3): verify_mtu_ipv6(l2tpv3) verify_address(l2tpv3) + verify_bond_bridge_member(l2tpv3) verify_mirror_redirect(l2tpv3) return None diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 279dd119b..03a010086 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 VyOS maintainers and contributors +# Copyright (C) 2020-2022 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 @@ -21,16 +21,20 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_interface_dict -from vyos.ifconfig import MACsecIf -from vyos.ifconfig import Interface -from vyos.template import render -from vyos.util import call +from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_source_interface +from vyos.configverify import verify_bond_bridge_member +from vyos.ifconfig import MACsecIf +from vyos.ifconfig import Interface +from vyos.template import render +from vyos.util import call +from vyos.util import dict_search +from vyos.util import is_systemd_service_running from vyos import ConfigError from vyos import airbag airbag.enable() @@ -55,6 +59,12 @@ def get_config(config=None): source_interface = conf.return_effective_value(['source-interface']) macsec.update({'source_interface': source_interface}) + if is_node_changed(conf, base + [ifname, 'security']): + macsec.update({'shutdown_required': {}}) + + if is_node_changed(conf, base + [ifname, 'source_interface']): + macsec.update({'shutdown_required': {}}) + return macsec @@ -67,22 +77,15 @@ def verify(macsec): verify_vrf(macsec) verify_mtu_ipv6(macsec) verify_address(macsec) + verify_bond_bridge_member(macsec) verify_mirror_redirect(macsec) - if not (('security' in macsec) and - ('cipher' in macsec['security'])): - raise ConfigError( - 'Cipher suite must be set for MACsec "{ifname}"'.format(**macsec)) + if dict_search('security.cipher', macsec) == None: + raise ConfigError('Cipher suite must be set for MACsec "{ifname}"'.format(**macsec)) - if (('security' in macsec) and - ('encrypt' in macsec['security'])): - tmp = macsec.get('security') - - if not (('mka' in tmp) and - ('cak' in tmp['mka']) and - ('ckn' in tmp['mka'])): - raise ConfigError('Missing mandatory MACsec security ' - 'keys as encryption is enabled!') + if dict_search('security.encrypt', macsec) != None: + if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None: + raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!') if 'source_interface' in macsec: # MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad @@ -97,33 +100,35 @@ def verify(macsec): def generate(macsec): - render(wpa_suppl_conf.format(**macsec), - 'macsec/wpa_supplicant.conf.j2', macsec) + render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec) return None def apply(macsec): - # Remove macsec interface - if 'deleted' in macsec: - call('systemctl stop wpa_supplicant-macsec@{source_interface}' - .format(**macsec)) + systemd_service = 'wpa_supplicant-macsec@{source_interface}'.format(**macsec) + + # Remove macsec interface on deletion or mandatory parameter change + if 'deleted' in macsec or 'shutdown_required' in macsec: + call(f'systemctl stop {systemd_service}') if macsec['ifname'] in interfaces(): tmp = MACsecIf(macsec['ifname']) tmp.remove() - # delete configuration on interface removal - if os.path.isfile(wpa_suppl_conf.format(**macsec)): - os.unlink(wpa_suppl_conf.format(**macsec)) + if 'deleted' in macsec: + # delete configuration on interface removal + if os.path.isfile(wpa_suppl_conf.format(**macsec)): + os.unlink(wpa_suppl_conf.format(**macsec)) - else: - # It is safe to "re-create" the interface always, there is a sanity - # check that the interface will only be create if its non existent - i = MACsecIf(**macsec) - i.update(macsec) + return None + + # It is safe to "re-create" the interface always, there is a sanity + # check that the interface will only be create if its non existent + i = MACsecIf(**macsec) + i.update(macsec) - call('systemctl restart wpa_supplicant-macsec@{source_interface}' - .format(**macsec)) + if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec: + call(f'systemctl reload-or-restart {systemd_service}') return None diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 4750ca3e8..ef745d737 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -36,9 +36,12 @@ from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mirror_redirect +from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import VTunIf from vyos.pki import load_dh_parameters from vyos.pki import load_private_key +from vyos.pki import sort_ca_chain +from vyos.pki import verify_ca_chain from vyos.pki import wrap_certificate from vyos.pki import wrap_crl from vyos.pki import wrap_dh_parameters @@ -148,8 +151,14 @@ def verify_pki(openvpn): if 'ca_certificate' not in tls: raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface}') - if tls['ca_certificate'] not in pki['ca']: - raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') + for ca_name in tls['ca_certificate']: + if ca_name not in pki['ca']: + raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') + + if len(tls['ca_certificate']) > 1: + sorted_chain = sort_ca_chain(tls['ca_certificate'], pki['ca']) + if not verify_ca_chain(sorted_chain, pki['ca']): + raise ConfigError(f'CA certificates are not a valid chain') if mode != 'client' and 'auth_key' not in tls: if 'certificate' not in tls: @@ -495,6 +504,7 @@ def verify(openvpn): raise ConfigError('Username for authentication is missing') verify_vrf(openvpn) + verify_bond_bridge_member(openvpn) verify_mirror_redirect(openvpn) return None @@ -516,21 +526,28 @@ def generate_pki_files(openvpn): if tls: if 'ca_certificate' in tls: - cert_name = tls['ca_certificate'] - pki_ca = pki['ca'][cert_name] + cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem') + crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem') - if 'certificate' in pki_ca: - cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem') - write_file(cert_path, wrap_certificate(pki_ca['certificate']), - user=user, group=group, mode=0o600) + if os.path.exists(cert_path): + os.unlink(cert_path) + + if os.path.exists(crl_path): + os.unlink(crl_path) + + for cert_name in sort_ca_chain(tls['ca_certificate'], pki['ca']): + pki_ca = pki['ca'][cert_name] + + if 'certificate' in pki_ca: + write_file(cert_path, wrap_certificate(pki_ca['certificate']) + "\n", + user=user, group=group, mode=0o600, append=True) - if 'crl' in pki_ca: - for crl in pki_ca['crl']: - crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem') - write_file(crl_path, wrap_crl(crl), user=user, group=group, - mode=0o600) + if 'crl' in pki_ca: + for crl in pki_ca['crl']: + write_file(crl_path, wrap_crl(crl) + "\n", user=user, group=group, + mode=0o600, append=True) - openvpn['tls']['crl'] = True + openvpn['tls']['crl'] = True if 'certificate' in tls: cert_name = tls['certificate'] diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 1cd3fe276..f26a50a0e 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -26,6 +26,7 @@ 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_mirror_redirect +from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import MACVLANIf from vyos import ConfigError diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index eff7f373c..acef1fda7 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -29,6 +29,7 @@ from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_vrf from vyos.configverify import verify_tunnel +from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import Interface from vyos.ifconfig import Section from vyos.ifconfig import TunnelIf @@ -158,6 +159,7 @@ def verify(tunnel): verify_mtu_ipv6(tunnel) verify_address(tunnel) verify_vrf(tunnel) + verify_bond_bridge_member(tunnel) verify_mirror_redirect(tunnel) if 'source_interface' in tunnel: diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index f44d754ba..bf0f6840d 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -29,6 +29,7 @@ from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_source_interface +from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import Interface from vyos.ifconfig import VXLANIf from vyos.template import is_ipv6 @@ -144,6 +145,7 @@ def verify(vxlan): verify_mtu_ipv6(vxlan) verify_address(vxlan) + verify_bond_bridge_member(vxlan) verify_mirror_redirect(vxlan) return None diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 180ffa507..61bab2feb 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -29,6 +29,7 @@ from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_mirror_redirect +from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import WireGuardIf from vyos.util import check_kmod from vyos.util import check_port_availability @@ -71,6 +72,7 @@ def verify(wireguard): verify_mtu_ipv6(wireguard) verify_address(wireguard) verify_vrf(wireguard) + verify_bond_bridge_member(wireguard) verify_mirror_redirect(wireguard) if 'private_key' not in wireguard: diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index d34297063..dd798b5a2 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -30,6 +30,7 @@ from vyos.configverify import verify_source_interface from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf +from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import WiFiIf from vyos.template import render from vyos.util import call @@ -194,6 +195,7 @@ def verify(wifi): verify_address(wifi) verify_vrf(wifi) + verify_bond_bridge_member(wifi) verify_mirror_redirect(wifi) # use common function to verify VLAN configuration diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 0d6ec9ace..5490a794d 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2021 VyOS maintainers and contributors +# Copyright (C) 2018-2022 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 @@ -18,9 +18,11 @@ import os from vyos.config import Config from vyos.configverify import verify_vrf -from vyos import ConfigError +from vyos.configverify import verify_interface_exists from vyos.util import call +from vyos.util import get_interface_config from vyos.template import render +from vyos import ConfigError from vyos import airbag airbag.enable() @@ -49,6 +51,20 @@ def verify(ntp): raise ConfigError('NTP server not configured') verify_vrf(ntp) + + if 'interface' in ntp: + # If ntpd should listen on a given interface, ensure it exists + for interface in ntp['interface']: + verify_interface_exists(interface) + + # If we run in a VRF, our interface must belong to this VRF, too + if 'vrf' in ntp: + tmp = get_interface_config(interface) + vrf_name = ntp['vrf'] + if 'master' not in tmp or tmp['master'] != vrf_name: + raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{interface}" '\ + f'does not belong to this VRF!') + return None def generate(ntp): diff --git a/src/conf_mode/policy-route-interface.py b/src/conf_mode/policy-route-interface.py index 1108aebe6..58c5fd93d 100755 --- a/src/conf_mode/policy-route-interface.py +++ b/src/conf_mode/policy-route-interface.py @@ -24,6 +24,7 @@ from vyos.config import Config from vyos.ifconfig import Section from vyos.template import render from vyos.util import cmd +from vyos.util import run from vyos import ConfigError from vyos import airbag airbag.enable() @@ -47,6 +48,11 @@ def get_config(config=None): return if_policy +def verify_chain(table, chain): + # Verify policy route applied + code = run(f'nft list chain {table} {chain}') + return code == 0 + def verify(if_policy): # bail out early - looks like removal from running config if not if_policy: @@ -62,6 +68,12 @@ def verify(if_policy): if route_name not in if_policy['policy'][route]: raise ConfigError(f'Invalid policy route name "{name}"') + nft_prefix = 'VYOS_PBR6_' if route == 'route6' else 'VYOS_PBR_' + nft_table = 'ip6 mangle' if route == 'route6' else 'ip mangle' + + if not verify_chain(nft_table, nft_prefix + route_name): + raise ConfigError('Policy route did not apply') + return None def generate(if_policy): diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py index 5de341beb..9fddbd2c6 100755 --- a/src/conf_mode/policy-route.py +++ b/src/conf_mode/policy-route.py @@ -25,6 +25,7 @@ from vyos.config import Config from vyos.template import render from vyos.util import cmd from vyos.util import dict_search_args +from vyos.util import dict_search_recursive from vyos.util import run from vyos import ConfigError from vyos import airbag @@ -33,6 +34,9 @@ airbag.enable() mark_offset = 0x7FFFFFFF nftables_conf = '/run/nftables_policy.conf' +ROUTE_PREFIX = 'VYOS_PBR_' +ROUTE6_PREFIX = 'VYOS_PBR6_' + preserve_chains = [ 'VYOS_PBR_PREROUTING', 'VYOS_PBR_POSTROUTING', @@ -46,6 +50,16 @@ valid_groups = [ 'port_group' ] +group_set_prefix = { + 'A_': 'address_group', + 'A6_': 'ipv6_address_group', +# 'D_': 'domain_group', + 'M_': 'mac_group', + 'N_': 'network_group', + 'N6_': 'ipv6_network_group', + 'P_': 'port_group' +} + def get_policy_interfaces(conf): out = {} interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True, @@ -166,37 +180,55 @@ def verify(policy): return None -def cleanup_rule(table, jump_chain): - commands = [] - results = cmd(f'nft -a list table {table}').split("\n") - for line in results: - if f'jump {jump_chain}' in line: - handle_search = re.search('handle (\d+)', line) - if handle_search: - commands.append(f'delete rule {table} {chain} handle {handle_search[1]}') - return commands - def cleanup_commands(policy): commands = [] + commands_chains = [] + commands_sets = [] for table in ['ip mangle', 'ip6 mangle']: - json_str = cmd(f'nft -j list table {table}') + route_node = 'route' if table == 'ip mangle' else 'route6' + chain_prefix = ROUTE_PREFIX if table == 'ip mangle' else ROUTE6_PREFIX + + json_str = cmd(f'nft -t -j list table {table}') obj = loads(json_str) if 'nftables' not in obj: continue for item in obj['nftables']: if 'chain' in item: chain = item['chain']['name'] - if not chain.startswith("VYOS_PBR"): + if chain in preserve_chains or not chain.startswith("VYOS_PBR"): continue + + if dict_search_args(policy, route_node, chain.replace(chain_prefix, "", 1)) != None: + commands.append(f'flush chain {table} {chain}') + else: + commands_chains.append(f'delete chain {table} {chain}') + + if 'rule' in item: + rule = item['rule'] + chain = rule['chain'] + handle = rule['handle'] + if chain not in preserve_chains: - if table == 'ip mangle' and dict_search_args(policy, 'route', chain.replace("VYOS_PBR_", "", 1)): - commands.append(f'flush chain {table} {chain}') - elif table == 'ip6 mangle' and dict_search_args(policy, 'route6', chain.replace("VYOS_PBR6_", "", 1)): - commands.append(f'flush chain {table} {chain}') - else: - commands += cleanup_rule(table, chain) - commands.append(f'delete chain {table} {chain}') - return commands + continue + + target, _ = next(dict_search_recursive(rule['expr'], 'target')) + + if target.startswith(chain_prefix): + if dict_search_args(policy, route_node, target.replace(chain_prefix, "", 1)) == None: + commands.append(f'delete rule {table} {chain} handle {handle}') + + if 'set' in item: + set_name = item['set']['name'] + + for prefix, group_type in group_set_prefix.items(): + if set_name.startswith(prefix): + group_name = set_name.replace(prefix, "", 1) + if dict_search_args(policy, 'firewall_group', group_type, group_name) != None: + commands_sets.append(f'flush set {table} {set_name}') + else: + commands_sets.append(f'delete set {table} {set_name}') + + return commands + commands_chains + commands_sets def generate(policy): if not os.path.exists(nftables_conf): diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index cd46cbcb4..5aa643476 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -19,6 +19,7 @@ import os from sys import exit from sys import argv +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.configverify import verify_prefix_list @@ -100,6 +101,17 @@ def verify_remote_as(peer_config, bgp_config): return None +def verify_afi(peer_config, bgp_config): + if 'address_family' in peer_config: + return True + + if 'peer_group' in peer_config: + peer_group_name = peer_config['peer_group'] + tmp = dict_search(f'peer_group.{peer_group_name}.address_family', bgp_config) + if tmp: return True + + return False + def verify(bgp): if not bgp or 'deleted' in bgp: if 'dependent_vrfs' in bgp: @@ -164,6 +176,9 @@ def verify(bgp): if not verify_remote_as(peer_config, bgp): raise ConfigError(f'Neighbor "{peer}" remote-as must be set!') + if not verify_afi(peer_config, bgp): + Warning(f'BGP neighbor "{peer}" requires address-family!') + # Peer-group member cannot override remote-as of peer-group if 'peer_group' in peer_config: peer_group = peer_config['peer_group'] diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py index 56939955d..b247ce2ab 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-2022 VyOS maintainers and contributors +# Copyright (C) 2021 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 @@ -81,11 +81,6 @@ def verify(nhrp): 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}') - - if 'cisco_authentication' in nhrp_conf: - if len(nhrp_conf['cisco_authentication']) > 8: - raise ConfigError('Maximum length of the secret is 8 characters!') - return None def generate(nhrp): diff --git a/src/conf_mode/service_event_handler.py b/src/conf_mode/service_event_handler.py new file mode 100755 index 000000000..5440d1056 --- /dev/null +++ b/src/conf_mode/service_event_handler.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 json +from pathlib import Path + +from vyos.config import Config +from vyos.util import call, dict_search +from vyos import ConfigError +from vyos import airbag + +airbag.enable() + +service_name = 'vyos-event-handler' +service_conf = Path(f'/run/{service_name}.conf') + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['service', 'event-handler', 'event'] + config = conf.get_config_dict(base, + get_first_key=True, + no_tag_node_value_mangle=True) + + return config + + +def verify(config): + # bail out early - looks like removal from running config + if not config: + return None + + for name, event_config in config.items(): + if not dict_search('filter.pattern', event_config) or not dict_search( + 'script.path', event_config): + raise ConfigError( + 'Event-handler: both pattern and script path items are mandatory' + ) + + if dict_search('script.environment.message', event_config): + raise ConfigError( + 'Event-handler: "message" environment variable is reserved for log message text' + ) + + +def generate(config): + if not config: + # Remove old config and return + service_conf.unlink(missing_ok=True) + return None + + # Write configuration file + conf_json = json.dumps(config, indent=4) + service_conf.write_text(conf_json) + + return None + + +def apply(config): + if config: + call(f'systemctl restart {service_name}.service') + else: + call(f'systemctl stop {service_name}.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_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 559d1bcd5..61f484129 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -53,6 +53,8 @@ default_config_data = { 'radius_nas_ip': '', 'radius_source_address': '', 'radius_shaper_attr': '', + 'radius_shaper_enable': False, + 'radius_shaper_multiplier': '', 'radius_shaper_vendor': '', 'radius_dynamic_author': '', 'thread_cnt': get_half_cpus() @@ -196,6 +198,18 @@ def get_config(config=None): if conf.exists(['nas-ip-address']): ipoe['radius_nas_ip'] = conf.return_value(['nas-ip-address']) + if conf.exists(['rate-limit', 'attribute']): + ipoe['radius_shaper_attr'] = conf.return_value(['rate-limit', 'attribute']) + + if conf.exists(['rate-limit', 'enable']): + ipoe['radius_shaper_enable'] = True + + if conf.exists(['rate-limit', 'multiplier']): + ipoe['radius_shaper_multiplier'] = conf.return_value(['rate-limit', 'multiplier']) + + if conf.exists(['rate-limit', 'vendor']): + ipoe['radius_shaper_vendor'] = conf.return_value(['rate-limit', 'vendor']) + if conf.exists(['source-address']): ipoe['radius_source_address'] = conf.return_value(['source-address']) diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index daf75d740..62f5e1ddf 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -99,10 +99,6 @@ def get_config(config=None): monitoring['interfaces_ethernet'] = get_interfaces('ethernet', vlan=False) monitoring['nft_chains'] = get_nft_filter_chains() - if 'authentication' in monitoring or \ - 'url' in monitoring: - monitoring['influxdb_configured'] = True - # Redefine azure group-metrics 'single-table' and 'table-per-metric' if 'azure_data_explorer' in monitoring: if 'single-table' in monitoring['azure_data_explorer']['group_metrics']: @@ -119,6 +115,9 @@ def get_config(config=None): # Ignore default XML values if config doesn't exists # Delete key from dict + if not conf.exists(base + ['influxdb']): + del monitoring['influxdb'] + if not conf.exists(base + ['prometheus-client']): del monitoring['prometheus_client'] @@ -132,14 +131,15 @@ def verify(monitoring): if not monitoring: return None - if 'influxdb_configured' in monitoring: - if 'authentication' not in monitoring or \ - 'organization' not in monitoring['authentication'] or \ - 'token' not in monitoring['authentication']: - raise ConfigError(f'Authentication "organization and token" are mandatory!') + # Verify influxdb + if 'influxdb' in monitoring: + if 'authentication' not in monitoring['influxdb'] or \ + 'organization' not in monitoring['influxdb']['authentication'] or \ + 'token' not in monitoring['influxdb']['authentication']: + raise ConfigError(f'influxdb authentication "organization and token" are mandatory!') - if 'url' not in monitoring: - raise ConfigError(f'Monitoring "url" is mandatory!') + if 'url' not in monitoring['influxdb']: + raise ConfigError(f'Monitoring influxdb "url" is mandatory!') # Verify azure-data-explorer if 'azure_data_explorer' in monitoring: diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py index 71b758399..ff7caaa84 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-2021 VyOS maintainers and contributors +# Copyright (C) 2018-2022 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 @@ -17,7 +17,7 @@ import os from sys import exit - +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render @@ -79,22 +79,35 @@ def verify(rtradv): if 'interface' not in rtradv: return None - for interface in rtradv['interface']: - interface = rtradv['interface'][interface] + for interface, interface_config in rtradv['interface'].items(): if 'prefix' in interface: - for prefix in interface['prefix']: - prefix = interface['prefix'][prefix] - valid_lifetime = prefix['valid_lifetime'] + for prefix, prefix_config in interface_config['prefix'].items(): + valid_lifetime = prefix_config['valid_lifetime'] if valid_lifetime == 'infinity': valid_lifetime = 4294967295 - preferred_lifetime = prefix['preferred_lifetime'] + preferred_lifetime = prefix_config['preferred_lifetime'] if preferred_lifetime == 'infinity': preferred_lifetime = 4294967295 if not (int(valid_lifetime) > int(preferred_lifetime)): raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime') + if 'name_server_lifetime' in interface_config: + # man page states: + # The maximum duration how long the RDNSS entries are used for name + # resolution. A value of 0 means the nameserver must no longer be + # used. The value, if not 0, must be at least MaxRtrAdvInterval. To + # ensure stale RDNSS info gets removed in a timely fashion, this + # should not be greater than 2*MaxRtrAdvInterval. + lifetime = int(interface_config['name_server_lifetime']) + interval_max = int(interface_config['interval']['max']) + if lifetime > 0: + if lifetime < int(interval_max): + raise ConfigError(f'RDNSS lifetime must be at least "{interval_max}" seconds!') + if lifetime > 2* interval_max: + Warning(f'RDNSS lifetime should not exceed "{2 * interval_max}" which is two times "interval max"!') + return None def generate(rtradv): @@ -105,15 +118,16 @@ def generate(rtradv): return None def apply(rtradv): + systemd_service = 'radvd.service' if not rtradv: # bail out early - looks like removal from running config - call('systemctl stop radvd.service') + call(f'systemctl stop {systemd_service}') if os.path.exists(config_file): os.unlink(config_file) return None - call('systemctl restart radvd.service') + call(f'systemctl reload-or-restart {systemd_service}') return None diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py index 05fc3a97a..0c5063ed3 100755 --- a/src/conf_mode/system-ip.py +++ b/src/conf_mode/system-ip.py @@ -64,6 +64,11 @@ def apply(opt): value = '0' if (tmp != None) else '1' write_file('/proc/sys/net/ipv4/conf/all/forwarding', value) + # enable/disable IPv4 directed broadcast forwarding + tmp = dict_search('disable_directed_broadcast', opt) + value = '0' if (tmp != None) else '1' + write_file('/proc/sys/net/ipv4/conf/all/bc_forwarding', value) + # configure multipath tmp = dict_search('multipath.ignore_unreachable_nexthops', opt) value = '1' if (tmp != None) else '0' diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index c717286ae..3dcbc995c 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -231,7 +231,7 @@ def apply(login): if tmp: command += f" --home '{tmp}'" else: command += f" --home '/home/{user}'" - command += f' --groups frrvty,vyattacfg,sudo,adm,dip,disk {user}' + command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk {user}' try: cmd(command) diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index a9d3bbe31..20132456c 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -52,8 +52,6 @@ def get_config(config=None): { 'global': { 'log-file': '/var/log/messages', - 'max-size': 262144, - 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog', 'selectors': '*.notice;local7.debug', 'max-files': '5', 'preserver_fqdn': False diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index db53463cf..23e5162ba 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-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2022 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 @@ -20,6 +20,7 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict +from vyos.configdict import dict_merge from vyos.configverify import verify_accel_ppp_base_service from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key @@ -50,10 +51,10 @@ def get_config(config=None): # retrieve common dictionary keys sstp = get_accel_dict(conf, base, sstp_chap_secrets) - if sstp: sstp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) + get_first_key=True, + no_tag_node_value_mangle=True) return sstp @@ -121,7 +122,6 @@ def generate(sstp): ca_cert_name = sstp['ssl']['ca_certificate'] pki_ca = sstp['pki']['ca'][ca_cert_name] - write_file(cert_file_path, wrap_certificate(pki_cert['certificate'])) write_file(cert_key_path, wrap_private_key(pki_cert['private']['key'])) write_file(ca_cert_file_path, wrap_certificate(pki_ca['certificate'])) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 972d0289b..1b4156895 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -113,8 +113,14 @@ 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"] table_ids = [] for name, config in vrf['name'].items(): + # Reserved VRF names + if name in reserved_names: + raise ConfigError(f'VRF name "{name}" is reserved and connot be used!') + # table id is mandatory if 'table' not in config: raise ConfigError(f'VRF "{name}" table id is mandatory!') diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py index 070a4deea..a52c52706 100755 --- a/src/conf_mode/zone_policy.py +++ b/src/conf_mode/zone_policy.py @@ -155,7 +155,7 @@ def get_local_from(zone_policy, local_zone_name): def cleanup_commands(): commands = [] for table in ['ip filter', 'ip6 filter']: - json_str = cmd(f'nft -j list table {table}') + json_str = cmd(f'nft -t -j list table {table}') obj = loads(json_str) if 'nftables' not in obj: continue diff --git a/src/etc/cron.d/vyos-geoip b/src/etc/cron.d/vyos-geoip new file mode 100644 index 000000000..9bb38a850 --- /dev/null +++ b/src/etc/cron.d/vyos-geoip @@ -0,0 +1 @@ +30 4 * * 1 root sg vyattacfg "/usr/libexec/vyos/geoip-update.py --force" >/tmp/geoip-update.log 2>&1 diff --git a/src/etc/cron.hourly/vyos-logrotate-hourly b/src/etc/cron.hourly/vyos-logrotate-hourly deleted file mode 100755 index f4f56a9c2..000000000 --- a/src/etc/cron.hourly/vyos-logrotate-hourly +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -test -x /usr/sbin/logrotate || exit 0 -/usr/sbin/logrotate /etc/logrotate.conf diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf index e03d3a29c..4feb7e09a 100644 --- a/src/etc/sysctl.d/30-vyos-router.conf +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -27,6 +27,12 @@ net.ipv4.conf.all.arp_announce=2 # Enable packet forwarding for IPv4 net.ipv4.ip_forward=1 +# Enable directed broadcast forwarding feature described in rfc1812#section-5.3.5.2 and rfc2644. +# Note that setting the 'all' entry to 1 doesn't enable directed broadcast forwarding on all interfaces. +# To enable directed broadcast forwarding on an interface, both the 'all' entry and the input interface entry should be set to 1. +net.ipv4.conf.all.bc_forwarding=1 +net.ipv4.conf.default.bc_forwarding=0 + # if a primary address is removed from an interface promote the # secondary address if available net.ipv4.conf.all.promote_secondaries=1 diff --git a/src/etc/systemd/system/frr.service.d/override.conf b/src/etc/systemd/system/frr.service.d/override.conf new file mode 100644 index 000000000..69eb1a86a --- /dev/null +++ b/src/etc/systemd/system/frr.service.d/override.conf @@ -0,0 +1,11 @@ +[Unit] +Before= +Before=vyos-router.service + +[Service] +ExecStartPre=/bin/bash -c 'mkdir -p /run/frr/config; \ + echo "log syslog" > /run/frr/config/frr.conf; \ + echo "log facility local7" >> /run/frr/config/frr.conf; \ + chown frr:frr /run/frr/config/frr.conf; \ + chmod 664 /run/frr/config/frr.conf; \ + mount --bind /run/frr/config/frr.conf /etc/frr/frr.conf' diff --git a/src/etc/systemd/system/logrotate.timer.d/10-override.conf b/src/etc/systemd/system/logrotate.timer.d/10-override.conf new file mode 100644 index 000000000..f50c2b082 --- /dev/null +++ b/src/etc/systemd/system/logrotate.timer.d/10-override.conf @@ -0,0 +1,2 @@ +[Timer] +OnCalendar=hourly diff --git a/src/helpers/geoip-update.py b/src/helpers/geoip-update.py new file mode 100755 index 000000000..34accf2cc --- /dev/null +++ b/src/helpers/geoip-update.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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 argparse +import sys + +from vyos.configquery import ConfigTreeQuery +from vyos.firewall import geoip_update + +def get_config(config=None): + if config: + conf = config + else: + conf = ConfigTreeQuery() + base = ['firewall'] + + if not conf.exists(base): + return None + + return conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("--force", help="Force update", action="store_true") + args = parser.parse_args() + + firewall = get_config() + + if not geoip_update(firewall, force=args.force): + sys.exit(1) diff --git a/src/helpers/vyos-domain-group-resolve.py b/src/helpers/vyos-domain-group-resolve.py new file mode 100755 index 000000000..6b677670b --- /dev/null +++ b/src/helpers/vyos-domain-group-resolve.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 time + +from vyos.configquery import ConfigTreeQuery +from vyos.firewall import get_ips_domains_dict +from vyos.firewall import nft_add_set_elements +from vyos.firewall import nft_flush_set +from vyos.firewall import nft_init_set +from vyos.firewall import nft_update_set_elements +from vyos.util import call + + +base = ['firewall', 'group', 'domain-group'] +check_required = True +# count_failed = 0 +# Timeout in sec between checks +timeout = 300 + +domain_state = {} + +if __name__ == '__main__': + + while check_required: + config = ConfigTreeQuery() + if config.exists(base): + domain_groups = config.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + for set_name, domain_config in domain_groups.items(): + list_domains = domain_config['address'] + elements = [] + ip_dict = get_ips_domains_dict(list_domains) + + for domain in list_domains: + # Resolution succeeded, update domain state + if domain in ip_dict: + domain_state[domain] = ip_dict[domain] + elements += ip_dict[domain] + # Resolution failed, use previous domain state + elif domain in domain_state: + elements += domain_state[domain] + + # Resolve successful + if elements: + nft_update_set_elements(f'D_{set_name}', elements) + time.sleep(timeout) diff --git a/src/migration-scripts/firewall/6-to-7 b/src/migration-scripts/firewall/6-to-7 index 5f4cff90d..626d6849f 100755 --- a/src/migration-scripts/firewall/6-to-7 +++ b/src/migration-scripts/firewall/6-to-7 @@ -194,11 +194,12 @@ if config.exists(base + ['ipv6-name']): if config.exists(rule_icmp + ['type']): tmp = config.return_value(rule_icmp + ['type']) - type_code_match = re.match(r'^(\d+)/(\d+)$', tmp) + type_code_match = re.match(r'^(\d+)(?:/(\d+))?$', tmp) if type_code_match: config.set(rule_icmp + ['type'], value=type_code_match[1]) - config.set(rule_icmp + ['code'], value=type_code_match[2]) + if type_code_match[2]: + config.set(rule_icmp + ['code'], value=type_code_match[2]) elif tmp in icmpv6_remove: config.delete(rule_icmp + ['type']) elif tmp in icmpv6_translations: diff --git a/src/migration-scripts/interfaces/24-to-25 b/src/migration-scripts/interfaces/24-to-25 index 93ce9215f..4095f2a3e 100755 --- a/src/migration-scripts/interfaces/24-to-25 +++ b/src/migration-scripts/interfaces/24-to-25 @@ -20,6 +20,7 @@ import os import sys from vyos.configtree import ConfigTree +from vyos.pki import CERT_BEGIN from vyos.pki import load_certificate from vyos.pki import load_crl from vyos.pki import load_dh_parameters @@ -27,6 +28,7 @@ from vyos.pki import load_private_key from vyos.pki import encode_certificate from vyos.pki import encode_dh_parameters from vyos.pki import encode_private_key +from vyos.pki import verify_crl from vyos.util import run def wrapped_pem_to_config_value(pem): @@ -129,6 +131,8 @@ if config.exists(base): config.delete(base + [interface, 'tls', 'crypt-file']) + ca_certs = {} + if config.exists(x509_base + ['ca-cert-file']): if not config.exists(pki_base + ['ca']): config.set(pki_base + ['ca']) @@ -136,20 +140,27 @@ if config.exists(base): cert_file = config.return_value(x509_base + ['ca-cert-file']) cert_path = os.path.join(AUTH_DIR, cert_file) - cert = None if os.path.isfile(cert_path): if not os.access(cert_path, os.R_OK): run(f'sudo chmod 644 {cert_path}') with open(cert_path, 'r') as f: - cert_data = f.read() - cert = load_certificate(cert_data, wrap_tags=False) - - if cert: - cert_pem = encode_certificate(cert) - config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) - config.set(x509_base + ['ca-certificate'], value=pki_name) + certs_str = f.read() + certs_data = certs_str.split(CERT_BEGIN) + index = 1 + for cert_data in certs_data[1:]: + cert = load_certificate(CERT_BEGIN + cert_data, wrap_tags=False) + + if cert: + ca_certs[f'{pki_name}_{index}'] = cert + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', f'{pki_name}_{index}', 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=f'{pki_name}_{index}', replace=False) + else: + print(f'Failed to migrate CA certificate on openvpn interface {interface}') + + index += 1 else: print(f'Failed to migrate CA certificate on openvpn interface {interface}') @@ -163,6 +174,7 @@ if config.exists(base): crl_file = config.return_value(x509_base + ['crl-file']) crl_path = os.path.join(AUTH_DIR, crl_file) crl = None + crl_ca_name = None if os.path.isfile(crl_path): if not os.access(crl_path, os.R_OK): @@ -172,9 +184,14 @@ if config.exists(base): crl_data = f.read() crl = load_crl(crl_data, wrap_tags=False) - if crl: + for ca_name, ca_cert in ca_certs.items(): + if verify_crl(crl, ca_cert): + crl_ca_name = ca_name + break + + if crl and crl_ca_name: crl_pem = encode_certificate(crl) - config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) + config.set(pki_base + ['ca', crl_ca_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) else: print(f'Failed to migrate CRL on openvpn interface {interface}') diff --git a/src/migration-scripts/monitoring/0-to-1 b/src/migration-scripts/monitoring/0-to-1 new file mode 100755 index 000000000..803cdb49c --- /dev/null +++ b/src/migration-scripts/monitoring/0-to-1 @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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/>. + +# T3417: migrate IS-IS tagNode to node as we can only have one IS-IS process + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['service', 'monitoring', 'telegraf'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +if config.exists(base + ['authentication', 'organization']): + tmp = config.return_value(base + ['authentication', 'organization']) + config.delete(base + ['authentication', 'organization']) + config.set(base + ['influxdb', 'authentication', 'organization'], value=tmp) + +if config.exists(base + ['authentication', 'token']): + tmp = config.return_value(base + ['authentication', 'token']) + config.delete(base + ['authentication', 'token']) + config.set(base + ['influxdb', 'authentication', 'token'], value=tmp) + +if config.exists(base + ['bucket']): + tmp = config.return_value(base + ['bucket']) + config.delete(base + ['bucket']) + config.set(base + ['influxdb', 'bucket'], value=tmp) + +if config.exists(base + ['port']): + tmp = config.return_value(base + ['port']) + config.delete(base + ['port']) + config.set(base + ['influxdb', 'port'], value=tmp) + +if config.exists(base + ['url']): + tmp = config.return_value(base + ['url']) + config.delete(base + ['url']) + config.set(base + ['influxdb', 'url'], value=tmp) + + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/system/23-to-24 b/src/migration-scripts/system/23-to-24 index 5ea71d51a..97fe82462 100755 --- a/src/migration-scripts/system/23-to-24 +++ b/src/migration-scripts/system/23-to-24 @@ -20,6 +20,7 @@ from ipaddress import ip_interface from ipaddress import ip_address from sys import exit, argv from vyos.configtree import ConfigTree +from vyos.template import is_ipv4 if (len(argv) < 1): print("Must specify file name!") @@ -37,6 +38,9 @@ def fixup_cli(config, path, interface): if config.exists(path + ['address']): for address in config.return_values(path + ['address']): tmp = ip_interface(address) + # ARP is only available for IPv4 ;-) + if not is_ipv4(tmp): + continue if ip_address(host) in tmp.network.hosts(): mac = config.return_value(tmp_base + [host, 'hwaddr']) iface_path = ['protocols', 'static', 'arp', 'interface'] diff --git a/src/migration-scripts/system/24-to-25 b/src/migration-scripts/system/24-to-25 new file mode 100755 index 000000000..c2f70689d --- /dev/null +++ b/src/migration-scripts/system/24-to-25 @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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/>. +# +# Migrate system syslog global archive to system logs logrotate messages + +from sys import exit, argv +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['system', 'syslog', 'global', 'archive'] +config = ConfigTree(config_file) + +if not config.exists(base): + exit(0) + +if config.exists(base + ['file']): + tmp = config.return_value(base + ['file']) + config.set(['system', 'logs', 'logrotate', 'messages', 'rotate'], value=tmp) + +if config.exists(base + ['size']): + tmp = config.return_value(base + ['size']) + tmp = max(round(int(tmp) / 1024), 1) # kb -> mb + config.set(['system', 'logs', 'logrotate', 'messages', 'max-size'], value=tmp) + +config.delete(base) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/op_mode/clear_dhcp_lease.py b/src/op_mode/clear_dhcp_lease.py new file mode 100755 index 000000000..250dbcce1 --- /dev/null +++ b/src/op_mode/clear_dhcp_lease.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import argparse +import re + +from isc_dhcp_leases import Lease +from isc_dhcp_leases import IscDhcpLeases + +from vyos.configquery import ConfigTreeQuery +from vyos.util import ask_yes_no +from vyos.util import call +from vyos.util import commit_in_progress + + +config = ConfigTreeQuery() +base = ['service', 'dhcp-server'] +lease_file = '/config/dhcpd.leases' + + +def del_lease_ip(address): + """ + Read lease_file and write data to this file + without specific section "lease ip" + Delete section "lease x.x.x.x { x;x;x; }" + """ + with open(lease_file, encoding='utf-8') as f: + data = f.read().rstrip() + lease_config_ip = '{(?P<config>[\s\S]+?)\n}' + pattern = rf"lease {address} {lease_config_ip}" + # Delete lease for ip block + data = re.sub(pattern, '', data) + + # Write new data to original lease_file + with open(lease_file, 'w', encoding='utf-8') as f: + f.write(data) + +def is_ip_in_leases(address): + """ + Return True if address found in the lease file + """ + leases = IscDhcpLeases(lease_file) + lease_ips = [] + for lease in leases.get(): + lease_ips.append(lease.ip) + if address not in lease_ips: + print(f'Address "{address}" not found in "{lease_file}"') + return False + return True + + +if not config.exists(base): + print('DHCP-server not configured!') + exit(0) + +if config.exists(base + ['failover']): + print('Lease cannot be reset in failover mode!') + exit(0) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--ip', help='IPv4 address', action='store', required=True) + + args = parser.parse_args() + address = args.ip + + if not is_ip_in_leases(address): + exit(1) + + if commit_in_progress(): + print('Cannot clear DHCP lease while a commit is in progress') + exit(1) + + if not ask_yes_no(f'This will restart DHCP server.\nContinue?'): + exit(1) + else: + del_lease_ip(address) + call('systemctl restart isc-dhcp-server.service') diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index ffc574362..936c20bcb 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -20,6 +20,7 @@ import argparse from psutil import process_iter from vyos.util import call +from vyos.util import commit_in_progress from vyos.util import DEVNULL from vyos.util import is_wwan_connected @@ -87,6 +88,9 @@ def main(): args = parser.parse_args() if args.connect: + if commit_in_progress(): + print('Cannot connect while a commit is in progress') + exit(1) connect(args.connect) elif args.disconnect: disconnect(args.disconnect) diff --git a/src/op_mode/conntrack_sync.py b/src/op_mode/conntrack_sync.py index e45c38f07..54ecd6d0e 100755 --- a/src/op_mode/conntrack_sync.py +++ b/src/op_mode/conntrack_sync.py @@ -22,6 +22,7 @@ from argparse import ArgumentParser from vyos.configquery import CliShellApiConfigQuery from vyos.configquery import ConfigTreeQuery from vyos.util import call +from vyos.util import commit_in_progress from vyos.util import cmd from vyos.util import run from vyos.template import render_to_string @@ -86,6 +87,9 @@ if __name__ == '__main__': if args.restart: is_configured() + if commit_in_progress(): + print('Cannot restart conntrackd while a commit is in progress') + exit(1) syslog.syslog('Restarting conntrack sync service...') cmd('systemctl restart conntrackd.service') diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index 3146fc357..0aea17b3a 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -270,7 +270,7 @@ def show_firewall_group(name=None): references = find_references(group_type, group_name) row = [group_name, group_type, '\n'.join(references) or 'N/A'] if 'address' in group_conf: - row.append("\n".join(sorted(group_conf['address'], key=ipaddress.ip_address))) + row.append("\n".join(sorted(group_conf['address']))) elif 'network' in group_conf: row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network))) elif 'mac_address' in group_conf: diff --git a/src/op_mode/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py index 6586cbceb..514143cd7 100755 --- a/src/op_mode/flow_accounting_op.py +++ b/src/op_mode/flow_accounting_op.py @@ -22,7 +22,9 @@ import ipaddress import os.path from tabulate import tabulate from json import loads -from vyos.util import cmd, run +from vyos.util import cmd +from vyos.util import commit_in_progress +from vyos.util import run from vyos.logger import syslog # some default values @@ -224,6 +226,9 @@ if not _uacctd_running(): # restart pmacct daemon if cmd_args.action == 'restart': + if commit_in_progress(): + print('Cannot restart flow-accounting while a commit is in progress') + exit(1) # run command to restart flow-accounting cmd('systemctl restart uacctd.service', message='Failed to restart flow-accounting') diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py index cbc9ef973..43e94048d 100755 --- a/src/op_mode/generate_ssh_server_key.py +++ b/src/op_mode/generate_ssh_server_key.py @@ -17,10 +17,15 @@ from sys import exit from vyos.util import ask_yes_no from vyos.util import cmd +from vyos.util import commit_in_progress if not ask_yes_no('Do you really want to remove the existing SSH host keys?'): exit(0) +if commit_in_progress(): + print('Cannot restart SSH while a commit is in progress') + exit(1) + cmd('rm -v /etc/ssh/ssh_host_*') cmd('dpkg-reconfigure openssh-server') cmd('systemctl restart ssh.service') diff --git a/src/op_mode/openconnect-control.py b/src/op_mode/openconnect-control.py index c3cd25186..a128cc011 100755 --- a/src/op_mode/openconnect-control.py +++ b/src/op_mode/openconnect-control.py @@ -19,7 +19,10 @@ import argparse import json from vyos.config import Config -from vyos.util import popen, run, DEVNULL +from vyos.util import commit_in_progress +from vyos.util import popen +from vyos.util import run +from vyos.util import DEVNULL from tabulate import tabulate occtl = '/usr/bin/occtl' @@ -57,6 +60,10 @@ def main(): # Check is Openconnect server configured is_ocserv_configured() + if commit_in_progress(): + print('Cannot restart openconnect while a commit is in progress') + exit(1) + if args.action == "restart": run("sudo systemctl restart ocserv.service") sys.exit(0) diff --git a/src/op_mode/reset_openvpn.py b/src/op_mode/reset_openvpn.py index dbd3eb4d1..efbf65083 100755 --- a/src/op_mode/reset_openvpn.py +++ b/src/op_mode/reset_openvpn.py @@ -17,6 +17,7 @@ import os from sys import argv, exit from vyos.util import call +from vyos.util import commit_in_progress if __name__ == '__main__': if (len(argv) < 1): @@ -25,6 +26,9 @@ if __name__ == '__main__': interface = argv[1] if os.path.isfile(f'/run/openvpn/{interface}.conf'): + if commit_in_progress(): + print('Cannot restart OpenVPN while a commit is in progress') + exit(1) call(f'systemctl restart openvpn@{interface}.service') else: print(f'OpenVPN interface "{interface}" does not exist!') diff --git a/src/op_mode/restart_dhcp_relay.py b/src/op_mode/restart_dhcp_relay.py index af4fb2d15..db5a48970 100755 --- a/src/op_mode/restart_dhcp_relay.py +++ b/src/op_mode/restart_dhcp_relay.py @@ -24,6 +24,7 @@ import os import vyos.config from vyos.util import call +from vyos.util import commit_in_progress parser = argparse.ArgumentParser() @@ -39,6 +40,9 @@ if __name__ == '__main__': if not c.exists_effective('service dhcp-relay'): print("DHCP relay service not configured") else: + if commit_in_progress(): + print('Cannot restart DHCP relay while a commit is in progress') + exit(1) call('systemctl restart isc-dhcp-server.service') sys.exit(0) @@ -47,6 +51,9 @@ if __name__ == '__main__': if not c.exists_effective('service dhcpv6-relay'): print("DHCPv6 relay service not configured") else: + if commit_in_progress(): + print('Cannot restart DHCPv6 relay while commit is in progress') + exit(1) call('systemctl restart isc-dhcp-server6.service') sys.exit(0) diff --git a/src/op_mode/show_conntrack.py b/src/op_mode/show_conntrack.py new file mode 100755 index 000000000..089a3e454 --- /dev/null +++ b/src/op_mode/show_conntrack.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 xmltodict + +from tabulate import tabulate +from vyos.util import cmd + + +def _get_raw_data(): + """ + Get conntrack XML output + """ + return cmd(f'sudo conntrack --dump --output xml') + + +def _xml_to_dict(xml): + """ + Convert XML to dictionary + Return: dictionary + """ + parse = xmltodict.parse(xml) + # If only one conntrack entry we must change dict + if 'meta' in parse['conntrack']['flow']: + return dict(conntrack={'flow': [parse['conntrack']['flow']]}) + return parse + + +def _get_formatted_output(xml): + """ + :param xml: + :return: formatted output + """ + data_entries = [] + dict_data = _xml_to_dict(xml) + for entry in dict_data['conntrack']['flow']: + orig_src, orig_dst, orig_sport, orig_dport = {}, {}, {}, {} + reply_src, reply_dst, reply_sport, reply_dport = {}, {}, {}, {} + proto = {} + for meta in entry['meta']: + direction = meta['@direction'] + if direction in ['original']: + if 'layer3' in meta: + orig_src = meta['layer3']['src'] + orig_dst = meta['layer3']['dst'] + if 'layer4' in meta: + if meta.get('layer4').get('sport'): + orig_sport = meta['layer4']['sport'] + if meta.get('layer4').get('dport'): + orig_dport = meta['layer4']['dport'] + proto = meta['layer4']['@protoname'] + if direction in ['reply']: + if 'layer3' in meta: + reply_src = meta['layer3']['src'] + reply_dst = meta['layer3']['dst'] + if 'layer4' in meta: + if meta.get('layer4').get('sport'): + reply_sport = meta['layer4']['sport'] + if meta.get('layer4').get('dport'): + reply_dport = meta['layer4']['dport'] + proto = meta['layer4']['@protoname'] + if direction == 'independent': + conn_id = meta['id'] + timeout = meta['timeout'] + orig_src = f'{orig_src}:{orig_sport}' if orig_sport else orig_src + orig_dst = f'{orig_dst}:{orig_dport}' if orig_dport else orig_dst + reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src + reply_dst = f'{reply_dst}:{reply_dport}' if reply_dport else reply_dst + state = meta['state'] if 'state' in meta else '' + mark = meta['mark'] + zone = meta['zone'] if 'zone' in meta else '' + data_entries.append( + [conn_id, orig_src, orig_dst, reply_src, reply_dst, proto, state, timeout, mark, zone]) + headers = ["Id", "Original src", "Original dst", "Reply src", "Reply dst", "Protocol", "State", "Timeout", "Mark", + "Zone"] + output = tabulate(data_entries, headers, numalign="left") + return output + + +def show(raw: bool): + conntrack_data = _get_raw_data() + if raw: + return conntrack_data + else: + return _get_formatted_output(conntrack_data) + + +if __name__ == '__main__': + print(show(raw=False)) diff --git a/src/op_mode/show_nat_rules.py b/src/op_mode/show_nat_rules.py index 98adb31dd..60a4bdd13 100755 --- a/src/op_mode/show_nat_rules.py +++ b/src/op_mode/show_nat_rules.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 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 @@ -99,8 +99,9 @@ if args.source or args.destination: if addr_tmp and len_tmp: tran_addr += addr_tmp + '/' + str(len_tmp) + ' ' - if isinstance(tran_addr_json['port'],int): - tran_addr += 'port ' + str(tran_addr_json['port']) + if tran_addr_json.get('port'): + if isinstance(tran_addr_json['port'],int): + tran_addr += 'port ' + str(tran_addr_json['port']) else: if 'masquerade' in data['expr'][i]: @@ -111,6 +112,8 @@ if args.source or args.destination: if srcdest != '': srcdests.append(srcdest) srcdest = '' + else: + srcdests.append('any') print(format_nat_rule.format(rule, srcdests[0], tran_addr, interface)) for i in range(1, len(srcdests)): diff --git a/src/op_mode/show_nat_translations.py b/src/op_mode/show_nat_translations.py index 25091e9fc..508845e23 100755 --- a/src/op_mode/show_nat_translations.py +++ b/src/op_mode/show_nat_translations.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2022 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 @@ -83,11 +83,23 @@ def pipe(): return xml +def xml_to_dict(xml): + """ + Convert XML to dictionary + Return: dictionary + """ + parse = xmltodict.parse(xml) + # If only one NAT entry we must change dict T4499 + if 'meta' in parse['conntrack']['flow']: + return dict(conntrack={'flow': [parse['conntrack']['flow']]}) + return parse + + def process(data, stats, protocol, pipe, verbose, flowtype=''): if not data: return - parsed = xmltodict.parse(data) + parsed = xml_to_dict(data) print(headers(verbose, pipe)) diff --git a/src/op_mode/vtysh_wrapper.sh b/src/op_mode/vtysh_wrapper.sh index 09980e14f..25d09ce77 100755 --- a/src/op_mode/vtysh_wrapper.sh +++ b/src/op_mode/vtysh_wrapper.sh @@ -1,5 +1,6 @@ #!/bin/sh declare -a tmp -# FRR uses ospf6 where we use ospfv3, thus alter the command -tmp=$(echo $@ | sed -e "s/ospfv3/ospf6/") +# FRR uses ospf6 where we use ospfv3, and we use reset over clear for BGP, +# thus alter the commands +tmp=$(echo $@ | sed -e "s/ospfv3/ospf6/" | sed -e "s/^reset bgp/clear bgp/" | sed -e "s/^reset ip bgp/clear ip bgp/") vtysh -c "$tmp" diff --git a/src/system/vyos-event-handler.py b/src/system/vyos-event-handler.py new file mode 100755 index 000000000..1c85380bc --- /dev/null +++ b/src/system/vyos-event-handler.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 argparse +import json +import re +import select +from copy import deepcopy +from os import getpid, environ +from pathlib import Path +from signal import signal, SIGTERM, SIGINT +from sys import exit +from systemd import journal + +from vyos.util import run, dict_search + +# Identify this script +my_pid = getpid() +my_name = Path(__file__).stem + + +# handle termination signal +def handle_signal(signal_type, frame): + if signal_type == SIGTERM: + journal.send('Received SIGTERM signal, stopping normally', + SYSLOG_IDENTIFIER=my_name) + if signal_type == SIGINT: + journal.send('Received SIGINT signal, stopping normally', + SYSLOG_IDENTIFIER=my_name) + exit(0) + + +# Class for analyzing and process messages +class Analyzer: + # Initialize settings + def __init__(self, config: dict) -> None: + self.config = {} + # Prepare compiled regex objects + for event_id, event_config in config.items(): + script = dict_search('script.path', event_config) + # Check for arguments + if dict_search('script.arguments', event_config): + script_arguments = dict_search('script.arguments', event_config) + script = f'{script} {script_arguments}' + # Prepare environment + environment = deepcopy(environ) + # Check for additional environment options + if dict_search('script.environment', event_config): + for env_variable, env_value in dict_search( + 'script.environment', event_config).items(): + environment[env_variable] = env_value.get('value') + # Create final config dictionary + pattern_raw = event_config['filter']['pattern'] + pattern_compiled = re.compile( + rf'{event_config["filter"]["pattern"]}') + pattern_config = { + pattern_compiled: { + 'pattern_raw': + pattern_raw, + 'syslog_id': + dict_search('filter.syslog-identifier', event_config), + 'pattern_script': { + 'path': script, + 'environment': environment + } + } + } + self.config.update(pattern_config) + + # Execute script safely + def script_run(self, pattern: str, script_path: str, + script_env: dict) -> None: + try: + run(script_path, env=script_env) + journal.send( + f'Pattern found: "{pattern}", script executed: "{script_path}"', + SYSLOG_IDENTIFIER=my_name) + except Exception as err: + journal.send( + f'Pattern found: "{pattern}", failed to execute script "{script_path}": {err}', + SYSLOG_IDENTIFIER=my_name) + + # Analyze a message + def process_message(self, message: dict) -> None: + for pattern_compiled, pattern_config in self.config.items(): + # Check if syslog id is presented in config and matches + syslog_id = pattern_config.get('syslog_id') + if syslog_id and message['SYSLOG_IDENTIFIER'] != syslog_id: + continue + if pattern_compiled.fullmatch(message['MESSAGE']): + # Add message to environment variables + pattern_config['pattern_script']['environment'][ + 'message'] = message['MESSAGE'] + # Run script + self.script_run( + pattern=pattern_config['pattern_raw'], + script_path=pattern_config['pattern_script']['path'], + script_env=pattern_config['pattern_script']['environment']) + + +if __name__ == '__main__': + # Parse command arguments and get config + parser = argparse.ArgumentParser() + parser.add_argument('-c', + '--config', + action='store', + help='Path to even-handler configuration', + required=True, + type=Path) + + args = parser.parse_args() + try: + config_path = Path(args.config) + config = json.loads(config_path.read_text()) + # Create an object for analazyng messages + analyzer = Analyzer(config) + except Exception as err: + print( + f'Configuration file "{config_path}" does not exist or malformed: {err}' + ) + exit(1) + + # Prepare for proper exitting + signal(SIGTERM, handle_signal) + signal(SIGINT, handle_signal) + + # Set up journal connection + data = journal.Reader() + data.seek_tail() + data.get_previous() + p = select.poll() + p.register(data, data.get_events()) + + journal.send(f'Started with configuration: {config}', + SYSLOG_IDENTIFIER=my_name) + + while p.poll(): + if data.process() != journal.APPEND: + continue + for entry in data: + message = entry['MESSAGE'] + pid = entry['_PID'] + # Skip empty messages and messages from this process + if message and pid != my_pid: + try: + analyzer.process_message(entry) + except Exception as err: + journal.send(f'Unable to process message: {err}', + SYSLOG_IDENTIFIER=my_name) diff --git a/src/systemd/dhclient@.service b/src/systemd/dhclient@.service index 2ced1038a..23cd4cfc3 100644 --- a/src/systemd/dhclient@.service +++ b/src/systemd/dhclient@.service @@ -13,6 +13,9 @@ PIDFile=/var/lib/dhcp/dhclient_%i.pid ExecStart=/sbin/dhclient -4 $DHCLIENT_OPTS ExecStop=/sbin/dhclient -4 $DHCLIENT_OPTS -r Restart=always +TimeoutStopSec=20 +SendSIGKILL=true +FinalKillSignal=SIGABRT [Install] WantedBy=multi-user.target diff --git a/src/systemd/vyos-domain-group-resolve.service b/src/systemd/vyos-domain-group-resolve.service new file mode 100644 index 000000000..29628fddb --- /dev/null +++ b/src/systemd/vyos-domain-group-resolve.service @@ -0,0 +1,11 @@ +[Unit] +Description=VyOS firewall domain-group resolver +After=vyos-router.service + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/python3 /usr/libexec/vyos/vyos-domain-group-resolve.py + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/vyos-event-handler.service b/src/systemd/vyos-event-handler.service new file mode 100644 index 000000000..6afe4f95b --- /dev/null +++ b/src/systemd/vyos-event-handler.service @@ -0,0 +1,11 @@ +[Unit] +Description=VyOS event handler +After=network.target vyos-router.service + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/python3 /usr/libexec/vyos/system/vyos-event-handler.py --config /run/vyos-event-handler.conf + +[Install] +WantedBy=multi-user.target |