diff options
| author | Antonio Quartulli <antonio@mandelbit.com> | 2026-09-10 16:56:21 +0200 |
|---|---|---|
| committer | Antonio Quartulli <antonio@mandelbit.com> | 2026-09-15 10:05:28 +0200 |
| commit | b8f6de86d9350fecfb4a58e48ac6414d8784690d (patch) | |
| tree | dca234b674a70971d41d327167951d8677fade26 /python | |
| parent | 4c4704fd6a01b92e32e6f74244163051452e2e5a (diff) | |
| download | vyos-1x-b8f6de86d9350fecfb4a58e48ac6414d8784690d.tar.gz vyos-1x-b8f6de86d9350fecfb4a58e48ac6414d8784690d.zip | |
smoketest: T8264: cover adopting a pre-created DCO interface
assertDcoDataPath() checks the link kind, the operating mode and that the
daemon is up, and none of those change when the offload carries nothing.
A client whose server is not up yet never opens its tun, so VyOS creates the
"ovpn" device for it and OpenVPN has to adopt that device once the server
appears. Get the adoption wrong and the daemon takes the device but not its
ifindex: dco_new_peer() fails, the daemon exits and deletes the interface on
its way out, systemd starts it again and the second attempt creates a device
of its own. The tunnel comes up either way, so the state it ends in proves
nothing - the restart it cost does.
Ask the Kernel for its peers as well, which is the one thing no amount of
userspace bookkeeping can fake.
Diffstat (limited to 'python')
| -rw-r--r-- | python/vyos/netlink/ovpn.py | 85 |
1 files changed, 84 insertions, 1 deletions
diff --git a/python/vyos/netlink/ovpn.py b/python/vyos/netlink/ovpn.py index fe325bd60..232c57810 100644 --- a/python/vyos/netlink/ovpn.py +++ b/python/vyos/netlink/ovpn.py @@ -14,7 +14,7 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. """ -Creation of "ovpn" interfaces for OpenVPN data channel offload. +Creation and inspection 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 @@ -23,14 +23,24 @@ mode - which a server then adopts and rejects every client on. Go through netlink directly instead. """ +from socket import if_nametoindex + from pyroute2 import IPRoute +from pyroute2.netlink import NLM_F_DUMP +from pyroute2.netlink import NLM_F_REQUEST +from pyroute2.netlink import genlmsg from pyroute2.netlink import nla +from pyroute2.netlink.generic import GenericNetlinkSocket from pyroute2.netlink.rtnl.ifinfmsg import ifinfmsg # uapi: enum ovpn_mode OVPN_MODE_P2P = 0 OVPN_MODE_MP = 1 +# uapi: OVPN_FAMILY_NAME and enum ovpn_nl_commands +OVPN_FAMILY_NAME = 'ovpn' +OVPN_CMD_PEER_GET = 3 + class ovpn_data(nla): prefix = 'IFLA_' @@ -44,6 +54,79 @@ class ovpn_data(nla): ifinfmsg.ifinfo.data_map.setdefault('ovpn', ovpn_data) +class ovpnmsg(genlmsg): + """OVPN_A_* of the "ovpn" generic netlink family. Only the attributes read + below are decoded; the rest stay opaque so a Kernel that grows new ones + does not break the parser.""" + + prefix = 'OVPN_A_' + nla_map = ( + ('OVPN_A_UNSPEC', 'none'), + ('OVPN_A_IFINDEX', 'uint32'), + ('OVPN_A_PEER', 'peer'), + ('OVPN_A_KEYCONF', 'hex'), + ) + + class peer(nla): + prefix = 'OVPN_A_PEER_' + nla_map = ( + ('OVPN_A_PEER_UNSPEC', 'none'), + ('OVPN_A_PEER_ID', 'uint32'), + ('OVPN_A_PEER_REMOTE_IPV4', 'hex'), + ('OVPN_A_PEER_REMOTE_IPV6', 'hex'), + ('OVPN_A_PEER_REMOTE_IPV6_SCOPE_ID', 'hex'), + ('OVPN_A_PEER_REMOTE_PORT', 'hex'), + ('OVPN_A_PEER_SOCKET', 'hex'), + ('OVPN_A_PEER_SOCKET_NETNSID', 'hex'), + ('OVPN_A_PEER_VPN_IPV4', 'hex'), + ('OVPN_A_PEER_VPN_IPV6', 'hex'), + ('OVPN_A_PEER_LOCAL_IPV4', 'hex'), + ('OVPN_A_PEER_LOCAL_IPV6', 'hex'), + ('OVPN_A_PEER_LOCAL_PORT', 'hex'), + ('OVPN_A_PEER_KEEPALIVE_INTERVAL', 'uint32'), + ('OVPN_A_PEER_KEEPALIVE_TIMEOUT', 'uint32'), + ('OVPN_A_PEER_DEL_REASON', 'hex'), + ('OVPN_A_PEER_VPN_RX_BYTES', 'uint64'), + ('OVPN_A_PEER_VPN_TX_BYTES', 'uint64'), + ('OVPN_A_PEER_VPN_RX_PACKETS', 'uint64'), + ('OVPN_A_PEER_VPN_TX_PACKETS', 'uint64'), + ('OVPN_A_PEER_LINK_RX_BYTES', 'uint64'), + ('OVPN_A_PEER_LINK_TX_BYTES', 'uint64'), + ('OVPN_A_PEER_LINK_RX_PACKETS', 'uint64'), + ('OVPN_A_PEER_LINK_TX_PACKETS', 'uint64'), + ) + + +def get_ovpn_peers(ifname: str) -> list: + """The peers the Kernel holds for an "ovpn" interface, as dictionaries of + the decoded OVPN_A_PEER_* attributes. + + A peer only exists here once OpenVPN's OVPN_CMD_PEER_NEW succeeded, so + this is the one way to tell an offload that carries traffic from a device + that merely looks right. The byte counters are the Kernel's own, unlike + the ones OpenVPN reports, which add its userspace socket accounting. + + OVPN_CMD_PEER_GET carries GENL_ADMIN_PERM, so this needs CAP_NET_ADMIN. + """ + peers = [] + with GenericNetlinkSocket() as sock: + sock.bind(OVPN_FAMILY_NAME, ovpnmsg) + + msg = ovpnmsg() + msg['cmd'] = OVPN_CMD_PEER_GET + msg['version'] = 1 + msg['attrs'] = [('OVPN_A_IFINDEX', if_nametoindex(ifname))] + + for reply in sock.nlm_request( + msg, msg_type=sock.prid, msg_flags=NLM_F_REQUEST | NLM_F_DUMP + ): + peer = reply.get_attr('OVPN_A_PEER') + if peer is not None: + peers.append(dict(peer['attrs'])) + + return peers + + 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, |
