From b7e0a79b7c028cf604fb3becef3f74ae0f1f06cc Mon Sep 17 00:00:00 2001 From: Antonio Quartulli Date: Sun, 30 Aug 2026 02:08:36 +0200 Subject: openvpn: T8264: reject configurations DCO cannot serve The Kernel module is tun only, has no compression and no static key data path, and implements AES-GCM only. Once 2.7 makes DCO effective these combinations no longer degrade silently, they carry no traffic. Raw options are matched on their value wherever OpenVPN keeps the offload for one of them - notably "compress migrate", which it suggests for exactly that purpose. --- src/conf_mode/interfaces_openvpn.py | 64 +++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) (limited to 'src') diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py index a2367cfcb..1f85d98f1 100755 --- a/src/conf_mode/interfaces_openvpn.py +++ b/src/conf_mode/interfaces_openvpn.py @@ -76,6 +76,26 @@ 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'] +# 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 = [ + 'comp-lzo', + 'disable-dco', + 'fragment', + 'http-proxy', + 'management-query-proxy', + 'socks-proxy', +] +# these rule out the offload for every value but one - notably "compress +# migrate", which is what OpenVPN suggests to keep it +dco_conditional_options = { + 'allow-compression': 'no', + 'compress': 'migrate', +} otp_path = '/config/auth/openvpn' otp_file = '/config/auth/openvpn/{ifname}-otp-secrets' secret_chars = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567') @@ -209,6 +229,49 @@ def verify_data_ciphers_fallback(openvpn): if dict_search('encryption.data_ciphers_fallback', openvpn): raise ConfigError('Cipher fallback is valid only in site-to-site mode') +def verify_dco(openvpn): + if dict_search('offload.dco', openvpn) is None: + return + + if openvpn['device_type'] != 'tun': + raise ConfigError('DCO requires "device-type tun"') + + if openvpn['mode'] == 'server': + topology = dict_search('server.topology', openvpn) + if topology != 'subnet': + raise ConfigError( + f'DCO requires "server topology subnet", got "{topology}"' + ) + + if 'shared_secret_key' in openvpn: + raise ConfigError('DCO is incompatible with "shared-secret-key"') + + if 'use_lzo_compression' in openvpn: + raise ConfigError('DCO is incompatible with "use-lzo-compression"') + + ciphers = dict_search('encryption.data_ciphers', openvpn) or [] + fallback = dict_search('encryption.data_ciphers_fallback', openvpn) + if fallback: + ciphers = ciphers + [fallback] + + for cipher in ciphers: + if cipher not in dco_ciphers: + raise ConfigError(f'DCO does not support cipher "{cipher}"') + + # A raw option OpenVPN refuses to offload leaves the daemon on the + # userspace data path, where it can not use the interface it was given + for option in dict_search('openvpn_option', openvpn) or []: + tmp = option.split() + if not tmp: + continue + keyword = tmp[0].lstrip('-') + if keyword in dco_incompatible_options: + raise ConfigError(f'DCO is incompatible with "openvpn-option {keyword}"') + if keyword in dco_conditional_options: + keep = dco_conditional_options[keyword] + if tmp[1:] != [keep]: + raise ConfigError(f'DCO requires "openvpn-option {keyword} {keep}"') + def verify_pki(openvpn): pki = openvpn['pki'] interface = openvpn['ifname'] @@ -662,6 +725,7 @@ def verify(openvpn): verify_mirror_redirect(openvpn) verify_data_ciphers_fallback(openvpn) + verify_dco(openvpn) return None -- cgit v1.2.3