summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
authorAntonio Quartulli <antonio@mandelbit.com>2026-08-30 19:58:00 +0200
committerAntonio Quartulli <antonio@mandelbit.com>2026-09-15 10:05:28 +0200
commit2c92731f1cfef0cd812754daf7dd4add6d165942 (patch)
tree4cb277d2319d8f41fa55c3462fd6eb2bee7495d0 /python
parent03229944aee8040520957e0041f81cbd7ab1586b (diff)
downloadvyos-1x-2c92731f1cfef0cd812754daf7dd4add6d165942.tar.gz
vyos-1x-2c92731f1cfef0cd812754daf7dd4add6d165942.zip
openvpn: T8264: create the interface type the data path needs
apply() restarts the daemon and then creates the interface itself, as a tun device. OpenVPN declines the offload when it finds a device of the wrong type, so DCO worked only until the first reconfiguration. The Kernel module takes its operating mode at creation time and iproute2 can neither set nor report it, so go through netlink - multipoint for a server, point-to-point otherwise, matching what OpenVPN asks for itself. A server adopting a point-to-point device rejects every client, and the attribute is dropped silently if it is ever unrecognised, so read the mode back. A raw option does not change this. verify() turns away the ones known to drop the offload, and beyond those "openvpn-option" is documented as the user's own responsibility - so an offloaded interface is created either way. A raw option that does decline the offload now leaves the daemon unable to open the device it is given, which is at least visible, rather than quietly carrying traffic in userspace.
Diffstat (limited to 'python')
-rw-r--r--python/vyos/ifconfig/vtun.py22
-rw-r--r--python/vyos/netlink/ovpn.py74
2 files changed, 96 insertions, 0 deletions
diff --git a/python/vyos/ifconfig/vtun.py b/python/vyos/ifconfig/vtun.py
index fbe7127d5..6490fdf83 100644
--- a/python/vyos/ifconfig/vtun.py
+++ b/python/vyos/ifconfig/vtun.py
@@ -14,6 +14,7 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
from vyos.ifconfig.interface import Interface
+from vyos.utils.dict import dict_search
@Interface.register
class VTunIf(Interface):
@@ -32,6 +33,27 @@ class VTunIf(Interface):
established (client mode). The latter will only be brought up once the
server can be reached, thus we might need to create this interface in
advance for the service to be operational. """
+ # An offloaded data path lives in the "ovpn" Kernel module, and OpenVPN
+ # declines the offload when it finds a device of any other type. The
+ # operating mode is fixed at creation time, so it has to be set here.
+ if dict_search('offload.dco', self.config) is not None:
+ # Imported here rather than at module scope: vyos.netlink.ovpn
+ # imports pyroute2, which every consumer of vyos.ifconfig would
+ # otherwise pay for on import.
+ from errno import EEXIST
+ from pyroute2.netlink.exceptions import NetlinkError
+ from vyos.netlink.ovpn import add_ovpn_interface
+
+ try:
+ return add_ovpn_interface(
+ self.ifname, self.config.get('mode') == 'server'
+ )
+ except NetlinkError as e:
+ if e.code != EEXIST:
+ raise
+ # interface created by OpenVPN daemon in the meantime ...
+ return None
+
try:
cmd = ['openvpn', '--mktun', '--dev-type', self.config['device_type'],
'--dev', self.ifname]
diff --git a/python/vyos/netlink/ovpn.py b/python/vyos/netlink/ovpn.py
new file mode 100644
index 000000000..fe325bd60
--- /dev/null
+++ b/python/vyos/netlink/ovpn.py
@@ -0,0 +1,74 @@
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Creation of "ovpn" interfaces for OpenVPN data channel offload.
+
+The Kernel module takes the operating mode as a link attribute at creation
+time and it can not be changed afterwards. iproute2 has no support for the
+link type, so it can only ever create a device in the default point-to-point
+mode - which a server then adopts and rejects every client on. Go through
+netlink directly instead.
+"""
+
+from pyroute2 import IPRoute
+from pyroute2.netlink import nla
+from pyroute2.netlink.rtnl.ifinfmsg import ifinfmsg
+
+# uapi: enum ovpn_mode
+OVPN_MODE_P2P = 0
+OVPN_MODE_MP = 1
+
+
+class ovpn_data(nla):
+ prefix = 'IFLA_'
+ nla_map = (
+ ('IFLA_OVPN_UNSPEC', 'none'),
+ ('IFLA_OVPN_MODE', 'uint8'),
+ )
+
+
+# pyroute2 does not know the link type either, so teach it
+ifinfmsg.ifinfo.data_map.setdefault('ovpn', ovpn_data)
+
+
+def get_ovpn_mode(ifname: str):
+ """Operating mode of an "ovpn" interface, or None when the interface is
+ missing or of any other type. iproute2 has no decoder for the attribute,
+ so this is the only way to tell a multipoint device from a point-to-point
+ one - and adopting the wrong one makes OpenVPN reject every peer."""
+ with IPRoute() as ipr:
+ links = ipr.get_links(ifname=ifname)
+
+ if not links:
+ return None
+ info = links[0].get_attr('IFLA_LINKINFO')
+ if info is None or info.get_attr('IFLA_INFO_KIND') != 'ovpn':
+ return None
+ data = info.get_attr('IFLA_INFO_DATA')
+ return None if data is None else data.get_attr('IFLA_OVPN_MODE')
+
+
+def add_ovpn_interface(ifname: str, multipoint: bool) -> None:
+ """Create an "ovpn" interface, multipoint for servers and point-to-point
+ for everything else - matching what OpenVPN would ask for itself."""
+ mode = OVPN_MODE_MP if multipoint else OVPN_MODE_P2P
+ with IPRoute() as ipr:
+ ipr.link('add', ifname=ifname, kind='ovpn', ovpn_mode=mode)
+
+ # the attribute is silently dropped if pyroute2 ever stops recognising the
+ # link type, and the Kernel then quietly falls back to point-to-point
+ if get_ovpn_mode(ifname) != mode:
+ raise ValueError(f'Could not set operating mode on interface {ifname}')