diff options
27 files changed, 394 insertions, 450 deletions
@@ -86,7 +86,7 @@ clean: .PHONY: test test: - set -e; python3 -m compileall -q . + set -e; python3 -m compileall -q -x '/vmware-tools/scripts/' . PYTHONPATH=python/ python3 -m "nose" --with-xunit src --with-coverage --cover-erase --cover-xml --cover-package src/conf_mode,src/op_mode,src/completion,src/helpers,src/validators,src/tests --verbose .PHONY: sonar diff --git a/data/templates/ipsec/interfaces_use.conf.tmpl b/data/templates/ipsec/interfaces_use.conf.tmpl index 3d285b9be..a77102396 100644 --- a/data/templates/ipsec/interfaces_use.conf.tmpl +++ b/data/templates/ipsec/interfaces_use.conf.tmpl @@ -1,6 +1,5 @@ -{% if ipsec_interfaces is defined and 'interface' in ipsec_interfaces %} -{% set interfaces = ipsec_interfaces['interface'] %} +{% if interface is defined %} charon { - interfaces_use = {{ ', '.join(interfaces) if interfaces is not string else interfaces }} + interfaces_use = {{ ', '.join(interface) }} } {% endif %}
\ No newline at end of file diff --git a/data/templates/ipsec/ipsec.conf.tmpl b/data/templates/ipsec/ipsec.conf.tmpl index a9ea1aac7..1cb531e76 100644 --- a/data/templates/ipsec/ipsec.conf.tmpl +++ b/data/templates/ipsec/ipsec.conf.tmpl @@ -16,9 +16,3 @@ config setup {% if include_ipsec_conf is defined %} include {{ include_ipsec_conf }} {% endif %} - -{% if delim_ipsec_l2tp_begin is defined %} -{{delim_ipsec_l2tp_begin}} -include {{ipsec_ra_conn_file}} -{{delim_ipsec_l2tp_end}} -{% endif %} diff --git a/data/templates/ipsec/ipsec.secrets.tmpl b/data/templates/ipsec/ipsec.secrets.tmpl index 43b5fe0d2..057e291ed 100644 --- a/data/templates/ipsec/ipsec.secrets.tmpl +++ b/data/templates/ipsec/ipsec.secrets.tmpl @@ -3,13 +3,3 @@ {% if include_ipsec_secrets is defined %} include {{ include_ipsec_secrets }} {% endif %} - -{% if delim_ipsec_l2tp_begin is defined %} -{{delim_ipsec_l2tp_begin}} -{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %} -{{outside_addr}} %any : PSK "{{ipsec_l2tp_secret}}" -{% elif ipsec_l2tp_auth_mode == 'x509' %} -: RSA {{server_key_file_copied}} -{% endif %} -{{delim_ipsec_l2tp_end}} -{% endif %} diff --git a/data/templates/ipsec/remote-access.tmpl b/data/templates/ipsec/remote-access.tmpl deleted file mode 100644 index fae48232f..000000000 --- a/data/templates/ipsec/remote-access.tmpl +++ /dev/null @@ -1,28 +0,0 @@ -{{delim_ipsec_l2tp_begin}} -conn {{ra_conn_name}} - type=transport - left={{outside_addr}} - leftsubnet=%dynamic[/1701] - rightsubnet=%dynamic - mark_in=%unique - auto=add - ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024! - dpddelay=15 - dpdtimeout=45 - dpdaction=clear - esp=aes256-sha1,3des-sha1! - rekey=no -{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %} - authby=secret - leftauth=psk - rightauth=psk -{% elif ipsec_l2tp_auth_mode == 'x509' %} - authby=rsasig - leftrsasigkey=%cert - rightrsasigkey=%cert - rightca=%same - leftcert={{server_cert_file_copied}} -{% endif %} - ikelifetime={{ipsec_l2tp_ike_lifetime}} - keylife={{ipsec_l2tp_lifetime}} -{{delim_ipsec_l2tp_end}} diff --git a/data/templates/ipsec/swanctl.conf.tmpl b/data/templates/ipsec/swanctl.conf.tmpl index a6ab73cc2..102d7583f 100644 --- a/data/templates/ipsec/swanctl.conf.tmpl +++ b/data/templates/ipsec/swanctl.conf.tmpl @@ -1,4 +1,5 @@ ### Autogenerated by vpn_ipsec.py ### +{% import 'ipsec/swanctl/l2tp.tmpl' as l2tp_tmpl %} {% import 'ipsec/swanctl/profile.tmpl' as profile_tmpl %} {% import 'ipsec/swanctl/peer.tmpl' as peer_tmpl %} {% import 'ipsec/swanctl/remote_access.tmpl' as remote_access_tmpl %} @@ -19,6 +20,9 @@ connections { {{ remote_access_tmpl.conn(rw, rw_conf, ike_group, esp_group) }} {% endfor %} {% endif %} +{% if l2tp %} +{{ l2tp_tmpl.conn(l2tp, l2tp_outside_address, l2tp_ike_default, l2tp_esp_default, ike_group, esp_group) }} +{% endif %} } pools { @@ -103,5 +107,21 @@ secrets { {% endif %} {% endfor %} {% endif %} +{% if l2tp %} +{% if l2tp.authentication.mode == 'pre-shared-secret' %} + ike_l2tp_remote_access { + id = "{{ l2tp_outside_address }}" + secret = "{{ l2tp.authentication.pre_shared_secret }}" + } +{% elif l2tp.authentication.mode == 'x509' %} + private_l2tp_remote_access { + id = "{{ l2tp_outside_address }}" + file = {{ l2tp.authentication.x509.certificate }}.pem +{% if l2tp.authentication.x509.passphrase is defined %} + secret = "{{ l2tp.authentication.x509.passphrase }}" +{% endif %} + } +{% endif %} +{% endif %} } diff --git a/data/templates/ipsec/swanctl/l2tp.tmpl b/data/templates/ipsec/swanctl/l2tp.tmpl new file mode 100644 index 000000000..2df5c2a4d --- /dev/null +++ b/data/templates/ipsec/swanctl/l2tp.tmpl @@ -0,0 +1,30 @@ +{% macro conn(l2tp, l2tp_outside_address, l2tp_ike_default, l2tp_esp_default, ike_group, esp_group) %} +{% set l2tp_ike = ike_group[l2tp.ike_group] if l2tp.ike_group is defined else None %} +{% set l2tp_esp = esp_group[l2tp.esp_group] if l2tp.esp_group is defined else None %} + l2tp_remote_access { + proposals = {{ l2tp_ike | get_esp_ike_cipher | join(',') if l2tp_ike else l2tp_ike_default }} + local_addrs = {{ l2tp_outside_address }} + dpd_delay = 15s + dpd_timeout = 45s + rekey_time = {{ l2tp_ike.lifetime if l2tp_ike else l2tp.ike_lifetime }}s + reauth_time = 0 + local { + auth = {{ 'psk' if l2tp.authentication.mode == 'pre-shared-secret' else 'pubkey' }} +{% if l2tp.authentication.mode == 'x509' %} + certs = {{ l2tp.authentication.x509.certificate }}.pem +{% endif %} + } + remote { + auth = {{ 'psk' if l2tp.authentication.mode == 'pre-shared-secret' else 'pubkey' }} + } + children { + l2tp_remote_access_esp { + mode = transport + esp_proposals = {{ l2tp_esp | get_esp_ike_cipher | join(',') if l2tp_esp else l2tp_esp_default }} + life_time = {{ l2tp_esp.lifetime if l2tp_esp else l2tp.lifetime }}s + local_ts = dynamic[/1701] + remote_ts = dynamic + } + } + } +{% endmacro %} diff --git a/interface-definitions/containers.xml.in b/interface-definitions/containers.xml.in index 6fc53c105..124b1f65e 100644 --- a/interface-definitions/containers.xml.in +++ b/interface-definitions/containers.xml.in @@ -3,6 +3,7 @@ <node name="container" owner="${vyos_conf_scripts_dir}/containers.py"> <properties> <help>Container applications</help> + <priority>1280</priority> </properties> <children> <tagNode name="name"> diff --git a/interface-definitions/include/dhcp-interface.xml.i b/interface-definitions/include/dhcp-interface.xml.i new file mode 100644 index 000000000..939b45f15 --- /dev/null +++ b/interface-definitions/include/dhcp-interface.xml.i @@ -0,0 +1,15 @@ + <leafNode name="dhcp-interface"> + <properties> + <help>DHCP interface supplying next-hop IP address</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>DHCP interface name</description> + </valueHelp> + <constraint> + <validator name="interface-name"/> + </constraint> + </properties> + </leafNode> diff --git a/interface-definitions/include/ipsec/authentication-pre-shared-secret.xml.i b/interface-definitions/include/ipsec/authentication-pre-shared-secret.xml.i new file mode 100644 index 000000000..af2669335 --- /dev/null +++ b/interface-definitions/include/ipsec/authentication-pre-shared-secret.xml.i @@ -0,0 +1,11 @@ +<!-- include start from ipsec/authentication-pre-shared-secret.xml.i --> +<leafNode name="pre-shared-secret"> + <properties> + <help>Pre-shared secret key</help> + <valueHelp> + <format>txt</format> + <description>Pre-shared secret key</description> + </valueHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/static/static-route.xml.i b/interface-definitions/include/static/static-route.xml.i index 254ea3163..21babc015 100644 --- a/interface-definitions/include/static/static-route.xml.i +++ b/interface-definitions/include/static/static-route.xml.i @@ -31,21 +31,7 @@ </leafNode> </children> </node> - <leafNode name="dhcp-interface"> - <properties> - <help>DHCP interface supplying next-hop IP address</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <valueHelp> - <format>txt</format> - <description>DHCP interface name</description> - </valueHelp> - <constraint> - <validator name="interface-name"/> - </constraint> - </properties> - </leafNode> + #include <include/dhcp-interface.xml.i> <tagNode name="interface"> <properties> <help>Next-hop IPv4 router interface</help> diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in index 56f8ea79c..6851c0354 100644 --- a/interface-definitions/interfaces-tunnel.xml.in +++ b/interface-definitions/interfaces-tunnel.xml.in @@ -61,21 +61,7 @@ </constraint> </properties> </leafNode> - <leafNode name="dhcp-interface"> - <properties> - <help>dhcp interface</help> - <valueHelp> - <format>interface</format> - <description>DHCP interface that supplies the local IP address for this tunnel</description> - </valueHelp> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <constraint> - <regex>^(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+$</regex> - </constraint> - </properties> - </leafNode> + #include <include/dhcp-interface.xml.i> <leafNode name="encapsulation"> <properties> <help>Encapsulation of this tunnel interface</help> diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index 147f351f2..9dbebdc0f 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -52,6 +52,7 @@ <regex>^(disable|enable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> <leafNode name="lifetime"> <properties> @@ -509,22 +510,15 @@ <help>Sets to include an additional secrets file for strongSwan. Use an absolute path to specify the included file.</help> </properties> </leafNode> - <node name="ipsec-interfaces"> + <leafNode name="interface"> <properties> - <help>Interface to use for VPN [REQUIRED]</help> + <help>Onterface used for IPsec communication</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> </properties> - <children> - <leafNode name="interface"> - <properties> - <help>IPsec interface [REQUIRED]</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <multi/> - </properties> - </leafNode> - </children> - </node> + </leafNode> <node name="log"> <properties> <help>IPsec logging</help> @@ -704,15 +698,7 @@ </valueHelp> </properties> </leafNode> - <leafNode name="pre-shared-secret"> - <properties> - <help>Pre-shared secret key</help> - <valueHelp> - <format>txt</format> - <description>Pre-shared secret key</description> - </valueHelp> - </properties> - </leafNode> + #include <include/ipsec/authentication-pre-shared-secret.xml.i> </children> </node> <node name="bind"> @@ -811,11 +797,7 @@ </properties> <defaultValue>x509</defaultValue> </leafNode> - <leafNode name="pre-shared-secret"> - <properties> - <help>Pre-shared-secret used for server authentication</help> - </properties> - </leafNode> + #include <include/ipsec/authentication-pre-shared-secret.xml.i> </children> </node> #include <include/generic-description.xml.i> @@ -947,15 +929,7 @@ </constraint> </properties> </leafNode> - <leafNode name="pre-shared-secret"> - <properties> - <help>Pre-shared secret key</help> - <valueHelp> - <format>txt</format> - <description>Pre-shared secret key</description> - </valueHelp> - </properties> - </leafNode> + #include <include/ipsec/authentication-pre-shared-secret.xml.i> <leafNode name="remote-id"> <properties> <help>ID for remote authentication</help> @@ -1001,14 +975,7 @@ </properties> </leafNode> #include <include/generic-description.xml.i> - <leafNode name="dhcp-interface"> - <properties> - <help>DHCP interface to listen on</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - </properties> - </leafNode> + #include <include/dhcp-interface.xml.i> <leafNode name="force-encapsulation"> <properties> <help>Force UDP Encapsulation for ESP Payloads</help> diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in index 4fbf3fa44..6cf5218ff 100644 --- a/interface-definitions/vpn_l2tp.xml.in +++ b/interface-definitions/vpn_l2tp.xml.in @@ -70,51 +70,8 @@ </completionHelp> </properties> </leafNode> - <leafNode name="pre-shared-secret"> - <properties> - <help>Pre-shared secret for IPsec</help> - </properties> - </leafNode> - <node name="x509"> - <properties> - <help>X.509 certificate</help> - </properties> - <children> - #include <include/certificate-ca.xml.i> - <leafNode name="crl-file"> - <properties> - <help>File containing the X.509 Certificate Revocation List (CRL)</help> - <valueHelp> - <format>txt</format> - <description>File in /config/auth</description> - </valueHelp> - </properties> - </leafNode> - <leafNode name="server-cert-file"> - <properties> - <help>File containing the X.509 certificate for the remote access VPN server (this host)</help> - <valueHelp> - <format>txt</format> - <description>File in /config/auth</description> - </valueHelp> - </properties> - </leafNode> - <leafNode name="server-key-file"> - <properties> - <help>File containing the private key for the X.509 certificate for the remote access VPN server (this host)</help> - <valueHelp> - <format>txt</format> - <description>File in /config/auth</description> - </valueHelp> - </properties> - </leafNode> - <leafNode name="server-key-password"> - <properties> - <help>Password that protects the private key</help> - </properties> - </leafNode> - </children> - </node> + #include <include/ipsec/authentication-pre-shared-secret.xml.i> + #include <include/ipsec/authentication-x509.xml.i> </children> </node> <leafNode name="ike-lifetime"> @@ -128,6 +85,7 @@ <validator name="numeric" argument="--range 30-86400"/> </constraint> </properties> + <defaultValue>3600</defaultValue> </leafNode> <leafNode name="lifetime"> <properties> @@ -140,7 +98,10 @@ <validator name="numeric" argument="--range 30-86400"/> </constraint> </properties> + <defaultValue>3600</defaultValue> </leafNode> + #include <include/ipsec/esp-group.xml.i> + #include <include/ipsec/ike-group.xml.i> </children> </node> #include <include/accel-ppp/wins-server.xml.i> @@ -159,11 +120,7 @@ <help>Description for L2TP remote-access settings</help> </properties> </leafNode> - <leafNode name="dhcp-interface"> - <properties> - <help>DHCP interface to listen on</help> - </properties> - </leafNode> + #include <include/dhcp-interface.xml.i> <leafNode name="idle"> <properties> <help>PPP idle timeout</help> diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py index 510ab7f46..a20f44207 100644 --- a/python/vyos/airbag.py +++ b/python/vyos/airbag.py @@ -18,7 +18,6 @@ from datetime import datetime from vyos import debug from vyos.logger import syslog -from vyos.version import get_version from vyos.version import get_full_version_data @@ -78,7 +77,7 @@ def bug_report(dtype, value, trace): information.update({ 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'trace': trace, - 'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED, + 'instructions': INSTRUCTIONS, 'note': note, }) @@ -162,20 +161,13 @@ When reporting problems, please include as much information as possible: """ -COMMUNITY = """\ -- Make sure you are running the latest version of the code available at - https://downloads.vyos.io/rolling/current/amd64/vyos-rolling-latest.iso -- Consult the forum to see how to handle this issue - https://forum.vyos.io -- Join our community on slack where our users exchange help and advice - https://vyos.slack.com -""".strip() - -SUPPORTED = """\ -- Make sure you are running the latest stable version of VyOS - the code is available at https://downloads.vyos.io/?dir=release/current -- Contact us using the online help desk +INSTRUCTIONS = """\ +- Contact us using the online help desk if you have a subscription: https://support.vyos.io/ -- Join our community on slack where our users exchange help and advice +- Make sure you are running the latest version of VyOS available at: + https://vyos.net/get/ +- Consult the community forum to see how to handle this issue: + https://forum.vyos.io +- Join us on Slack where our users exchange help and advice: https://vyos.slack.com """.strip() diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py index d3e9d5df2..b522cc1ab 100644 --- a/python/vyos/ifconfig/vrrp.py +++ b/python/vyos/ifconfig/vrrp.py @@ -92,11 +92,14 @@ class VRRP(object): try: # send signal to generate the configuration file pid = util.read_file(cls.location['pid']) - os.kill(int(pid), cls._signal[what]) + util.wait_for_file_write_complete(fname, + pre_hook=(lambda: os.kill(int(pid), cls._signal[what])), + timeout=30) - # should look for file size change? - sleep(0.2) return util.read_file(fname) + except OSError: + # raised by vyos.util.read_file + raise VRRPNoData("VRRP data is not available (wait time exceeded)") except FileNotFoundError: raise VRRPNoData("VRRP data is not available (process not running or no active groups)") except Exception: diff --git a/python/vyos/util.py b/python/vyos/util.py index 5a96013ea..d5cd46a6c 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -515,6 +515,7 @@ def wait_for_inotify(file_path, pre_hook=None, event_type=None, timeout=None, sl from inotify.adapters import Inotify from time import time + from time import sleep time_start = time() @@ -530,11 +531,14 @@ def wait_for_inotify(file_path, pre_hook=None, event_type=None, timeout=None, sl # the file failed to have been written to and closed within the timeout raise OSError("Waiting for file {} to be written has failed".format(file_path)) + # Most such events don't take much time, so it's better to check right away + # and sleep later. if event is not None: (_, type_names, path, filename) = event if filename == os.path.basename(file_path): if event_type in type_names: return + sleep(sleep_interval) def wait_for_file_write_complete(file_path, pre_hook=None, timeout=None, sleep_interval=0.1): """ Waits for a process to close a file after opening it in write mode. """ diff --git a/smoketest/configs/pki-ipsec b/smoketest/configs/pki-ipsec index 5025117f7..6fc239d27 100644 --- a/smoketest/configs/pki-ipsec +++ b/smoketest/configs/pki-ipsec @@ -105,6 +105,33 @@ vpn { } } } + l2tp { + remote-access { + authentication { + local-users { + username alice { + password notsecure + } + } + mode local + } + client-ip-pool { + start 192.168.255.2 + stop 192.168.255.254 + } + ipsec-settings { + authentication { + mode x509 + x509 { + ca-cert-file /config/auth/ovpn_test_ca.pem + server-cert-file /config/auth/ovpn_test_server.pem + server-key-file /config/auth/ovpn_test_server.key + } + } + } + outside-address 192.168.150.1 + } + } rsa-keys { local-key { file /config/auth/ovpn_test_server.key diff --git a/smoketest/scripts/cli/test_protocols_nhrp.py b/smoketest/scripts/cli/test_protocols_nhrp.py index 8389e42e9..aa0ac268d 100755 --- a/smoketest/scripts/cli/test_protocols_nhrp.py +++ b/smoketest/scripts/cli/test_protocols_nhrp.py @@ -68,7 +68,7 @@ class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase): self.cli_set(vpn_path + ["ike-group", "IKE-HUB", "proposal", "2", "hash", "sha1"]) # Profile - Not doing full DMVPN checks here, just want to verify the profile name in the output - self.cli_set(vpn_path + ["ipsec-interfaces", "interface", "eth0"]) + self.cli_set(vpn_path + ["interface", "eth0"]) self.cli_set(vpn_path + ["profile", "NHRPVPN", "authentication", "mode", "pre-shared-secret"]) self.cli_set(vpn_path + ["profile", "NHRPVPN", "authentication", "pre-shared-secret", "secret"]) self.cli_set(vpn_path + ["profile", "NHRPVPN", "bind", "tunnel", "tun100"]) diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index fda8b74b1..a34387dc9 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -112,7 +112,7 @@ rgiyCHemtMepq57Pl1Nmj49eEA== class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): def setUp(self): - self.cli_set(base_path + ['ipsec-interfaces', 'interface', f'{interface}.{vif}']) + self.cli_set(base_path + ['interface', f'{interface}.{vif}']) # Set IKE/ESP Groups self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes128']) diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 8e6247a30..804f2d14f 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -25,7 +25,9 @@ from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_source_interface -from vyos.ifconfig import VXLANIf, Interface +from vyos.ifconfig import Interface +from vyos.ifconfig import VXLANIf +from vyos.template import is_ipv6 from vyos import ConfigError from vyos import airbag airbag.enable() @@ -65,12 +67,19 @@ def verify(vxlan): raise ConfigError('Must configure VNI for VXLAN') if 'source_interface' in vxlan: - # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU - # if our configured MTU is at least 50 bytes less + # VXLAN adds at least an overhead of 50 byte - we need to check the + # underlaying device if our VXLAN package is not going to be fragmented! + vxlan_overhead = 50 + if 'source_address' in vxlan and is_ipv6(vxlan['source_address']): + # IPv6 adds an extra 20 bytes overhead because the IPv6 header is 20 + # bytes larger than the IPv4 header - assuming no extra options are + # in use. + vxlan_overhead += 20 + lower_mtu = Interface(vxlan['source_interface']).get_mtu() - if lower_mtu < (int(vxlan['mtu']) + 50): - raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ - f'MTU is to small ({lower_mtu} bytes)') + if lower_mtu < (int(vxlan['mtu']) + vxlan_overhead): + raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\ + f'bytes) for VXLAN overhead ({vxlan_overhead} bytes!)') verify_mtu_ipv6(vxlan) verify_address(vxlan) diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py deleted file mode 100755 index 0599bf101..000000000 --- a/src/conf_mode/ipsec-settings.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2020 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import re -import os - -from time import sleep -from sys import exit - -from vyos.config import Config -from vyos import ConfigError -from vyos.util import call -from vyos.template import render - -from vyos import airbag -airbag.enable() - -ra_conn_name = "remote-access" -ipsec_secrets_file = "/etc/ipsec.secrets" -ipsec_ra_conn_dir = "/etc/ipsec.d/tunnels/" -ipsec_ra_conn_file = ipsec_ra_conn_dir + ra_conn_name -ipsec_conf_file = "/etc/ipsec.conf" -ca_cert_path = "/etc/ipsec.d/cacerts" -server_cert_path = "/etc/ipsec.d/certs" -server_key_path = "/etc/ipsec.d/private" -delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###" -delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###" -charon_pidfile = "/var/run/charon.pid" - -def get_config(config=None): - if config: - config = config - else: - config = Config() - - data = {} - if config.exists("vpn ipsec ipsec-interfaces interface"): - data["ipsec_interfaces"] = config.return_values("vpn ipsec ipsec-interfaces interface") - - # Init config variables - data["delim_ipsec_l2tp_begin"] = delim_ipsec_l2tp_begin - data["delim_ipsec_l2tp_end"] = delim_ipsec_l2tp_end - data["ipsec_ra_conn_file"] = ipsec_ra_conn_file - data["ra_conn_name"] = ra_conn_name - # Get l2tp ipsec settings - data["ipsec_l2tp"] = False - conf_ipsec_command = "vpn l2tp remote-access ipsec-settings " #last space is useful - if config.exists(conf_ipsec_command): - data["ipsec_l2tp"] = True - - # Authentication params - if config.exists(conf_ipsec_command + "authentication mode"): - data["ipsec_l2tp_auth_mode"] = config.return_value(conf_ipsec_command + "authentication mode") - if config.exists(conf_ipsec_command + "authentication pre-shared-secret"): - data["ipsec_l2tp_secret"] = config.return_value(conf_ipsec_command + "authentication pre-shared-secret") - - # mode x509 - if config.exists(conf_ipsec_command + "authentication x509 ca-cert-file"): - data["ipsec_l2tp_x509_ca_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 ca-cert-file") - if config.exists(conf_ipsec_command + "authentication x509 crl-file"): - data["ipsec_l2tp_x509_crl_file"] = config.return_value(conf_ipsec_command + "authentication x509 crl-file") - if config.exists(conf_ipsec_command + "authentication x509 server-cert-file"): - data["ipsec_l2tp_x509_server_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-cert-file") - data["server_cert_file_copied"] = server_cert_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-cert-file")).group(0) - if config.exists(conf_ipsec_command + "authentication x509 server-key-file"): - data["ipsec_l2tp_x509_server_key_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-file") - data["server_key_file_copied"] = server_key_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-key-file")).group(0) - if config.exists(conf_ipsec_command + "authentication x509 server-key-password"): - data["ipsec_l2tp_x509_server_key_password"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-password") - - # Common l2tp ipsec params - if config.exists(conf_ipsec_command + "ike-lifetime"): - data["ipsec_l2tp_ike_lifetime"] = config.return_value(conf_ipsec_command + "ike-lifetime") - else: - data["ipsec_l2tp_ike_lifetime"] = "3600" - - if config.exists(conf_ipsec_command + "lifetime"): - data["ipsec_l2tp_lifetime"] = config.return_value(conf_ipsec_command + "lifetime") - else: - data["ipsec_l2tp_lifetime"] = "3600" - - if config.exists("vpn l2tp remote-access outside-address"): - data['outside_addr'] = config.return_value('vpn l2tp remote-access outside-address') - - return data - -def write_ipsec_secrets(c): - if c.get("ipsec_l2tp_auth_mode") == "pre-shared-secret": - secret_txt = "{0}\n{1} %any : PSK \"{2}\"\n{3}\n".format(delim_ipsec_l2tp_begin, c['outside_addr'], c['ipsec_l2tp_secret'], delim_ipsec_l2tp_end) - elif c.get("ipsec_l2tp_auth_mode") == "x509": - secret_txt = "{0}\n: RSA {1}\n{2}\n".format(delim_ipsec_l2tp_begin, c['server_key_file_copied'], delim_ipsec_l2tp_end) - - old_umask = os.umask(0o077) - with open(ipsec_secrets_file, 'a+') as f: - f.write(secret_txt) - os.umask(old_umask) - -def write_ipsec_conf(c): - ipsec_confg_txt = "{0}\ninclude {1}\n{2}\n".format(delim_ipsec_l2tp_begin, ipsec_ra_conn_file, delim_ipsec_l2tp_end) - - old_umask = os.umask(0o077) - with open(ipsec_conf_file, 'a+') as f: - f.write(ipsec_confg_txt) - os.umask(old_umask) - -### Remove config from file by delimiter -def remove_confs(delim_begin, delim_end, conf_file): - call("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file) - - -### Checking certificate storage and notice if certificate not in /config directory -def check_cert_file_store(cert_name, file_path, dts_path): - if not re.search('^\/config\/.+', file_path): - print("Warning: \"" + file_path + "\" lies outside of /config/auth directory. It will not get preserved during image upgrade.") - #Checking file existence - if not os.path.isfile(file_path): - raise ConfigError("L2TP VPN configuration error: Invalid "+cert_name+" \""+file_path+"\"") - else: - ### Cpy file to /etc/ipsec.d/certs/ /etc/ipsec.d/cacerts/ - # todo make check - ret = call('cp -f '+file_path+' '+dts_path) - if ret: - raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path) - -def verify(data): - # l2tp ipsec check - if 'ipsec_l2tp' in data: - # Checking dependecies for "authentication mode pre-shared-secret" - if data.get("ipsec_l2tp_auth_mode") == "pre-shared-secret": - if not data.get("ipsec_l2tp_secret"): - raise ConfigError("pre-shared-secret required") - if not data.get("outside_addr"): - raise ConfigError("outside-address not defined") - - # Checking dependecies for "authentication mode x509" - if data.get("ipsec_l2tp_auth_mode") == "x509": - if not data.get("ipsec_l2tp_x509_server_key_file"): - raise ConfigError("L2TP VPN configuration error: \"server-key-file\" not defined.") - else: - check_cert_file_store("server-key-file", data['ipsec_l2tp_x509_server_key_file'], server_key_path) - - if not data.get("ipsec_l2tp_x509_server_cert_file"): - raise ConfigError("L2TP VPN configuration error: \"server-cert-file\" not defined.") - else: - check_cert_file_store("server-cert-file", data['ipsec_l2tp_x509_server_cert_file'], server_cert_path) - - if not data.get("ipsec_l2tp_x509_ca_cert_file"): - raise ConfigError("L2TP VPN configuration error: \"ca-cert-file\" must be defined for X.509") - else: - check_cert_file_store("ca-cert-file", data['ipsec_l2tp_x509_ca_cert_file'], ca_cert_path) - - if not data.get('ipsec_interfaces'): - raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.") - -def generate(data): - if 'ipsec_l2tp' in data: - remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file) - # old_umask = os.umask(0o077) - # render(ipsec_secrets_file, 'ipsec/ipsec.secrets.tmpl', data) - # os.umask(old_umask) - ## Use this method while IPSec CLI handler won't be overwritten to python - write_ipsec_secrets(data) - - old_umask = os.umask(0o077) - - # Create tunnels directory if does not exist - if not os.path.exists(ipsec_ra_conn_dir): - os.makedirs(ipsec_ra_conn_dir) - - render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', data) - os.umask(old_umask) - - remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file) - # old_umask = os.umask(0o077) - # render(ipsec_conf_file, 'ipsec/ipsec.conf.tmpl', data) - # os.umask(old_umask) - ## Use this method while IPSec CLI handler won't be overwritten to python - write_ipsec_conf(data) - - else: - if os.path.exists(ipsec_ra_conn_file): - remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file) - remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file) - remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file) - -def restart_ipsec(): - call('ipsec restart >&/dev/null') - # counter for apply swanctl config - counter = 10 - while counter <= 10: - if os.path.exists(charon_pidfile): - call('swanctl -q >&/dev/null') - break - counter -=1 - sleep(1) - if counter == 0: - raise ConfigError('VPN configuration error: IPSec is not running.') - -def apply(data): - # Restart IPSec daemon - restart_ipsec() - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 3fab8e868..a359361f3 100755..100644 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -73,6 +73,7 @@ def get_config(config=None): else: conf = Config() base = ['vpn', 'ipsec'] + l2tp_base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings'] if not conf.exists(base): return None @@ -108,15 +109,22 @@ def get_config(config=None): ipsec['dhcp_no_address'] = {} ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes - ipsec['interface_change'] = leaf_node_changed(conf, base + ['ipsec-interfaces', - 'interface']) - ipsec['l2tp_exists'] = conf.exists(['vpn', 'l2tp', 'remote-access', - 'ipsec-settings']) + ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface']) ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel']) ipsec['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + ipsec['l2tp'] = conf.get_config_dict(l2tp_base, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + if ipsec['l2tp']: + l2tp_defaults = defaults(l2tp_base) + ipsec['l2tp'] = dict_merge(l2tp_defaults, ipsec['l2tp']) + ipsec['l2tp_outside_address'] = conf.return_value(['vpn', 'l2tp', 'remote-access', 'outside-address']) + ipsec['l2tp_ike_default'] = 'aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024' + ipsec['l2tp_esp_default'] = 'aes256-sha1,3des-sha1' + return ipsec def get_dhcp_address(iface): @@ -165,14 +173,43 @@ def verify(ipsec): if not ipsec: return None - if 'ipsec_interfaces' in ipsec and 'interface' in ipsec['ipsec_interfaces']: - interfaces = ipsec['ipsec_interfaces']['interface'] - if isinstance(interfaces, str): - interfaces = [interfaces] - - for ifname in interfaces: + if 'interfaces' in ipsec : + for ifname in ipsec['interface']: verify_interface_exists(ifname) + if ipsec['l2tp']: + if 'esp_group' in ipsec['l2tp']: + if 'esp_group' not in ipsec or ipsec['l2tp']['esp_group'] not in ipsec['esp_group']: + raise ConfigError(f"Invalid esp-group on L2TP remote-access config") + + if 'ike_group' in ipsec['l2tp']: + if 'ike_group' not in ipsec or ipsec['l2tp']['ike_group'] not in ipsec['ike_group']: + raise ConfigError(f"Invalid ike-group on L2TP remote-access config") + + if 'authentication' not in ipsec['l2tp']: + raise ConfigError(f'Missing authentication settings on L2TP remote-access config') + + if 'mode' not in ipsec['l2tp']['authentication']: + raise ConfigError(f'Missing authentication mode on L2TP remote-access config') + + if not ipsec['l2tp_outside_address']: + raise ConfigError(f'Missing outside-address on L2TP remote-access config') + + if ipsec['l2tp']['authentication']['mode'] == 'pre-shared-secret': + if 'pre_shared_secret' not in ipsec['l2tp']['authentication']: + raise ConfigError(f'Missing pre shared secret on L2TP remote-access config') + + if ipsec['l2tp']['authentication']['mode'] == 'x509': + if 'x509' not in ipsec['l2tp']['authentication']: + raise ConfigError(f'Missing x509 settings on L2TP remote-access config') + + x509 = ipsec['l2tp']['authentication']['x509'] + + if 'ca_certificate' not in x509 or 'certificate' not in x509: + raise ConfigError(f'Missing x509 certificates on L2TP remote-access config') + + verify_pki_x509(ipsec['pki'], x509) + if 'profile' in ipsec: for profile, profile_conf in ipsec['profile'].items(): if 'esp_group' in profile_conf: @@ -389,6 +426,10 @@ def generate(ipsec): if not os.path.exists(KEY_PATH): os.mkdir(KEY_PATH, mode=0o700) + if ipsec['l2tp']: + if 'authentication' in ipsec['l2tp'] and 'x509' in ipsec['l2tp']['authentication']: + generate_pki_files_x509(ipsec['pki'], ipsec['l2tp']['authentication']['x509']) + if 'remote_access' in ipsec: for rw, rw_conf in ipsec['remote_access'].items(): if 'authentication' in rw_conf and 'x509' in rw_conf['authentication']: @@ -439,14 +480,6 @@ def generate(ipsec): render(interface_conf, 'ipsec/interfaces_use.conf.tmpl', ipsec) render(swanctl_conf, 'ipsec/swanctl.conf.tmpl', ipsec) -def resync_l2tp(ipsec): - if ipsec and not ipsec['l2tp_exists']: - return - - tmp = run('/usr/libexec/vyos/conf_mode/ipsec-settings.py') - if tmp > 0: - print('ERROR: failed to reapply L2TP IPSec settings!') - def resync_nhrp(ipsec): if ipsec and not ipsec['nhrp_exists']: return @@ -480,7 +513,6 @@ def apply(ipsec): if wait_for_vici_socket(): call('sudo swanctl -q') - resync_l2tp(ipsec) resync_nhrp(ipsec) if __name__ == '__main__': diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index e970d2ef5..9c52f77ca 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -20,7 +20,6 @@ import re from copy import deepcopy from stat import S_IRUSR, S_IWUSR, S_IRGRP from sys import exit -from time import sleep from ipaddress import ip_network diff --git a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py index ec33906ba..4e7fb117c 100755 --- a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py +++ b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py @@ -25,9 +25,8 @@ def get_config(): c = Config() interfaces = dict() for intf in c.list_effective_nodes('interfaces ethernet'): - # skip interfaces that are disabled or is configured for dhcp + # skip interfaces that are disabled check_disable = f'interfaces ethernet {intf} disable' - check_dhcp = f'interfaces ethernet {intf} address dhcp' if c.exists_effective(check_disable): continue @@ -49,10 +48,10 @@ def apply(config): # add configured addresses to interface for addr in addresses: - if addr == 'dhcp': - cmd = ['dhclient', intf] - else: - cmd = f'ip address add {addr} dev {intf}' + # dhcp is handled by netplug + if addr in ['dhcp', 'dhcpv6']: + continue + cmd = f'ip address add {addr} dev {intf}' syslog.syslog(cmd) run(cmd) diff --git a/src/migration-scripts/ipsec/5-to-6 b/src/migration-scripts/ipsec/5-to-6 index ba5ce0fca..76ee9ecba 100755 --- a/src/migration-scripts/ipsec/5-to-6 +++ b/src/migration-scripts/ipsec/5-to-6 @@ -74,6 +74,12 @@ log_mode = log + ['log-modes'] if config.exists(log_mode): config.rename(log_mode, 'subsystem') +# Rename "ipsec-interfaces interface" to "interface" +base_interfaces = base + ['ipsec-interfaces', 'interface'] +if config.exists(base_interfaces): + config.copy(base_interfaces, base + ['interface']) + config.delete(base_interfaces) + try: with open(file_name, 'w') as f: f.write(config.to_string()) diff --git a/src/migration-scripts/l2tp/3-to-4 b/src/migration-scripts/l2tp/3-to-4 new file mode 100755 index 000000000..18eabadec --- /dev/null +++ b/src/migration-scripts/l2tp/3-to-4 @@ -0,0 +1,169 @@ +#!/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/>. + +# - remove primary/secondary identifier from nameserver +# - TODO: remove radius server req-limit + +import os + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree +from vyos.pki import load_certificate +from vyos.pki import load_crl +from vyos.pki import load_private_key +from vyos.pki import encode_certificate +from vyos.pki import encode_private_key +from vyos.util import run + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings'] +pki_base = ['pki'] + +if not config.exists(base): + exit(0) + +AUTH_DIR = '/config/auth' + +def wrapped_pem_to_config_value(pem): + return "".join(pem.strip().split("\n")[1:-1]) + +if not config.exists(base + ['authentication', 'x509']): + exit(0) + +x509_base = base + ['authentication', 'x509'] +pki_name = 'l2tp_remote_access' + +if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + +if not config.exists(pki_base + ['certificate']): + config.set(pki_base + ['certificate']) + config.set_tag(pki_base + ['certificate']) + +if config.exists(x509_base + ['ca-cert-file']): + cert_file = config.return_value(x509_base + ['ca-cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=pki_name) + else: + print(f'Failed to migrate CA certificate on l2tp remote-access config') + + config.delete(x509_base + ['ca-cert-file']) + +if config.exists(x509_base + ['crl-file']): + crl_file = config.return_value(x509_base + ['crl-file']) + crl_path = os.path.join(AUTH_DIR, crl_file) + crl = None + + if os.path.isfile(crl_path): + if not os.access(crl_path, os.R_OK): + run(f'sudo chmod 644 {crl_path}') + + with open(crl_path, 'r') as f: + crl_data = f.read() + crl = load_certificate(crl_data, wrap_tags=False) + + if crl: + crl_pem = encode_certificate(crl) + config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) + else: + print(f'Failed to migrate CRL on l2tp remote-access config') + + config.delete(x509_base + ['crl-file']) + +if config.exists(x509_base + ['server-cert-file']): + cert_file = config.return_value(x509_base + ['server-cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['certificate'], value=pki_name) + else: + print(f'Failed to migrate certificate on l2tp remote-access config') + + config.delete(x509_base + ['server-cert-file']) + +if config.exists(x509_base + ['server-key-file']): + key_file = config.return_value(x509_base + ['server-key-file']) + key_passphrase = None + + if config.exists(x509_base + ['server-key-password']): + key_passphrase = config.return_value(x509_base + ['server-key-password']) + + key_path = os.path.join(AUTH_DIR, key_file) + key = None + + if os.path.isfile(key_path): + if not os.access(key_path, os.R_OK): + run(f'sudo chmod 644 {key_path}') + + with open(key_path, 'r') as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=key_passphrase, wrap_tags=False) + + if key: + key_pem = encode_private_key(key, passphrase=key_passphrase) + config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) + + if key_passphrase: + config.set(pki_base + ['certificate', pki_name, 'private', 'password-protected']) + config.set(x509_base + ['private-key-passphrase'], value=key_passphrase) + else: + print(f'Failed to migrate private key on l2tp remote-access config') + + config.delete(x509_base + ['server-key-file']) + if config.exists(x509_base + ['server-key-password']): + config.delete(x509_base + ['server-key-password']) + +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) |