From 8d99a2f6f81b225014d246dcf6c7bb03c641d4da Mon Sep 17 00:00:00 2001 From: Antonio Quartulli Date: Thu, 1 Oct 2026 15:41:53 +0200 Subject: openvpn: T9370: add chacha20poly1305 data cipher Offer chacha20poly1305 in "encryption data-ciphers" and "encryption data-ciphers-fallback", rendered as CHACHA20-POLY1305 and allowed with DCO. --- interface-definitions/interfaces_openvpn.xml.in | 16 ++++-- python/vyos/template.py | 3 +- smoketest/scripts/cli/test_interfaces_openvpn.py | 70 ++++++++++++++++++++++++ src/conf_mode/interfaces_openvpn.py | 5 +- src/op_mode/generate_ovpn_client_file.py | 3 +- src/tests/test_template.py | 12 ++++ 6 files changed, 100 insertions(+), 9 deletions(-) diff --git a/interface-definitions/interfaces_openvpn.xml.in b/interface-definitions/interfaces_openvpn.xml.in index 26b004520..ff16a5610 100644 --- a/interface-definitions/interfaces_openvpn.xml.in +++ b/interface-definitions/interfaces_openvpn.xml.in @@ -91,7 +91,7 @@ Cipher negotiation list for use in server or client mode - none 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm + none 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm chacha20poly1305 none @@ -125,8 +125,12 @@ aes256gcm AES algorithm with 256-bit key GCM + + chacha20poly1305 + ChaCha20 stream cipher with Poly1305 authenticator + - (none|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm) + (none|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm|chacha20poly1305) @@ -135,7 +139,7 @@ Fallback cipher to use for site-to-site tunnels - none 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm + none 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm chacha20poly1305 none @@ -169,8 +173,12 @@ aes256gcm AES algorithm with 256-bit key GCM + + chacha20poly1305 + ChaCha20 stream cipher with Poly1305 authenticator + - (none|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm) + (none|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm|chacha20poly1305) diff --git a/python/vyos/template.py b/python/vyos/template.py index c35a25d21..2c07d79da 100755 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -610,7 +610,8 @@ openvpn_translate = { 'aes192gcm': 'aes-192-gcm', 'aes192': 'aes-192-cbc', 'aes256gcm': 'aes-256-gcm', - 'aes256': 'aes-256-cbc' + 'aes256': 'aes-256-cbc', + 'chacha20poly1305': 'chacha20-poly1305', } @register_filter('openvpn_cipher') diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py index 93c79e646..476fcbdf4 100755 --- a/smoketest/scripts/cli/test_interfaces_openvpn.py +++ b/smoketest/scripts/cli/test_interfaces_openvpn.py @@ -329,6 +329,76 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): f'{self._openvpn_log(client)}', ) + def test_openvpn_dco_chacha20poly1305(self): + # ChaCha20-Poly1305 is offloaded just like AES-GCM. A peer only shows + # up in the Kernel once its key got installed, so a client and a server + # offering nothing else prove the module took that very cipher. + server = 'vtun5110' + client = 'vtun5111' + port = '1196' + + path = base_path + [server] + self.cli_set(path + ['mode', 'server']) + self.cli_set(path + ['local-port', port]) + self.cli_set(path + ['server', 'subnet', '10.98.0.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', 'chacha20poly1305']) + self.cli_set(path + ['offload', 'dco']) + self.cli_commit() + + config = read_file(f'/run/openvpn/{server}.conf') + self.assertIn('data-ciphers CHACHA20-POLY1305', config) + self.assertDcoDataPath(server) + + path = base_path + [client] + self.cli_set(path + ['mode', 'client']) + self.cli_set(path + ['remote-host', '127.0.0.1']) + self.cli_set(path + ['remote-port', port]) + self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test']) + self.cli_set(path + ['tls', 'certificate', 'ovpn_test']) + self.cli_set(path + ['encryption', 'data-ciphers', 'chacha20poly1305']) + self.cli_set(path + ['offload', 'dco']) + self.cli_commit() + + self.assertDcoDataPath(client, multipoint=False) + self.assertGreater( + self._peers(server), + 0, + f'the Kernel holds no peer for {server}\n{self._openvpn_log(server)}', + ) + self.assertGreater( + self._peers(client), + 0, + f'the Kernel holds no peer for {client}\n{self._openvpn_log(client)}', + ) + + # and it is the cipher the two of them settled on + log = cmdl( + ['sudo', 'journalctl', '-u', f'openvpn@{client}.service', '-o', 'cat'] + ) + self.assertIn("Data Channel: cipher 'CHACHA20-POLY1305'", log) + + def test_openvpn_site2site_dco_chacha20poly1305(self): + # the fallback cipher decides the offload in site-to-site mode + interface = 'vtun5000' + path = base_path + [interface] + + 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', 'chacha20poly1305']) + self.cli_set(path + ['offload', 'dco']) + self.cli_commit() + + config = read_file(f'/run/openvpn/{interface}.conf') + self.assertIn('data-ciphers-fallback CHACHA20-POLY1305', config) + self.assertDcoDataPath(interface, multipoint=False) + # OVPN_CMD_PEER_GET carries GENL_ADMIN_PERM, hence the detour through # sudo. The probe swallows its own errors so a missing interface or a # refused dump reads as "no peer" with the reason attached, instead of diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py index 109e0f401..911b0f467 100755 --- a/src/conf_mode/interfaces_openvpn.py +++ b/src/conf_mode/interfaces_openvpn.py @@ -80,9 +80,8 @@ group = 'openvpn' cfg_dir = '/run/openvpn' cfg_file = '/run/openvpn/{ifname}.conf' # Ciphers implemented by the in-tree "ovpn" Kernel module. Any other cipher -# must be handled in userspace and thus rules out DCO. The module also does -# ChaCha20-Poly1305, which the CLI does not offer. -dco_ciphers = ['aes128gcm', 'aes192gcm', 'aes256gcm'] +# must be handled in userspace and thus rules out DCO. +dco_ciphers = ['aes128gcm', 'aes192gcm', 'aes256gcm', 'chacha20poly1305'] # Raw options that make OpenVPN fall back to the userspace data path, taken # from dco_check_option() and dco_check_option_ce() dco_incompatible_options = [ diff --git a/src/op_mode/generate_ovpn_client_file.py b/src/op_mode/generate_ovpn_client_file.py index a55f2e3c5..21c933932 100755 --- a/src/op_mode/generate_ovpn_client_file.py +++ b/src/op_mode/generate_ovpn_client_file.py @@ -47,7 +47,8 @@ verb 3 'aes192gcm': 'AES-192-GCM', 'aes192': 'AES-192-CBC', 'aes256gcm': 'AES-256-GCM', - 'aes256': 'AES-256-CBC' + 'aes256': 'AES-256-CBC', + 'chacha20poly1305': 'CHACHA20-POLY1305' } %} {% if encryption is defined and encryption is not none %} diff --git a/src/tests/test_template.py b/src/tests/test_template.py index 7ac396b68..65266584e 100644 --- a/src/tests/test_template.py +++ b/src/tests/test_template.py @@ -208,3 +208,15 @@ class TestVyOSTemplate(TestCase): internal_ports['certbot_haproxy']) self.assertEqual(vyos.template.nft_accept_invalid('arp'), 'ct state invalid ether type arp counter accept') + + def test_openvpn_ciphers(self): + # CLI values map onto the names OpenVPN expects + self.assertEqual( + vyos.template.get_openvpn_data_ciphers(['chacha20poly1305', 'aes256gcm']), + 'CHACHA20-POLY1305:AES-256-GCM', + ) + self.assertEqual( + vyos.template.get_openvpn_data_ciphers_fallback('chacha20poly1305'), + 'CHACHA20-POLY1305', + ) + self.assertEqual(vyos.template.get_openvpn_cipher('aes128'), 'AES-128-CBC') -- cgit v1.2.3