diff options
| author | Christian Breunig <christian@breunig.cc> | 2026-09-30 20:13:23 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-30 20:13:23 +0200 |
| commit | dfe2203a7fe7fa24de3fa2d8e82fa85dbb0eba52 (patch) | |
| tree | c32d3be6c03f706f81e97be0bb41114cc8efadb2 | |
| parent | 04843e7a470c36a11a51ee865da96d167295f4aa (diff) | |
| parent | 12d3aa212f35b8cdd9a0ccd849b45beea46ed94d (diff) | |
| download | vyos-1x-dfe2203a7fe7fa24de3fa2d8e82fa85dbb0eba52.tar.gz vyos-1x-dfe2203a7fe7fa24de3fa2d8e82fa85dbb0eba52.zip | |
Merge pull request #5515 from ordex/T9364-openvpn-dco-mtu
T9364: openvpn: default the MTU to 1420 when DCO is enabled
| -rw-r--r-- | data/templates/openvpn/server.conf.j2 | 3 | ||||
| -rw-r--r-- | interface-definitions/interfaces_openvpn.xml.in | 14 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_interfaces_openvpn.py | 255 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces_openvpn.py | 70 | ||||
| -rw-r--r-- | src/tests/test_interfaces_openvpn.py | 117 |
5 files changed, 457 insertions, 2 deletions
diff --git a/data/templates/openvpn/server.conf.j2 b/data/templates/openvpn/server.conf.j2 index 999af5086..729ab7a80 100644 --- a/data/templates/openvpn/server.conf.j2 +++ b/data/templates/openvpn/server.conf.j2 @@ -52,6 +52,9 @@ compress lzo {% if offload.dco is not vyos_defined %} disable-dco {% endif %} +{% if tun_mtu is vyos_defined %} +tun-mtu {{ tun_mtu }} +{% endif %} {% if mode is vyos_defined('client') %} # diff --git a/interface-definitions/interfaces_openvpn.xml.in b/interface-definitions/interfaces_openvpn.xml.in index 9ddd95518..26b004520 100644 --- a/interface-definitions/interfaces_openvpn.xml.in +++ b/interface-definitions/interfaces_openvpn.xml.in @@ -313,6 +313,20 @@ </constraint> </properties> </leafNode> + <leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU) - with DCO the default is 1420</help> + <valueHelp> + <format>u32:68-16000</format> + <description>Maximum Transmission Unit in byte</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 68-16000"/> + </constraint> + <constraintErrorMessage>MTU must be between 68 and 16000</constraintErrorMessage> + </properties> + <defaultValue>1500</defaultValue> + </leafNode> <node name="offload"> <properties> <help>Configurable offload options</help> diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py index 7c7c3f69e..93c79e646 100755 --- a/smoketest/scripts/cli/test_interfaces_openvpn.py +++ b/smoketest/scripts/cli/test_interfaces_openvpn.py @@ -44,6 +44,7 @@ from vyos.template import address_from_cidr from vyos.template import inc_ip from vyos.template import last_host_address from vyos.template import netmask_from_cidr +from vyos.xml_ref import default_value PROCESS_NAME = 'openvpn' @@ -78,6 +79,10 @@ interface = '' remote_host = '' vrf_name = 'orange' dummy_if = 'dum1301' +# the MTU interfaces_openvpn.py picks when data channel offload is enabled +dco_default_mtu = 1420 +# the one OpenVPN uses on its own +default_mtu = int(default_value(base_path + ['vtun0', 'mtu'])) def get_vrf(interface): for upper in glob(f'/sys/class/net/{interface}/upper*'): @@ -240,6 +245,12 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.assertDcoDataPath(interface, multipoint=False) + # the default is left to OpenVPN, which applies it - or whatever the + # server pushes - once connected, so only the rendered option can tell + self.assertIn( + f'tun-mtu {dco_default_mtu}', read_file(f'/run/openvpn/{interface}.conf') + ) + def test_openvpn_dco_peer_reaches_the_kernel(self): # VyOS creates the "ovpn" device itself whenever the daemon cannot - # a client whose server is not up yet never opens its tun, which is @@ -718,6 +729,250 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): tmp = json.loads(cmdl(['ip', '-d', '-j', 'link', 'show', 'dev', interface])) self.assertEqual(tmp[0].get('linkinfo', {}).get('info_kind'), 'tun') + def assertMtu(self, interface, mtu): + # the daemon applies "tun-mtu" on its own once it brings the tunnel up + tmp = None + for _ in range(10): + # the interface can briefly be gone while the daemon restarts + tmp = (get_interface_config(interface) or {}).get('mtu') + if tmp == mtu: + break + sleep(1) + self.assertEqual(tmp, mtu) + + def main_pid(self, interface): + return cmdl( + [ + 'systemctl', + 'show', + '--property=MainPID', + '--value', + f'openvpn@{interface}.service', + ] + ).strip() + + def assertRestarted(self, interface, old_pid): + # OpenVPN applies "tun-mtu" only when it brings the tunnel up, so the + # MTU on the link alone - which VyOS sets too - can not tell + pid = old_pid + for _ in range(10): + pid = self.main_pid(interface) + if pid not in (old_pid, '0'): + break + sleep(1) + self.assertNotIn(pid, (old_pid, '0'), f'openvpn@{interface} not restarted') + + def test_openvpn_server_dco_mtu(self): + # The "ovpn" Kernel module does not clamp the MSS, so enabling the + # offload must lower the MTU unless one is configured explicitly + interface = 'vtun5000' + path = base_path + [interface] + config_file = f'/run/openvpn/{interface}.conf' + + self.cli_set(path + ['mode', 'server']) + self.cli_set(path + ['local-port', '2000']) + self.cli_set(path + ['server', 'subnet', '192.0.2.0/24']) + self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test']) + self.cli_set(path + ['tls', 'certificate', 'ovpn_test']) + self.cli_set(path + ['tls', 'dh-params', 'ovpn_test']) + self.cli_set(path + ['encryption', 'data-ciphers', 'aes256gcm']) + self.cli_commit() + + # the userspace data path keeps OpenVPN's own default and its mssfix + self.assertNotIn('tun-mtu', read_file(config_file)) + self.assertMtu(interface, default_mtu) + + self.cli_set(path + ['offload', 'dco']) + self.cli_commit() + + self.assertIn(f'tun-mtu {dco_default_mtu}', read_file(config_file)) + self.assertDcoDataPath(interface) + self.assertMtu(interface, dco_default_mtu) + + # an explicit MTU wins over the default, and takes a restart to apply + pid = self.main_pid(interface) + self.cli_set(path + ['mtu', '1380']) + self.cli_commit() + + self.assertIn('tun-mtu 1380', read_file(config_file)) + self.assertRestarted(interface, pid) + self.assertDcoDataPath(interface) + self.assertMtu(interface, 1380) + + # check validate() - one MTU only, a raw one would silently win + self.cli_set(path + ['openvpn-option', '--tun-mtu 1400']) + with self.assertRaisesRegex(ConfigSessionError, r'openvpn-option\s+tun-mtu'): + self.cli_commit() + + # without the CLI one the raw option is honoured, no default added + self.cli_delete(path + ['mtu']) + self.cli_commit() + + self.assertNotIn('tun-mtu', read_file(config_file)) + self.assertDcoDataPath(interface) + self.assertMtu(interface, 1400) + + # and once the raw option is gone, the default is back + self.cli_delete(path + ['openvpn-option']) + self.cli_commit() + + self.assertIn(f'tun-mtu {dco_default_mtu}', read_file(config_file)) + self.assertDcoDataPath(interface) + self.assertMtu(interface, dco_default_mtu) + + # an option merely named alike does not size the tunnel + self.cli_set(path + ['openvpn-option', '--tun-mtu-max 1600']) + self.cli_commit() + + self.assertIn(f'tun-mtu {dco_default_mtu}', read_file(config_file)) + self.assertDcoDataPath(interface) + self.assertMtu(interface, dco_default_mtu) + self.cli_delete(path + ['openvpn-option']) + + # OpenVPN refuses "tun-mtu" next to "link-mtu", so a raw "link-mtu" + # must keep the default out or the daemon would not start + self.cli_set(path + ['openvpn-option', '--link-mtu 1450']) + self.cli_commit() + + self.assertNotIn('tun-mtu', read_file(config_file)) + self.assertDcoDataPath(interface) + + # check validate() - the same goes for an explicit MTU + self.cli_set(path + ['mtu', '1380']) + with self.assertRaisesRegex(ConfigSessionError, r'openvpn-option\s+link-mtu'): + self.cli_commit() + self.cli_delete(path + ['mtu']) + self.cli_delete(path + ['openvpn-option']) + self.cli_commit() + + self.assertIn(f'tun-mtu {dco_default_mtu}', read_file(config_file)) + self.assertDcoDataPath(interface) + self.assertMtu(interface, dco_default_mtu) + + # check validate() - IPv6 inside the tunnel needs its minimum MTU + self.cli_set(path + ['server', 'subnet', '2001:db8::/64']) + self.cli_set(path + ['mtu', '1200']) + with self.assertRaisesRegex(ConfigSessionError, r'minimum\s+MTU\s+is\s+"1280"'): + self.cli_commit() + self.cli_delete(path + ['server', 'subnet', '2001:db8::/64']) + self.cli_delete(path + ['mtu']) + + # and without the offload the default goes away with it + self.cli_delete(path + ['offload']) + self.cli_commit() + + self.assertNotIn('tun-mtu', read_file(config_file)) + self.assertMtu(interface, default_mtu) + + def test_openvpn_server_mtu(self): + # Without the offload an MTU is only ever what the CLI asks for + interface = 'vtun5000' + path = base_path + [interface] + config_file = f'/run/openvpn/{interface}.conf' + + self.cli_set(path + ['mode', 'server']) + self.cli_set(path + ['local-port', '2000']) + self.cli_set(path + ['server', 'subnet', '192.0.2.0/24']) + self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test']) + self.cli_set(path + ['tls', 'certificate', 'ovpn_test']) + self.cli_set(path + ['tls', 'dh-params', 'ovpn_test']) + self.cli_set(path + ['mtu', '1380']) + self.cli_commit() + + config = read_file(config_file) + self.assertIn('disable-dco', config) + self.assertIn('tun-mtu 1380', config) + self.assertMtu(interface, 1380) + self.assertTrue(is_systemd_service_running(f'openvpn@{interface}.service')) + + # check validate() - OpenVPN refuses anything below its own minimum + self.cli_set(path + ['mtu', '99']) + with self.assertRaisesRegex(ConfigSessionError, r'at\s+least\s+100'): + self.cli_commit() + self.cli_set(path + ['mtu', '1380']) + + # check validate() - one MTU only, whichever raw option sizes it + for option in ['--tun-mtu 1400', '--link-mtu 1450', '--udp-mtu 1450']: + keyword = option.split()[0].lstrip('-') + self.cli_set(path + ['openvpn-option', option]) + with self.assertRaisesRegex( + ConfigSessionError, rf'openvpn-option\s+{keyword}' + ): + self.cli_commit() + self.cli_delete(path + ['openvpn-option']) + + # dropping the MTU restarts the daemon, which then falls back to its + # own default + pid = self.main_pid(interface) + self.cli_delete(path + ['mtu']) + self.cli_commit() + + self.assertNotIn('tun-mtu', read_file(config_file)) + self.assertRestarted(interface, pid) + self.assertMtu(interface, default_mtu) + # a new PID alone would also come from a daemon stuck restarting + self.assertTrue(is_systemd_service_running(f'openvpn@{interface}.service')) + + # the XML default is no configured MTU, so a raw option sizing the + # tunnel neither conflicts with it nor is overridden by it + self.cli_set(path + ['openvpn-option', '--tun-mtu 1400']) + self.cli_commit() + + self.assertNotIn('tun-mtu', read_file(config_file)) + self.assertMtu(interface, 1400) + self.assertTrue(is_systemd_service_running(f'openvpn@{interface}.service')) + + def test_openvpn_site2site_dco_mtu(self): + # the default applies to the point-to-point data path just the same + interface = 'vtun5000' + path = base_path + [interface] + config_file = f'/run/openvpn/{interface}.conf' + + self.cli_set(path + ['mode', 'site-to-site']) + self.cli_set(path + ['local-address', '10.0.0.1']) + self.cli_set(path + ['remote-address', '192.168.0.1']) + self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test']) + self.cli_set(path + ['tls', 'certificate', 'ovpn_test']) + self.cli_set(path + ['tls', 'role', 'active']) + self.cli_set(path + ['encryption', 'data-ciphers-fallback', 'aes256gcm']) + self.cli_set(path + ['offload', 'dco']) + self.cli_commit() + + self.assertIn(f'tun-mtu {dco_default_mtu}', read_file(config_file)) + self.assertDcoDataPath(interface, multipoint=False) + self.assertMtu(interface, dco_default_mtu) + + self.cli_set(path + ['mtu', '1380']) + self.cli_commit() + + self.assertIn('tun-mtu 1380', read_file(config_file)) + self.assertDcoDataPath(interface, multipoint=False) + self.assertMtu(interface, 1380) + + def test_openvpn_site2site_mtu_ipv6(self): + # IPv6 inside a site-to-site tunnel comes from its local-address + interface = 'vtun5000' + path = base_path + [interface] + config_file = f'/run/openvpn/{interface}.conf' + + self.cli_set(path + ['mode', 'site-to-site']) + self.cli_set(path + ['local-address', '2001:db8:1::1']) + self.cli_set(path + ['remote-address', '2001:db8:ffff::1']) + self.cli_set(path + ['shared-secret-key', 'ovpn_test']) + self.cli_set(path + ['encryption', 'cipher', 'aes256']) + self.cli_set(path + ['mtu', '1279']) + + # check validate() - IPv6 needs its minimum link MTU + with self.assertRaisesRegex(ConfigSessionError, r'minimum\s+MTU\s+is\s+"1280"'): + self.cli_commit() + + self.cli_set(path + ['mtu', '1280']) + self.cli_commit() + + self.assertIn('tun-mtu 1280', read_file(config_file)) + self.assertMtu(interface, 1280) + self.assertTrue(is_systemd_service_running(f'openvpn@{interface}.service')) + def test_openvpn_server_dco_verify(self): # Configurations the "ovpn" Kernel module can not serve must be # rejected once data channel offload is requested diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py index 130c027a4..109e0f401 100755 --- a/src/conf_mode/interfaces_openvpn.py +++ b/src/conf_mode/interfaces_openvpn.py @@ -107,6 +107,18 @@ dco_cipher_options = ['data-ciphers', 'data-ciphers-fallback', 'ncp-ciphers'] # dco_ciphers again, spelled the way OpenVPN reports them in # dco_get_supported_ciphers() dco_raw_ciphers = ['AES-128-GCM', 'AES-192-GCM', 'AES-256-GCM', 'CHACHA20-POLY1305'] +# The "ovpn" Kernel module does not clamp the TCP MSS the way the userspace data +# path does with "mssfix", so a 1500 bytes tunnel MTU fragments every full sized +# packet on the underlay. Leave room for the worst case encapsulation: IPv6 (40) +# plus UDP (8) plus the DCO data channel header (24: opcode and peer-id 4, +# packet-id 4, AEAD tag 16) is 72 bytes, rounded down to the WireGuard default. +dco_default_mtu = '1420' +# Raw options that size the tunnel themselves. OpenVPN refuses "tun-mtu" next to +# "link-mtu" (or its alias "udp-mtu"), so any of them rules out the one rendered +# from "mtu". +raw_mtu_options = ['tun-mtu', 'link-mtu', 'udp-mtu'] +# TUN_MTU_MIN in OpenVPN's mtu.h +openvpn_min_mtu = 100 otp_path = '/config/auth/openvpn' otp_file = '/config/auth/openvpn/{ifname}-otp-secrets' secret_chars = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567') @@ -183,8 +195,9 @@ def get_config(config=None): if is_node_changed(conf, base + [ifname, 'openvpn-option']): openvpn.update({'restart_required': {}}) # the offload, the operating mode and the device type all decide what kind - # of interface the data path needs, which can only change on a restart - for node in [['offload', 'dco'], ['mode'], ['device-type']]: + # of interface the data path needs, which can only change on a restart. + # OpenVPN also applies "tun-mtu" only when it brings the interface up. + for node in [['offload', 'dco'], ['mode'], ['device-type'], ['mtu']]: if is_node_changed(conf, base + [ifname] + node): openvpn.update({'restart_required': {}}) break @@ -219,12 +232,38 @@ def get_config(config=None): else: openvpn['protocol_modifier'] = '' + # The XML default only documents what OpenVPN uses on its own, so only an + # MTU set on the CLI counts. Anything else is left to OpenVPN to apply: VyOS + # sets an explicit "mtu" on the link only, so neither a raw option nor a + # tun-mtu pushed by the server is overridden. + if conf.exists(base + [ifname, 'mtu']): + openvpn['tun_mtu'] = openvpn['mtu'] + else: + openvpn.pop('mtu', None) + # DCO can not fix the MSS, so pick an MTU that does not fragment on + # the underlay - unless a raw option sizes the tunnel + if ( + dict_search('offload.dco', openvpn) is not None + and raw_mtu_option(openvpn) is None + ): + openvpn['tun_mtu'] = dco_default_mtu + # Check vrf membership, to ensure firewall is updated if is_vrf_changed(conf, ifname): set_dependents('firewall', conf) return openvpn + +def raw_mtu_option(openvpn): + """The raw "openvpn-option" keyword that sizes the tunnel, if any.""" + for option in dict_search('openvpn_option', openvpn) or []: + tmp = option.split() + if tmp and tmp[0].lstrip('-') in raw_mtu_options: + return tmp[0].lstrip('-') + return None + + def is_ec_private_key(pki, cert_name): if not pki or 'certificate' not in pki: return False @@ -331,6 +370,32 @@ def verify_shared_secret(pki: dict, interface: str, path: list, name: str): f'Invalid "{path_str}" value "{name}" on OpenVPN interface {interface}' ) + +def verify_openvpn_mtu(openvpn): + if 'mtu' not in openvpn: + return + + # The raw option lands on the command line: "tun-mtu" silently overrides the + # rendered one while VyOS still applies "mtu" to the link, and "link-mtu" + # keeps the daemon from starting at all + keyword = raw_mtu_option(openvpn) + if keyword is not None: + raise ConfigError(f'Cannot use "mtu" together with "openvpn-option {keyword}"') + + if int(openvpn['mtu']) < openvpn_min_mtu: + raise ConfigError(f'OpenVPN requires an MTU of at least {openvpn_min_mtu}') + + # The tunnel addresses tell whether IPv6 runs inside the tunnel, which then + # needs the IPv6 minimum link MTU. "ip-version" is about the underlay only. + addresses = list(dict_search('local_address', openvpn) or []) + addresses += dict_search('server.subnet', openvpn) or [] + min_mtu = 1280 + if int(openvpn['mtu']) < min_mtu and any(is_ipv6(addr) for addr in addresses): + raise ConfigError( + f'IPv6 is used on interface "{openvpn["ifname"]}", ' + f'the required minimum MTU is "{min_mtu}"!' + ) + def verify_pki(openvpn): pki = openvpn['pki'] interface = openvpn['ifname'] @@ -816,6 +881,7 @@ def verify(openvpn): verify_data_ciphers_fallback(openvpn) verify_dco(openvpn) + verify_openvpn_mtu(openvpn) return None diff --git a/src/tests/test_interfaces_openvpn.py b/src/tests/test_interfaces_openvpn.py new file mode 100644 index 000000000..886bf5b05 --- /dev/null +++ b/src/tests/test_interfaces_openvpn.py @@ -0,0 +1,117 @@ +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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 importlib +import os +import unittest + +from vyos import ConfigError + +try: + openvpn = importlib.import_module('src.conf_mode.interfaces_openvpn') +except ModuleNotFoundError: # for unittest.main() + import sys + + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + openvpn = importlib.import_module('src.conf_mode.interfaces_openvpn') + + +class TestRawMtuOption(unittest.TestCase): + def test_sizing_options(self): + # with or without the dashes, and wherever it is in the list + for options, keyword in [ + (['--tun-mtu 1400'], 'tun-mtu'), + (['tun-mtu 1400'], 'tun-mtu'), + (['--verb 3', '--tun-mtu 1400'], 'tun-mtu'), + (['--link-mtu 1450'], 'link-mtu'), + (['link-mtu 1450'], 'link-mtu'), + (['--udp-mtu 1450'], 'udp-mtu'), + ]: + with self.subTest(options=options): + config = {'openvpn_option': options} + self.assertEqual(openvpn.raw_mtu_option(config), keyword) + + def test_lookalikes(self): + # options merely named alike, or carrying tun-mtu as an argument, do + # not size the tunnel + for options in [ + ['--tun-mtu-max 1600'], + ['tun-mtu-extra 32'], + ['push "tun-mtu 1400"'], + ['--verb 3'], + [''], + [], + ]: + with self.subTest(options=options): + config = {'openvpn_option': options} + self.assertIsNone(openvpn.raw_mtu_option(config)) + + self.assertIsNone(openvpn.raw_mtu_option({})) + + +class TestVerifyOpenvpnMtu(unittest.TestCase): + def config(self, **kwargs): + return {'ifname': 'vtun0', 'mode': 'server', **kwargs} + + def test_no_mtu(self): + # nothing to check - not even a raw option on its own + openvpn.verify_openvpn_mtu(self.config()) + openvpn.verify_openvpn_mtu(self.config(openvpn_option=['--tun-mtu 1400'])) + + def test_raw_option_conflict(self): + for option, keyword in [ + ('--tun-mtu 1400', 'tun-mtu'), + ('--link-mtu 1450', 'link-mtu'), + ('--udp-mtu 1450', 'udp-mtu'), + ]: + with self.subTest(option=option): + config = self.config(mtu='1420', openvpn_option=[option]) + with self.assertRaisesRegex( + ConfigError, rf'openvpn-option\s+{keyword}' + ): + openvpn.verify_openvpn_mtu(config) + + # a lookalike is no conflict + config = self.config(mtu='1420', openvpn_option=['--tun-mtu-max 1600']) + openvpn.verify_openvpn_mtu(config) + + def test_minimum_mtu(self): + with self.assertRaisesRegex(ConfigError, r'at\s+least\s+100'): + openvpn.verify_openvpn_mtu(self.config(mtu='99')) + openvpn.verify_openvpn_mtu(self.config(mtu='100')) + + def test_ipv6_minimum_mtu(self): + server = {'subnet': ['192.0.2.0/24', '2001:db8::/64']} + local_address = {'2001:db8::1': {}} + for kwargs in [ + {'server': server}, + {'mode': 'site-to-site', 'local_address': local_address}, + ]: + with self.subTest(**kwargs): + config = self.config(mtu='1279', **kwargs) + with self.assertRaisesRegex(ConfigError, r'minimum\s+MTU'): + openvpn.verify_openvpn_mtu(config) + openvpn.verify_openvpn_mtu(self.config(mtu='1280', **kwargs)) + + # IPv4 alone may go below the IPv6 minimum + for kwargs in [ + {'server': {'subnet': ['192.0.2.0/24']}}, + {'mode': 'site-to-site', 'local_address': {'10.0.0.1': {}}}, + ]: + with self.subTest(**kwargs): + openvpn.verify_openvpn_mtu(self.config(mtu='1200', **kwargs)) + + +if __name__ == '__main__': + unittest.main() |
