summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2026-09-16 20:48:14 +0200
committerGitHub <noreply@github.com>2026-09-16 20:48:14 +0200
commit7943d798dcb4c860610d3e1b2869175a9147c083 (patch)
treeb84e4178b13a9c9f0a39b96fdd46f1754e283bd1 /src
parent5b0cce51f6b9e43ef204d6fba54f5f5c802e759f (diff)
parent963e4cc5b36b9efd8fec9fe52a6fbacee6f4a2e5 (diff)
downloadvyos-1x-7943d798dcb4c860610d3e1b2869175a9147c083.tar.gz
vyos-1x-7943d798dcb4c860610d3e1b2869175a9147c083.zip
Merge pull request #5441 from ordex/T8264-openvpn-dco-lifecycle
T8264: openvpn dco lifecycle
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/interfaces_openvpn.py61
1 files changed, 47 insertions, 14 deletions
diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py
index ff5dcf07b..2b235c057 100755
--- a/src/conf_mode/interfaces_openvpn.py
+++ b/src/conf_mode/interfaces_openvpn.py
@@ -41,6 +41,9 @@ from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import VTunIf
+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.pki import load_dh_parameters
from vyos.pki import load_private_key
from vyos.pki import sort_ca_chain
@@ -65,6 +68,7 @@ from vyos.utils.permission import chown
from vyos.utils.process import cmdl
from vyos.utils.network import is_addr_assigned
from vyos.utils.network import interface_exists
+from vyos.utils.network import get_interface_config
from vyos import ConfigError
from vyos import airbag
@@ -144,6 +148,18 @@ def get_config(config=None):
ifname, openvpn = get_interface_dict(conf, base, with_pki=True)
openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
+ # OpenVPN Data-Channel-Offload (DCO) is a Kernel module. If loaded it applies to all
+ # OpenVPN interfaces. Check if DCO is used by any other interface instance.
+ tmp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ for interface, interface_config in tmp.items():
+ # If one interface has DCO configured, enable it. No need to further check
+ # all other OpenVPN interfaces. We must use a dedicated key to indicate
+ # the Kernel module must be loaded or not. The per interface "offload.dco"
+ # key is required per OpenVPN interface instance.
+ if dict_search('offload.dco', interface_config) != None:
+ openvpn['module_load_dco'] = {}
+ break
+
if 'deleted' in openvpn:
return openvpn
@@ -166,8 +182,12 @@ def get_config(config=None):
if is_node_changed(conf, base + [ifname, 'openvpn-option']):
openvpn.update({'restart_required': {}})
- if is_node_changed(conf, base + [ifname, 'offload', 'dco']):
- 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']]:
+ if is_node_changed(conf, base + [ifname] + node):
+ openvpn.update({'restart_required': {}})
+ break
# Detect changes that are limited to per-client CCD entries (T6478).
# OpenVPN reads client-config-dir files at connect time, so adding or
@@ -187,18 +207,6 @@ def get_config(config=None):
if dict_search('server.mfa.totp', tmp) == None:
del openvpn['server']['mfa']
- # OpenVPN Data-Channel-Offload (DCO) is a Kernel module. If loaded it applies to all
- # OpenVPN interfaces. Check if DCO is used by any other interface instance.
- tmp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
- for interface, interface_config in tmp.items():
- # If one interface has DCO configured, enable it. No need to further check
- # all other OpenVPN interfaces. We must use a dedicated key to indicate
- # the Kernel module must be loaded or not. The per interface "offload.dco"
- # key is required per OpenVPN interface instance.
- if dict_search('offload.dco', interface_config) != None:
- openvpn['module_load_dco'] = {}
- break
-
# Calculate the protocol modifier. This is concatenated to the protocol string to direct
# OpenVPN to use a specific IP protocol version. If unspecified, the kernel decides which
# type of socket to open. In server mode, an additional "ipv6-dual-stack" option forces
@@ -978,6 +986,31 @@ def apply(openvpn):
if not is_addr_assigned(openvpn['local_host']):
cmdl(['sysctl', '-w', 'net.ipv4.ip_nonlocal_bind=1'])
+ # The interface type follows the data path, and OpenVPN adopts whatever it
+ # finds - including an "ovpn" device in the wrong operating mode, which
+ # then rejects every peer. Drop a leftover that no longer matches. Only do
+ # so when the daemon is restarted below, or a commit that leaves it running
+ # would take the interface away from underneath it.
+ if 'restart_required' in openvpn and interface_exists(interface):
+ if dict_search('offload.dco', openvpn) is None:
+ drop = get_ovpn_mode(interface) is not None
+ elif openvpn['mode'] == 'server':
+ drop = get_ovpn_mode(interface) != OVPN_MODE_MP
+ else:
+ drop = get_ovpn_mode(interface) != OVPN_MODE_P2P
+
+ # The Kernel pins tun against tap when the device is made and refuses
+ # to hand a "tap" device to a daemon asking for a "tun" one. An "ovpn"
+ # device carries no such type, hence the None.
+ if not drop:
+ tmp = dict_search(
+ 'linkinfo.info_data.type', get_interface_config(interface)
+ )
+ drop = tmp is not None and tmp != openvpn['device_type']
+
+ if drop:
+ VTunIf(interface).remove()
+
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process