ovpn: introduce the ovpn_peer object

An ovpn_peer object holds the whole status of a remote peer
(regardless whether it is a server or a client).

This includes status for crypto, tx/rx buffers, napi, etc.

Only support for one peer is introduced (P2P mode).
Multi peer support is introduced with a later patch.

Along with the ovpn_peer, also the ovpn_bind object is introcued
as the two are strictly related.
An ovpn_bind object wraps a sockaddr representing the local
coordinates being used to talk to a specific peer.

Signed-off-by: Antonio Quartulli <antonio@openvpn.net>
Link: https://patch.msgid.link/20250415-b4-ovpn-v26-5-577f6097b964@openvpn.net
Reviewed-by: Sabrina Dubroca <sd@queasysnail.net>
Tested-by: Oleksandr Natalenko <oleksandr@natalenko.name>
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
This commit is contained in:
Antonio Quartulli 2025-04-15 13:17:22 +02:00 committed by Paolo Abeni
parent 8327a3baa9
commit 80747caef3
8 changed files with 670 additions and 1 deletions

View file

@ -119,6 +119,7 @@ config OVPN
tristate "OpenVPN data channel offload"
depends on NET && INET
depends on IPV6 || !IPV6
select DST_CACHE
help
This module enhances the performance of the OpenVPN userspace software
by offloading the data channel processing to kernelspace.

View file

@ -7,7 +7,9 @@
# Author: Antonio Quartulli <antonio@openvpn.net>
obj-$(CONFIG_OVPN) := ovpn.o
ovpn-y += bind.o
ovpn-y += main.o
ovpn-y += io.o
ovpn-y += netlink.o
ovpn-y += netlink-gen.o
ovpn-y += peer.o

58
drivers/net/ovpn/bind.c Normal file
View file

@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-2.0
/* OpenVPN data channel offload
*
* Copyright (C) 2012-2025 OpenVPN, Inc.
*
* Author: James Yonan <james@openvpn.net>
* Antonio Quartulli <antonio@openvpn.net>
*/
#include <linux/netdevice.h>
#include <linux/socket.h>
#include "ovpnpriv.h"
#include "bind.h"
#include "peer.h"
/**
* ovpn_bind_from_sockaddr - retrieve binding matching sockaddr
* @ss: the sockaddr to match
*
* Return: the bind matching the passed sockaddr if found, NULL otherwise
*/
struct ovpn_bind *ovpn_bind_from_sockaddr(const struct sockaddr_storage *ss)
{
struct ovpn_bind *bind;
size_t sa_len;
if (ss->ss_family == AF_INET)
sa_len = sizeof(struct sockaddr_in);
else if (ss->ss_family == AF_INET6)
sa_len = sizeof(struct sockaddr_in6);
else
return ERR_PTR(-EAFNOSUPPORT);
bind = kzalloc(sizeof(*bind), GFP_ATOMIC);
if (unlikely(!bind))
return ERR_PTR(-ENOMEM);
memcpy(&bind->remote, ss, sa_len);
return bind;
}
/**
* ovpn_bind_reset - assign new binding to peer
* @peer: the peer whose binding has to be replaced
* @new: the new bind to assign
*/
void ovpn_bind_reset(struct ovpn_peer *peer, struct ovpn_bind *new)
{
struct ovpn_bind *old;
spin_lock_bh(&peer->lock);
old = rcu_replace_pointer(peer->bind, new, true);
spin_unlock_bh(&peer->lock);
kfree_rcu(old, rcu);
}

101
drivers/net/ovpn/bind.h Normal file
View file

@ -0,0 +1,101 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/* OpenVPN data channel offload
*
* Copyright (C) 2012-2025 OpenVPN, Inc.
*
* Author: James Yonan <james@openvpn.net>
* Antonio Quartulli <antonio@openvpn.net>
*/
#ifndef _NET_OVPN_OVPNBIND_H_
#define _NET_OVPN_OVPNBIND_H_
#include <net/ip.h>
#include <linux/in.h>
#include <linux/in6.h>
#include <linux/rcupdate.h>
#include <linux/skbuff.h>
#include <linux/spinlock.h>
struct ovpn_peer;
/**
* union ovpn_sockaddr - basic transport layer address
* @in4: IPv4 address
* @in6: IPv6 address
*/
union ovpn_sockaddr {
struct sockaddr_in in4;
struct sockaddr_in6 in6;
};
/**
* struct ovpn_bind - remote peer binding
* @remote: the remote peer sockaddress
* @local: local endpoint used to talk to the peer
* @local.ipv4: local IPv4 used to talk to the peer
* @local.ipv6: local IPv6 used to talk to the peer
* @rcu: used to schedule RCU cleanup job
*/
struct ovpn_bind {
union ovpn_sockaddr remote; /* remote sockaddr */
union {
struct in_addr ipv4;
struct in6_addr ipv6;
} local;
struct rcu_head rcu;
};
/**
* ovpn_bind_skb_src_match - match packet source with binding
* @bind: the binding to match
* @skb: the packet to match
*
* Return: true if the packet source matches the remote peer sockaddr
* in the binding
*/
static inline bool ovpn_bind_skb_src_match(const struct ovpn_bind *bind,
const struct sk_buff *skb)
{
const union ovpn_sockaddr *remote;
if (unlikely(!bind))
return false;
remote = &bind->remote;
switch (skb->protocol) {
case htons(ETH_P_IP):
if (unlikely(remote->in4.sin_family != AF_INET))
return false;
if (unlikely(remote->in4.sin_addr.s_addr != ip_hdr(skb)->saddr))
return false;
if (unlikely(remote->in4.sin_port != udp_hdr(skb)->source))
return false;
break;
case htons(ETH_P_IPV6):
if (unlikely(remote->in6.sin6_family != AF_INET6))
return false;
if (unlikely(!ipv6_addr_equal(&remote->in6.sin6_addr,
&ipv6_hdr(skb)->saddr)))
return false;
if (unlikely(remote->in6.sin6_port != udp_hdr(skb)->source))
return false;
break;
default:
return false;
}
return true;
}
struct ovpn_bind *ovpn_bind_from_sockaddr(const struct sockaddr_storage *sa);
void ovpn_bind_reset(struct ovpn_peer *peer, struct ovpn_bind *bind);
#endif /* _NET_OVPN_OVPNBIND_H_ */

View file

@ -19,6 +19,7 @@
#include "main.h"
#include "netlink.h"
#include "io.h"
#include "peer.h"
#include "proto.h"
static const struct net_device_ops ovpn_netdev_ops = {
@ -92,6 +93,7 @@ static int ovpn_newlink(struct net_device *dev,
ovpn->dev = dev;
ovpn->mode = mode;
spin_lock_init(&ovpn->lock);
/* Set carrier explicitly after registration, this way state is
* clearly defined.
@ -109,6 +111,16 @@ static int ovpn_newlink(struct net_device *dev,
return register_netdevice(dev);
}
static void ovpn_dellink(struct net_device *dev, struct list_head *head)
{
struct ovpn_priv *ovpn = netdev_priv(dev);
if (ovpn->mode == OVPN_MODE_P2P)
ovpn_peer_release_p2p(ovpn, OVPN_DEL_PEER_REASON_TEARDOWN);
unregister_netdevice_queue(dev, head);
}
static int ovpn_fill_info(struct sk_buff *skb, const struct net_device *dev)
{
struct ovpn_priv *ovpn = netdev_priv(dev);
@ -127,7 +139,7 @@ static struct rtnl_link_ops ovpn_link_ops = {
.policy = ovpn_policy,
.maxtype = IFLA_OVPN_MAX,
.newlink = ovpn_newlink,
.dellink = unregister_netdevice_queue,
.dellink = ovpn_dellink,
.fill_info = ovpn_fill_info,
};

View file

@ -17,10 +17,14 @@
* struct ovpn_priv - per ovpn interface state
* @dev: the actual netdev representing the tunnel
* @mode: device operation mode (i.e. p2p, mp, ..)
* @lock: protect this object
* @peer: in P2P mode, this is the only remote peer
*/
struct ovpn_priv {
struct net_device *dev;
enum ovpn_mode mode;
spinlock_t lock; /* protect writing to the ovpn_priv object */
struct ovpn_peer __rcu *peer;
};
#endif /* _NET_OVPN_OVPNSTRUCT_H_ */

411
drivers/net/ovpn/peer.c Normal file
View file

@ -0,0 +1,411 @@
// SPDX-License-Identifier: GPL-2.0
/* OpenVPN data channel offload
*
* Copyright (C) 2020-2025 OpenVPN, Inc.
*
* Author: James Yonan <james@openvpn.net>
* Antonio Quartulli <antonio@openvpn.net>
*/
#include <linux/skbuff.h>
#include <linux/list.h>
#include "ovpnpriv.h"
#include "bind.h"
#include "io.h"
#include "main.h"
#include "netlink.h"
#include "peer.h"
static void unlock_ovpn(struct ovpn_priv *ovpn,
struct llist_head *release_list)
__releases(&ovpn->lock)
{
struct ovpn_peer *peer;
spin_unlock_bh(&ovpn->lock);
llist_for_each_entry(peer, release_list->first, release_entry)
ovpn_peer_put(peer);
}
/**
* ovpn_peer_new - allocate and initialize a new peer object
* @ovpn: the openvpn instance inside which the peer should be created
* @id: the ID assigned to this peer
*
* Return: a pointer to the new peer on success or an error code otherwise
*/
struct ovpn_peer *ovpn_peer_new(struct ovpn_priv *ovpn, u32 id)
{
struct ovpn_peer *peer;
int ret;
/* alloc and init peer object */
peer = kzalloc(sizeof(*peer), GFP_KERNEL);
if (!peer)
return ERR_PTR(-ENOMEM);
peer->id = id;
peer->ovpn = ovpn;
peer->vpn_addrs.ipv4.s_addr = htonl(INADDR_ANY);
peer->vpn_addrs.ipv6 = in6addr_any;
RCU_INIT_POINTER(peer->bind, NULL);
spin_lock_init(&peer->lock);
kref_init(&peer->refcount);
ret = dst_cache_init(&peer->dst_cache, GFP_KERNEL);
if (ret < 0) {
netdev_err(ovpn->dev,
"cannot initialize dst cache for peer %u\n",
peer->id);
kfree(peer);
return ERR_PTR(ret);
}
netdev_hold(ovpn->dev, &peer->dev_tracker, GFP_KERNEL);
return peer;
}
/**
* ovpn_peer_release_rcu - RCU callback performing last peer release steps
* @head: RCU member of the ovpn_peer
*/
static void ovpn_peer_release_rcu(struct rcu_head *head)
{
struct ovpn_peer *peer = container_of(head, struct ovpn_peer, rcu);
/* this call will immediately free the dst_cache, therefore we
* perform it in the RCU callback, when all contexts are done
*/
dst_cache_destroy(&peer->dst_cache);
kfree(peer);
}
/**
* ovpn_peer_release - release peer private members
* @peer: the peer to release
*/
static void ovpn_peer_release(struct ovpn_peer *peer)
{
ovpn_bind_reset(peer, NULL);
call_rcu(&peer->rcu, ovpn_peer_release_rcu);
netdev_put(peer->ovpn->dev, &peer->dev_tracker);
}
/**
* ovpn_peer_release_kref - callback for kref_put
* @kref: the kref object belonging to the peer
*/
void ovpn_peer_release_kref(struct kref *kref)
{
struct ovpn_peer *peer = container_of(kref, struct ovpn_peer, refcount);
ovpn_peer_release(peer);
}
/**
* ovpn_peer_skb_to_sockaddr - fill sockaddr with skb source address
* @skb: the packet to extract data from
* @ss: the sockaddr to fill
*
* Return: sockaddr length on success or -1 otherwise
*/
static int ovpn_peer_skb_to_sockaddr(struct sk_buff *skb,
struct sockaddr_storage *ss)
{
struct sockaddr_in6 *sa6;
struct sockaddr_in *sa4;
switch (skb->protocol) {
case htons(ETH_P_IP):
sa4 = (struct sockaddr_in *)ss;
sa4->sin_family = AF_INET;
sa4->sin_addr.s_addr = ip_hdr(skb)->saddr;
sa4->sin_port = udp_hdr(skb)->source;
return sizeof(*sa4);
case htons(ETH_P_IPV6):
sa6 = (struct sockaddr_in6 *)ss;
sa6->sin6_family = AF_INET6;
sa6->sin6_addr = ipv6_hdr(skb)->saddr;
sa6->sin6_port = udp_hdr(skb)->source;
return sizeof(*sa6);
}
return -1;
}
/**
* ovpn_peer_transp_match - check if sockaddr and peer binding match
* @peer: the peer to get the binding from
* @ss: the sockaddr to match
*
* Return: true if sockaddr and binding match or false otherwise
*/
static bool ovpn_peer_transp_match(const struct ovpn_peer *peer,
const struct sockaddr_storage *ss)
{
struct ovpn_bind *bind = rcu_dereference(peer->bind);
struct sockaddr_in6 *sa6;
struct sockaddr_in *sa4;
if (unlikely(!bind))
return false;
if (ss->ss_family != bind->remote.in4.sin_family)
return false;
switch (ss->ss_family) {
case AF_INET:
sa4 = (struct sockaddr_in *)ss;
if (sa4->sin_addr.s_addr != bind->remote.in4.sin_addr.s_addr)
return false;
if (sa4->sin_port != bind->remote.in4.sin_port)
return false;
break;
case AF_INET6:
sa6 = (struct sockaddr_in6 *)ss;
if (!ipv6_addr_equal(&sa6->sin6_addr,
&bind->remote.in6.sin6_addr))
return false;
if (sa6->sin6_port != bind->remote.in6.sin6_port)
return false;
break;
default:
return false;
}
return true;
}
/**
* ovpn_peer_get_by_transp_addr_p2p - get peer by transport address in a P2P
* instance
* @ovpn: the openvpn instance to search
* @ss: the transport socket address
*
* Return: the peer if found or NULL otherwise
*/
static struct ovpn_peer *
ovpn_peer_get_by_transp_addr_p2p(struct ovpn_priv *ovpn,
struct sockaddr_storage *ss)
{
struct ovpn_peer *tmp, *peer = NULL;
rcu_read_lock();
tmp = rcu_dereference(ovpn->peer);
if (likely(tmp && ovpn_peer_transp_match(tmp, ss) &&
ovpn_peer_hold(tmp)))
peer = tmp;
rcu_read_unlock();
return peer;
}
/**
* ovpn_peer_get_by_transp_addr - retrieve peer by transport address
* @ovpn: the openvpn instance to search
* @skb: the skb to retrieve the source transport address from
*
* Return: a pointer to the peer if found or NULL otherwise
*/
struct ovpn_peer *ovpn_peer_get_by_transp_addr(struct ovpn_priv *ovpn,
struct sk_buff *skb)
{
struct ovpn_peer *peer = NULL;
struct sockaddr_storage ss = { 0 };
if (unlikely(!ovpn_peer_skb_to_sockaddr(skb, &ss)))
return NULL;
if (ovpn->mode == OVPN_MODE_P2P)
peer = ovpn_peer_get_by_transp_addr_p2p(ovpn, &ss);
return peer;
}
/**
* ovpn_peer_get_by_id_p2p - get peer by ID in a P2P instance
* @ovpn: the openvpn instance to search
* @peer_id: the ID of the peer to find
*
* Return: the peer if found or NULL otherwise
*/
static struct ovpn_peer *ovpn_peer_get_by_id_p2p(struct ovpn_priv *ovpn,
u32 peer_id)
{
struct ovpn_peer *tmp, *peer = NULL;
rcu_read_lock();
tmp = rcu_dereference(ovpn->peer);
if (likely(tmp && tmp->id == peer_id && ovpn_peer_hold(tmp)))
peer = tmp;
rcu_read_unlock();
return peer;
}
/**
* ovpn_peer_get_by_id - retrieve peer by ID
* @ovpn: the openvpn instance to search
* @peer_id: the unique peer identifier to match
*
* Return: a pointer to the peer if found or NULL otherwise
*/
struct ovpn_peer *ovpn_peer_get_by_id(struct ovpn_priv *ovpn, u32 peer_id)
{
struct ovpn_peer *peer = NULL;
if (ovpn->mode == OVPN_MODE_P2P)
peer = ovpn_peer_get_by_id_p2p(ovpn, peer_id);
return peer;
}
static void ovpn_peer_remove(struct ovpn_peer *peer,
enum ovpn_del_peer_reason reason,
struct llist_head *release_list)
{
switch (peer->ovpn->mode) {
case OVPN_MODE_P2P:
/* prevent double remove */
if (peer != rcu_access_pointer(peer->ovpn->peer))
return;
RCU_INIT_POINTER(peer->ovpn->peer, NULL);
/* in P2P mode the carrier is switched off when the peer is
* deleted so that third party protocols can react accordingly
*/
netif_carrier_off(peer->ovpn->dev);
break;
default:
return;
}
peer->delete_reason = reason;
/* append to provided list for later socket release and ref drop */
llist_add(&peer->release_entry, release_list);
}
/**
* ovpn_peer_add_p2p - add peer to related tables in a P2P instance
* @ovpn: the instance to add the peer to
* @peer: the peer to add
*
* Return: 0 on success or a negative error code otherwise
*/
static int ovpn_peer_add_p2p(struct ovpn_priv *ovpn, struct ovpn_peer *peer)
{
LLIST_HEAD(release_list);
struct ovpn_peer *tmp;
spin_lock_bh(&ovpn->lock);
/* in p2p mode it is possible to have a single peer only, therefore the
* old one is released and substituted by the new one
*/
tmp = rcu_dereference_protected(ovpn->peer,
lockdep_is_held(&ovpn->lock));
if (tmp)
ovpn_peer_remove(tmp, OVPN_DEL_PEER_REASON_TEARDOWN,
&release_list);
rcu_assign_pointer(ovpn->peer, peer);
/* in P2P mode the carrier is switched on when the peer is added */
netif_carrier_on(ovpn->dev);
unlock_ovpn(ovpn, &release_list);
return 0;
}
/**
* ovpn_peer_add - add peer to the related tables
* @ovpn: the openvpn instance the peer belongs to
* @peer: the peer object to add
*
* Assume refcounter was increased by caller
*
* Return: 0 on success or a negative error code otherwise
*/
int ovpn_peer_add(struct ovpn_priv *ovpn, struct ovpn_peer *peer)
{
switch (ovpn->mode) {
case OVPN_MODE_P2P:
return ovpn_peer_add_p2p(ovpn, peer);
default:
return -EOPNOTSUPP;
}
}
/**
* ovpn_peer_del_p2p - delete peer from related tables in a P2P instance
* @peer: the peer to delete
* @reason: reason why the peer was deleted (sent to userspace)
* @release_list: list where delete peer should be appended
*
* Return: 0 on success or a negative error code otherwise
*/
static int ovpn_peer_del_p2p(struct ovpn_peer *peer,
enum ovpn_del_peer_reason reason,
struct llist_head *release_list)
{
struct ovpn_peer *tmp;
lockdep_assert_held(&peer->ovpn->lock);
tmp = rcu_dereference_protected(peer->ovpn->peer,
lockdep_is_held(&peer->ovpn->lock));
if (tmp != peer)
return -ENOENT;
ovpn_peer_remove(peer, reason, release_list);
return 0;
}
/**
* ovpn_peer_del - delete peer from related tables
* @peer: the peer object to delete
* @reason: reason for deleting peer (will be sent to userspace)
*
* Return: 0 on success or a negative error code otherwise
*/
int ovpn_peer_del(struct ovpn_peer *peer, enum ovpn_del_peer_reason reason)
{
LLIST_HEAD(release_list);
int ret = -EOPNOTSUPP;
spin_lock_bh(&peer->ovpn->lock);
switch (peer->ovpn->mode) {
case OVPN_MODE_P2P:
ret = ovpn_peer_del_p2p(peer, reason, &release_list);
break;
default:
break;
}
unlock_ovpn(peer->ovpn, &release_list);
return ret;
}
/**
* ovpn_peer_release_p2p - release peer upon P2P device teardown
* @ovpn: the instance being torn down
* @reason: the reason for releasing the peer
*/
void ovpn_peer_release_p2p(struct ovpn_priv *ovpn,
enum ovpn_del_peer_reason reason)
{
LLIST_HEAD(release_list);
struct ovpn_peer *peer;
spin_lock_bh(&ovpn->lock);
peer = rcu_dereference_protected(ovpn->peer,
lockdep_is_held(&ovpn->lock));
if (peer)
ovpn_peer_remove(peer, reason, &release_list);
unlock_ovpn(ovpn, &release_list);
}

80
drivers/net/ovpn/peer.h Normal file
View file

@ -0,0 +1,80 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/* OpenVPN data channel offload
*
* Copyright (C) 2020-2025 OpenVPN, Inc.
*
* Author: James Yonan <james@openvpn.net>
* Antonio Quartulli <antonio@openvpn.net>
*/
#ifndef _NET_OVPN_OVPNPEER_H_
#define _NET_OVPN_OVPNPEER_H_
#include <net/dst_cache.h>
/**
* struct ovpn_peer - the main remote peer object
* @ovpn: main openvpn instance this peer belongs to
* @dev_tracker: reference tracker for associated dev
* @id: unique identifier
* @vpn_addrs: IP addresses assigned over the tunnel
* @vpn_addrs.ipv4: IPv4 assigned to peer on the tunnel
* @vpn_addrs.ipv6: IPv6 assigned to peer on the tunnel
* @dst_cache: cache for dst_entry used to send to peer
* @bind: remote peer binding
* @delete_reason: why peer was deleted (i.e. timeout, transport error, ..)
* @lock: protects binding to peer (bind)
* @refcount: reference counter
* @rcu: used to free peer in an RCU safe way
* @release_entry: entry for the socket release list
*/
struct ovpn_peer {
struct ovpn_priv *ovpn;
netdevice_tracker dev_tracker;
u32 id;
struct {
struct in_addr ipv4;
struct in6_addr ipv6;
} vpn_addrs;
struct dst_cache dst_cache;
struct ovpn_bind __rcu *bind;
enum ovpn_del_peer_reason delete_reason;
spinlock_t lock; /* protects bind */
struct kref refcount;
struct rcu_head rcu;
struct llist_node release_entry;
};
/**
* ovpn_peer_hold - increase reference counter
* @peer: the peer whose counter should be increased
*
* Return: true if the counter was increased or false if it was zero already
*/
static inline bool ovpn_peer_hold(struct ovpn_peer *peer)
{
return kref_get_unless_zero(&peer->refcount);
}
void ovpn_peer_release_kref(struct kref *kref);
/**
* ovpn_peer_put - decrease reference counter
* @peer: the peer whose counter should be decreased
*/
static inline void ovpn_peer_put(struct ovpn_peer *peer)
{
kref_put(&peer->refcount, ovpn_peer_release_kref);
}
struct ovpn_peer *ovpn_peer_new(struct ovpn_priv *ovpn, u32 id);
int ovpn_peer_add(struct ovpn_priv *ovpn, struct ovpn_peer *peer);
int ovpn_peer_del(struct ovpn_peer *peer, enum ovpn_del_peer_reason reason);
void ovpn_peer_release_p2p(struct ovpn_priv *ovpn,
enum ovpn_del_peer_reason reason);
struct ovpn_peer *ovpn_peer_get_by_transp_addr(struct ovpn_priv *ovpn,
struct sk_buff *skb);
struct ovpn_peer *ovpn_peer_get_by_id(struct ovpn_priv *ovpn, u32 peer_id);
#endif /* _NET_OVPN_OVPNPEER_H_ */