summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntonio Quartulli <antonio@mandelbit.com>2026-09-02 01:29:29 +0200
committerAntonio Quartulli <antonio@mandelbit.com>2026-09-15 10:05:28 +0200
commit9df5dc73cb3c0b4ff554436fae3f5ed2d020fcf7 (patch)
tree8135130b084575b9ecc8dd5f93980746793ed763
parent06dee60d87c6aa498cb43a7b3f43dd00e9b74ac6 (diff)
downloadvyos-1x-9df5dc73cb3c0b4ff554436fae3f5ed2d020fcf7.tar.gz
vyos-1x-9df5dc73cb3c0b4ff554436fae3f5ed2d020fcf7.zip
smoketest: T8264: cover the DCO data path
Checking the link kind cannot tell a working offload from a device in the wrong operating mode, so compare the mode the Kernel reports. Cover enabling DCO on a running interface, a client, which is the only case where VyOS is definitively the one creating the device, and a raw option where it deliberately is not. Every rejection in the verify test stops before apply(), so the offload is re-checked after a change that actually reaches it.
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_openvpn.py176
1 files changed, 168 insertions, 8 deletions
diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py
index 74a0bf287..e003d3c85 100755
--- a/smoketest/scripts/cli/test_interfaces_openvpn.py
+++ b/smoketest/scripts/cli/test_interfaces_openvpn.py
@@ -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 json
import os
import unittest
from glob import glob
from ipaddress import IPv4Network
from netifaces import interfaces # pylint: disable = no-name-in-module
+from time import sleep
from base_vyostest_shim import VyOSUnitTestSHIM
@@ -28,6 +30,10 @@ from vyos.utils.process import cmdl
from vyos.utils.process import process_named_running
from vyos.utils.process import is_systemd_service_running
from vyos.utils.file import read_file
+from vyos.utils.network import get_interface_config
+from vyos.netlink.ovpn import get_ovpn_mode
+from vyos.netlink.ovpn import OVPN_MODE_MP
+from vyos.netlink.ovpn import OVPN_MODE_P2P
from vyos.template import address_from_cidr
from vyos.template import inc_ip
from vyos.template import last_host_address
@@ -189,6 +195,70 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.assertTrue(process_named_running(PROCESS_NAME))
self.assertIn(interface, interfaces())
+ def test_openvpn_client_dco(self):
+ # A client that can not reach its server never gets far enough to make
+ # the interface, so this is the one case where VyOS is definitively the
+ # creator - and the only coverage of the point-to-point mode.
+ interface = 'vtun5000'
+ path = base_path + [interface]
+
+ self.cli_set(path + ['device-type', 'tun'])
+ self.cli_set(path + ['mode', 'client'])
+ self.cli_set(path + ['remote-host', '192.0.2.1'])
+ self.cli_set(path + ['remote-port', '1194'])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes256gcm'])
+ self.cli_set(path + ['offload', 'dco'])
+ self.cli_commit()
+
+ self.assertDcoDataPath(interface, multipoint=False)
+
+ def test_openvpn_client_dco_raw_option(self):
+ # A raw option no longer keeps VyOS from creating the device: the
+ # offload is the user's responsibility once they pass one, and a
+ # client that cannot reach its server would otherwise never get an
+ # interface for update() to configure.
+ interface = 'vtun5040'
+ path = base_path + [interface]
+
+ self.cli_set(path + ['device-type', 'tun'])
+ self.cli_set(path + ['mode', 'client'])
+ self.cli_set(path + ['remote-host', '192.0.2.1'])
+ self.cli_set(path + ['remote-port', '1194'])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes256gcm'])
+ self.cli_set(path + ['offload', 'dco'])
+ self.cli_set(path + ['openvpn-option', '--verb 3'])
+ self.cli_commit()
+
+ self.assertDcoDataPath(interface, multipoint=False)
+
+ def test_openvpn_device_type_change(self):
+ # The Kernel pins tun against tap when the device is made, so changing
+ # "device-type" has to recreate it - a daemon asking for "tap" can not
+ # be handed the "tun" device left behind.
+ interface = 'vtun5010'
+ path = base_path + [interface]
+
+ self.cli_set(path + ['device-type', 'tun'])
+ self.cli_set(path + ['mode', 'client'])
+ self.cli_set(path + ['remote-host', '192.0.2.1'])
+ self.cli_set(path + ['remote-port', '1194'])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+ self.cli_commit()
+
+ tmp = get_interface_config(interface)
+ self.assertEqual(tmp['linkinfo']['info_data']['type'], 'tun')
+
+ self.cli_set(path + ['device-type', 'tap'])
+ self.cli_commit()
+
+ tmp = get_interface_config(interface)
+ self.assertEqual(tmp['linkinfo']['info_data']['type'], 'tap')
+ self.assertTrue(is_systemd_service_running(f'openvpn@{interface}.service'))
def test_openvpn_client_interfaces(self):
# Create OpenVPN client interfaces connecting to different
@@ -420,6 +490,88 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.assertTrue(process_named_running(PROCESS_NAME))
self.assertIn(interface, interfaces())
+ def assertDcoDataPath(self, interface, multipoint=True):
+ # An "ovpn" device in the wrong operating mode is adopted by the daemon
+ # just the same and then rejects every peer, and iproute2 cannot show
+ # the mode - so ask the Kernel for it directly. Checking only the link
+ # kind would not tell a working tunnel from a broken one.
+ wanted = OVPN_MODE_MP if multipoint else OVPN_MODE_P2P
+ mode = None
+ for _ in range(10):
+ mode = get_ovpn_mode(interface)
+ if mode == wanted:
+ break
+ sleep(1)
+
+ # tell "the daemon never came up" apart from "DCO was declined"
+ self.assertIn(interface, interfaces(), f'{interface} does not exist')
+ self.assertEqual(mode, wanted, f'{interface} is not DCO backed, mode is {mode}')
+
+ # VyOS creates the device itself, so finding it in the right mode does
+ # not yet mean the daemon took the configuration and stayed up
+ self.assertTrue(
+ is_systemd_service_running(f'openvpn@{interface}.service'),
+ f'openvpn@{interface}.service is not running',
+ )
+
+ def test_openvpn_server_dco_raw_option(self):
+ # A raw option OpenVPN still offloads must leave the tunnel offloaded.
+ # VyOS creates the device either way - verify() turns away the options
+ # known to drop the offload, and beyond those it is the user's own
+ # responsibility.
+ interface = 'vtun5000'
+ path = base_path + [interface]
+
+ self.cli_set(path + ['device-type', 'tun'])
+ 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_set(path + ['offload', 'dco'])
+ self.cli_set(path + ['openvpn-option', '--persist-tun'])
+ self.cli_commit()
+
+ self.assertDcoDataPath(interface)
+
+ def test_openvpn_server_dco_toggle(self):
+ # Enabling the offload on a running interface must move the data path
+ # into the Kernel. The interface already exists as a tun device, so a
+ # stale one has to be dropped or the offload is silently declined.
+ interface = 'vtun5000'
+ path = base_path + [interface]
+
+ 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()
+
+ # without the offload the daemon is told to keep away from the Kernel
+ self.assertTrue(is_systemd_service_running(f'openvpn@{interface}.service'))
+ self.assertIn('disable-dco', read_file(f'/run/openvpn/{interface}.conf'))
+
+ self.cli_set(path + ['offload', 'dco'])
+ self.cli_commit()
+
+ self.assertNotIn('disable-dco', read_file(f'/run/openvpn/{interface}.conf'))
+ self.assertDcoDataPath(interface)
+
+ # and back off again - the offloaded device must not survive either
+ self.cli_delete(path + ['offload'])
+ self.cli_commit()
+
+ self.assertTrue(is_systemd_service_running(f'openvpn@{interface}.service'))
+ self.assertIn('disable-dco', read_file(f'/run/openvpn/{interface}.conf'))
+ self.assertIsNone(get_ovpn_mode(interface))
+ tmp = json.loads(cmdl(['ip', '-d', '-j', 'link', 'show', 'dev', interface]))
+ self.assertEqual(tmp[0].get('linkinfo', {}).get('info_kind'), 'tun')
+
def test_openvpn_server_dco_verify(self):
# Configurations the "ovpn" Kernel module can not serve must be
# rejected once data channel offload is requested
@@ -437,6 +589,9 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_set(path + ['offload', 'dco'])
self.cli_commit()
+ config_file = f'/run/openvpn/{interface}.conf'
+ self.assertDcoDataPath(interface)
+
# check validate() - DCO is tun only
self.cli_set(path + ['device-type', 'tap'])
with self.assertRaisesRegex(ConfigSessionError, r'device-type\s+tun'):
@@ -496,9 +651,14 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
- # every rejection above stopped in verify(), so the daemon still has to
- # be running on the configuration this test started from
- self.assertTrue(is_systemd_service_running(f'openvpn@{interface}.service'))
+ # every rejection above stopped in verify(), so nothing has touched the
+ # interface yet - reconfigure it for real and check the offload survives
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes128gcm'])
+ self.cli_delete(path + ['encryption', 'data-ciphers', 'aes256gcm'])
+ self.cli_commit()
+
+ self.assertIn('data-ciphers AES-128-GCM', read_file(config_file))
+ self.assertDcoDataPath(interface)
def test_openvpn_two_dco_interfaces(self):
# The script runs once per interface, so the first invocation must not
@@ -520,11 +680,11 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
+ # the premise of this test is that both daemons hold an "ovpn" device
+ # when the first one goes away, so check the offload, not just the
+ # service - assertDcoDataPath() covers the service too
for ifname in ifnames:
- self.assertTrue(
- is_systemd_service_running(f'openvpn@{ifname}.service'),
- f'openvpn@{ifname}.service is not running',
- )
+ self.assertDcoDataPath(ifname)
# both at once, so one daemon still holds the module when the other goes
for ifname in ifnames:
@@ -831,7 +991,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_set(path + ['openvpn-option', '--cipher AES-256-GCM'])
self.cli_commit()
- self.assertTrue(is_systemd_service_running(f'openvpn@{interface}.service'))
+ self.assertDcoDataPath(interface, multipoint=False)
def test_openvpn_options(self):
# Ensure OpenVPN process restart on openvpn-option CLI node change