diff options
Diffstat (limited to 'src')
35 files changed, 922 insertions, 1115 deletions
diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py deleted file mode 100755 index ab1c69259..000000000 --- a/src/conf_mode/firewall-interface.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/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 os -import re - -from sys import argv -from sys import exit - -from vyos.config import Config -from vyos.configdict import leaf_node_changed -from vyos.ifconfig import Section -from vyos.template import render -from vyos.util import cmd -from vyos.util import dict_search_args -from vyos.util import run -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -NAME_PREFIX = 'NAME_' -NAME6_PREFIX = 'NAME6_' - -NFT_CHAINS = { - 'in': 'VYOS_FW_FORWARD', - 'out': 'VYOS_FW_FORWARD', - 'local': 'VYOS_FW_LOCAL' -} -NFT6_CHAINS = { - 'in': 'VYOS_FW6_FORWARD', - 'out': 'VYOS_FW6_FORWARD', - 'local': 'VYOS_FW6_LOCAL' -} - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - ifname = argv[1] - ifpath = Section.get_config_path(ifname) - if_firewall_path = f'interfaces {ifpath} firewall' - - if_firewall = conf.get_config_dict(if_firewall_path, key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - - if_firewall['ifname'] = ifname - if_firewall['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - - 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: - return None - - for direction in ['in', 'out', 'local']: - if direction in if_firewall: - if 'name' in if_firewall[direction]: - name = if_firewall[direction]['name'] - - if 'name' not in if_firewall['firewall']: - raise ConfigError('Firewall name not configured') - - 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'] - - if 'ipv6_name' not in if_firewall['firewall']: - raise ConfigError('Firewall ipv6-name not configured') - - 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): - return None - -def cleanup_rule(table, chain, prefix, ifname, new_name=None): - results = cmd(f'nft -a list chain {table} {chain}').split("\n") - retval = None - for line in results: - if f'{prefix}ifname "{ifname}"' in line: - if new_name and f'jump {new_name}' in line: - # new_name is used to clear rules for any previously referenced chains - # returns true when rule exists and doesn't need to be created - retval = True - continue - - handle_search = re.search('handle (\d+)', line) - if handle_search: - run(f'nft delete rule {table} {chain} handle {handle_search[1]}') - return retval - -def state_policy_handle(table, chain): - # Find any state-policy rule to ensure interface rules are only inserted afterwards - results = cmd(f'nft -a list chain {table} {chain}').split("\n") - for line in results: - if 'jump VYOS_STATE_POLICY' in line: - handle_search = re.search('handle (\d+)', line) - if handle_search: - return handle_search[1] - return None - -def apply(if_firewall): - ifname = if_firewall['ifname'] - - for direction in ['in', 'out', 'local']: - chain = NFT_CHAINS[direction] - ipv6_chain = NFT6_CHAINS[direction] - if_prefix = 'i' if direction in ['in', 'local'] else 'o' - - name = dict_search_args(if_firewall, direction, 'name') - if name: - rule_exists = cleanup_rule('ip filter', chain, if_prefix, ifname, f'{NAME_PREFIX}{name}') - - if not rule_exists: - rule_action = 'insert' - rule_prefix = '' - - handle = state_policy_handle('ip filter', chain) - if handle: - rule_action = 'add' - rule_prefix = f'position {handle}' - - run(f'nft {rule_action} rule ip filter {chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {NAME_PREFIX}{name}') - else: - cleanup_rule('ip filter', chain, if_prefix, ifname) - - ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name') - if ipv6_name: - rule_exists = cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname, f'{NAME6_PREFIX}{ipv6_name}') - - if not rule_exists: - rule_action = 'insert' - rule_prefix = '' - - handle = state_policy_handle('ip6 filter', ipv6_chain) - if handle: - rule_action = 'add' - rule_prefix = f'position {handle}' - - run(f'nft {rule_action} rule ip6 filter {ipv6_chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {NAME6_PREFIX}{ipv6_name}') - else: - cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname) - - 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/firewall.py b/src/conf_mode/firewall.py index f0ea1a1e5..cbd9cbe90 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -26,6 +26,7 @@ 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.configverify import verify_interface_exists from vyos.firewall import geoip_update from vyos.firewall import get_ips_domains_dict from vyos.firewall import nft_add_set_elements @@ -38,7 +39,7 @@ 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.util import rc_cmd from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -47,7 +48,6 @@ airbag.enable() policy_route_conf_script = '/usr/libexec/vyos/conf_mode/policy-route.py' nftables_conf = '/run/nftables.conf' -nftables_defines_conf = '/run/nftables_defines.conf' sysfs_config = { 'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'enable': '0', 'disable': '1'}, @@ -63,28 +63,6 @@ sysfs_config = { 'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337'} } -NAME_PREFIX = 'NAME_' -NAME6_PREFIX = 'NAME6_' - -preserve_chains = [ - 'INPUT', - 'FORWARD', - 'OUTPUT', - 'VYOS_FW_FORWARD', - 'VYOS_FW_LOCAL', - 'VYOS_FW_OUTPUT', - 'VYOS_POST_FW', - 'VYOS_FRAG_MARK', - 'VYOS_FW6_FORWARD', - 'VYOS_FW6_LOCAL', - 'VYOS_FW6_OUTPUT', - 'VYOS_POST_FW6', - 'VYOS_FRAG6_MARK' -] - -nft_iface_chains = ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL'] -nft6_iface_chains = ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL'] - valid_groups = [ 'address_group', 'domain_group', @@ -97,16 +75,6 @@ nested_group_types = [ '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, @@ -117,51 +85,6 @@ snmp_event_source = 1 snmp_trap_mib = 'VYATTA-TRAP-MIB' snmp_trap_name = 'mgmtEventTrap' -def get_firewall_interfaces(conf): - out = {} - interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - def find_interfaces(iftype_conf, output={}, prefix=''): - for ifname, if_conf in iftype_conf.items(): - if 'firewall' in if_conf: - output[prefix + ifname] = if_conf['firewall'] - for vif in ['vif', 'vif_s', 'vif_c']: - if vif in if_conf: - output.update(find_interfaces(if_conf[vif], output, f'{prefix}{ifname}.')) - return output - for iftype, iftype_conf in interfaces.items(): - out.update(find_interfaces(iftype_conf)) - return out - -def get_firewall_zones(conf): - used_v4 = [] - used_v6 = [] - zone_policy = conf.get_config_dict(['zone-policy'], key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - - if 'zone' in zone_policy: - for zone, zone_conf in zone_policy['zone'].items(): - if 'from' in zone_conf: - for from_zone, from_conf in zone_conf['from'].items(): - name = dict_search_args(from_conf, 'firewall', 'name') - if name: - used_v4.append(name) - - ipv6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name') - if ipv6_name: - used_v6.append(ipv6_name) - - if 'intra_zone_filtering' in zone_conf: - name = dict_search_args(zone_conf, 'intra_zone_filtering', 'firewall', 'name') - if name: - used_v4.append(name) - - ipv6_name = dict_search_args(zone_conf, 'intra_zone_filtering', 'firewall', 'ipv6_name') - if ipv6_name: - used_v6.append(ipv6_name) - - 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) @@ -215,6 +138,9 @@ def get_config(config=None): if tmp in default_values: del default_values[tmp] + if 'zone' in default_values: + del default_values['zone'] + firewall = dict_merge(default_values, firewall) # Merge in defaults for IPv4 ruleset @@ -231,9 +157,12 @@ def get_config(config=None): firewall['ipv6_name'][ipv6_name] = dict_merge(default_values, firewall['ipv6_name'][ipv6_name]) + if 'zone' in firewall: + default_values = defaults(base + ['zone']) + for zone in firewall['zone']: + firewall['zone'][zone] = dict_merge(default_values, firewall['zone'][zone]) + firewall['policy_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) - firewall['interfaces'] = get_firewall_interfaces(conf) - firewall['zone_policy'] = get_firewall_zones(conf) if 'config_trap' in firewall and firewall['config_trap'] == 'enable': diff = get_config_diff(conf) @@ -250,6 +179,20 @@ def verify_rule(firewall, rule_conf, ipv6): if 'action' not in rule_conf: raise ConfigError('Rule action must be defined') + if 'jump' in rule_conf['action'] and 'jump_target' not in rule_conf: + raise ConfigError('Action set to jump, but no jump-target specified') + + if 'jump_target' in 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 not ipv6: + if target not in dict_search_args(firewall, 'name'): + raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') + else: + if target not in dict_search_args(firewall, 'ipv6_name'): + raise ConfigError(f'Invalid jump-target. Firewall ipv6-name {target} does not exist on the system') + if 'fragment' in rule_conf: if {'match_frag', 'match_non_frag'} <= set(rule_conf['fragment']): raise ConfigError('Cannot specify both "match-frag" and "match-non-frag"') @@ -358,109 +301,111 @@ def verify(firewall): for name in ['name', 'ipv6_name']: if name in firewall: for name_id, name_conf in firewall[name].items(): - if name_id in preserve_chains: - raise ConfigError(f'Firewall name "{name_id}" is reserved for VyOS') - - if name_id.startswith("VZONE"): - raise ConfigError(f'Firewall name "{name_id}" uses reserved prefix') + if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf: + raise ConfigError('default-action set to jump, but no default-jump-target specified') + if 'default_jump_target' in name_conf: + target = name_conf['default_jump_target'] + if 'jump' not in name_conf['default_action']: + raise ConfigError('default-jump-target defined,but default-action jump needed and it is not defined') + if name_conf['default_jump_target'] == name_id: + raise ConfigError(f'Loop detected on default-jump-target.') + ## Now need to check that default-jump-target exists (other firewall chain/name) + if target not in dict_search_args(firewall, name): + raise ConfigError(f'Invalid jump-target. Firewall {name} {target} does not exist on the system') if 'rule' in name_conf: for rule_id, rule_conf in name_conf['rule'].items(): verify_rule(firewall, rule_conf, name == 'ipv6_name') - for ifname, if_firewall in firewall['interfaces'].items(): - for direction in ['in', 'out', 'local']: - name = dict_search_args(if_firewall, direction, 'name') - ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name') + if 'interface' in firewall: + for ifname, if_firewall in firewall['interface'].items(): + # verify ifname needs to be disabled, dynamic devices come up later + # verify_interface_exists(ifname) - if name and dict_search_args(firewall, 'name', name) == None: - raise ConfigError(f'Firewall name "{name}" is still referenced on interface {ifname}') + for direction in ['in', 'out', 'local']: + name = dict_search_args(if_firewall, direction, 'name') + ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name') - if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None: - raise ConfigError(f'Firewall ipv6-name "{ipv6_name}" is still referenced on interface {ifname}') + if name and dict_search_args(firewall, 'name', name) == None: + raise ConfigError(f'Invalid firewall name "{name}" referenced on interface {ifname}') - for fw_name, used_names in firewall['zone_policy'].items(): - for name in used_names: - if dict_search_args(firewall, fw_name, name) == None: - raise ConfigError(f'Firewall {fw_name.replace("_", "-")} "{name}" is still referenced in zone-policy') + if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None: + raise ConfigError(f'Invalid firewall ipv6-name "{ipv6_name}" referenced on interface {ifname}') - return None + local_zone = False + zone_interfaces = [] -def cleanup_commands(firewall): - commands = [] - 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' - 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 preserve_chains or chain.startswith("VZONE"): - continue + 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: + 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: + 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 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 'interface' in zone_conf: + found_duplicates = [intf for intf in zone_conf['interface'] if intf in zone_interfaces] - if 'rule' in item: - rule = item['rule'] - chain = rule['chain'] - handle = rule['handle'] + if found_duplicates: + raise ConfigError(f'Interfaces cannot be assigned to multiple zones') - if chain in iface_chains: - target, _ = next(dict_search_recursive(rule['expr'], 'target')) + zone_interfaces += zone_conf['interface'] - if target == state_chain and 'state_policy' not in firewall: - commands.append(f'delete rule {table} {chain} handle {handle}') + if 'intra_zone_filtering' in zone_conf: + intra_zone = zone_conf['intra_zone_filtering'] - 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}') + if len(intra_zone) > 1: + raise ConfigError('Only one intra-zone-filtering action must be specified') - if 'set' in item: - set_name = item['set']['name'] + if 'firewall' in intra_zone: + v4_name = dict_search_args(intra_zone, 'firewall', 'name') + if v4_name and not dict_search_args(firewall, 'name', v4_name): + raise ConfigError(f'Firewall name "{v4_name}" does not exist') - if set_name.startswith('GEOIP_CC_') and set_name in geoip_list: - commands_sets.append(f'delete set {table} {set_name}') - continue + v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6_name') + if v6_name and not dict_search_args(firewall, 'ipv6_name', v6_name): + raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') - if set_name.startswith("RECENT_"): - commands_sets.append(f'delete set {table} {set_name}') - continue + if not v4_name and not v6_name: + raise ConfigError('No firewall names specified for intra-zone-filtering') + + if 'from' in zone_conf: + for from_zone, from_conf in zone_conf['from'].items(): + if from_zone not in firewall['zone']: + raise ConfigError(f'Zone "{zone}" refers to a non-existent or deleted zone "{from_zone}"') + + v4_name = dict_search_args(from_conf, 'firewall', 'name') + if v4_name and not dict_search_args(firewall, 'name', v4_name): + raise ConfigError(f'Firewall name "{v4_name}" does not exist') + + v6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name') + if v6_name and not dict_search_args(firewall, 'ipv6_name', v6_name): + raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') - 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 + return None def generate(firewall): if not os.path.exists(nftables_conf): firewall['first_install'] = True - else: - firewall['cleanup_commands'] = cleanup_commands(firewall) + + 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) return None @@ -521,26 +466,16 @@ def post_apply_trap(firewall): cmd(base_cmd + ' '.join(objects)) -def state_policy_rule_exists(): - # Determine if state policy rules already exist in nft - search_str = cmd(f'nft list chain ip filter VYOS_FW_FORWARD') - return 'VYOS_STATE_POLICY' in search_str - def resync_policy_route(): # Update policy route as firewall groups were updated - tmp = run(policy_route_conf_script) + tmp, out = rc_cmd(policy_route_conf_script) if tmp > 0: - Warning('Failed to re-apply policy route configuration!') + Warning(f'Failed to re-apply policy route configuration! {out}') def apply(firewall): - if 'first_install' in firewall: - run('nfct helper add rpc inet tcp') - run('nfct helper add rpc inet udp') - run('nfct helper add tns inet tcp') - - install_result = run(f'nft -f {nftables_conf}') + install_result, output = rc_cmd(f'nft -f {nftables_conf}') if install_result == 1: - raise ConfigError('Failed to apply firewall') + raise ConfigError(f'Failed to apply firewall: {output}') # set firewall group domain-group xxx if 'group' in firewall: @@ -563,13 +498,6 @@ def apply(firewall): 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') - - for chain in ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']: - cmd(f'nft insert rule ip6 filter {chain} jump VYOS_STATE_POLICY6') - apply_sysfs(firewall) if firewall['policy_resync']: diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index ef745d737..8155f36c2 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -55,6 +55,7 @@ from vyos.util import chown from vyos.util import cmd from vyos.util import dict_search from vyos.util import dict_search_args +from vyos.util import is_list_equal from vyos.util import makedir from vyos.util import read_file from vyos.util import write_file @@ -274,7 +275,7 @@ def verify(openvpn): elif v6remAddr and not v6loAddr: raise ConfigError('IPv6 "remote-address" requires IPv6 "local-address"') - if (v4loAddr == v4remAddr) or (v6remAddr == v4remAddr): + if is_list_equal(v4loAddr, v4remAddr) or is_list_equal(v6loAddr, v6remAddr): raise ConfigError('"local-address" and "remote-address" cannot be the same') if dict_search('local_host', openvpn) in dict_search('local_address', openvpn): diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 61bab2feb..8d738f55e 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -14,16 +14,12 @@ # 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 copy import deepcopy from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import get_interface_dict -from vyos.configdict import node_changed -from vyos.configdict import leaf_node_changed +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 @@ -50,17 +46,20 @@ def get_config(config=None): ifname, wireguard = get_interface_dict(conf, base) # Check if a port was changed - wireguard['port_changed'] = leaf_node_changed(conf, base + [ifname, 'port']) + tmp = is_node_changed(conf, base + [ifname, 'port']) + if tmp: wireguard['port_changed'] = {} # Determine which Wireguard peer has been removed. # Peers can only be removed with their public key! - dict = {} - tmp = node_changed(conf, base + [ifname, 'peer'], key_mangling=('-', '_')) - for peer in (tmp or []): - public_key = leaf_node_changed(conf, base + [ifname, 'peer', peer, 'public_key']) - if public_key: - dict = dict_merge({'peer_remove' : {peer : {'public_key' : public_key[0]}}}, dict) - wireguard.update(dict) + if 'peer' in wireguard: + peer_remove = {} + for peer, peer_config in wireguard['peer'].items(): + # T4702: If anything on a peer changes we remove the peer first and re-add it + if is_node_changed(conf, base + [ifname, 'peer', peer]): + if 'public_key' in peer_config: + peer_remove = dict_merge({'peer_remove' : {peer : peer_config['public_key']}}, peer_remove) + if peer_remove: + wireguard.update(peer_remove) return wireguard @@ -81,12 +80,11 @@ def verify(wireguard): if 'peer' not in wireguard: raise ConfigError('At least one Wireguard peer is required!') - if 'port' in wireguard and wireguard['port_changed']: + 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: - raise ConfigError( - f'The UDP port {listen_port} is busy or unavailable and cannot be used for the interface' - ) + 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 for tmp in wireguard['peer']: diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index e75418ba5..8b1a5a720 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -147,14 +147,10 @@ def verify(nat): Warning(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') addr = dict_search('translation.address', config) - if addr != None: - if 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!') - elif 'exclude' not in config: - raise ConfigError(f'{err_msg}\n' \ - 'translation address not specified') + 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) @@ -167,14 +163,8 @@ def verify(nat): if 'inbound_interface' not in config: raise ConfigError(f'{err_msg}\n' \ 'inbound-interface not specified') - else: - if config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces(): - Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') - - - if dict_search('translation.address', config) == None and 'exclude' not in config: - raise ConfigError(f'{err_msg}\n' \ - 'translation address not specified') + elif config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces(): + Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') # common rule verification verify_rule(config, err_msg) @@ -193,6 +183,9 @@ def verify(nat): return None def generate(nat): + if not os.path.exists(nftables_nat_config): + nat['first_install'] = True + render(nftables_nat_config, 'firewall/nftables-nat.j2', nat) render(nftables_static_nat_conf, 'firewall/nftables-static-nat.j2', nat) @@ -201,7 +194,9 @@ def generate(nat): if tmp > 0: raise ConfigError('Configuration file errors encountered!') - tmp = run(f'nft -c -f {nftables_nat_config}') + tmp = run(f'nft -c -f {nftables_static_nat_conf}') + if tmp > 0: + raise ConfigError('Configuration file errors encountered!') return None diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index f64102d88..d8f913b0c 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -36,7 +36,7 @@ airbag.enable() k_mod = ['nft_nat', 'nft_chain_nat'] -nftables_nat66_config = '/tmp/vyos-nat66-rules.nft' +nftables_nat66_config = '/run/nftables_nat66.nft' ndppd_config = '/run/ndppd/ndppd.conf' def get_handler(json, chain, target): @@ -147,6 +147,9 @@ def verify(nat): return None def generate(nat): + if not os.path.exists(nftables_nat66_config): + nat['first_install'] = True + render(nftables_nat66_config, 'firewall/nftables-nat66.j2', nat, permission=0o755) render(ndppd_config, 'ndppd/ndppd.conf.j2', nat, permission=0o755) return None @@ -154,15 +157,15 @@ def generate(nat): def apply(nat): if not nat: return None - cmd(f'{nftables_nat66_config}') + + cmd(f'nft -f {nftables_nat66_config}') + if 'deleted' in nat or not dict_search('source.rule', nat): cmd('systemctl stop ndppd') if os.path.isfile(ndppd_config): os.unlink(ndppd_config) else: cmd('systemctl restart ndppd') - if os.path.isfile(nftables_nat66_config): - os.unlink(nftables_nat66_config) return None diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py index 9fddbd2c6..00539b9c7 100755 --- a/src/conf_mode/policy-route.py +++ b/src/conf_mode/policy-route.py @@ -92,7 +92,7 @@ def get_config(config=None): return policy -def verify_rule(policy, name, rule_conf, ipv6): +def verify_rule(policy, name, rule_conf, ipv6, rule_id): icmp = 'icmp' if not ipv6 else 'icmpv6' if icmp in rule_conf: icmp_defined = False @@ -166,7 +166,7 @@ def verify(policy): for name, pol_conf in policy[route].items(): if 'rule' in pol_conf: for rule_id, rule_conf in pol_conf['rule'].items(): - verify_rule(policy, name, rule_conf, ipv6) + verify_rule(policy, name, rule_conf, ipv6, rule_id) for ifname, if_policy in policy['interfaces'].items(): name = dict_search_args(if_policy, 'route') diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index 5dafd26d0..cb8ea3be4 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -203,6 +203,28 @@ def verify(isis): if list(set(global_range) & set(local_range)): raise ConfigError(f'Segment-Routing Global Block ({g_low_label_value}/{g_high_label_value}) '\ f'conflicts with Local Block ({l_low_label_value}/{l_high_label_value})!') + + # Check for a blank or invalid value per prefix + if dict_search('segment_routing.prefix', isis): + for prefix, prefix_config in isis['segment_routing']['prefix'].items(): + if 'absolute' in prefix_config: + if prefix_config['absolute'].get('value') is None: + raise ConfigError(f'Segment routing prefix {prefix} absolute value cannot be blank.') + elif 'index' in prefix_config: + if prefix_config['index'].get('value') is None: + raise ConfigError(f'Segment routing prefix {prefix} index value cannot be blank.') + + # Check for explicit-null and no-php-flag configured at the same time per prefix + if dict_search('segment_routing.prefix', isis): + for prefix, prefix_config in isis['segment_routing']['prefix'].items(): + if 'absolute' in prefix_config: + if ("explicit_null" in prefix_config['absolute']) and ("no_php_flag" in prefix_config['absolute']): + raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ + f'and no-php-flag configured at the same time.') + elif 'index' in prefix_config: + if ("explicit_null" in prefix_config['index']) and ("no_php_flag" in prefix_config['index']): + raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ + f'and no-php-flag configured at the same time.') return None diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py index b247ce2ab..d28ced4fd 100755 --- a/src/conf_mode/protocols_nhrp.py +++ b/src/conf_mode/protocols_nhrp.py @@ -14,10 +14,10 @@ # 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 vyos.config import Config from vyos.configdict import node_changed -from vyos.firewall import find_nftables_rule -from vyos.firewall import remove_nftables_rule from vyos.template import render from vyos.util import process_named_running from vyos.util import run @@ -26,6 +26,7 @@ from vyos import airbag airbag.enable() opennhrp_conf = '/run/opennhrp/opennhrp.conf' +nhrp_nftables_conf = '/run/nftables_nhrp.conf' def get_config(config=None): if config: @@ -84,28 +85,23 @@ def verify(nhrp): 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) return None def apply(nhrp): - if 'tunnel' in nhrp: - for tunnel, tunnel_conf in nhrp['tunnel'].items(): - if 'source_address' in nhrp['if_tunnel'][tunnel]: - comment = f'VYOS_NHRP_{tunnel}' - source_address = nhrp['if_tunnel'][tunnel]['source_address'] - - rule_handle = find_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', ['ip protocol gre', f'ip saddr {source_address}', 'ip daddr 224.0.0.0/4']) - if not rule_handle: - run(f'sudo nft insert rule ip filter VYOS_FW_OUTPUT ip protocol gre ip saddr {source_address} ip daddr 224.0.0.0/4 counter drop comment "{comment}"') - - for tunnel in nhrp['del_tunnels']: - comment = f'VYOS_NHRP_{tunnel}' - rule_handle = find_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', [f'comment "{comment}"']) - if rule_handle: - remove_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', rule_handle) + nft_rc = run(f'nft -f {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' - run(f'systemctl {action} opennhrp.service') + service_rc = run(f'systemctl {action} opennhrp.service') + if service_rc != 0: + raise ConfigError(f'Failed to {action} the NHRP service') + return None if __name__ == '__main__': diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 61f484129..e9afd6a55 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -15,266 +15,34 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import re -from copy import deepcopy -from stat import S_IRUSR, S_IWUSR, S_IRGRP from sys import exit from vyos.config import Config +from vyos.configdict import get_accel_dict +from vyos.configverify import verify_accel_ppp_base_service +from vyos.configverify import verify_interface_exists from vyos.template import render -from vyos.template import is_ipv4 -from vyos.template import is_ipv6 -from vyos.util import call, get_half_cpus +from vyos.util import call +from vyos.util import dict_search from vyos import ConfigError - from vyos import airbag airbag.enable() ipoe_conf = '/run/accel-pppd/ipoe.conf' ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets' -default_config_data = { - 'auth_mode': 'local', - 'auth_interfaces': [], - 'chap_secrets_file': ipoe_chap_secrets, # used in Jinja2 template - 'interfaces': [], - 'dnsv4': [], - 'dnsv6': [], - 'client_named_ip_pool': [], - 'client_ipv6_pool': [], - 'client_ipv6_delegate_prefix': [], - 'radius_server': [], - 'radius_acct_inter_jitter': '', - 'radius_acct_tmo': '3', - 'radius_max_try': '3', - 'radius_timeout': '3', - 'radius_nas_id': '', - '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() -} - def get_config(config=None): if config: conf = config else: conf = Config() - base_path = ['service', 'ipoe-server'] - if not conf.exists(base_path): + base = ['service', 'ipoe-server'] + if not conf.exists(base): return None - conf.set_level(base_path) - ipoe = deepcopy(default_config_data) - - for interface in conf.list_nodes(['interface']): - tmp = { - 'mode': 'L2', - 'name': interface, - 'shared': '1', - # may need a config option, can be dhcpv4 or up for unclassified pkts - 'sess_start': 'dhcpv4', - 'range': None, - 'ifcfg': '1', - 'vlan_mon': [] - } - - conf.set_level(base_path + ['interface', interface]) - - if conf.exists(['network-mode']): - tmp['mode'] = conf.return_value(['network-mode']) - - if conf.exists(['network']): - mode = conf.return_value(['network']) - if mode == 'vlan': - tmp['shared'] = '0' - - if conf.exists(['vlan-id']): - tmp['vlan_mon'] += conf.return_values(['vlan-id']) - - if conf.exists(['vlan-range']): - tmp['vlan_mon'] += conf.return_values(['vlan-range']) - - if conf.exists(['client-subnet']): - tmp['range'] = conf.return_value(['client-subnet']) - - ipoe['interfaces'].append(tmp) - - conf.set_level(base_path) - - if conf.exists(['name-server']): - for name_server in conf.return_values(['name-server']): - if is_ipv4(name_server): - ipoe['dnsv4'].append(name_server) - else: - ipoe['dnsv6'].append(name_server) - - if conf.exists(['authentication', 'mode']): - ipoe['auth_mode'] = conf.return_value(['authentication', 'mode']) - - if conf.exists(['authentication', 'interface']): - for interface in conf.list_nodes(['authentication', 'interface']): - tmp = { - 'name': interface, - 'mac': [] - } - for mac in conf.list_nodes(['authentication', 'interface', interface, 'mac-address']): - client = { - 'address': mac, - 'rate_download': '', - 'rate_upload': '', - 'vlan_id': '' - } - conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', mac]) - - if conf.exists(['rate-limit', 'download']): - client['rate_download'] = conf.return_value(['rate-limit', 'download']) - - if conf.exists(['rate-limit', 'upload']): - client['rate_upload'] = conf.return_value(['rate-limit', 'upload']) - - if conf.exists(['vlan-id']): - client['vlan'] = conf.return_value(['vlan-id']) - - tmp['mac'].append(client) - - ipoe['auth_interfaces'].append(tmp) - - conf.set_level(base_path) - - # - # authentication mode radius servers and settings - if conf.exists(['authentication', 'mode', 'radius']): - for server in conf.list_nodes(['authentication', 'radius', 'server']): - radius = { - 'server' : server, - 'key' : '', - 'fail_time' : 0, - 'port' : '1812', - 'acct_port' : '1813' - } - - conf.set_level(base_path + ['authentication', 'radius', 'server', server]) - - if conf.exists(['fail-time']): - radius['fail_time'] = conf.return_value(['fail-time']) - - if conf.exists(['port']): - radius['port'] = conf.return_value(['port']) - - if conf.exists(['acct-port']): - radius['acct_port'] = conf.return_value(['acct-port']) - - if conf.exists(['key']): - radius['key'] = conf.return_value(['key']) - - if not conf.exists(['disable']): - ipoe['radius_server'].append(radius) - - # - # advanced radius-setting - conf.set_level(base_path + ['authentication', 'radius']) - - if conf.exists(['acct-interim-jitter']): - ipoe['radius_acct_inter_jitter'] = conf.return_value(['acct-interim-jitter']) - - if conf.exists(['acct-timeout']): - ipoe['radius_acct_tmo'] = conf.return_value(['acct-timeout']) - - if conf.exists(['max-try']): - ipoe['radius_max_try'] = conf.return_value(['max-try']) - - if conf.exists(['timeout']): - ipoe['radius_timeout'] = conf.return_value(['timeout']) - - if conf.exists(['nas-identifier']): - ipoe['radius_nas_id'] = conf.return_value(['nas-identifier']) - - 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']) - - # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) - if conf.exists(['dynamic-author']): - dae = { - 'port' : '', - 'server' : '', - 'key' : '' - } - - if conf.exists(['dynamic-author', 'server']): - dae['server'] = conf.return_value(['dynamic-author', 'server']) - - if conf.exists(['dynamic-author', 'port']): - dae['port'] = conf.return_value(['dynamic-author', 'port']) - - if conf.exists(['dynamic-author', 'key']): - dae['key'] = conf.return_value(['dynamic-author', 'key']) - - ipoe['radius_dynamic_author'] = dae - - - conf.set_level(base_path) - # Named client-ip-pool - if conf.exists(['client-ip-pool', 'name']): - for name in conf.list_nodes(['client-ip-pool', 'name']): - tmp = { - 'name': name, - 'gateway_address': '', - 'subnet': '' - } - - if conf.exists(['client-ip-pool', 'name', name, 'gateway-address']): - tmp['gateway_address'] += conf.return_value(['client-ip-pool', 'name', name, 'gateway-address']) - if conf.exists(['client-ip-pool', 'name', name, 'subnet']): - tmp['subnet'] += conf.return_value(['client-ip-pool', 'name', name, 'subnet']) - - ipoe['client_named_ip_pool'].append(tmp) - - if conf.exists(['client-ipv6-pool', 'prefix']): - for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']): - tmp = { - 'prefix': prefix, - 'mask': '64' - } - - if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']): - tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask']) - - ipoe['client_ipv6_pool'].append(tmp) - - - if conf.exists(['client-ipv6-pool', 'delegate']): - for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']): - tmp = { - 'prefix': prefix, - 'mask': '' - } - - if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']): - tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']) - - ipoe['client_ipv6_delegate_prefix'].append(tmp) - + # retrieve common dictionary keys + ipoe = get_accel_dict(conf, base, ipoe_chap_secrets) return ipoe @@ -282,26 +50,17 @@ def verify(ipoe): if not ipoe: return None - if not ipoe['interfaces']: + if 'interface' not in ipoe: raise ConfigError('No IPoE interface configured') - if len(ipoe['dnsv4']) > 2: - raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') - - if len(ipoe['dnsv6']) > 3: - raise ConfigError('Not more then three IPv6 DNS name-servers can be configured') - - if ipoe['auth_mode'] == 'radius': - if len(ipoe['radius_server']) == 0: - raise ConfigError('RADIUS authentication requires at least one server') + for interface in ipoe['interface']: + verify_interface_exists(interface) - for radius in ipoe['radius_server']: - if not radius['key']: - server = radius['server'] - raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') + #verify_accel_ppp_base_service(ipoe, local_users=False) - if ipoe['client_ipv6_delegate_prefix'] and not ipoe['client_ipv6_pool']: - raise ConfigError('IPoE IPv6 deletate-prefix requires IPv6 prefix to be configured!') + if 'client_ipv6_pool' in ipoe: + if 'delegate' in ipoe['client_ipv6_pool'] and 'prefix' not in ipoe['client_ipv6_pool']: + raise ConfigError('IPoE IPv6 deletate-prefix requires IPv6 prefix to be configured!') return None @@ -312,27 +71,23 @@ def generate(ipoe): render(ipoe_conf, 'accel-ppp/ipoe.config.j2', ipoe) - if ipoe['auth_mode'] == 'local': - render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.j2', ipoe) - os.chmod(ipoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) - - else: - if os.path.exists(ipoe_chap_secrets): - os.unlink(ipoe_chap_secrets) - + if dict_search('authentication.mode', ipoe) == 'local': + 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: - call('systemctl stop accel-ppp@ipoe.service') + call(f'systemctl stop {systemd_service}') for file in [ipoe_conf, ipoe_chap_secrets]: if os.path.exists(file): os.unlink(file) return None - call('systemctl restart accel-ppp@ipoe.service') + call(f'systemctl reload-or-restart {systemd_service}') if __name__ == '__main__': try: diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index 53df006a4..427cb6911 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -42,7 +42,7 @@ systemd_override = '/etc/systemd/system/telegraf.service.d/10-override.conf' def get_nft_filter_chains(): """ Get nft chains for table filter """ - nft = cmd('nft --json list table ip filter') + nft = cmd('nft --json list table ip vyos_filter') nft = json.loads(nft) chain_list = [] diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 6086ef859..ba0249efd 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-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 @@ -21,14 +21,12 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict from vyos.configverify import verify_accel_ppp_base_service +from vyos.configverify import verify_interface_exists from vyos.template import render from vyos.util import call from vyos.util import dict_search -from vyos.util import get_interface_config from vyos import ConfigError from vyos import airbag -from vyos.range_regex import range_to_regex - airbag.enable() pppoe_conf = r'/run/accel-pppd/pppoe.conf' @@ -54,15 +52,14 @@ def verify(pppoe): verify_accel_ppp_base_service(pppoe) if 'wins_server' in pppoe and len(pppoe['wins_server']) > 2: - raise ConfigError('Not more then two IPv4 WINS name-servers can be configured') + raise ConfigError('Not more then two WINS name-servers can be configured') if 'interface' not in pppoe: raise ConfigError('At least one listen interface must be defined!') # Check is interface exists in the system - for iface in pppoe['interface']: - if not get_interface_config(iface): - raise ConfigError(f'Interface {iface} does not exist!') + for interface in pppoe['interface']: + verify_interface_exists(interface) # local ippool and gateway settings config checks if not (dict_search('client_ip_pool.subnet', pppoe) or @@ -81,35 +78,24 @@ def generate(pppoe): if not pppoe: return None - # Generate special regex for dynamic interfaces - for iface in pppoe['interface']: - if 'vlan_range' in pppoe['interface'][iface]: - pppoe['interface'][iface]['regex'] = [] - for vlan_range in pppoe['interface'][iface]['vlan_range']: - pppoe['interface'][iface]['regex'].append(range_to_regex(vlan_range)) - render(pppoe_conf, 'accel-ppp/pppoe.config.j2', pppoe) if dict_search('authentication.mode', pppoe) == 'local': render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.config_dict.j2', pppoe, permission=0o640) - else: - if os.path.exists(pppoe_chap_secrets): - os.unlink(pppoe_chap_secrets) - return None def apply(pppoe): + systemd_service = 'accel-ppp@pppoe.service' if not pppoe: - call('systemctl stop accel-ppp@pppoe.service') + call(f'systemctl stop {systemd_service}') for file in [pppoe_conf, pppoe_chap_secrets]: if os.path.exists(file): os.unlink(file) - return None - call('systemctl restart accel-ppp@pppoe.service') + call(f'systemctl reload-or-restart {systemd_service}') if __name__ == '__main__': try: diff --git a/src/conf_mode/system_update_check.py b/src/conf_mode/system_update_check.py new file mode 100755 index 000000000..08ecfcb81 --- /dev/null +++ b/src/conf_mode/system_update_check.py @@ -0,0 +1,93 @@ +#!/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 os +import json +import jmespath + +from pathlib import Path +from sys import exit + +from vyos.config import Config +from vyos.util import call +from vyos import ConfigError +from vyos import airbag +airbag.enable() + + +base = ['system', 'update-check'] +service_name = 'vyos-system-update' +service_conf = Path(f'/run/{service_name}.conf') +motd_file = Path('/run/motd.d/10-vyos-update') + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + if not conf.exists(base): + return None + + config = conf.get_config_dict(base, key_mangling=('-', '_'), + 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 config is None: + return + + if 'url' not in config: + raise ConfigError('URL is required!') + + +def generate(config): + # bail out early - looks like removal from running config + if config is None: + # Remove old config and return + service_conf.unlink(missing_ok=True) + # MOTD used in /run/motd.d/10-update + motd_file.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: + if 'auto_check' in 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/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 5ca32d23e..c9061366d 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 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 @@ -16,6 +16,7 @@ import ipaddress import os +import re from sys import exit from time import sleep @@ -348,6 +349,14 @@ def verify(ipsec): if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']: for peer, peer_conf in ipsec['site_to_site']['peer'].items(): has_default_esp = False + # Peer name it is swanctl connection name and shouldn't contain dots or colons, T4118 + if bool(re.search(':|\.', peer)): + raise ConfigError(f'Incorrect peer name "{peer}" ' + f'Peer name can contain alpha-numeric letters, hyphen and underscore') + + if 'remote_address' not in peer_conf: + print(f'You should set correct remote-address "peer {peer} remote-address x.x.x.x"\n') + if 'default_esp_group' in peer_conf: has_default_esp = True if 'esp_group' not in ipsec or peer_conf['default_esp_group'] not in ipsec['esp_group']: diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 240546817..c050b796b 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -58,15 +58,16 @@ def get_config(): default_values = defaults(base) ocserv = dict_merge(default_values, ocserv) - # workaround a "know limitation" - https://phabricator.vyos.net/T2665 - del ocserv['authentication']['local_users']['username']['otp'] - if not ocserv["authentication"]["local_users"]["username"]: - raise ConfigError('openconnect mode local required at least one user') - default_ocserv_usr_values = default_values['authentication']['local_users']['username']['otp'] - for user, params in ocserv['authentication']['local_users']['username'].items(): - # Not every configuration requires OTP settings - if ocserv['authentication']['local_users']['username'][user].get('otp'): - ocserv['authentication']['local_users']['username'][user]['otp'] = dict_merge(default_ocserv_usr_values, ocserv['authentication']['local_users']['username'][user]['otp']) + if "local" in ocserv["authentication"]["mode"]: + # workaround a "know limitation" - https://phabricator.vyos.net/T2665 + del ocserv['authentication']['local_users']['username']['otp'] + if not ocserv["authentication"]["local_users"]["username"]: + raise ConfigError('openconnect mode local required at least one user') + default_ocserv_usr_values = default_values['authentication']['local_users']['username']['otp'] + for user, params in ocserv['authentication']['local_users']['username'].items(): + # Not every configuration requires OTP settings + if ocserv['authentication']['local_users']['username'][user].get('otp'): + ocserv['authentication']['local_users']['username'][user]['otp'] = dict_merge(default_ocserv_usr_values, ocserv['authentication']['local_users']['username'][user]['otp']) if ocserv: ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), @@ -80,9 +81,10 @@ def verify(ocserv): # 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('0.0.0.0', int(port), proto) is not True and \ + 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 authentication if "authentication" in ocserv: if "mode" in ocserv["authentication"]: diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py deleted file mode 100755 index a52c52706..000000000 --- a/src/conf_mode/zone_policy.py +++ /dev/null @@ -1,213 +0,0 @@ -#!/usr/bin/env python3 -# -# 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 -# 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 json import loads -from sys import exit - -from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.template import render -from vyos.util import cmd -from vyos.util import dict_search_args -from vyos.util import run -from vyos.xml import defaults -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -nftables_conf = '/run/nftables_zone.conf' - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - base = ['zone-policy'] - zone_policy = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - - zone_policy['firewall'] = conf.get_config_dict(['firewall'], - key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) - - if 'zone' in zone_policy: - # 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 = defaults(base + ['zone']) - for zone in zone_policy['zone']: - zone_policy['zone'][zone] = dict_merge(default_values, - zone_policy['zone'][zone]) - - return zone_policy - -def verify(zone_policy): - # bail out early - looks like removal from running config - if not zone_policy: - return None - - local_zone = False - interfaces = [] - - if 'zone' in zone_policy: - for zone, zone_conf in zone_policy['zone'].items(): - if 'local_zone' not in zone_conf and 'interface' 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: - 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 interfaces] - - if found_duplicates: - raise ConfigError(f'Interfaces cannot be assigned to multiple zones') - - interfaces += zone_conf['interface'] - - if 'intra_zone_filtering' in zone_conf: - intra_zone = zone_conf['intra_zone_filtering'] - - if len(intra_zone) > 1: - raise ConfigError('Only one intra-zone-filtering action must be specified') - - if 'firewall' in intra_zone: - v4_name = dict_search_args(intra_zone, 'firewall', 'name') - if v4_name and not dict_search_args(zone_policy, 'firewall', 'name', v4_name): - raise ConfigError(f'Firewall name "{v4_name}" does not exist') - - v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6-name') - if v6_name and not dict_search_args(zone_policy, 'firewall', 'ipv6-name', v6_name): - raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') - - if not v4_name and not v6_name: - raise ConfigError('No firewall names specified for intra-zone-filtering') - - if 'from' in zone_conf: - for from_zone, from_conf in zone_conf['from'].items(): - if from_zone not in zone_policy['zone']: - raise ConfigError(f'Zone "{zone}" refers to a non-existent or deleted zone "{from_zone}"') - - v4_name = dict_search_args(from_conf, 'firewall', 'name') - if v4_name: - if 'name' not in zone_policy['firewall']: - raise ConfigError(f'Firewall name "{v4_name}" does not exist') - - if not dict_search_args(zone_policy, 'firewall', 'name', v4_name): - raise ConfigError(f'Firewall name "{v4_name}" does not exist') - - v6_name = dict_search_args(from_conf, 'firewall', 'v6_name') - if v6_name: - if 'ipv6_name' not in zone_policy['firewall']: - raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') - - if not dict_search_args(zone_policy, 'firewall', 'ipv6_name', v6_name): - raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist') - - return None - -def has_ipv4_fw(zone_conf): - if 'from' not in zone_conf: - return False - zone_from = zone_conf['from'] - return any([True for fz in zone_from if dict_search_args(zone_from, fz, 'firewall', 'name')]) - -def has_ipv6_fw(zone_conf): - if 'from' not in zone_conf: - return False - zone_from = zone_conf['from'] - return any([True for fz in zone_from if dict_search_args(zone_from, fz, 'firewall', 'ipv6_name')]) - -def get_local_from(zone_policy, local_zone_name): - # Get all zone firewall names from the local zone - out = {} - for zone, zone_conf in zone_policy['zone'].items(): - if zone == local_zone_name: - continue - if 'from' not in zone_conf: - continue - if local_zone_name in zone_conf['from']: - out[zone] = zone_conf['from'][local_zone_name] - return out - -def cleanup_commands(): - commands = [] - for table in ['ip filter', 'ip6 filter']: - 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 'rule' in item: - chain = item['rule']['chain'] - handle = item['rule']['handle'] - if 'expr' not in item['rule']: - continue - for expr in item['rule']['expr']: - target = dict_search_args(expr, 'jump', 'target') - if not target: - continue - if target.startswith("VZONE") or target.startswith("VYOS_STATE_POLICY"): - commands.append(f'delete rule {table} {chain} handle {handle}') - for item in obj['nftables']: - if 'chain' in item: - if item['chain']['name'].startswith("VZONE"): - chain = item['chain']['name'] - commands.append(f'delete chain {table} {chain}') - return commands - -def generate(zone_policy): - data = zone_policy or {} - - if os.path.exists(nftables_conf): # Check to see if we've run before - data['cleanup_commands'] = cleanup_commands() - - if 'zone' in data: - for zone, zone_conf in data['zone'].items(): - zone_conf['ipv4'] = has_ipv4_fw(zone_conf) - zone_conf['ipv6'] = has_ipv6_fw(zone_conf) - - if 'local_zone' in zone_conf: - zone_conf['from_local'] = get_local_from(data, zone) - - render(nftables_conf, 'zone_policy/nftables.j2', data) - return None - -def apply(zone_policy): - install_result = run(f'nft -f {nftables_conf}') - if install_result != 0: - raise ConfigError('Failed to apply zone-policy') - - 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/etc/telegraf/custom_scripts/show_firewall_input_filter.py b/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py index bf4bfd05d..cbc2bfe6b 100755 --- a/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py +++ b/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py @@ -11,7 +11,7 @@ def get_nft_filter_chains(): """ Get list of nft chains for table filter """ - nft = cmd('/usr/sbin/nft --json list table ip filter') + nft = cmd('/usr/sbin/nft --json list table ip vyos_filter') nft = json.loads(nft) chain_list = [] @@ -27,7 +27,7 @@ def get_nftables_details(name): """ Get dict, counters packets and bytes for chain """ - command = f'/usr/sbin/nft list chain ip filter {name}' + command = f'/usr/sbin/nft list chain ip vyos_filter {name}' try: results = cmd(command) except: @@ -60,7 +60,7 @@ def get_nft_telegraf(name): Get data for telegraf in influxDB format """ for rule, rule_config in get_nftables_details(name).items(): - print(f'nftables,table=filter,chain={name},' + print(f'nftables,table=vyos_filter,chain={name},' f'ruleid={rule} ' f'pkts={rule_config["packets"]}i,' f'bytes={rule_config["bytes"]}i ' diff --git a/src/migration-scripts/firewall/7-to-8 b/src/migration-scripts/firewall/7-to-8 new file mode 100755 index 000000000..ce527acf5 --- /dev/null +++ b/src/migration-scripts/firewall/7-to-8 @@ -0,0 +1,98 @@ +#!/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/>. + +# T2199: Migrate interface firewall nodes to firewall interfaces <ifname> <direction> name/ipv6-name <name> +# T2199: Migrate zone-policy to firewall node + +import re + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree +from vyos.ifconfig import Section + +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 = ['firewall'] +zone_base = ['zone-policy'] +config = ConfigTree(config_file) + +if not config.exists(base) and not config.exists(zone_base): + # Nothing to do + exit(0) + +def migrate_interface(config, iftype, ifname, vif=None, vifs=None, vifc=None): + if_path = ['interfaces', iftype, ifname] + ifname_full = ifname + + if vif: + if_path += ['vif', vif] + ifname_full = f'{ifname}.{vif}' + elif vifs: + if_path += ['vif-s', vifs] + ifname_full = f'{ifname}.{vifs}' + if vifc: + if_path += ['vif-c', vifc] + ifname_full = f'{ifname}.{vifs}.{vifc}' + + if not config.exists(if_path + ['firewall']): + return + + if not config.exists(['firewall', 'interface']): + config.set(['firewall', 'interface']) + config.set_tag(['firewall', 'interface']) + + config.copy(if_path + ['firewall'], ['firewall', 'interface', ifname_full]) + config.delete(if_path + ['firewall']) + +for iftype in config.list_nodes(['interfaces']): + for ifname in config.list_nodes(['interfaces', iftype]): + migrate_interface(config, iftype, ifname) + + if config.exists(['interfaces', iftype, ifname, 'vif']): + for vif in config.list_nodes(['interfaces', iftype, ifname, 'vif']): + migrate_interface(config, iftype, ifname, vif=vif) + + if config.exists(['interfaces', iftype, ifname, 'vif-s']): + for vifs in config.list_nodes(['interfaces', iftype, ifname, 'vif-s']): + migrate_interface(config, iftype, ifname, vifs=vifs) + + if config.exists(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']): + for vifc in config.list_nodes(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']): + migrate_interface(config, iftype, ifname, vifs=vifs, vifc=vifc) + +if config.exists(zone_base + ['zone']): + config.set(['firewall', 'zone']) + config.set_tag(['firewall', 'zone']) + + for zone in config.list_nodes(zone_base + ['zone']): + config.copy(zone_base + ['zone', zone], ['firewall', 'zone', zone]) + config.delete(zone_base) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/ipoe-server/0-to-1 b/src/migration-scripts/ipoe-server/0-to-1 index f328ebced..da1f3f761 100755 --- a/src/migration-scripts/ipoe-server/0-to-1 +++ b/src/migration-scripts/ipoe-server/0-to-1 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# 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 @@ -14,8 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# - remove primary/secondary identifier from nameserver -# - Unifi RADIUS configuration by placing it all under "authentication radius" node +# - T4703: merge vlan-id and vlan-range to vlan CLI node + +# L2|L3 -> l2|l3 +# mac-address -> mac +# network-mode -> mode import os import sys @@ -37,97 +40,29 @@ base = ['service', 'ipoe-server'] if not config.exists(base): # Nothing to do exit(0) -else: - - # Migrate IPv4 DNS servers - dns_base = base + ['dns-server'] - if config.exists(dns_base): - for server in ['server-1', 'server-2']: - if config.exists(dns_base + [server]): - dns = config.return_value(dns_base + [server]) - config.set(base + ['name-server'], value=dns, replace=False) - - config.delete(dns_base) - - # Migrate IPv6 DNS servers - dns_base = base + ['dnsv6-server'] - if config.exists(dns_base): - for server in ['server-1', 'server-2', 'server-3']: - if config.exists(dns_base + [server]): - dns = config.return_value(dns_base + [server]) - config.set(base + ['name-server'], value=dns, replace=False) - - config.delete(dns_base) - - # Migrate radius-settings node to RADIUS and use this as base for the - # later migration of the RADIUS servers - this will save a lot of code - radius_settings = base + ['authentication', 'radius-settings'] - if config.exists(radius_settings): - config.rename(radius_settings, 'radius') - - # Migrate RADIUS dynamic author / change of authorisation server - dae_old = base + ['authentication', 'radius', 'dae-server'] - if config.exists(dae_old): - config.rename(dae_old, 'dynamic-author') - dae_new = base + ['authentication', 'radius', 'dynamic-author'] - - if config.exists(dae_new + ['ip-address']): - config.rename(dae_new + ['ip-address'], 'server') - - if config.exists(dae_new + ['secret']): - config.rename(dae_new + ['secret'], 'key') - # Migrate RADIUS server - radius_server = base + ['authentication', 'radius-server'] - if config.exists(radius_server): - new_base = base + ['authentication', 'radius', 'server'] - config.set(new_base) - config.set_tag(new_base) - for server in config.list_nodes(radius_server): - old_base = radius_server + [server] - config.copy(old_base, new_base + [server]) - - # migrate key - if config.exists(new_base + [server, 'secret']): - config.rename(new_base + [server, 'secret'], 'key') - - # remove old req-limit node - if config.exists(new_base + [server, 'req-limit']): - config.delete(new_base + [server, 'req-limit']) - - config.delete(radius_server) - - # Migrate IPv6 prefixes - ipv6_base = base + ['client-ipv6-pool'] - if config.exists(ipv6_base + ['prefix']): - prefix_old = config.return_values(ipv6_base + ['prefix']) - # delete old prefix CLI nodes - config.delete(ipv6_base + ['prefix']) - # create ned prefix tag node - config.set(ipv6_base + ['prefix']) - config.set_tag(ipv6_base + ['prefix']) - - for p in prefix_old: - prefix = p.split(',')[0] - mask = p.split(',')[1] - config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask) - - if config.exists(ipv6_base + ['delegate-prefix']): - prefix_old = config.return_values(ipv6_base + ['delegate-prefix']) - # delete old delegate prefix CLI nodes - config.delete(ipv6_base + ['delegate-prefix']) - # create ned delegation tag node - config.set(ipv6_base + ['delegate']) - config.set_tag(ipv6_base + ['delegate']) - - for p in prefix_old: - prefix = p.split(',')[0] - mask = p.split(',')[1] - config.set(ipv6_base + ['delegate', prefix, 'delegation-prefix'], value=mask) - - try: - with open(file_name, 'w') as f: - f.write(config.to_string()) - except OSError as e: - print("Failed to save the modified config: {}".format(e)) - exit(1) +if config.exists(base + ['authentication', 'interface']): + for interface in config.list_nodes(base + ['authentication', 'interface']): + config.rename(base + ['authentication', 'interface', interface, 'mac-address'], 'mac') + +for interface in config.list_nodes(base + ['interface']): + base_path = base + ['interface', interface] + for vlan in ['vlan-id', 'vlan-range']: + if config.exists(base_path + [vlan]): + print(interface, vlan) + for tmp in config.return_values(base_path + [vlan]): + config.set(base_path + ['vlan'], value=tmp, replace=False) + config.delete(base_path + [vlan]) + + if config.exists(base_path + ['network-mode']): + tmp = config.return_value(base_path + ['network-mode']) + config.delete(base_path + ['network-mode']) + # Change L2|L3 to lower case l2|l3 + config.set(base_path + ['mode'], value=tmp.lower()) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/ipsec/9-to-10 b/src/migration-scripts/ipsec/9-to-10 new file mode 100755 index 000000000..1254104cb --- /dev/null +++ b/src/migration-scripts/ipsec/9-to-10 @@ -0,0 +1,134 @@ +#!/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 re + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 + + +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 = ['vpn', 'ipsec'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +# IKE changes, T4118: +if config.exists(base + ['ike-group']): + for ike_group in config.list_nodes(base + ['ike-group']): + # replace 'ipsec ike-group <tag> mobike disable' + # => 'ipsec ike-group <tag> disable-mobike' + mobike = base + ['ike-group', ike_group, 'mobike'] + if config.exists(mobike): + if config.return_value(mobike) == 'disable': + config.set(base + ['ike-group', ike_group, 'disable-mobike']) + config.delete(mobike) + + # replace 'ipsec ike-group <tag> ikev2-reauth yes' + # => 'ipsec ike-group <tag> ikev2-reauth' + reauth = base + ['ike-group', ike_group, 'ikev2-reauth'] + if config.exists(reauth): + if config.return_value(reauth) == 'yes': + config.delete(reauth) + config.set(reauth) + else: + config.delete(reauth) + +# ESP changes +# replace 'ipsec esp-group <tag> compression enable' +# => 'ipsec esp-group <tag> compression' +if config.exists(base + ['esp-group']): + for esp_group in config.list_nodes(base + ['esp-group']): + compression = base + ['esp-group', esp_group, 'compression'] + if config.exists(compression): + if config.return_value(compression) == 'enable': + config.delete(compression) + config.set(compression) + else: + config.delete(compression) + +# PEER changes +if config.exists(base + ['site-to-site', 'peer']): + for peer in config.list_nodes(base + ['site-to-site', 'peer']): + peer_base = base + ['site-to-site', 'peer', peer] + + # replace: 'peer <tag> id x' + # => 'peer <tag> local-id x' + if config.exists(peer_base + ['authentication', 'id']): + config.rename(peer_base + ['authentication', 'id'], 'local-id') + + # For the peer '@foo' set remote-id 'foo' if remote-id is not defined + if peer.startswith('@'): + if not config.exists(peer_base + ['authentication', 'remote-id']): + tmp = peer.replace('@', '') + config.set(peer_base + ['authentication', 'remote-id'], value=tmp) + + # replace: 'peer <tag> force-encapsulation enable' + # => 'peer <tag> force-udp-encapsulation' + force_enc = peer_base + ['force-encapsulation'] + if config.exists(force_enc): + if config.return_value(force_enc) == 'enable': + config.delete(force_enc) + config.set(peer_base + ['force-udp-encapsulation']) + else: + config.delete(force_enc) + + # add option: 'peer <tag> remote-address x.x.x.x' + remote_address = peer + if peer.startswith('@'): + remote_address = 'any' + config.set(peer_base + ['remote-address'], value=remote_address) + # Peer name it is swanctl connection name and shouldn't contain dots or colons + # rename peer: + # peer 192.0.2.1 => peer peer_192-0-2-1 + # peer 2001:db8::2 => peer peer_2001-db8--2 + # peer @foo => peer peer_foo + re_peer_name = re.sub(':|\.', '-', peer) + if re_peer_name.startswith('@'): + re_peer_name = re.sub('@', '', re_peer_name) + new_peer_name = f'peer_{re_peer_name}' + + config.rename(peer_base, new_peer_name) + +# remote-access/road-warrior changes +if config.exists(base + ['remote-access', 'connection']): + for connection in config.list_nodes(base + ['remote-access', 'connection']): + ra_base = base + ['remote-access', 'connection', connection] + # replace: 'remote-access connection <tag> authentication id x' + # => 'remote-access connection <tag> authentication local-id x' + if config.exists(ra_base + ['authentication', 'id']): + config.rename(ra_base + ['authentication', 'id'], 'local-id') + +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/pppoe-server/5-to-6 b/src/migration-scripts/pppoe-server/5-to-6 new file mode 100755 index 000000000..e4888f4db --- /dev/null +++ b/src/migration-scripts/pppoe-server/5-to-6 @@ -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/>. + +# - T4703: merge vlan-id and vlan-range to vlan CLI node + +from vyos.configtree import ConfigTree +from sys import argv +from sys import exit + +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() + +config = ConfigTree(config_file) +base_path = ['service', 'pppoe-server', 'interface'] +if not config.exists(base_path): + # Nothing to do + exit(0) + +for interface in config.list_nodes(base_path): + for vlan in ['vlan-id', 'vlan-range']: + if config.exists(base_path + [interface, vlan]): + print(interface, vlan) + for tmp in config.return_values(base_path + [interface, vlan]): + config.set(base_path + [interface, 'vlan'], value=tmp, replace=False) + config.delete(base_path + [interface, vlan]) + +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/bridge.py b/src/op_mode/bridge.py index fe8dadd70..5a821a287 100755 --- a/src/op_mode/bridge.py +++ b/src/op_mode/bridge.py @@ -22,7 +22,7 @@ import typing from sys import exit from tabulate import tabulate -from vyos.util import cmd +from vyos.util import cmd, rc_cmd from vyos.util import dict_search import vyos.opmode @@ -57,7 +57,11 @@ def _get_raw_data_fdb(bridge): """Get MAC-address for the bridge brX :returns list """ - json_data = cmd(f'sudo bridge --json fdb show br {bridge}') + code, json_data = rc_cmd(f'sudo bridge --json fdb show br {bridge}') + # From iproute2 fdb.c, fdb_show() will only exit(-1) in case of + # non-existent bridge device; raise error. + if code == 255: + raise vyos.opmode.UnconfiguredSubsystem(f"no such bridge device {bridge}") data_dict = json.loads(json_data) return data_dict diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py index 1339d5b92..a0496dedb 100755 --- a/src/op_mode/nat.py +++ b/src/op_mode/nat.py @@ -60,7 +60,7 @@ def _get_json_data(direction, family): if direction == 'destination': chain = 'PREROUTING' family = 'ip6' if family == 'inet6' else 'ip' - return cmd(f'sudo nft --json list chain {family} nat {chain}') + return cmd(f'sudo nft --json list chain {family} vyos_nat {chain}') def _get_raw_data_rules(direction, family): diff --git a/src/op_mode/route.py b/src/op_mode/route.py index e1eee5bbf..e1eee5bbf 100644..100755 --- a/src/op_mode/route.py +++ b/src/op_mode/route.py diff --git a/src/op_mode/show_nat66_statistics.py b/src/op_mode/show_nat66_statistics.py index bc81692ae..cb10aed9f 100755 --- a/src/op_mode/show_nat66_statistics.py +++ b/src/op_mode/show_nat66_statistics.py @@ -44,7 +44,7 @@ group.add_argument("--destination", help="Show statistics for configured destina args = parser.parse_args() if args.source or args.destination: - tmp = cmd('sudo nft -j list table ip6 nat') + tmp = cmd('sudo nft -j list table ip6 vyos_nat') tmp = json.loads(tmp) source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }" diff --git a/src/op_mode/show_nat_statistics.py b/src/op_mode/show_nat_statistics.py index c568c8305..be41e083b 100755 --- a/src/op_mode/show_nat_statistics.py +++ b/src/op_mode/show_nat_statistics.py @@ -44,7 +44,7 @@ group.add_argument("--destination", help="Show statistics for configured destina args = parser.parse_args() if args.source or args.destination: - tmp = cmd('sudo nft -j list table ip nat') + tmp = cmd('sudo nft -j list table ip vyos_nat') tmp = json.loads(tmp) source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }" diff --git a/src/op_mode/storage.py b/src/op_mode/storage.py new file mode 100755 index 000000000..75964c493 --- /dev/null +++ b/src/op_mode/storage.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 sys + +import vyos.opmode +from vyos.util import cmd + + +def _get_system_storage(only_persistent=False): + if not only_persistent: + cmd_str = 'df -h -x squashf' + else: + cmd_str = 'df -h -t ext4 --output=source,size,used,avail,pcent' + + res = cmd(cmd_str) + + return res + +def _get_raw_data(): + out = _get_system_storage(only_persistent=True) + lines = out.splitlines() + lists = [l.split() for l in lines] + res = {lists[0][i]: lists[1][i] for i in range(len(lists[0]))} + + return res + +def _get_formatted_output(): + return _get_system_storage() + +def show(raw: bool): + if raw: + return _get_raw_data() + + return _get_formatted_output() + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) + diff --git a/src/op_mode/system.py b/src/op_mode/system.py new file mode 100755 index 000000000..11a3a8730 --- /dev/null +++ b/src/op_mode/system.py @@ -0,0 +1,92 @@ +#!/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 jmespath +import json +import sys +import requests +import typing + +from sys import exit + +from vyos.configquery import ConfigTreeQuery + +import vyos.opmode +import vyos.version + +config = ConfigTreeQuery() +base = ['system', 'update-check'] + + +def _compare_version_raw(): + url = config.value(base + ['url']) + local_data = vyos.version.get_full_version_data() + remote_data = vyos.version.get_remote_version(url) + if not remote_data: + return {"error": True, + "reason": "Unable to get remote version"} + if local_data.get('version') and remote_data: + local_version = local_data.get('version') + remote_version = jmespath.search('[0].version', remote_data) + image_url = jmespath.search('[0].url', remote_data) + if local_data.get('version') != remote_version: + return {"error": False, + "update_available": True, + "local_version": local_version, + "remote_version": remote_version, + "url": image_url} + return {"update_available": False, + "local_version": local_version, + "remote_version": remote_version} + + +def _formatted_compare_version(data): + local_version = data.get('local_version') + remote_version = data.get('remote_version') + url = data.get('url') + if {'update_available','local_version', 'remote_version', 'url'} <= set(data): + return f'Current version: {local_version}\n\nUpdate available: {remote_version}\nUpdate URL: {url}' + elif local_version == remote_version and remote_version is not None: + return f'No available updates for your system \n' \ + f'current version: {local_version}\nremote version: {remote_version}' + else: + return 'Update not found' + + +def _verify(): + if not config.exists(base): + return False + return True + + +def show_update(raw: bool): + if not _verify(): + raise vyos.opmode.UnconfiguredSubsystem("system update-check not configured") + data = _compare_version_raw() + if raw: + return data + else: + return _formatted_compare_version(data) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/show_uptime.py b/src/op_mode/uptime.py index b70c60cf8..2ebe6783b 100755 --- a/src/op_mode/show_uptime.py +++ b/src/op_mode/uptime.py @@ -14,7 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -def get_uptime_seconds(): +import sys + +import vyos.opmode + +def _get_uptime_seconds(): from re import search from vyos.util import read_file @@ -23,7 +27,7 @@ def get_uptime_seconds(): return int(float(seconds)) -def get_load_averages(): +def _get_load_averages(): from re import search from vyos.util import cmd from vyos.cpu import get_core_count @@ -40,19 +44,17 @@ def get_load_averages(): return res -def get_raw_data(): +def _get_raw_data(): from vyos.util import seconds_to_human res = {} - res["uptime_seconds"] = get_uptime_seconds() - res["uptime"] = seconds_to_human(get_uptime_seconds()) - res["load_average"] = get_load_averages() + res["uptime_seconds"] = _get_uptime_seconds() + res["uptime"] = seconds_to_human(_get_uptime_seconds()) + res["load_average"] = _get_load_averages() return res -def get_formatted_output(): - data = get_raw_data() - +def _get_formatted_output(data): out = "Uptime: {}\n\n".format(data["uptime"]) avgs = data["load_average"] out += "Load averages:\n" @@ -62,5 +64,19 @@ def get_formatted_output(): return out +def show(raw: bool): + uptime_data = _get_raw_data() + + if raw: + return uptime_data + else: + return _get_formatted_output(uptime_data) + if __name__ == '__main__': - print(get_formatted_output()) + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 1b77cff87..5ccc9b0b6 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -90,11 +90,12 @@ def make_mutation_resolver(mutation_name, class_name, session_func): } except OpModeError as e: typename = type(e).__name__ + msg = str(e) return { "success": False, "errore": ['op_mode_error'], "op_mode_error": {"name": f"{typename}", - "message": op_mode_err_msg.get(typename, "Unknown"), + "message": msg if msg else op_mode_err_msg.get(typename, "Unknown"), "vyos_code": op_mode_err_code.get(typename, 9999)} } except Exception as error: diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index 8ae61b704..b46914dcc 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -90,11 +90,12 @@ def make_query_resolver(query_name, class_name, session_func): } except OpModeError as e: typename = type(e).__name__ + msg = str(e) return { "success": False, "errors": ['op_mode_error'], "op_mode_error": {"name": f"{typename}", - "message": op_mode_err_msg.get(typename, "Unknown"), + "message": msg if msg else op_mode_err_msg.get(typename, "Unknown"), "vyos_code": op_mode_err_code.get(typename, 9999)} } except Exception as error: diff --git a/src/services/api/graphql/session/composite/system_status.py b/src/services/api/graphql/session/composite/system_status.py index 8dadcc9f3..3c1a3d45b 100755 --- a/src/services/api/graphql/session/composite/system_status.py +++ b/src/services/api/graphql/session/composite/system_status.py @@ -30,8 +30,8 @@ def get_system_version() -> dict: return show_version.show(raw=True, funny=False) def get_system_uptime() -> dict: - show_uptime = load_op_mode_as_module('show_uptime.py') - return show_uptime.get_raw_data() + show_uptime = load_op_mode_as_module('uptime.py') + return show_uptime._get_raw_data() def get_system_ram_usage() -> dict: show_ram = load_op_mode_as_module('memory.py') diff --git a/src/system/vyos-system-update-check.py b/src/system/vyos-system-update-check.py new file mode 100755 index 000000000..c9597721b --- /dev/null +++ b/src/system/vyos-system-update-check.py @@ -0,0 +1,70 @@ +#!/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 jmespath + +from pathlib import Path +from sys import exit +from time import sleep + +from vyos.util import call + +import vyos.version + +motd_file = Path('/run/motd.d/10-vyos-update') + + +if __name__ == '__main__': + # Parse command arguments and get config + parser = argparse.ArgumentParser() + parser.add_argument('-c', + '--config', + action='store', + help='Path to system-update-check configuration', + required=True, + type=Path) + + args = parser.parse_args() + try: + config_path = Path(args.config) + config = json.loads(config_path.read_text()) + except Exception as err: + print( + f'Configuration file "{config_path}" does not exist or malformed: {err}' + ) + exit(1) + + url_json = config.get('url') + local_data = vyos.version.get_full_version_data() + local_version = local_data.get('version') + + while True: + remote_data = vyos.version.get_remote_version(url_json) + if remote_data: + url = jmespath.search('[0].url', remote_data) + remote_version = jmespath.search('[0].version', remote_data) + if local_version != remote_version and remote_version: + call(f'wall -n "Update available: {remote_version} \nUpdate URL: {url}"') + # MOTD used in /run/motd.d/10-update + motd_file.parent.mkdir(exist_ok=True) + motd_file.write_text(f'---\n' + f'Current version: {local_version}\n' + f'Update available: \033[1;34m{remote_version}\033[0m\n' + f'---\n') + # Check every 12 hours + sleep(43200) diff --git a/src/systemd/vyos-system-update.service b/src/systemd/vyos-system-update.service new file mode 100644 index 000000000..032e5a14c --- /dev/null +++ b/src/systemd/vyos-system-update.service @@ -0,0 +1,11 @@ +[Unit] +Description=VyOS system udpate-check service +After=network.target vyos-router.service + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/python3 /usr/libexec/vyos/system/vyos-system-update-check.py --config /run/vyos-system-update.conf + +[Install] +WantedBy=multi-user.target diff --git a/src/validators/range b/src/validators/range deleted file mode 100755 index d4c25f3c4..000000000 --- a/src/validators/range +++ /dev/null @@ -1,56 +0,0 @@ -#!/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 re -import sys -import argparse - -class MalformedRange(Exception): - pass - -def validate_range(value, min=None, max=None): - try: - lower, upper = re.match(r'^(\d+)-(\d+)$', value).groups() - - lower, upper = int(lower), int(upper) - - if int(lower) > int(upper): - raise MalformedRange("the lower bound exceeds the upper bound".format(value)) - - if min is not None: - if lower < min: - raise MalformedRange("the lower bound must not be less than {}".format(min)) - - if max is not None: - if upper > max: - raise MalformedRange("the upper bound must not be greater than {}".format(max)) - - except (AttributeError, ValueError): - raise MalformedRange("range syntax error") - -parser = argparse.ArgumentParser(description='Range validator.') -parser.add_argument('--min', type=int, action='store') -parser.add_argument('--max', type=int, action='store') -parser.add_argument('value', action='store') - -if __name__ == '__main__': - args = parser.parse_args() - - try: - validate_range(args.value, min=args.min, max=args.max) - except MalformedRange as e: - print("Incorrect range '{}': {}".format(args.value, e)) - sys.exit(1) |