diff options
-rw-r--r-- | data/templates/chrony/chrony.conf.j2 | 4 | ||||
-rw-r--r-- | data/templates/openvpn/server.conf.j2 | 3 | ||||
-rw-r--r-- | interface-definitions/include/version/ntp-version.xml.i | 2 | ||||
-rw-r--r-- | interface-definitions/interfaces-openvpn.xml.in | 19 | ||||
-rw-r--r-- | interface-definitions/ntp.xml.in | 2 | ||||
-rw-r--r-- | python/vyos/remote.py | 72 | ||||
-rw-r--r-- | python/vyos/utils/kernel.py | 11 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_ntp.py | 2 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-openvpn.py | 68 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-wwan.py | 1 | ||||
-rwxr-xr-x | src/conf_mode/ntp.py | 34 | ||||
-rwxr-xr-x | src/conf_mode/protocols_static.py | 2 | ||||
-rw-r--r-- | src/etc/modprobe.d/openvpn.conf | 1 | ||||
-rwxr-xr-x | src/migration-scripts/container/0-to-1 | 8 | ||||
-rwxr-xr-x | src/migration-scripts/ntp/2-to-3 | 62 | ||||
-rwxr-xr-x | src/op_mode/show_ntp.sh | 2 |
16 files changed, 226 insertions, 67 deletions
diff --git a/data/templates/chrony/chrony.conf.j2 b/data/templates/chrony/chrony.conf.j2 index 7a36fe69d..0daec8fb8 100644 --- a/data/templates/chrony/chrony.conf.j2 +++ b/data/templates/chrony/chrony.conf.j2 @@ -53,8 +53,6 @@ bindaddress {{ address }} {% endfor %} {% endif %} {% if interface is vyos_defined %} -{% for ifname in interface %} -binddevice {{ ifname }} -{% endfor %} +binddevice {{ interface }} {% endif %} {% endif %} diff --git a/data/templates/openvpn/server.conf.j2 b/data/templates/openvpn/server.conf.j2 index 525605240..d144529f3 100644 --- a/data/templates/openvpn/server.conf.j2 +++ b/data/templates/openvpn/server.conf.j2 @@ -48,11 +48,10 @@ push "redirect-gateway def1" {% if use_lzo_compression is vyos_defined %} compress lzo {% endif %} -{% if enable_dco is not vyos_defined %} +{% if offload.dco is not vyos_defined %} disable-dco {% endif %} - {% if mode is vyos_defined('client') %} # # OpenVPN Client mode diff --git a/interface-definitions/include/version/ntp-version.xml.i b/interface-definitions/include/version/ntp-version.xml.i index 9eafbf7f0..155c824dc 100644 --- a/interface-definitions/include/version/ntp-version.xml.i +++ b/interface-definitions/include/version/ntp-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/ntp-version.xml.i --> -<syntaxVersion component='ntp' version='2'></syntaxVersion> +<syntaxVersion component='ntp' version='3'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index ca6d80f8b..127a8179b 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -285,6 +285,19 @@ </constraint> </properties> </leafNode> + <node name="offload"> + <properties> + <help>Configurable offload options</help> + </properties> + <children> + <leafNode name="dco"> + <properties> + <help>Enable data channel offload on this interface</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> <leafNode name="openvpn-option"> <properties> <help>Additional OpenVPN options. You must use the syntax of openvpn.conf in this text-field. Using this without proper knowledge may result in a crashed OpenVPN server. Check system log to look for errors.</help> @@ -793,12 +806,6 @@ <valueless/> </properties> </leafNode> - <leafNode name="enable-dco"> - <properties> - <help>Use to enable OpenVPN data channel offload on this TUN interface</help> - <valueless/> - </properties> - </leafNode> #include <include/interface/redirect.xml.i> #include <include/interface/vrf.xml.i> </children> diff --git a/interface-definitions/ntp.xml.in b/interface-definitions/ntp.xml.in index 2275dd61c..4e874434b 100644 --- a/interface-definitions/ntp.xml.in +++ b/interface-definitions/ntp.xml.in @@ -57,7 +57,7 @@ </children> </tagNode> #include <include/allow-client.xml.i> - #include <include/generic-interface-multi.xml.i> + #include <include/generic-interface.xml.i> #include <include/listen-address.xml.i> #include <include/interface/vrf.xml.i> </children> diff --git a/python/vyos/remote.py b/python/vyos/remote.py index 16fe2b2c2..cf731c881 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -25,7 +25,7 @@ import urllib.parse from ftplib import FTP from ftplib import FTP_TLS -from paramiko import SSHClient +from paramiko import SSHClient, SSHException from paramiko import MissingHostKeyPolicy from requests import Session @@ -50,7 +50,7 @@ class InteractivePolicy(MissingHostKeyPolicy): def missing_host_key(self, client, hostname, key): print_error(f"Host '{hostname}' not found in known hosts.") print_error('Fingerprint: ' + key.get_fingerprint().hex()) - if ask_yes_no('Do you wish to continue?'): + if sys.stdout.isatty() and ask_yes_no('Do you wish to continue?'): if client._host_keys_filename\ and ask_yes_no('Do you wish to permanently add this host/key pair to known hosts?'): client._host_keys.add(hostname, key.get_name(), key) @@ -96,7 +96,13 @@ def check_storage(path, size): class FtpC: - def __init__(self, url, progressbar=False, check_space=False, source_host='', source_port=0): + def __init__(self, + url, + progressbar=False, + check_space=False, + source_host='', + source_port=0, + timeout=10): self.secure = url.scheme == 'ftps' self.hostname = url.hostname self.path = url.path @@ -106,12 +112,15 @@ class FtpC: self.source = (source_host, source_port) self.progressbar = progressbar self.check_space = check_space + self.timeout = timeout def _establish(self): if self.secure: - return FTP_TLS(source_address=self.source, context=ssl.create_default_context()) + return FTP_TLS(source_address=self.source, + context=ssl.create_default_context(), + timeout=self.timeout) else: - return FTP(source_address=self.source) + return FTP(source_address=self.source, timeout=self.timeout) def download(self, location: str): # Open the file upfront before establishing connection. @@ -150,7 +159,13 @@ class FtpC: class SshC: known_hosts = os.path.expanduser('~/.ssh/known_hosts') - def __init__(self, url, progressbar=False, check_space=False, source_host='', source_port=0): + def __init__(self, + url, + progressbar=False, + check_space=False, + source_host='', + source_port=0, + timeout=10.0): self.hostname = url.hostname self.path = url.path self.username = url.username or os.getenv('REMOTE_USERNAME') @@ -159,6 +174,7 @@ class SshC: self.source = (source_host, source_port) self.progressbar = progressbar self.check_space = check_space + self.timeout = timeout def _establish(self): ssh = SSHClient() @@ -169,7 +185,7 @@ class SshC: ssh.set_missing_host_key_policy(InteractivePolicy()) # `socket.create_connection()` automatically picks a NIC and an IPv4/IPv6 address family # for us on dual-stack systems. - sock = socket.create_connection((self.hostname, self.port), socket.getdefaulttimeout(), self.source) + sock = socket.create_connection((self.hostname, self.port), self.timeout, self.source) ssh.connect(self.hostname, self.port, self.username, self.password, sock=sock) return ssh @@ -198,13 +214,20 @@ class SshC: class HttpC: - def __init__(self, url, progressbar=False, check_space=False, source_host='', source_port=0): + def __init__(self, + url, + progressbar=False, + check_space=False, + source_host='', + source_port=0, + timeout=10.0): self.urlstring = urllib.parse.urlunsplit(url) self.progressbar = progressbar self.check_space = check_space self.source_pair = (source_host, source_port) self.username = url.username or os.getenv('REMOTE_USERNAME') self.password = url.password or os.getenv('REMOTE_PASSWORD') + self.timeout = timeout def _establish(self): session = Session() @@ -220,8 +243,11 @@ class HttpC: # Not only would it potentially mess up with the progress bar but # `shutil.copyfileobj(request.raw, file)` does not handle automatic decoding. s.headers.update({'Accept-Encoding': 'identity'}) - with s.head(self.urlstring, allow_redirects=True) as r: + with s.head(self.urlstring, + allow_redirects=True, + timeout=self.timeout) as r: # Abort early if the destination is inaccessible. + print('pre-3') r.raise_for_status() # If the request got redirected, keep the last URL we ended up with. final_urlstring = r.url @@ -235,7 +261,8 @@ class HttpC: size = None if self.check_space: check_storage(location, size) - with s.get(final_urlstring, stream=True) as r, open(location, 'wb') as f: + with s.get(final_urlstring, stream=True, + timeout=self.timeout) as r, open(location, 'wb') as f: if self.progressbar and size: progress = make_incremental_progressbar(CHUNK_SIZE / size) next(progress) @@ -249,7 +276,10 @@ class HttpC: def upload(self, location: str): # Does not yet support progressbars. with self._establish() as s, open(location, 'rb') as f: - s.post(self.urlstring, data=f, allow_redirects=True) + s.post(self.urlstring, + data=f, + allow_redirects=True, + timeout=self.timeout) class TftpC: @@ -258,10 +288,16 @@ class TftpC: # 2. Since there's no concept authentication, we don't need to deal with keys/passwords. # 3. It would be a waste to import, audit and maintain a third-party library for TFTP. # 4. I'd rather not implement the entire protocol here, no matter how simple it is. - def __init__(self, url, progressbar=False, check_space=False, source_host=None, source_port=0): + def __init__(self, + url, + progressbar=False, + check_space=False, + source_host=None, + source_port=0, + timeout=10): source_option = f'--interface {source_host} --local-port {source_port}' if source_host else '' progress_flag = '--progress-bar' if progressbar else '-s' - self.command = f'curl {source_option} {progress_flag}' + self.command = f'curl {source_option} {progress_flag} --connect-timeout {timeout}' self.urlstring = urllib.parse.urlunsplit(url) def download(self, location: str): @@ -286,10 +322,16 @@ def urlc(urlstring, *args, **kwargs): raise ValueError(f'Unsupported URL scheme: "{url.scheme}"') def download(local_path, urlstring, *args, **kwargs): - urlc(urlstring, *args, **kwargs).download(local_path) + try: + urlc(urlstring, *args, **kwargs).download(local_path) + except Exception as err: + print_error(f'Unable to download "{urlstring}": {err}') def upload(local_path, urlstring, *args, **kwargs): - urlc(urlstring, *args, **kwargs).upload(local_path) + try: + urlc(urlstring, *args, **kwargs).upload(local_path) + except Exception as err: + print_error(f'Unable to upload "{urlstring}": {err}') def get_remote_config(urlstring, source_host='', source_port=0): """ diff --git a/python/vyos/utils/kernel.py b/python/vyos/utils/kernel.py index 0eb113174..1f3bbdffe 100644 --- a/python/vyos/utils/kernel.py +++ b/python/vyos/utils/kernel.py @@ -25,3 +25,14 @@ def check_kmod(k_mod): if not os.path.exists(f'/sys/module/{module}'): if call(f'modprobe {module}') != 0: raise ConfigError(f'Loading Kernel module {module} failed') + +def unload_kmod(k_mod): + """ Common utility function to unload required kernel modules on demand """ + from vyos import ConfigError + from vyos.utils.process import call + if isinstance(k_mod, str): + k_mod = k_mod.split() + for module in k_mod: + if os.path.exists(f'/sys/module/{module}'): + if call(f'rmmod {module}') != 0: + raise ConfigError(f'Unloading Kernel module {module} failed') diff --git a/smoketest/scripts/cli/test_service_ntp.py b/smoketest/scripts/cli/test_service_ntp.py index 9a63fbbd1..5e385d5ad 100755 --- a/smoketest/scripts/cli/test_service_ntp.py +++ b/smoketest/scripts/cli/test_service_ntp.py @@ -108,7 +108,7 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'bindaddress {listen}', config) def test_03_ntp_interface(self): - interfaces = ['eth0', 'eth1'] + interfaces = ['eth0'] for interface in interfaces: self.cli_set(base_path + ['interface', interface]) diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 607a19385..3bef9b8f6 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -56,6 +56,8 @@ from vyos.utils.list import is_list_equal from vyos.utils.file import makedir from vyos.utils.file import read_file from vyos.utils.file import write_file +from vyos.utils.kernel import check_kmod +from vyos.utils.kernel import unload_kmod from vyos.utils.process import call from vyos.utils.permission import chown from vyos.utils.process import cmd @@ -86,30 +88,45 @@ def get_config(config=None): conf = Config() base = ['interfaces', 'openvpn'] - tmp_pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - ifname, openvpn = get_interface_dict(conf, base) - - if 'deleted' not in openvpn: - openvpn['pki'] = tmp_pki - if is_node_changed(conf, base + [ifname, 'openvpn-option']): - openvpn.update({'restart_required': {}}) - - # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict' - # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there. - tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True) - - # We have to cleanup the config dict, as default values could enable features - # which are not explicitly enabled on the CLI. Example: server mfa totp - # originate comes with defaults, which will enable the - # totp plugin, even when not set via CLI so we - # need to check this first and drop those keys - if dict_search('server.mfa.totp', tmp) == None: - del openvpn['server']['mfa'] - openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn) + if 'deleted' in openvpn: + return openvpn + + openvpn['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + + if is_node_changed(conf, base + [ifname, 'openvpn-option']): + openvpn.update({'restart_required': {}}) + if is_node_changed(conf, base + [ifname, 'enable-dco']): + openvpn.update({'restart_required': {}}) + + # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict' + # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there. + tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True) + + # We have to cleanup the config dict, as default values could enable features + # which are not explicitly enabled on the CLI. Example: server mfa totp + # originate comes with defaults, which will enable the + # totp plugin, even when not set via CLI so we + # need to check this first and drop those keys + if dict_search('server.mfa.totp', tmp) == None: + del openvpn['server']['mfa'] + + # OpenVPN Data-Channel-Offload (DCO) is a Kernel module. If loaded it applies to all + # OpenVPN interfaces. Check if DCO is used by any other interface instance. + tmp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + for interface, interface_config in tmp.items(): + # If one interface has DCO configured, enable it. No need to further check + # all other OpenVPN interfaces. We must use a dedicated key to indicate + # the Kernel module must be loaded or not. The per interface "offload.dco" + # key is required per OpenVPN interface instance. + if dict_search('offload.dco', interface_config) != None: + openvpn['module_load_dco'] = {} + break + return openvpn def is_ec_private_key(pki, cert_name): @@ -670,6 +687,15 @@ def apply(openvpn): if interface in interfaces(): VTunIf(interface).remove() + # dynamically load/unload DCO Kernel extension if requested + dco_module = 'ovpn_dco_v2' + if 'module_load_dco' in openvpn: + check_kmod(dco_module) + else: + unload_kmod(dco_module) + + # Now bail out early if interface is disabled or got deleted + if 'deleted' in openvpn or 'disable' in openvpn: return None # verify specified IP address is present on any interface on this system diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py index 6658ca86a..2515dc838 100755 --- a/src/conf_mode/interfaces-wwan.py +++ b/src/conf_mode/interfaces-wwan.py @@ -75,7 +75,6 @@ def get_config(config=None): # We need to know the amount of other WWAN interfaces as ModemManager needs # to be started or stopped. - conf.set_level(base) wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 917f6e058..1cc23a7df 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -24,6 +24,7 @@ from vyos.utils.process import call from vyos.utils.permission import chmod_750 from vyos.utils.network import get_interface_config from vyos.template import render +from vyos.template import is_ipv4 from vyos import ConfigError from vyos import airbag airbag.enable() @@ -62,16 +63,29 @@ def verify(ntp): if 'interface' in ntp: # If ntpd should listen on a given interface, ensure it exists - for interface in ntp['interface']: - verify_interface_exists(interface) - - # If we run in a VRF, our interface must belong to this VRF, too - if 'vrf' in ntp: - tmp = get_interface_config(interface) - vrf_name = ntp['vrf'] - if 'master' not in tmp or tmp['master'] != vrf_name: - raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{interface}" '\ - f'does not belong to this VRF!') + interface = ntp['interface'] + verify_interface_exists(interface) + + # If we run in a VRF, our interface must belong to this VRF, too + if 'vrf' in ntp: + tmp = get_interface_config(interface) + vrf_name = ntp['vrf'] + if 'master' not in tmp or tmp['master'] != vrf_name: + raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{interface}" '\ + f'does not belong to this VRF!') + + if 'listen_address' in ntp: + ipv4_addresses = 0 + ipv6_addresses = 0 + for address in ntp['listen_address']: + if is_ipv4(address): + ipv4_addresses += 1 + else: + ipv6_addresses += 1 + if ipv4_addresses > 1: + raise ConfigError(f'NTP Only admits one ipv4 value for listen-address parameter ') + if ipv6_addresses > 1: + raise ConfigError(f'NTP Only admits one ipv6 value for listen-address parameter ') return None diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index 7b6150696..5def8d645 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -47,7 +47,7 @@ def get_config(config=None): base_path = ['protocols', 'static'] # eqivalent of the C foo ? 'a' : 'b' statement base = vrf and ['vrf', 'name', vrf, 'protocols', 'static'] or base_path - static = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + static = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) # Assign the name of our VRF context if vrf: static['vrf'] = vrf diff --git a/src/etc/modprobe.d/openvpn.conf b/src/etc/modprobe.d/openvpn.conf new file mode 100644 index 000000000..a9259fea2 --- /dev/null +++ b/src/etc/modprobe.d/openvpn.conf @@ -0,0 +1 @@ +blacklist ovpn-dco-v2 diff --git a/src/migration-scripts/container/0-to-1 b/src/migration-scripts/container/0-to-1 index 9fcf295e8..86f89ee04 100755 --- a/src/migration-scripts/container/0-to-1 +++ b/src/migration-scripts/container/0-to-1 @@ -39,12 +39,12 @@ config = ConfigTree(config_file) if config.exists(base): for container in config.list_nodes(base): # Stop any given container first - call(f'systemctl stop vyos-container-{container}.service') + call(f'sudo systemctl stop vyos-container-{container}.service') # Export container image for later re-import to new filesystem. We store # the backup on a real disk as a tmpfs (like /tmp) could probably lack # memory if a host has too many containers stored. image_name = config.return_value(base + [container, 'image']) - call(f'podman image save --quiet --output /root/{container}.tar --format oci-archive {image_name}') + call(f'sudo podman image save --quiet --output /root/{container}.tar --format oci-archive {image_name}') # No need to adjust the strage driver online (this is only used for testing and # debugging on a live system) - it is already overlay2 when the migration script @@ -66,10 +66,10 @@ if config.exists(base): # Export container image for later re-import to new filesystem image_name = config.return_value(base + [container, 'image']) image_path = f'/root/{container}.tar' - call(f'podman image load --quiet --input {image_path}') + call(f'sudo podman image load --quiet --input {image_path}') # Start any given container first - call(f'systemctl start vyos-container-{container}.service') + call(f'sudo systemctl start vyos-container-{container}.service') # Delete temporary container image if os.path.exists(image_path): diff --git a/src/migration-scripts/ntp/2-to-3 b/src/migration-scripts/ntp/2-to-3 new file mode 100755 index 000000000..7d4e0bd83 --- /dev/null +++ b/src/migration-scripts/ntp/2-to-3 @@ -0,0 +1,62 @@ +#!/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/>. + +# T5154: allow only one ip address per family for parameter 'listen-address' +# Allow only one interface for parameter 'interface' +# If more than one are specified, remove such entries + +import sys + +from vyos.configtree import ConfigTree +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base_path = ['service', 'ntp'] +if not config.exists(base_path): + # Nothing to do + sys.exit(0) + +if config.exists(base_path + ['listen-address']) and (len([addr for addr in config.return_values(base_path + ['listen-address']) if is_ipv4(addr)]) > 1): + for addr in config.return_values(base_path + ['listen-address']): + if is_ipv4(addr): + config.delete_value(base_path + ['listen-address'], addr) + +if config.exists(base_path + ['listen-address']) and (len([addr for addr in config.return_values(base_path + ['listen-address']) if is_ipv6(addr)]) > 1): + for addr in config.return_values(base_path + ['listen-address']): + if is_ipv6(addr): + config.delete_value(base_path + ['listen-address'], addr) + +if config.exists(base_path + ['interface']): + if len(config.return_values(base_path + ['interface'])) > 1: + config.delete(base_path + ['interface']) + +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)) + sys.exit(1) diff --git a/src/op_mode/show_ntp.sh b/src/op_mode/show_ntp.sh index 85f8eda15..4b59b801e 100755 --- a/src/op_mode/show_ntp.sh +++ b/src/op_mode/show_ntp.sh @@ -18,7 +18,7 @@ if ! ps -C chronyd &>/dev/null; then fi PID=$(pgrep chronyd | head -n1) -VRF_NAME=$(ip vrf identify ) +VRF_NAME=$(ip vrf identify ${PID}) if [ ! -z ${VRF_NAME} ]; then VRF_CMD="sudo ip vrf exec ${VRF_NAME}" |