diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/container.py | 21 | ||||
-rwxr-xr-x | src/conf_mode/protocols_babel.py | 163 | ||||
-rwxr-xr-x | src/conf_mode/system-login.py | 22 | ||||
-rwxr-xr-x | src/conf_mode/vpn_openconnect.py | 12 | ||||
-rw-r--r-- | src/etc/dhcp/dhclient-enter-hooks.d/99-run-user-hooks | 5 | ||||
-rwxr-xr-x | src/etc/dhcp/dhclient-exit-hooks.d/99-run-user-hooks | 5 | ||||
-rwxr-xr-x | src/op_mode/generate_public_key_command.py | 59 | ||||
-rwxr-xr-x | src/op_mode/openvpn.py | 6 | ||||
-rwxr-xr-x | src/op_mode/restart_frr.py | 2 | ||||
-rw-r--r-- | src/services/api/graphql/graphql/auth_token_mutation.py | 14 | ||||
-rw-r--r-- | src/services/api/graphql/libs/token_auth.py | 7 | ||||
-rw-r--r-- | src/services/api/graphql/session/session.py | 38 |
12 files changed, 300 insertions, 54 deletions
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 90e5f84f2..4f93c93a1 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -279,8 +279,22 @@ def generate_run_arguments(name, container_config): f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \ f'--name {name} {device} {port} {volume} {env_opt}' + entrypoint = '' + if 'entrypoint' in container_config: + # it needs to be json-formatted with single quote on the outside + entrypoint = json_write(container_config['entrypoint'].split()).replace('"', """) + entrypoint = f'--entrypoint '{entrypoint}'' + + command = '' + if 'command' in container_config: + command = container_config['command'].strip() + + command_arguments = '' + if 'arguments' in container_config: + command_arguments = container_config['arguments'].strip() + if 'allow_host_networks' in container_config: - return f'{container_base_cmd} --net host {image}' + return f'{container_base_cmd} --net host {entrypoint} {image} {command} {command_arguments}'.strip() ip_param = '' networks = ",".join(container_config['network']) @@ -289,7 +303,7 @@ def generate_run_arguments(name, container_config): address = container_config['network'][network]['address'] ip_param = f'--ip {address}' - return f'{container_base_cmd} --net {networks} {ip_param} {image}' + return f'{container_base_cmd} --net {networks} {ip_param} {entrypoint} {image} {command} {command_arguments}'.strip() def generate(container): # bail out early - looks like removal from running config @@ -341,7 +355,8 @@ def generate(container): file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service') run_args = generate_run_arguments(name, container_config) - render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args}) + render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args,}, + formater=lambda _: _.replace(""", '"').replace("'", "'")) return None diff --git a/src/conf_mode/protocols_babel.py b/src/conf_mode/protocols_babel.py new file mode 100755 index 000000000..20821c7f2 --- /dev/null +++ b/src/conf_mode/protocols_babel.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021-2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configdict import node_changed +from vyos.configverify import verify_common_route_maps +from vyos.configverify import verify_access_list +from vyos.configverify import verify_prefix_list +from vyos.util import dict_search +from vyos.xml import defaults +from vyos.template import render_to_string +from vyos import ConfigError +from vyos import frr +from vyos import airbag +airbag.enable() + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['protocols', 'babel'] + babel = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + # FRR has VRF support for different routing daemons. As interfaces belong + # to VRFs - or the global VRF, we need to check for changed interfaces so + # that they will be properly rendered for the FRR config. Also this eases + # removal of interfaces from the running configuration. + interfaces_removed = node_changed(conf, base + ['interface']) + if interfaces_removed: + babel['interface_removed'] = list(interfaces_removed) + + # Bail out early if configuration tree does not exist + if not conf.exists(base): + babel.update({'deleted' : ''}) + return babel + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(base) + + # XXX: T2665: we currently have no nice way for defaults under tag nodes, + # clean them out and add them manually :( + del default_values['interface'] + + # merge in remaining default values + babel = dict_merge(default_values, babel) + + # We also need some additional information from the config, prefix-lists + # and route-maps for instance. They will be used in verify(). + # + # XXX: one MUST always call this without the key_mangling() option! See + # vyos.configverify.verify_common_route_maps() for more information. + tmp = conf.get_config_dict(['policy']) + # Merge policy dict into "regular" config dict + babel = dict_merge(tmp, babel) + return babel + +def verify(babel): + if not babel: + return None + + # verify distribute_list + if "distribute_list" in babel: + acl_keys = { + "ipv4": [ + "distribute_list.ipv4.access_list.in", + "distribute_list.ipv4.access_list.out", + ], + "ipv6": [ + "distribute_list.ipv6.access_list.in", + "distribute_list.ipv6.access_list.out", + ] + } + prefix_list_keys = { + "ipv4": [ + "distribute_list.ipv4.prefix_list.in", + "distribute_list.ipv4.prefix_list.out", + ], + "ipv6":[ + "distribute_list.ipv6.prefix_list.in", + "distribute_list.ipv6.prefix_list.out", + ] + } + for address_family in ["ipv4", "ipv6"]: + for iface_key in babel["distribute_list"].get(address_family, {}).get("interface", {}).keys(): + acl_keys[address_family].extend([ + f"distribute_list.{address_family}.interface.{iface_key}.access_list.in", + f"distribute_list.{address_family}.interface.{iface_key}.access_list.out" + ]) + prefix_list_keys[address_family].extend([ + f"distribute_list.{address_family}.interface.{iface_key}.prefix_list.in", + f"distribute_list.{address_family}.interface.{iface_key}.prefix_list.out" + ]) + + for address_family, keys in acl_keys.items(): + for key in keys: + acl = dict_search(key, babel) + if acl: + verify_access_list(acl, babel, version='6' if address_family == 'ipv6' else '') + + for address_family, keys in prefix_list_keys.items(): + for key in keys: + prefix_list = dict_search(key, babel) + if prefix_list: + verify_prefix_list(prefix_list, babel, version='6' if address_family == 'ipv6' else '') + + +def generate(babel): + if not babel or 'deleted' in babel: + return None + + babel['new_frr_config'] = render_to_string('frr/babeld.frr.j2', babel) + return None + +def apply(babel): + babel_daemon = 'babeld' + + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + + frr_cfg.load_configuration(babel_daemon) + frr_cfg.modify_section('^router babel', stop_pattern='^exit', remove_stop_mark=True) + + for key in ['interface', 'interface_removed']: + if key not in babel: + continue + for interface in babel[key]: + frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) + + if 'new_frr_config' in babel: + frr_cfg.add_before(frr.default_add_before, babel['new_frr_config']) + frr_cfg.commit_configuration(babel_daemon) + + 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/system-login.py b/src/conf_mode/system-login.py index 74e8827ef..0a4a88bf8 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -42,6 +42,11 @@ airbag.enable() autologout_file = "/etc/profile.d/autologout.sh" radius_config_file = "/etc/pam_radius_auth.conf" +# LOGIN_TIMEOUT from /etc/loign.defs minus 10 sec +MAX_RADIUS_TIMEOUT: int = 50 +# MAX_RADIUS_TIMEOUT divided by 2 sec (minimum recomended timeout) +MAX_RADIUS_COUNT: int = 25 + def get_local_users(): """Return list of dynamically allocated users (see Debian Policy Manual)""" local_users = [] @@ -124,18 +129,27 @@ def verify(login): if 'radius' in login: if 'server' not in login['radius']: raise ConfigError('No RADIUS server defined!') - + sum_timeout: int = 0 + radius_servers_count: int = 0 fail = True for server, server_config in dict_search('radius.server', login).items(): if 'key' not in server_config: raise ConfigError(f'RADIUS server "{server}" requires key!') - - if 'disabled' not in server_config: + if 'disable' not in server_config: + sum_timeout += int(server_config['timeout']) + radius_servers_count += 1 fail = False - continue + if fail: raise ConfigError('All RADIUS servers are disabled') + if radius_servers_count > MAX_RADIUS_COUNT: + raise ConfigError('Number of RADIUS servers more than 25 ') + + if sum_timeout > MAX_RADIUS_TIMEOUT: + raise ConfigError('Sum of RADIUS servers timeouts ' + 'has to be less or eq 50 sec') + verify_vrf(login['radius']) if 'source_address' in login['radius']: diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index bf5d3ac84..68da70d7d 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -47,7 +47,7 @@ def get_hash(password): return crypt(password, mksalt(METHOD_SHA512)) -def T2665_default_dict_cleanup(origin: dict, default_values: dict) -> dict: +def _default_dict_cleanup(origin: dict, default_values: dict) -> dict: """ https://vyos.dev/T2665 Clear unnecessary key values in merged config by dict_merge function @@ -63,7 +63,7 @@ def T2665_default_dict_cleanup(origin: dict, default_values: dict) -> dict: del origin['authentication']['local_users']['username']['otp'] if not origin["authentication"]["local_users"]["username"]: raise ConfigError( - 'Openconnect mode local required at least one user') + 'Openconnect authentication mode local requires at least one user') default_ocserv_usr_values = \ default_values['authentication']['local_users']['username']['otp'] for user, params in origin['authentication']['local_users'][ @@ -82,7 +82,7 @@ def T2665_default_dict_cleanup(origin: dict, default_values: dict) -> dict: del origin['authentication']['radius']['server']['port'] if not origin["authentication"]['radius']['server']: raise ConfigError( - 'Openconnect authentication mode radius required at least one radius server') + 'Openconnect authentication mode radius requires at least one RADIUS server') default_values_radius_port = \ default_values['authentication']['radius']['server']['port'] for server, params in origin['authentication']['radius'][ @@ -95,7 +95,7 @@ def T2665_default_dict_cleanup(origin: dict, default_values: dict) -> dict: del origin['accounting']['radius']['server']['port'] if not origin["accounting"]['radius']['server']: raise ConfigError( - 'Openconnect accounting mode radius required at least one radius server') + 'Openconnect accounting mode radius requires at least one RADIUS server') default_values_radius_port = \ default_values['accounting']['radius']['server']['port'] for server, params in origin['accounting']['radius'][ @@ -120,7 +120,7 @@ def get_config(config=None): default_values = defaults(base) ocserv = dict_merge(default_values, ocserv) # workaround a "know limitation" - https://vyos.dev/T2665 - ocserv = T2665_default_dict_cleanup(ocserv, default_values) + ocserv = _default_dict_cleanup(ocserv, default_values) if ocserv: ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/99-run-user-hooks b/src/etc/dhcp/dhclient-enter-hooks.d/99-run-user-hooks new file mode 100644 index 000000000..b4b4d516d --- /dev/null +++ b/src/etc/dhcp/dhclient-enter-hooks.d/99-run-user-hooks @@ -0,0 +1,5 @@ +#!/bin/bash +DHCP_PRE_HOOKS="/config/scripts/dhcp-client/pre-hooks.d/" +if [ -d "${DHCP_PRE_HOOKS}" ] ; then + run-parts "${DHCP_PRE_HOOKS}" +fi diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/99-run-user-hooks b/src/etc/dhcp/dhclient-exit-hooks.d/99-run-user-hooks new file mode 100755 index 000000000..442419d79 --- /dev/null +++ b/src/etc/dhcp/dhclient-exit-hooks.d/99-run-user-hooks @@ -0,0 +1,5 @@ +#!/bin/bash +DHCP_POST_HOOKS="/config/scripts/dhcp-client/post-hooks.d/" +if [ -d "${DHCP_POST_HOOKS}" ] ; then + run-parts "${DHCP_POST_HOOKS}" +fi diff --git a/src/op_mode/generate_public_key_command.py b/src/op_mode/generate_public_key_command.py index f071ae350..8ba55c901 100755 --- a/src/op_mode/generate_public_key_command.py +++ b/src/op_mode/generate_public_key_command.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -19,28 +19,51 @@ import sys import urllib.parse import vyos.remote +from vyos.template import generate_uuid4 -def get_key(path): + +def get_key(path) -> list: + """Get public keys from a local file or remote URL + + Args: + path: Path to the public keys file + + Returns: list of public keys split by new line + + """ url = urllib.parse.urlparse(path) if url.scheme == 'file' or url.scheme == '': with open(os.path.expanduser(path), 'r') as f: key_string = f.read() else: key_string = vyos.remote.get_remote_config(path) - return key_string.split() - -try: - username = sys.argv[1] - algorithm, key, identifier = get_key(sys.argv[2]) -except Exception as e: - print("Failed to retrieve the public key: {}".format(e)) - sys.exit(1) - -print('# To add this key as an embedded key, run the following commands:') -print('configure') -print(f'set system login user {username} authentication public-keys {identifier} key {key}') -print(f'set system login user {username} authentication public-keys {identifier} type {algorithm}') -print('commit') -print('save') -print('exit') + return key_string.split('\n') + + +if __name__ == "__main__": + first_loop = True + + for k in get_key(sys.argv[2]): + k = k.split() + # Skip empty list entry + if k == []: + continue + + try: + username = sys.argv[1] + # Github keys don't have identifier for example 'vyos@localhost' + # 'ssh-rsa AAAA... vyos@localhost' + # Generate uuid4 identifier + identifier = f'github@{generate_uuid4("")}' if sys.argv[2].startswith('https://github.com') else k[2] + algorithm, key = k[0], k[1] + except Exception as e: + print("Failed to retrieve the public key: {}".format(e)) + sys.exit(1) + + if first_loop: + print('# To add this key as an embedded key, run the following commands:') + print('configure') + print(f'set system login user {username} authentication public-keys {identifier} key {key}') + print(f'set system login user {username} authentication public-keys {identifier} type {algorithm}') + first_loop = False diff --git a/src/op_mode/openvpn.py b/src/op_mode/openvpn.py index d957a1d01..79130c7c0 100755 --- a/src/op_mode/openvpn.py +++ b/src/op_mode/openvpn.py @@ -173,8 +173,8 @@ def _format_openvpn(data: dict) -> str: 'TX bytes', 'RX bytes', 'Connected Since'] out = '' - data_out = [] for intf in list(data): + data_out = [] l_host = data[intf]['local_host'] l_port = data[intf]['local_port'] for client in list(data[intf]['clients']): @@ -192,7 +192,9 @@ def _format_openvpn(data: dict) -> str: data_out.append([name, remote, tunnel, local, tx_bytes, rx_bytes, online_since]) - out += tabulate(data_out, headers) + if data_out: + out += tabulate(data_out, headers) + out += "\n" return out diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py index 91b25567a..680d9f8cc 100755 --- a/src/op_mode/restart_frr.py +++ b/src/op_mode/restart_frr.py @@ -139,7 +139,7 @@ def _reload_config(daemon): # define program arguments cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons') cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons') -cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ldpd', 'ospfd', 'ospf6d', 'isisd', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons') +cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ldpd', 'ospfd', 'ospf6d', 'isisd', 'ripd', 'ripngd', 'staticd', 'zebra', 'babeld'], required=False, nargs='*', help='select single or multiple daemons') # parse arguments cmd_args = cmd_args_parser.parse_args() diff --git a/src/services/api/graphql/graphql/auth_token_mutation.py b/src/services/api/graphql/graphql/auth_token_mutation.py index 21ac40094..603a13758 100644 --- a/src/services/api/graphql/graphql/auth_token_mutation.py +++ b/src/services/api/graphql/graphql/auth_token_mutation.py @@ -20,6 +20,7 @@ from ariadne import ObjectType, UnionType from graphql import GraphQLResolveInfo from .. libs.token_auth import generate_token +from .. session.session import get_user_info from .. import state auth_token_mutation = ObjectType("Mutation") @@ -36,13 +37,24 @@ def auth_token_resolver(obj: Any, info: GraphQLResolveInfo, data: Dict): datetime.timedelta(seconds=exp_interval)) res = generate_token(user, passwd, secret, expiration) - if res: + try: + res |= get_user_info(user) + except ValueError: + # non-existent user already caught + pass + if 'token' in res: data['result'] = res return { "success": True, "data": data } + if 'errors' in res: + return { + "success": False, + "errors": res['errors'] + } + return { "success": False, "errors": ['token generation failed'] diff --git a/src/services/api/graphql/libs/token_auth.py b/src/services/api/graphql/libs/token_auth.py index 2100eba7f..8585485c9 100644 --- a/src/services/api/graphql/libs/token_auth.py +++ b/src/services/api/graphql/libs/token_auth.py @@ -29,14 +29,13 @@ def generate_token(user: str, passwd: str, secret: str, exp: int) -> dict: payload_data = {'iss': user, 'sub': user_id, 'exp': exp} secret = state.settings.get('secret') if secret is None: - return { - "success": False, - "errors": ['failed secret generation'] - } + return {"errors": ['missing secret']} token = jwt.encode(payload=payload_data, key=secret, algorithm="HS256") users |= {user_id: user} return {'token': token} + else: + return {"errors": ['failed pam authentication']} def get_user_context(request): context = {} diff --git a/src/services/api/graphql/session/session.py b/src/services/api/graphql/session/session.py index b2aef9bd9..3c5a062b6 100644 --- a/src/services/api/graphql/session/session.py +++ b/src/services/api/graphql/session/session.py @@ -29,6 +29,28 @@ from api.graphql.libs.op_mode import normalize_output op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json') +def get_config_dict(path=[], effective=False, key_mangling=None, + get_first_key=False, no_multi_convert=False, + no_tag_node_value_mangle=False): + config = Config() + return config.get_config_dict(path=path, effective=effective, + key_mangling=key_mangling, + get_first_key=get_first_key, + no_multi_convert=no_multi_convert, + no_tag_node_value_mangle=no_tag_node_value_mangle) + +def get_user_info(user): + user_info = {} + info = get_config_dict(['system', 'login', 'user', user], + get_first_key=True) + if not info: + raise ValueError("No such user") + + user_info['user'] = user + user_info['full_name'] = info.get('full-name', '') + + return user_info + class Session: """ Wrapper for calling configsession functions based on GraphQL requests. @@ -46,17 +68,6 @@ class Session: except Exception: self._op_mode_list = None - @staticmethod - def _get_config_dict(path=[], effective=False, key_mangling=None, - get_first_key=False, no_multi_convert=False, - no_tag_node_value_mangle=False): - config = Config() - return config.get_config_dict(path=path, effective=effective, - key_mangling=key_mangling, - get_first_key=get_first_key, - no_multi_convert=no_multi_convert, - no_tag_node_value_mangle=no_tag_node_value_mangle) - def show_config(self): session = self._session data = self._data @@ -134,10 +145,7 @@ class Session: user_info = {} user = data['user'] try: - info = self._get_config_dict(['system', 'login', 'user', user, - 'full-name']) - user_info['user'] = user - user_info['full_name'] = info.get('full-name', '') + user_info = get_user_info(user) except Exception as error: raise error |