diff options
Diffstat (limited to 'src')
33 files changed, 862 insertions, 425 deletions
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 783adec46..38a332be3 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.configdep import set_dependents, call_dependents # from vyos.configverify import verify_interface_exists from vyos.firewall import fqdn_config_parse from vyos.firewall import geoip_update @@ -41,8 +42,8 @@ from vyos import ConfigError from vyos import airbag airbag.enable() -nat_conf_script = '/usr/libexec/vyos/conf_mode/nat.py' -policy_route_conf_script = '/usr/libexec/vyos/conf_mode/policy-route.py' +nat_conf_script = 'nat.py' +policy_route_conf_script = 'policy-route.py' nftables_conf = '/run/nftables.conf' @@ -160,6 +161,9 @@ def get_config(config=None): firewall['zone'][zone] = dict_merge(default_values, firewall['zone'][zone]) firewall['group_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) + if firewall['group_resync']: + # Update nat and policy-route as firewall groups were updated + set_dependents('group_resync', conf) if 'config_trap' in firewall and firewall['config_trap'] == 'enable': diff = get_config_diff(conf) @@ -464,18 +468,6 @@ def post_apply_trap(firewall): cmd(base_cmd + ' '.join(objects)) -def resync_nat(): - # Update nat as firewall groups were updated - tmp, out = rc_cmd(nat_conf_script) - if tmp > 0: - Warning(f'Failed to re-apply nat configuration! {out}') - -def resync_policy_route(): - # Update policy route as firewall groups were updated - tmp, out = rc_cmd(policy_route_conf_script) - if tmp > 0: - Warning(f'Failed to re-apply policy route configuration! {out}') - def apply(firewall): install_result, output = rc_cmd(f'nft -f {nftables_conf}') if install_result == 1: @@ -484,8 +476,7 @@ def apply(firewall): apply_sysfs(firewall) if firewall['group_resync']: - resync_nat() - resync_policy_route() + call_dependents() # T970 Enable a resolver (systemd daemon) that checks # domain-group/fqdn addresses and update entries for domains by timeout diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index e02841831..b49c945cd 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -175,7 +175,7 @@ def generate(ethernet): loaded_pki_cert = load_certificate(pki_cert['certificate']) loaded_ca_certs = {load_certificate(c['certificate']) - for c in ethernet['pki']['ca'].values()} + for c in ethernet['pki']['ca'].values()} if 'ca' in ethernet['pki'] else {} cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs) diff --git a/src/conf_mode/interfaces-virtual-ethernet.py b/src/conf_mode/interfaces-virtual-ethernet.py new file mode 100755 index 000000000..53422ad2d --- /dev/null +++ b/src/conf_mode/interfaces-virtual-ethernet.py @@ -0,0 +1,105 @@ +#!/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/>. + +from sys import exit + +from netifaces import interfaces +from vyos import ConfigError +from vyos import airbag +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_vrf +from vyos.ifconfig import VethIf + +airbag.enable() + +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at + least the interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'virtual-ethernet'] + ifname, veth = get_interface_dict(conf, base) + + # We need to know all other veth related interfaces as veth requires a 1:1 + # mapping for the peer-names. The Linux kernel automatically creates both + # interfaces, the local one and the peer-name, but VyOS also needs a peer + # interfaces configrued on the CLI so we can assign proper IP addresses etc. + veth['other_interfaces'] = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + + return veth + + +def verify(veth): + if 'deleted' in veth: + verify_bridge_delete(veth) + # Prevent to delete veth interface which used for another "vethX peer-name" + for iface, iface_config in veth['other_interfaces'].items(): + if veth['ifname'] in iface_config['peer_name']: + ifname = veth['ifname'] + raise ConfigError( + f'Cannot delete "{ifname}" used for "interface {iface} peer-name"' + ) + return None + + verify_vrf(veth) + verify_address(veth) + + if 'peer_name' not in veth: + raise ConfigError(f'Remote peer name must be set for "{veth["ifname"]}"!') + + if veth['peer_name'] not in veth['other_interfaces']: + peer_name = veth['peer_name'] + ifname = veth['ifname'] + raise ConfigError(f'Used peer-name "{peer_name}" on interface "{ifname}" ' \ + 'is not configured!') + + return None + + +def generate(peth): + return None + +def apply(veth): + # Check if the Veth interface already exists + if 'rebuild_required' in veth or 'deleted' in veth: + if veth['ifname'] in interfaces(): + p = VethIf(veth['ifname']) + p.remove() + + if 'deleted' not in veth: + p = VethIf(**veth) + p.update(veth) + + 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/pki.py b/src/conf_mode/pki.py index 29ed7b1b7..e8f3cc87a 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -16,20 +16,16 @@ from sys import exit -import jmespath - from vyos.config import Config +from vyos.configdep import set_dependents, call_dependents from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.pki import is_ca_certificate from vyos.pki import load_certificate -from vyos.pki import load_certificate_request from vyos.pki import load_public_key from vyos.pki import load_private_key from vyos.pki import load_crl from vyos.pki import load_dh_parameters -from vyos.util import ask_input -from vyos.util import call from vyos.util import dict_search_args from vyos.util import dict_search_recursive from vyos.xml import defaults @@ -121,6 +117,39 @@ def get_config(config=None): get_first_key=True, no_tag_node_value_mangle=True) + if 'changed' in pki: + for search in sync_search: + for key in search['keys']: + changed_key = sync_translate[key] + + if changed_key not in pki['changed']: + continue + + for item_name in pki['changed'][changed_key]: + node_present = False + if changed_key == 'openvpn': + node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) + else: + node_present = dict_search_args(pki, changed_key, item_name) + + if node_present: + search_dict = dict_search_args(pki['system'], *search['path']) + + if not search_dict: + continue + + for found_name, found_path in dict_search_recursive(search_dict, key): + if found_name == item_name: + path = search['path'] + path_str = ' '.join(path + found_path) + print(f'pki: Updating config: {path_str} {found_name}') + + if path[0] == 'interfaces': + ifname = found_path[0] + set_dependents(path[1], conf, ifname) + else: + set_dependents(path[1], conf) + return pki def is_valid_certificate(raw_data): @@ -259,37 +288,7 @@ def apply(pki): return None if 'changed' in pki: - for search in sync_search: - for key in search['keys']: - changed_key = sync_translate[key] - - if changed_key not in pki['changed']: - continue - - for item_name in pki['changed'][changed_key]: - node_present = False - if changed_key == 'openvpn': - node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) - else: - node_present = dict_search_args(pki, changed_key, item_name) - - if node_present: - search_dict = dict_search_args(pki['system'], *search['path']) - - if not search_dict: - continue - - for found_name, found_path in dict_search_recursive(search_dict, key): - if found_name == item_name: - path_str = ' '.join(search['path'] + found_path) - print(f'pki: Updating config: {path_str} {found_name}') - - script = search['script'] - if found_path[0] == 'interfaces': - ifname = found_path[2] - call(f'VYOS_TAGNODE_VALUE={ifname} {script}') - else: - call(script) + call_dependents() return None diff --git a/src/conf_mode/policy-route-interface.py b/src/conf_mode/policy-route-interface.py deleted file mode 100755 index 58c5fd93d..000000000 --- a/src/conf_mode/policy-route-interface.py +++ /dev/null @@ -1,132 +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.ifconfig import Section -from vyos.template import render -from vyos.util import cmd -from vyos.util import run -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - ifname = argv[1] - ifpath = Section.get_config_path(ifname) - if_policy_path = f'interfaces {ifpath} policy' - - if_policy = conf.get_config_dict(if_policy_path, key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - - if_policy['ifname'] = ifname - if_policy['policy'] = conf.get_config_dict(['policy'], key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - - return if_policy - -def verify_chain(table, chain): - # Verify policy route applied - code = run(f'nft list chain {table} {chain}') - return code == 0 - -def verify(if_policy): - # bail out early - looks like removal from running config - if not if_policy: - return None - - for route in ['route', 'route6']: - if route in if_policy: - if route not in if_policy['policy']: - raise ConfigError('Policy route not configured') - - route_name = if_policy[route] - - if route_name not in if_policy['policy'][route]: - raise ConfigError(f'Invalid policy route name "{name}"') - - nft_prefix = 'VYOS_PBR6_' if route == 'route6' else 'VYOS_PBR_' - nft_table = 'ip6 mangle' if route == 'route6' else 'ip mangle' - - if not verify_chain(nft_table, nft_prefix + route_name): - raise ConfigError('Policy route did not apply') - - return None - -def generate(if_policy): - return None - -def cleanup_rule(table, chain, ifname, new_name=None): - results = cmd(f'nft -a list chain {table} {chain}').split("\n") - retval = None - for line in results: - if f'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: - cmd(f'nft delete rule {table} {chain} handle {handle_search[1]}') - return retval - -def apply(if_policy): - ifname = if_policy['ifname'] - - route_chain = 'VYOS_PBR_PREROUTING' - ipv6_route_chain = 'VYOS_PBR6_PREROUTING' - - if 'route' in if_policy: - name = 'VYOS_PBR_' + if_policy['route'] - rule_exists = cleanup_rule('ip mangle', route_chain, ifname, name) - - if not rule_exists: - cmd(f'nft insert rule ip mangle {route_chain} iifname {ifname} counter jump {name}') - else: - cleanup_rule('ip mangle', route_chain, ifname) - - if 'route6' in if_policy: - name = 'VYOS_PBR6_' + if_policy['route6'] - rule_exists = cleanup_rule('ip6 mangle', ipv6_route_chain, ifname, name) - - if not rule_exists: - cmd(f'nft insert rule ip6 mangle {ipv6_route_chain} iifname {ifname} counter jump {name}') - else: - cleanup_rule('ip6 mangle', ipv6_route_chain, 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/policy-route.py b/src/conf_mode/policy-route.py index 00539b9c7..1d016695e 100755 --- a/src/conf_mode/policy-route.py +++ b/src/conf_mode/policy-route.py @@ -15,7 +15,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import re from json import loads from sys import exit @@ -25,7 +24,6 @@ from vyos.config import Config from vyos.template import render from vyos.util import cmd from vyos.util import dict_search_args -from vyos.util import dict_search_recursive from vyos.util import run from vyos import ConfigError from vyos import airbag @@ -34,48 +32,13 @@ airbag.enable() mark_offset = 0x7FFFFFFF nftables_conf = '/run/nftables_policy.conf' -ROUTE_PREFIX = 'VYOS_PBR_' -ROUTE6_PREFIX = 'VYOS_PBR6_' - -preserve_chains = [ - 'VYOS_PBR_PREROUTING', - 'VYOS_PBR_POSTROUTING', - 'VYOS_PBR6_PREROUTING', - 'VYOS_PBR6_POSTROUTING' -] - valid_groups = [ 'address_group', + 'domain_group', 'network_group', 'port_group' ] -group_set_prefix = { - 'A_': 'address_group', - 'A6_': 'ipv6_address_group', -# 'D_': 'domain_group', - 'M_': 'mac_group', - 'N_': 'network_group', - 'N6_': 'ipv6_network_group', - 'P_': 'port_group' -} - -def get_policy_interfaces(conf): - out = {} - interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - def find_interfaces(iftype_conf, output={}, prefix=''): - for ifname, if_conf in iftype_conf.items(): - if 'policy' in if_conf: - output[prefix + ifname] = if_conf['policy'] - 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_config(config=None): if config: conf = config @@ -88,7 +51,6 @@ def get_config(config=None): policy['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - policy['interfaces'] = get_policy_interfaces(conf) return policy @@ -132,8 +94,8 @@ def verify_rule(policy, name, rule_conf, ipv6, rule_id): side_conf = rule_conf[side] if 'group' in side_conf: - if {'address_group', 'network_group'} <= set(side_conf['group']): - raise ConfigError('Only one address-group or network-group can be specified') + if len({'address_group', 'domain_group', 'network_group'} & set(side_conf['group'])) > 1: + raise ConfigError('Only one address-group, domain-group or network-group can be specified') for group in valid_groups: if group in side_conf['group']: @@ -168,73 +130,11 @@ def verify(policy): for rule_id, rule_conf in pol_conf['rule'].items(): verify_rule(policy, name, rule_conf, ipv6, rule_id) - for ifname, if_policy in policy['interfaces'].items(): - name = dict_search_args(if_policy, 'route') - ipv6_name = dict_search_args(if_policy, 'route6') - - if name and not dict_search_args(policy, 'route', name): - raise ConfigError(f'Policy route "{name}" is still referenced on interface {ifname}') - - if ipv6_name and not dict_search_args(policy, 'route6', ipv6_name): - raise ConfigError(f'Policy route6 "{ipv6_name}" is still referenced on interface {ifname}') - return None -def cleanup_commands(policy): - commands = [] - commands_chains = [] - commands_sets = [] - for table in ['ip mangle', 'ip6 mangle']: - route_node = 'route' if table == 'ip mangle' else 'route6' - chain_prefix = ROUTE_PREFIX if table == 'ip mangle' else ROUTE6_PREFIX - - json_str = cmd(f'nft -t -j list table {table}') - obj = loads(json_str) - if 'nftables' not in obj: - continue - for item in obj['nftables']: - if 'chain' in item: - chain = item['chain']['name'] - if chain in preserve_chains or not chain.startswith("VYOS_PBR"): - continue - - if dict_search_args(policy, route_node, chain.replace(chain_prefix, "", 1)) != None: - commands.append(f'flush chain {table} {chain}') - else: - commands_chains.append(f'delete chain {table} {chain}') - - if 'rule' in item: - rule = item['rule'] - chain = rule['chain'] - handle = rule['handle'] - - if chain not in preserve_chains: - continue - - target, _ = next(dict_search_recursive(rule['expr'], 'target')) - - if target.startswith(chain_prefix): - if dict_search_args(policy, route_node, target.replace(chain_prefix, "", 1)) == None: - commands.append(f'delete rule {table} {chain} handle {handle}') - - if 'set' in item: - set_name = item['set']['name'] - - for prefix, group_type in group_set_prefix.items(): - if set_name.startswith(prefix): - group_name = set_name.replace(prefix, "", 1) - if dict_search_args(policy, 'firewall_group', group_type, group_name) != None: - commands_sets.append(f'flush set {table} {set_name}') - else: - commands_sets.append(f'delete set {table} {set_name}') - - return commands + commands_chains + commands_sets - def generate(policy): if not os.path.exists(nftables_conf): policy['first_install'] = True - else: - policy['cleanup_commands'] = cleanup_commands(policy) render(nftables_conf, 'firewall/nftables-policy.j2', policy) return None diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py index a0d288e91..331194fec 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -167,6 +167,11 @@ def verify(policy): continue for rule, rule_config in route_map_config['rule'].items(): + # Action 'deny' cannot be used with "continue" + # FRR does not validate it T4827 + if rule_config['action'] == 'deny' and 'continue' in rule_config: + raise ConfigError(f'rule {rule} "continue" cannot be used with action deny!') + # Specified community-list must exist tmp = dict_search('match.community.community_list', rule_config) diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index 5da8e7b06..73af6595b 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -24,6 +24,7 @@ from vyos.template import render_to_string from vyos.util import dict_search from vyos.util import read_file from vyos.util import sysctl_write +from vyos.configverify import verify_interface_exists from vyos import ConfigError from vyos import frr from vyos import airbag @@ -46,6 +47,10 @@ def verify(mpls): if not mpls: return None + if 'interface' in mpls: + for interface in mpls['interface']: + verify_interface_exists(interface) + # Checks to see if LDP is properly configured if 'ldp' in mpls: # If router ID not defined diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index cfefcfbe8..b79e9847a 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -22,6 +22,7 @@ from sys import exit from time import sleep from time import time +from vyos.base import Warning from vyos.config import Config from vyos.configdict import leaf_node_changed from vyos.configverify import verify_interface_exists @@ -438,6 +439,10 @@ def verify(ipsec): if 'local_address' in peer_conf and 'dhcp_interface' in peer_conf: raise ConfigError(f"A single local-address or dhcp-interface is required when using VTI on site-to-site peer {peer}") + if dict_search('options.disable_route_autoinstall', + ipsec) == None: + Warning('It\'s recommended to use ipsec vty with the next command\n[set vpn ipsec option disable-route-autoinstall]') + if 'bind' in peer_conf['vti']: vti_interface = peer_conf['vti']['bind'] if not os.path.exists(f'/sys/class/net/{vti_interface}'): diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf index b1902b585..518abeaec 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf +++ b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf @@ -33,8 +33,8 @@ if /usr/bin/systemctl -q is-active vyos-hostsd; then if [ -n "$new_dhcp6_name_servers" ]; then logmsg info "Deleting nameservers with tag \"dhcpv6-$interface\" via vyos-hostsd-client" $hostsd_client --delete-name-servers --tag "dhcpv6-$interface" - logmsg info "Adding nameservers \"$new_dhcpv6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" - $hostsd_client --add-name-servers $new_dhcpv6_name_servers --tag "dhcpv6-$interface" + logmsg info "Adding nameservers \"$new_dhcp6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" + $hostsd_client --add-name-servers $new_dhcp6_name_servers --tag "dhcpv6-$interface" hostsd_changes=y fi diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup index ad6a1d5eb..da1bda137 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup +++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup @@ -8,7 +8,7 @@ hostsd_changes= /usr/bin/systemctl -q is-active vyos-hostsd hostsd_status=$? -if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then +if [[ $reason =~ ^(EXPIRE|FAIL|RELEASE|STOP)$ ]]; then if [[ $hostsd_status -eq 0 ]]; then # delete search domains and nameservers via vyos-hostsd logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client" @@ -96,7 +96,7 @@ if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then fi fi -if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then +if [[ $reason =~ ^(EXPIRE6|RELEASE6|STOP6)$ ]]; then if [[ $hostsd_status -eq 0 ]]; then # delete search domains and nameservers via vyos-hostsd logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client" diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook index eeb8b0782..49bb18372 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook @@ -8,12 +8,12 @@ # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 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. -# +# # This code was originally developed by Vyatta, Inc. # Portions created by Vyatta are Copyright (C) 2006, 2007, 2008 Vyatta, Inc. # All Rights Reserved. @@ -23,7 +23,7 @@ RUN="yes" proto="" -if [[ $reason =~ (REBOOT6|INIT6|EXPIRE6|RELEASE6|STOP6|INFORM6|BOUND6|REBIND6|DELEGATED6) ]]; then +if [[ $reason =~ ^(REBOOT6|INIT6|EXPIRE6|RELEASE6|STOP6|INFORM6|BOUND6|REBIND6|DELEGATED6)$ ]]; then proto="v6" fi diff --git a/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers b/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers new file mode 100755 index 000000000..222c75f21 --- /dev/null +++ b/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers @@ -0,0 +1,15 @@ +#!/bin/bash +### Autogenerated by interfaces-pppoe.py ### + +interface=$6 +if [ -z "$interface" ]; then + exit +fi + +if ! /usr/bin/systemctl -q is-active vyos-hostsd; then + exit # vyos-hostsd is not running +fi + +hostsd_client="/usr/bin/vyos-hostsd-client" +$hostsd_client --delete-name-servers --tag "dhcp-$interface" +$hostsd_client --apply diff --git a/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers b/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers new file mode 100755 index 000000000..0fcedbedc --- /dev/null +++ b/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers @@ -0,0 +1,24 @@ +#!/bin/bash +### Autogenerated by interfaces-pppoe.py ### + +interface=$6 +if [ -z "$interface" ]; then + exit +fi + +if ! /usr/bin/systemctl -q is-active vyos-hostsd; then + exit # vyos-hostsd is not running +fi + +hostsd_client="/usr/bin/vyos-hostsd-client" + +$hostsd_client --delete-name-servers --tag "dhcp-$interface" + +if [ "$USEPEERDNS" ] && [ -n "$DNS1" ]; then +$hostsd_client --add-name-servers "$DNS1" --tag "dhcp-$interface" +fi +if [ "$USEPEERDNS" ] && [ -n "$DNS2" ]; then +$hostsd_client --add-name-servers "$DNS2" --tag "dhcp-$interface" +fi + +$hostsd_client --apply diff --git a/src/helpers/vyos-domain-resolver.py b/src/helpers/vyos-domain-resolver.py index 035c208b2..e31d9238e 100755 --- a/src/helpers/vyos-domain-resolver.py +++ b/src/helpers/vyos-domain-resolver.py @@ -35,13 +35,13 @@ cache = False domain_state = {} ipv4_tables = { - 'ip mangle', + 'ip vyos_mangle', 'ip vyos_filter', 'ip vyos_nat' } ipv6_tables = { - 'ip6 mangle', + 'ip6 vyos_mangle', 'ip6 vyos_filter' } diff --git a/src/migration-scripts/policy/4-to-5 b/src/migration-scripts/policy/4-to-5 new file mode 100755 index 000000000..33c9e6ade --- /dev/null +++ b/src/migration-scripts/policy/4-to-5 @@ -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/>. + +# T2199: Migrate interface policy nodes to policy route <name> interface <ifname> + +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() + +base4 = ['policy', 'route'] +base6 = ['policy', 'route6'] +config = ConfigTree(config_file) + +if not config.exists(base4) and not config.exists(base6): + # 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 + ['policy']): + return + + if config.exists(if_path + ['policy', 'route']): + route_name = config.return_value(if_path + ['policy', 'route']) + config.set(base4 + [route_name, 'interface'], value=ifname_full, replace=False) + + if config.exists(if_path + ['policy', 'route6']): + route_name = config.return_value(if_path + ['policy', 'route6']) + config.set(base6 + [route_name, 'interface'], value=ifname_full, replace=False) + + config.delete(if_path + ['policy']) + +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) + +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/op_mode/accelppp.py b/src/op_mode/accelppp.py new file mode 100755 index 000000000..2fd045dc3 --- /dev/null +++ b/src/op_mode/accelppp.py @@ -0,0 +1,133 @@ +#!/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.accel_ppp +import vyos.opmode + +from vyos.configquery import ConfigTreeQuery +from vyos.util import rc_cmd + + +accel_dict = { + 'ipoe': { + 'port': 2002, + 'path': 'service ipoe-server' + }, + 'pppoe': { + 'port': 2001, + 'path': 'service pppoe-server' + }, + 'pptp': { + 'port': 2003, + 'path': 'vpn pptp' + }, + 'l2tp': { + 'port': 2004, + 'path': 'vpn l2tp' + }, + 'sstp': { + 'port': 2005, + 'path': 'vpn sstp' + } +} + + +def _get_raw_statistics(accel_output, pattern): + return vyos.accel_ppp.get_server_statistics(accel_output, pattern, sep=':') + + +def _get_raw_sessions(port): + cmd_options = 'show sessions ifname,username,ip,ip6,ip6-dp,type,state,' \ + 'uptime-raw,calling-sid,called-sid,sid,comp,rx-bytes-raw,' \ + 'tx-bytes-raw,rx-pkts,tx-pkts' + output = vyos.accel_ppp.accel_cmd(port, cmd_options) + parsed_data: list[dict[str, str]] = vyos.accel_ppp.accel_out_parse( + output.splitlines()) + return parsed_data + + +def _verify(func): + """Decorator checks if accel-ppp protocol + ipoe/pppoe/pptp/l2tp/sstp is configured + + for example: + service ipoe-server + vpn sstp + """ + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + protocol_list = accel_dict.keys() + protocol = kwargs.get('protocol') + # unknown or incorrect protocol query + if protocol not in protocol_list: + unconf_message = f'unknown protocol "{protocol}"' + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + # Check if config does not exist + config_protocol_path = accel_dict[protocol]['path'] + if not config.exists(config_protocol_path): + unconf_message = f'"{config_protocol_path}" is not configured' + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + return func(*args, **kwargs) + + return _wrapper + + +@_verify +def show_statistics(raw: bool, protocol: str): + """show accel-cmd statistics + CPU utilization and amount of sessions + + protocol: ipoe/pppoe/ppptp/l2tp/sstp + """ + pattern = f'{protocol}:' + port = accel_dict[protocol]['port'] + rc, output = rc_cmd(f'/usr/bin/accel-cmd -p {port} show stat') + + if raw: + return _get_raw_statistics(output, pattern) + + return output + + +@_verify +def show_sessions(raw: bool, protocol: str): + """show accel-cmd sessions + + protocol: ipoe/pppoe/ppptp/l2tp/sstp + """ + port = accel_dict[protocol]['port'] + if raw: + return _get_raw_sessions(port) + + return vyos.accel_ppp.accel_cmd(port, + 'show sessions ifname,username,ip,ip6,ip6-dp,' + 'calling-sid,rate-limit,state,uptime,rx-bytes,tx-bytes') + + +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/bridge.py b/src/op_mode/bridge.py index 5a821a287..d6098c158 100755 --- a/src/op_mode/bridge.py +++ b/src/op_mode/bridge.py @@ -32,7 +32,7 @@ def _get_json_data(): """ Get bridge data format JSON """ - return cmd(f'sudo bridge --json link show') + return cmd(f'bridge --json link show') def _get_raw_data_summary(): @@ -48,7 +48,7 @@ def _get_raw_data_vlan(): """ :returns dict """ - json_data = cmd('sudo bridge --json --compressvlans vlan show') + json_data = cmd('bridge --json --compressvlans vlan show') data_dict = json.loads(json_data) return data_dict @@ -57,7 +57,7 @@ def _get_raw_data_fdb(bridge): """Get MAC-address for the bridge brX :returns list """ - code, json_data = rc_cmd(f'sudo bridge --json fdb show br {bridge}') + code, json_data = rc_cmd(f'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: diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py index 9e5b1040c..a0e47d7ad 100755 --- a/src/op_mode/dns.py +++ b/src/op_mode/dns.py @@ -54,10 +54,10 @@ def _data_to_dict(data, sep="\t") -> dict: def _get_raw_forwarding_statistics() -> dict: - command = cmd('sudo /usr/bin/rec_control --socket-dir=/run/powerdns get-all') + command = cmd('rec_control --socket-dir=/run/powerdns get-all') data = _data_to_dict(command) data['cache-size'] = "{0:.2f}".format( int( - cmd('sudo /usr/bin/rec_control --socket-dir=/run/powerdns get cache-bytes')) / 1024 ) + cmd('rec_control --socket-dir=/run/powerdns get cache-bytes')) / 1024 ) return data diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index 950feb625..46bda5f7e 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -63,7 +63,7 @@ def get_config_firewall(conf, name=None, ipv6=False, interfaces=True): get_first_key=True, no_tag_node_value_mangle=True) if firewall and interfaces: if name: - firewall['interface'] = [] + firewall['interface'] = {} else: if 'name' in firewall: for fw_name, name_conf in firewall['name'].items(): diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index aaa0cec5a..e0d204a0a 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -14,13 +14,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os import re import sys +import typing from collections import OrderedDict from hurry import filesize from re import split as re_split from tabulate import tabulate +from subprocess import TimeoutExpired from vyos.util import call from vyos.util import convert_data @@ -135,41 +138,305 @@ def _get_formatted_output_sas(sas): return output -def get_peer_connections(peer, tunnel, return_all = False): +# Connections block +def _get_vici_connections(): + from vici import Session as vici_session + + try: + session = vici_session() + except Exception: + raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized") + connections = list(session.list_conns()) + return connections + + +def _get_convert_data_connections(): + get_connections = _get_vici_connections() + connections = convert_data(get_connections) + return connections + + +def _get_parent_sa_proposal(connection_name: str, data: list) -> dict: + """Get parent SA proposals by connection name + if connections not in the 'down' state + + Args: + connection_name (str): Connection name + data (list): List of current SAs from vici + + Returns: + str: Parent SA connection proposal + AES_CBC/256/HMAC_SHA2_256_128/MODP_1024 + """ + if not data: + return {} + for sa in data: + # check if parent SA exist + if connection_name not in sa.keys(): + return {} + if 'encr-alg' in sa[connection_name]: + encr_alg = sa.get(connection_name, '').get('encr-alg') + cipher = encr_alg.split('_')[0] + mode = encr_alg.split('_')[1] + encr_keysize = sa.get(connection_name, '').get('encr-keysize') + integ_alg = sa.get(connection_name, '').get('integ-alg') + # prf_alg = sa.get(connection_name, '').get('prf-alg') + dh_group = sa.get(connection_name, '').get('dh-group') + proposal = { + 'cipher': cipher, + 'mode': mode, + 'key_size': encr_keysize, + 'hash': integ_alg, + 'dh': dh_group + } + return proposal + return {} + + +def _get_parent_sa_state(connection_name: str, data: list) -> str: + """Get parent SA state by connection name + + Args: + connection_name (str): Connection name + data (list): List of current SAs from vici + + Returns: + Parent SA connection state + """ + if not data: + return 'down' + for sa in data: + # check if parent SA exist + if connection_name not in sa.keys(): + return 'down' + if sa[connection_name]['state'].lower() == 'established': + return 'up' + else: + return 'down' + + +def _get_child_sa_state(connection_name: str, tunnel_name: str, + data: list) -> str: + """Get child SA state by connection and tunnel name + + Args: + connection_name (str): Connection name + tunnel_name (str): Tunnel name + data (list): List of current SAs from vici + + Returns: + str: `up` if child SA state is 'installed' otherwise `down` + """ + if not data: + return 'down' + for sa in data: + # check if parent SA exist + if connection_name not in sa.keys(): + return 'down' + child_sas = sa[connection_name]['child-sas'] + # Get all child SA states + # there can be multiple SAs per tunnel + child_sa_states = [ + v['state'] for k, v in child_sas.items() if v['name'] == tunnel_name + ] + return 'up' if 'INSTALLED' in child_sa_states else 'down' + + +def _get_child_sa_info(connection_name: str, tunnel_name: str, + data: list) -> dict: + """Get child SA installed info by connection and tunnel name + + Args: + connection_name (str): Connection name + tunnel_name (str): Tunnel name + data (list): List of current SAs from vici + + Returns: + dict: Info of the child SA in the dictionary format + """ + for sa in data: + # check if parent SA exist + if connection_name not in sa.keys(): + return {} + child_sas = sa[connection_name]['child-sas'] + # Get all child SA data + # Skip temp SA name (first key), get only SA values as dict + # {'OFFICE-B-tunnel-0-46': {'name': 'OFFICE-B-tunnel-0'}...} + # i.e get all data after 'OFFICE-B-tunnel-0-46' + child_sa_info = [ + v for k, v in child_sas.items() if 'name' in v and + v['name'] == tunnel_name and v['state'] == 'INSTALLED' + ] + return child_sa_info[-1] if child_sa_info else {} + + +def _get_child_sa_proposal(child_sa_data: dict) -> dict: + if child_sa_data and 'encr-alg' in child_sa_data: + encr_alg = child_sa_data.get('encr-alg') + cipher = encr_alg.split('_')[0] + mode = encr_alg.split('_')[1] + key_size = child_sa_data.get('encr-keysize') + integ_alg = child_sa_data.get('integ-alg') + dh_group = child_sa_data.get('dh-group') + proposal = { + 'cipher': cipher, + 'mode': mode, + 'key_size': key_size, + 'hash': integ_alg, + 'dh': dh_group + } + return proposal + return {} + + +def _get_raw_data_connections(list_connections: list, list_sas: list) -> list: + """Get configured VPN IKE connections and IPsec states + + Args: + list_connections (list): List of configured connections from vici + list_sas (list): List of current SAs from vici + + Returns: + list: List and status of IKE/IPsec connections/tunnels + """ + base_dict = [] + for connections in list_connections: + base_list = {} + for connection, conn_conf in connections.items(): + base_list['ike_connection_name'] = connection + base_list['ike_connection_state'] = _get_parent_sa_state( + connection, list_sas) + base_list['ike_remote_address'] = conn_conf['remote_addrs'] + base_list['ike_proposal'] = _get_parent_sa_proposal( + connection, list_sas) + base_list['local_id'] = conn_conf.get('local-1', '').get('id') + base_list['remote_id'] = conn_conf.get('remote-1', '').get('id') + base_list['version'] = conn_conf.get('version', 'IKE') + base_list['children'] = [] + children = conn_conf['children'] + for tunnel, tun_options in children.items(): + state = _get_child_sa_state(connection, tunnel, list_sas) + local_ts = tun_options.get('local-ts') + remote_ts = tun_options.get('remote-ts') + dpd_action = tun_options.get('dpd_action') + close_action = tun_options.get('close_action') + sa_info = _get_child_sa_info(connection, tunnel, list_sas) + esp_proposal = _get_child_sa_proposal(sa_info) + base_list['children'].append({ + 'name': tunnel, + 'state': state, + 'local_ts': local_ts, + 'remote_ts': remote_ts, + 'dpd_action': dpd_action, + 'close_action': close_action, + 'sa': sa_info, + 'esp_proposal': esp_proposal + }) + base_dict.append(base_list) + return base_dict + + +def _get_raw_connections_summary(list_conn, list_sas): + import jmespath + data = _get_raw_data_connections(list_conn, list_sas) + match = '[*].children[]' + child = jmespath.search(match, data) + tunnels_down = len([k for k in child if k['state'] == 'down']) + tunnels_up = len([k for k in child if k['state'] == 'up']) + tun_dict = { + 'tunnels': child, + 'total': len(child), + 'down': tunnels_down, + 'up': tunnels_up + } + return tun_dict + + +def _get_formatted_output_conections(data): + from tabulate import tabulate + data_entries = '' + connections = [] + for entry in data: + tunnels = [] + ike_name = entry['ike_connection_name'] + ike_state = entry['ike_connection_state'] + conn_type = entry.get('version', 'IKE') + remote_addrs = ','.join(entry['ike_remote_address']) + local_ts, remote_ts = '-', '-' + local_id = entry['local_id'] + remote_id = entry['remote_id'] + proposal = '-' + if entry.get('ike_proposal'): + proposal = (f'{entry["ike_proposal"]["cipher"]}_' + f'{entry["ike_proposal"]["mode"]}/' + f'{entry["ike_proposal"]["key_size"]}/' + f'{entry["ike_proposal"]["hash"]}/' + f'{entry["ike_proposal"]["dh"]}') + connections.append([ + ike_name, ike_state, conn_type, remote_addrs, local_ts, remote_ts, + local_id, remote_id, proposal + ]) + for tun in entry['children']: + tun_name = tun.get('name') + tun_state = tun.get('state') + conn_type = 'IPsec' + local_ts = '\n'.join(tun.get('local_ts')) + remote_ts = '\n'.join(tun.get('remote_ts')) + proposal = '-' + if tun.get('esp_proposal'): + proposal = (f'{tun["esp_proposal"]["cipher"]}_' + f'{tun["esp_proposal"]["mode"]}/' + f'{tun["esp_proposal"]["key_size"]}/' + f'{tun["esp_proposal"]["hash"]}/' + f'{tun["esp_proposal"]["dh"]}') + connections.append([ + tun_name, tun_state, conn_type, remote_addrs, local_ts, + remote_ts, local_id, remote_id, proposal + ]) + connection_headers = [ + 'Connection', 'State', 'Type', 'Remote address', 'Local TS', + 'Remote TS', 'Local id', 'Remote id', 'Proposal' + ] + output = tabulate(connections, connection_headers, numalign='left') + return output + + +# Connections block end + + +def get_peer_connections(peer, tunnel): search = rf'^[\s]*({peer}-(tunnel-[\d]+|vti)).*' matches = [] + if not os.path.exists(SWANCTL_CONF): + raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized") + suffix = None if tunnel is None else (f'tunnel-{tunnel}' if + tunnel.isnumeric() else tunnel) with open(SWANCTL_CONF, 'r') as f: for line in f.readlines(): result = re.match(search, line) if result: - suffix = f'tunnel-{tunnel}' if tunnel.isnumeric() else tunnel - if return_all or (result[2] == suffix): + if tunnel is None: matches.append(result[1]) + else: + if result[2] == suffix: + matches.append(result[1]) return matches -def reset_peer(peer: str, tunnel:str): - if not peer: - print('Invalid peer, aborting') - return - - conns = get_peer_connections(peer, tunnel, return_all = (not tunnel or tunnel == 'all')) +def reset_peer(peer: str, tunnel:typing.Optional[str]): + conns = get_peer_connections(peer, tunnel) if not conns: - print('Tunnel(s) not found, aborting') - return + raise vyos.opmode.IncorrectValue('Peer or tunnel(s) not found, aborting') - result = True for conn in conns: try: call(f'sudo /usr/sbin/ipsec down {conn}{{*}}', timeout = 10) call(f'sudo /usr/sbin/ipsec up {conn}', timeout = 10) except TimeoutExpired as e: - print(f'Timed out while resetting {conn}') - result = False + raise vyos.opmode.InternalError(f'Timed out while resetting {conn}') - - print('Peer reset result: ' + ('success' if result else 'failed')) + print('Peer reset result: success') def show_sa(raw: bool): @@ -179,6 +446,23 @@ def show_sa(raw: bool): return _get_formatted_output_sas(sa_data) +def show_connections(raw: bool): + list_conns = _get_convert_data_connections() + list_sas = _get_raw_data_sas() + if raw: + return _get_raw_data_connections(list_conns, list_sas) + + connections = _get_raw_data_connections(list_conns, list_sas) + return _get_formatted_output_conections(connections) + + +def show_connections_summary(raw: bool): + list_conns = _get_convert_data_connections() + list_sas = _get_raw_data_sas() + if raw: + return _get_raw_connections_summary(list_conns, list_sas) + + if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py index 5e5b95a0a..610e63cb3 100755 --- a/src/op_mode/ping.py +++ b/src/op_mode/ping.py @@ -18,8 +18,7 @@ import os import sys import socket import ipaddress -import json -from vyos.util import cmd, rc_cmd +from vyos.util import get_all_vrfs from vyos.ifconfig import Section @@ -36,13 +35,7 @@ def vrf_list() -> list: Get list of VRFs in system :rtype: list """ - result = cmd(f'sudo ip --json --brief link show type vrf') - data = json.loads(result) - vrflist: list = [] - for o in data: - if 'ifname' in o: - vrflist.append(o['ifname']) - return vrflist + return list(get_all_vrfs().keys()) options = { diff --git a/src/op_mode/policy_route.py b/src/op_mode/policy_route.py index 5be40082f..5953786f3 100755 --- a/src/op_mode/policy_route.py +++ b/src/op_mode/policy_route.py @@ -22,53 +22,13 @@ from vyos.config import Config from vyos.util import cmd from vyos.util import dict_search_args -def get_policy_interfaces(conf, policy, name=None, ipv6=False): - interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - - routes = ['route', 'route6'] - - def parse_if(ifname, if_conf): - if 'policy' in if_conf: - for route in routes: - if route in if_conf['policy']: - route_name = if_conf['policy'][route] - name_str = f'({ifname},{route})' - - if not name: - policy[route][route_name]['interface'].append(name_str) - elif not ipv6 and name == route_name: - policy['interface'].append(name_str) - - for iftype in ['vif', 'vif_s', 'vif_c']: - if iftype in if_conf: - for vifname, vif_conf in if_conf[iftype].items(): - parse_if(f'{ifname}.{vifname}', vif_conf) - - for iftype, iftype_conf in interfaces.items(): - for ifname, if_conf in iftype_conf.items(): - parse_if(ifname, if_conf) - -def get_config_policy(conf, name=None, ipv6=False, interfaces=True): +def get_config_policy(conf, name=None, ipv6=False): config_path = ['policy'] if name: config_path += ['route6' if ipv6 else 'route', name] policy = conf.get_config_dict(config_path, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - if policy and interfaces: - if name: - policy['interface'] = [] - else: - if 'route' in policy: - for route_name, route_conf in policy['route'].items(): - route_conf['interface'] = [] - - if 'route6' in policy: - for route_name, route_conf in policy['route6'].items(): - route_conf['interface'] = [] - - get_policy_interfaces(conf, policy, name, ipv6) return policy diff --git a/src/op_mode/route.py b/src/op_mode/route.py index d11b00ba0..d07a34180 100755 --- a/src/op_mode/route.py +++ b/src/op_mode/route.py @@ -54,6 +54,18 @@ frr_command_template = Template(""" {% endif %} """) +def show_summary(raw: bool): + from vyos.util import cmd + + if raw: + from json import loads + + output = cmd(f"vtysh -c 'show ip route summary json'") + return loads(output) + else: + output = cmd(f"vtysh -c 'show ip route summary'") + return output + def show(raw: bool, family: str, net: typing.Optional[str], diff --git a/src/op_mode/traceroute.py b/src/op_mode/traceroute.py index 4299d6e5f..6c7030ea0 100755 --- a/src/op_mode/traceroute.py +++ b/src/op_mode/traceroute.py @@ -18,6 +18,25 @@ import os import sys import socket import ipaddress +from vyos.util import get_all_vrfs +from vyos.ifconfig import Section + + +def interface_list() -> list: + """ + Get list of interfaces in system + :rtype: list + """ + return Section.interfaces() + + +def vrf_list() -> list: + """ + Get list of VRFs in system + :rtype: list + """ + return list(get_all_vrfs().keys()) + options = { 'backward-hops': { @@ -48,6 +67,7 @@ options = { 'interface': { 'traceroute': '{command} -i {value}', 'type': '<interface>', + 'helpfunction': interface_list, 'help': 'Source interface' }, 'lookup-as': { @@ -99,6 +119,7 @@ options = { 'traceroute': 'sudo ip vrf exec {value} {command}', 'type': '<vrf>', 'help': 'Use specified VRF table', + 'helpfunction': vrf_list, 'dflt': 'default'} } @@ -108,20 +129,33 @@ traceroute = { } -class List (list): - def first (self): +class List(list): + def first(self): return self.pop(0) if self else '' def last(self): return self.pop() if self else '' - def prepend(self,value): - self.insert(0,value) + def prepend(self, value): + self.insert(0, value) + + +def completion_failure(option: str) -> None: + """ + Shows failure message after TAB when option is wrong + :param option: failure option + :type str: + """ + sys.stderr.write('\n\n Invalid option: {}\n\n'.format(option)) + sys.stdout.write('<nocomps>') + sys.exit(1) def expension_failure(option, completions): reason = 'Ambiguous' if completions else 'Invalid' - sys.stderr.write('\n\n {} command: {} [{}]\n\n'.format(reason,' '.join(sys.argv), option)) + sys.stderr.write( + '\n\n {} command: {} [{}]\n\n'.format(reason, ' '.join(sys.argv), + option)) if completions: sys.stderr.write(' Possible completions:\n ') sys.stderr.write('\n '.join(completions)) @@ -160,30 +194,46 @@ if __name__ == '__main__': sys.exit("traceroute: Missing host") if host == '--get-options': - args.first() # pop traceroute + args.first() # pop ping args.first() # pop IP + usedoptionslist = [] while args: - option = args.first() - - matched = complete(option) + option = args.first() # pop option + matched = complete(option) # get option parameters + usedoptionslist.append(option) # list of used options + # Select options if not args: + # remove from Possible completions used options + for o in usedoptionslist: + if o in matched: + matched.remove(o) sys.stdout.write(' '.join(matched)) sys.exit(0) - if len(matched) > 1 : + if len(matched) > 1: sys.stdout.write(' '.join(matched)) sys.exit(0) + # If option doesn't have value + if matched: + if options[matched[0]]['type'] == 'noarg': + continue + else: + # Unexpected option + completion_failure(option) - if options[matched[0]]['type'] == 'noarg': - continue - - value = args.first() + value = args.first() # pop option's value if not args: matched = complete(option) - sys.stdout.write(options[matched[0]]['type']) + helplines = options[matched[0]]['type'] + # Run helpfunction to get list of possible values + if 'helpfunction' in options[matched[0]]: + result = options[matched[0]]['helpfunction']() + if result: + helplines = '\n' + ' '.join(result) + sys.stdout.write(helplines) sys.exit(0) - for name,option in options.items(): + for name, option in options.items(): if 'dflt' in option and name not in args: args.append(name) args.append(option['dflt']) @@ -200,8 +250,7 @@ if __name__ == '__main__': except ValueError: sys.exit(f'traceroute: Unknown host: {host}') - command = convert(traceroute[version],args) + command = convert(traceroute[version], args) # print(f'{command} {host}') os.system(f'{command} {host}') - diff --git a/src/op_mode/vrf.py b/src/op_mode/vrf.py index aeb50fe6e..a9a416761 100755 --- a/src/op_mode/vrf.py +++ b/src/op_mode/vrf.py @@ -31,14 +31,14 @@ def _get_raw_data(name=None): If vrf name is set - get only this name data If vrf name set and not found - return [] """ - output = cmd('sudo ip --json --brief link show type vrf') + output = cmd('ip --json --brief link show type vrf') data = json.loads(output) if not data: return [] if name: is_vrf_exists = True if [vrf for vrf in data if vrf.get('ifname') == name] else False if is_vrf_exists: - output = cmd(f'sudo ip --json --brief link show dev {name}') + output = cmd(f'ip --json --brief link show dev {name}') data = json.loads(output) return data return [] @@ -51,7 +51,7 @@ def _get_vrf_members(vrf: str) -> list: :param vrf: str :return: list """ - output = cmd(f'sudo ip --json --brief link show master {vrf}') + output = cmd(f'ip --json --brief link show master {vrf}') answer = json.loads(output) interfaces = [] for data in answer: diff --git a/src/op_mode/webproxy_update_blacklist.sh b/src/op_mode/webproxy_update_blacklist.sh index 43a4b79fc..d5f301b75 100755 --- a/src/op_mode/webproxy_update_blacklist.sh +++ b/src/op_mode/webproxy_update_blacklist.sh @@ -88,7 +88,7 @@ if [[ -n $update ]] && [[ $update -eq "yes" ]]; then # fix permissions chown -R proxy:proxy ${db_dir} - chmod 2770 ${db_dir} + chmod 755 ${db_dir} logger --priority WARNING "webproxy blacklist entries updated (${count_before}/${count_after})" diff --git a/src/services/api/graphql/generate/schema_from_op_mode.py b/src/services/api/graphql/generate/schema_from_op_mode.py index 1fd198a37..fc63b0100 100755 --- a/src/services/api/graphql/generate/schema_from_op_mode.py +++ b/src/services/api/graphql/generate/schema_from_op_mode.py @@ -25,15 +25,16 @@ from inspect import signature, getmembers, isfunction, isclass, getmro from jinja2 import Template from vyos.defaults import directories +from vyos.util import load_as_module if __package__ is None or __package__ == '': sys.path.append("/usr/libexec/vyos/services/api") - from graphql.libs.op_mode import load_as_module, is_op_mode_function_name, is_show_function_name + from graphql.libs.op_mode import is_op_mode_function_name, is_show_function_name from graphql.libs.op_mode import snake_to_pascal_case, map_type_name from vyos.config import Config from vyos.configdict import dict_merge from vyos.xml import defaults else: - from .. libs.op_mode import load_as_module, is_op_mode_function_name, is_show_function_name + from .. libs.op_mode import is_op_mode_function_name, is_show_function_name from .. libs.op_mode import snake_to_pascal_case, map_type_name from .. import state diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 2778feb69..87ea59c43 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -14,7 +14,7 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. from importlib import import_module -from typing import Any, Dict +from typing import Any, Dict, Optional from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake from graphql import GraphQLResolveInfo from makefun import with_signature @@ -42,7 +42,7 @@ def make_mutation_resolver(mutation_name, class_name, session_func): func_base_name = convert_camel_case_to_snake(class_name) resolver_name = f'resolve_{func_base_name}' - func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict = {})' + func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Optional[Dict]=None)' @mutation.field(mutation_name) @convert_kwargs_to_snake_case @@ -67,20 +67,18 @@ def make_mutation_resolver(mutation_name, class_name, session_func): del data['key'] elif auth_type == 'token': - # there is a subtlety here: with the removal of the key entry, - # some requests will now have empty input, hence no data arg, so - # make it optional in the func_sig. However, it can not be None, - # as the makefun package provides accurate TypeError exceptions; - # hence set it to {}, but now it is a mutable default argument, - # so clear the key 'result', which is added at the end of - # this function. data = kwargs['data'] - if 'result' in data: - del data['result'] - + if data is None: + data = {} info = kwargs['info'] user = info.context.get('user') if user is None: + error = info.context.get('error') + if error is not None: + return { + "success": False, + "errors": [error] + } return { "success": False, "errors": ['not authenticated'] diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index 9c8a4f064..1ad586428 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -14,7 +14,7 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. from importlib import import_module -from typing import Any, Dict +from typing import Any, Dict, Optional from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake from graphql import GraphQLResolveInfo from makefun import with_signature @@ -42,7 +42,7 @@ def make_query_resolver(query_name, class_name, session_func): func_base_name = convert_camel_case_to_snake(class_name) resolver_name = f'resolve_{func_base_name}' - func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict = {})' + func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Optional[Dict]=None)' @query.field(query_name) @convert_kwargs_to_snake_case @@ -67,20 +67,18 @@ def make_query_resolver(query_name, class_name, session_func): del data['key'] elif auth_type == 'token': - # there is a subtlety here: with the removal of the key entry, - # some requests will now have empty input, hence no data arg, so - # make it optional in the func_sig. However, it can not be None, - # as the makefun package provides accurate TypeError exceptions; - # hence set it to {}, but now it is a mutable default argument, - # so clear the key 'result', which is added at the end of - # this function. data = kwargs['data'] - if 'result' in data: - del data['result'] - + if data is None: + data = {} info = kwargs['info'] user = info.context.get('user') if user is None: + error = info.context.get('error') + if error is not None: + return { + "success": False, + "errors": [error] + } return { "success": False, "errors": ['not authenticated'] diff --git a/src/services/api/graphql/libs/op_mode.py b/src/services/api/graphql/libs/op_mode.py index 97a26520e..6939ed5d6 100644 --- a/src/services/api/graphql/libs/op_mode.py +++ b/src/services/api/graphql/libs/op_mode.py @@ -21,14 +21,9 @@ from typing import Union from humps import decamelize from vyos.defaults import directories +from vyos.util import load_as_module from vyos.opmode import _normalize_field_names -def load_as_module(name: str, path: str): - spec = importlib.util.spec_from_file_location(name, path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - def load_op_mode_as_module(name: str): path = os.path.join(directories['op_mode'], name) name = os.path.splitext(name)[0].replace('-', '_') diff --git a/src/services/api/graphql/libs/token_auth.py b/src/services/api/graphql/libs/token_auth.py index 3ecd8b855..2100eba7f 100644 --- a/src/services/api/graphql/libs/token_auth.py +++ b/src/services/api/graphql/libs/token_auth.py @@ -54,6 +54,9 @@ def get_user_context(request): user_id: str = payload.get('sub') if user_id is None: return context + except jwt.exceptions.ExpiredSignatureError: + context['error'] = 'expired token' + return context except jwt.PyJWTError: return context try: diff --git a/src/services/api/graphql/session/errors/op_mode_errors.py b/src/services/api/graphql/session/errors/op_mode_errors.py index 7ba75455d..7bc1d1d81 100644 --- a/src/services/api/graphql/session/errors/op_mode_errors.py +++ b/src/services/api/graphql/session/errors/op_mode_errors.py @@ -3,11 +3,13 @@ op_mode_err_msg = { "UnconfiguredSubsystem": "subsystem is not configured or not running", "DataUnavailable": "data currently unavailable", - "PermissionDenied": "client does not have permission" + "PermissionDenied": "client does not have permission", + "IncorrectValue": "argument value is incorrect" } op_mode_err_code = { "UnconfiguredSubsystem": 2000, "DataUnavailable": 2001, - "PermissionDenied": 1003 + "PermissionDenied": 1003, + "IncorrectValue": 1002 } |