diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/dhcp_relay.py | 15 | ||||
-rwxr-xr-x | src/conf_mode/high-availability.py | 24 | ||||
-rwxr-xr-x | src/conf_mode/protocols_bgp.py | 5 | ||||
-rwxr-xr-x | src/conf_mode/qos.py | 15 | ||||
-rwxr-xr-x | src/conf_mode/vpn_ipsec.py | 17 | ||||
-rwxr-xr-x | src/conf_mode/vpn_openconnect.py | 97 | ||||
-rwxr-xr-x | src/etc/commit/post-hooks.d/00vyos-sync | 7 | ||||
-rw-r--r-- | src/etc/sysctl.d/30-vyos-router.conf | 3 | ||||
-rwxr-xr-x | src/migration-scripts/ipsec/10-to-11 | 85 | ||||
-rw-r--r-- | src/services/api/graphql/generate/config_session_function.py | 6 | ||||
-rw-r--r-- | src/services/api/graphql/session/session.py | 27 |
11 files changed, 272 insertions, 29 deletions
diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py index 4de2ca2f3..7e702a446 100755 --- a/src/conf_mode/dhcp_relay.py +++ b/src/conf_mode/dhcp_relay.py @@ -18,9 +18,11 @@ import os from sys import exit +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render +from vyos.base import Warning from vyos.util import call from vyos.util import dict_search from vyos.xml import defaults @@ -59,6 +61,19 @@ def verify(relay): raise ConfigError('No DHCP relay server(s) configured.\n' \ 'At least one DHCP relay server required.') + if 'interface' in relay: + Warning('DHCP relay interface is DEPRECATED - please use upstream-interface and listen-interface instead!') + if 'upstream_interface' in relay or 'listen_interface' in relay: + raise ConfigError('<interface> configuration is not compatible with upstream/listen interface') + else: + Warning('<interface> is going to be deprecated.\n' \ + 'Please use <listen-interface> and <upstream-interface>') + + if 'upstream_interface' in relay and 'listen_interface' not in relay: + raise ConfigError('No listen-interface configured') + if 'listen_interface' in relay and 'upstream_interface' not in relay: + raise ConfigError('No upstream-interface configured') + return None def generate(relay): diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py index bc3e67b40..79e407efd 100755 --- a/src/conf_mode/high-availability.py +++ b/src/conf_mode/high-availability.py @@ -28,6 +28,7 @@ from vyos.template import render from vyos.template import is_ipv4 from vyos.template import is_ipv6 from vyos.util import call +from vyos.util import dict_search from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -49,12 +50,27 @@ def get_config(config=None): # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. if 'vrrp' in ha: + if dict_search('vrrp.global_parameters.garp', ha) != None: + default_values = defaults(base_vrrp + ['global-parameters', 'garp']) + ha['vrrp']['global_parameters']['garp'] = dict_merge( + default_values, ha['vrrp']['global_parameters']['garp']) + if 'group' in ha['vrrp']: - default_values_vrrp = defaults(base_vrrp + ['group']) - if 'garp' in default_values_vrrp: - del default_values_vrrp['garp'] + default_values = defaults(base_vrrp + ['group']) + default_values_garp = defaults(base_vrrp + ['group', 'garp']) + + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if 'garp' in default_values: + del default_values['garp'] for group in ha['vrrp']['group']: - ha['vrrp']['group'][group] = dict_merge(default_values_vrrp, ha['vrrp']['group'][group]) + ha['vrrp']['group'][group] = dict_merge(default_values, ha['vrrp']['group'][group]) + + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if 'garp' in ha['vrrp']['group'][group]: + ha['vrrp']['group'][group]['garp'] = dict_merge( + default_values_garp, ha['vrrp']['group'][group]['garp']) # Merge per virtual-server default values if 'virtual_server' in ha: diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index c410258ee..4f05957fa 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -235,6 +235,11 @@ def verify(bgp): raise ConfigError(f'Specified peer-group "{peer_group}" for '\ f'neighbor "{neighbor}" does not exist!') + if 'local_role' in peer_config: + #Ensure Local Role has only one value. + if len(peer_config['local_role']) > 1: + raise ConfigError(f'Only one local role can be specified for peer "{peer}"!') + if 'local_as' in peer_config: if len(peer_config['local_as']) > 1: raise ConfigError(f'Only one local-as number can be specified for peer "{peer}"!') diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index 0418e8d82..dca713283 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 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 @@ -14,12 +14,14 @@ # 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 netifaces import interfaces +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge -from vyos.configverify import verify_interface_exists from vyos.qos import CAKE from vyos.qos import DropTail from vyos.qos import FairQueue @@ -194,8 +196,6 @@ def verify(qos): # we should check interface ingress/egress configuration after verifying that # the policy name is used only once - this makes the logic easier! for interface, interface_config in qos['interface'].items(): - verify_interface_exists(interface) - for direction in ['egress', 'ingress']: # bail out early if shaper for given direction is not used at all if direction not in interface_config: @@ -229,6 +229,13 @@ def apply(qos): return None for interface, interface_config in qos['interface'].items(): + if not os.path.exists(f'/sys/class/net/{interface}'): + # When shaper is bound to a dialup (e.g. PPPoE) interface it is + # possible that it is yet not availbale when to QoS code runs. + # Skip the configuration and inform the user + Warning(f'Interface "{interface}" does not exist!') + continue + for direction in ['egress', 'ingress']: # bail out early if shaper for given direction is not used at all if direction not in interface_config: diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 3af2af4d9..ce4f13d27 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-2022 VyOS maintainers and contributors +# 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 @@ -17,6 +17,7 @@ import ipaddress import os import re +import jmespath from sys import exit from time import sleep @@ -219,6 +220,12 @@ def verify(ipsec): if not ipsec: return None + if 'authentication' in ipsec: + if 'psk' in ipsec['authentication']: + for psk, psk_config in ipsec['authentication']['psk'].items(): + if 'id' not in psk_config or 'secret' not in psk_config: + raise ConfigError(f'Authentication psk "{psk}" missing "id" or "secret"') + if 'interfaces' in ipsec : for ifname in ipsec['interface']: verify_interface_exists(ifname) @@ -602,6 +609,14 @@ def generate(ipsec): ipsec['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough + # auth psk <tag> dhcp-interface <xxx> + if jmespath.search('authentication.psk.*.dhcp_interface', ipsec): + for psk, psk_config in ipsec['authentication']['psk'].items(): + if 'dhcp_interface' in psk_config: + for iface in psk_config['dhcp_interface']: + id = get_dhcp_address(iface) + if id: + ipsec['authentication']['psk'][psk]['id'].append(id) render(ipsec_conf, 'ipsec/ipsec.conf.j2', ipsec) render(ipsec_secrets, 'ipsec/ipsec.secrets.j2', ipsec) diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index af3c51efc..63ffe2a41 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -46,6 +46,65 @@ radius_servers = cfg_dir + '/radius_servers' def get_hash(password): return crypt(password, mksalt(METHOD_SHA512)) + +def T2665_default_dict_cleanup(origin: dict, default_values: dict) -> dict: + """ + https://phabricator.vyos.net/T2665 + Clear unnecessary key values in merged config by dict_merge function + :param origin: config + :type origin: dict + :param default_values: default values + :type default_values: dict + :return: merged dict + :rtype: dict + """ + if 'mode' in origin["authentication"] and "local" in \ + origin["authentication"]["mode"]: + del origin['authentication']['local_users']['username']['otp'] + if not origin["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 origin['authentication']['local_users'][ + 'username'].items(): + # Not every configuration requires OTP settings + if origin['authentication']['local_users']['username'][user].get( + 'otp'): + origin['authentication']['local_users']['username'][user][ + 'otp'] = dict_merge(default_ocserv_usr_values, + origin['authentication'][ + 'local_users']['username'][user][ + 'otp']) + + if 'mode' in origin["authentication"] and "radius" in \ + origin["authentication"]["mode"]: + del origin['authentication']['radius']['server']['port'] + if not origin["authentication"]['radius']['server']: + raise ConfigError( + 'Openconnect authentication mode radius required at least one radius server') + default_values_radius_port = \ + default_values['authentication']['radius']['server']['port'] + for server, params in origin['authentication']['radius'][ + 'server'].items(): + if 'port' not in params: + params['port'] = default_values_radius_port + + if 'mode' in origin["accounting"] and "radius" in \ + origin["accounting"]["mode"]: + del origin['accounting']['radius']['server']['port'] + if not origin["accounting"]['radius']['server']: + raise ConfigError( + 'Openconnect accounting mode radius required at least one radius server') + default_values_radius_port = \ + default_values['accounting']['radius']['server']['port'] + for server, params in origin['accounting']['radius'][ + 'server'].items(): + if 'port' not in params: + params['port'] = default_values_radius_port + return origin + + def get_config(): conf = Config() base = ['vpn', 'openconnect'] @@ -57,18 +116,8 @@ def get_config(): # options which we need to update into the dictionary retrived. default_values = defaults(base) ocserv = dict_merge(default_values, ocserv) - - if 'mode' in ocserv["authentication"] and "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']) - + # workaround a "know limitation" - https://phabricator.vyos.net/T2665 + ocserv = T2665_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) @@ -85,6 +134,14 @@ def verify(ocserv): not is_listen_port_bind_service(int(port), 'ocserv-main'): raise ConfigError(f'"{proto}" port "{port}" is used by another service') + # Check accounting + if "accounting" in ocserv: + if "mode" in ocserv["accounting"] and "radius" in ocserv["accounting"]["mode"]: + if "authentication" not in ocserv or "mode" not in ocserv["authentication"]: + raise ConfigError('Accounting depends on OpenConnect authentication configuration') + elif "radius" not in ocserv["authentication"]["mode"]: + raise ConfigError('RADIUS accounting must be used with RADIUS authentication') + # Check authentication if "authentication" in ocserv: if "mode" in ocserv["authentication"]: @@ -166,10 +223,18 @@ def generate(ocserv): return None if "radius" in ocserv["authentication"]["mode"]: - # Render radius client configuration - render(radius_cfg, 'ocserv/radius_conf.j2', ocserv["authentication"]["radius"]) - # Render radius servers - render(radius_servers, 'ocserv/radius_servers.j2', ocserv["authentication"]["radius"]) + if dict_search(ocserv, 'accounting.mode.radius'): + # Render radius client configuration + render(radius_cfg, 'ocserv/radius_conf.j2', ocserv) + merged_servers = ocserv["accounting"]["radius"]["server"] | ocserv["authentication"]["radius"]["server"] + # Render radius servers + # Merge the accounting and authentication servers into a single dictionary + render(radius_servers, 'ocserv/radius_servers.j2', {'server': merged_servers}) + else: + # Render radius client configuration + render(radius_cfg, 'ocserv/radius_conf.j2', ocserv) + # Render radius servers + render(radius_servers, 'ocserv/radius_servers.j2', ocserv["authentication"]["radius"]) elif "local" in ocserv["authentication"]["mode"]: # if mode "OTP", generate OTP users file parameters if "otp" in ocserv["authentication"]["mode"]["local"]: diff --git a/src/etc/commit/post-hooks.d/00vyos-sync b/src/etc/commit/post-hooks.d/00vyos-sync new file mode 100755 index 000000000..e3bde3abb --- /dev/null +++ b/src/etc/commit/post-hooks.d/00vyos-sync @@ -0,0 +1,7 @@ +#!/bin/sh +# When power is lost right after a commit modified files, the +# system can be corrupted and e.g. login is no longer possible. +# Always sync files to the backend storage after a commit. +# https://phabricator.vyos.net/T4975 +sync + diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf index 411429510..4880605d6 100644 --- a/src/etc/sysctl.d/30-vyos-router.conf +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -98,9 +98,6 @@ net.ipv6.route.skip_notify_on_dev_down=1 # Default value of 20 seems to interfere with larger OSPF and VRRP setups net.ipv4.igmp_max_memberships = 512 -# Enable conntrack helper by default -net.netfilter.nf_conntrack_helper=1 - # Increase default garbage collection thresholds net.ipv4.neigh.default.gc_thresh1 = 1024 net.ipv4.neigh.default.gc_thresh2 = 4096 diff --git a/src/migration-scripts/ipsec/10-to-11 b/src/migration-scripts/ipsec/10-to-11 new file mode 100755 index 000000000..ec38d0034 --- /dev/null +++ b/src/migration-scripts/ipsec/10-to-11 @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 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 re + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['vpn', 'ipsec'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +# 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: 'ipsec site-to-site peer <tag> authentication pre-shared-secret xxx' + # => 'ipsec authentication psk <tag> secret xxx' + if config.exists(peer_base + ['authentication', 'pre-shared-secret']): + tmp = config.return_value(peer_base + ['authentication', 'pre-shared-secret']) + config.delete(peer_base + ['authentication', 'pre-shared-secret']) + config.set(base + ['authentication', 'psk', peer, 'secret'], value=tmp) + # format as tag node to avoid loading problems + config.set_tag(base + ['authentication', 'psk']) + + # Get id's from peers for "ipsec auth psk <tag> id xxx" + if config.exists(peer_base + ['authentication', 'local-id']): + local_id = config.return_value(peer_base + ['authentication', 'local-id']) + config.set(base + ['authentication', 'psk', peer, 'id'], value=local_id, replace=False) + if config.exists(peer_base + ['authentication', 'remote-id']): + remote_id = config.return_value(peer_base + ['authentication', 'remote-id']) + config.set(base + ['authentication', 'psk', peer, 'id'], value=remote_id, replace=False) + + if config.exists(peer_base + ['local-address']): + tmp = config.return_value(peer_base + ['local-address']) + config.set(base + ['authentication', 'psk', peer, 'id'], value=tmp, replace=False) + if config.exists(peer_base + ['remote-address']): + tmp = config.return_value(peer_base + ['remote-address']) + if tmp: + for remote_addr in tmp: + if remote_addr == 'any': + remote_addr = '%any' + config.set(base + ['authentication', 'psk', peer, 'id'], value=remote_addr, replace=False) + + # get DHCP peer interface as psk dhcp-interface + if config.exists(peer_base + ['dhcp-interface']): + tmp = config.return_value(peer_base + ['dhcp-interface']) + config.set(base + ['authentication', 'psk', peer, 'dhcp-interface'], value=tmp) + + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/services/api/graphql/generate/config_session_function.py b/src/services/api/graphql/generate/config_session_function.py index fc0dd7a87..20fc7cc1d 100644 --- a/src/services/api/graphql/generate/config_session_function.py +++ b/src/services/api/graphql/generate/config_session_function.py @@ -8,8 +8,12 @@ def show_config(path: list[str], configFormat: typing.Optional[str]): def show(path: list[str]): pass +def show_user_info(user: str): + pass + queries = {'show_config': show_config, - 'show': show} + 'show': show, + 'show_user_info': show_user_info} def save_config_file(fileName: typing.Optional[str]): pass diff --git a/src/services/api/graphql/session/session.py b/src/services/api/graphql/session/session.py index 0b77b1433..b2aef9bd9 100644 --- a/src/services/api/graphql/session/session.py +++ b/src/services/api/graphql/session/session.py @@ -46,6 +46,17 @@ 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 @@ -116,6 +127,22 @@ class Session: return res + def show_user_info(self): + session = self._session + data = self._data + + 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', '') + except Exception as error: + raise error + + return user_info + def system_status(self): import api.graphql.session.composite.system_status as system_status |