summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2026-10-07 20:04:21 +0200
committerGitHub <noreply@github.com>2026-10-07 20:04:21 +0200
commit58ccdee2d255f4e952322467902f7d69b4f828fa (patch)
tree3b0a15a5241c413bad09e702ddc705bc9bf008f9
parentfff4e828d2a7ec1549ca20db30906ff47396e374 (diff)
parent8d99a2f6f81b225014d246dcf6c7bb03c641d4da (diff)
downloadvyos-1x-58ccdee2d255f4e952322467902f7d69b4f828fa.tar.gz
vyos-1x-58ccdee2d255f4e952322467902f7d69b4f828fa.zip
Merge pull request #5523 from mandelbitdev/T9370-openvpn-chacha20poly1305
openvpn: T9370: add chacha20poly1305 data cipher
-rw-r--r--interface-definitions/interfaces_openvpn.xml.in16
-rwxr-xr-xpython/vyos/template.py3
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_openvpn.py70
-rwxr-xr-xsrc/conf_mode/interfaces_openvpn.py5
-rwxr-xr-xsrc/op_mode/generate_ovpn_client_file.py3
-rw-r--r--src/tests/test_template.py12
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 @@
<properties>
<help>Cipher negotiation list for use in server or client mode</help>
<completionHelp>
- <list>none 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list>
+ <list>none 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm chacha20poly1305</list>
</completionHelp>
<valueHelp>
<format>none</format>
@@ -125,8 +125,12 @@
<format>aes256gcm</format>
<description>AES algorithm with 256-bit key GCM</description>
</valueHelp>
+ <valueHelp>
+ <format>chacha20poly1305</format>
+ <description>ChaCha20 stream cipher with Poly1305 authenticator</description>
+ </valueHelp>
<constraint>
- <regex>(none|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex>
+ <regex>(none|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm|chacha20poly1305)</regex>
</constraint>
<multi/>
</properties>
@@ -135,7 +139,7 @@
<properties>
<help>Fallback cipher to use for site-to-site tunnels</help>
<completionHelp>
- <list>none 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list>
+ <list>none 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm chacha20poly1305</list>
</completionHelp>
<valueHelp>
<format>none</format>
@@ -169,8 +173,12 @@
<format>aes256gcm</format>
<description>AES algorithm with 256-bit key GCM</description>
</valueHelp>
+ <valueHelp>
+ <format>chacha20poly1305</format>
+ <description>ChaCha20 stream cipher with Poly1305 authenticator</description>
+ </valueHelp>
<constraint>
- <regex>(none|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex>
+ <regex>(none|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm|chacha20poly1305)</regex>
</constraint>
</properties>
</leafNode>
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')