Commit 8534731d authored by Antonio Quartulli's avatar Antonio Quartulli Committed by Paolo Abeni
Browse files

ovpn: implement packet processing



This change implements encryption/decryption and
encapsulation/decapsulation of OpenVPN packets.

Support for generic crypto state is added along with
a wrapper for the AEAD crypto kernel API.

Signed-off-by: default avatarAntonio Quartulli <antonio@openvpn.net>
Link: https://patch.msgid.link/20250415-b4-ovpn-v26-9-577f6097b964@openvpn.net


Reviewed-by: default avatarSabrina Dubroca <sd@queasysnail.net>
Tested-by: default avatarOleksandr Natalenko <oleksandr@natalenko.name>
Signed-off-by: default avatarPaolo Abeni <pabeni@redhat.com>
parent ab66abbc
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -121,6 +121,10 @@ config OVPN
	depends on IPV6 || !IPV6
	select DST_CACHE
	select NET_UDP_TUNNEL
	select CRYPTO
	select CRYPTO_AES
	select CRYPTO_GCM
	select CRYPTO_CHACHA20POLY1305
	help
	  This module enhances the performance of the OpenVPN userspace software
	  by offloading the data channel processing to kernelspace.
+3 −0
Original line number Diff line number Diff line
@@ -8,10 +8,13 @@

obj-$(CONFIG_OVPN) := ovpn.o
ovpn-y += bind.o
ovpn-y += crypto.o
ovpn-y += crypto_aead.o
ovpn-y += main.o
ovpn-y += io.o
ovpn-y += netlink.o
ovpn-y += netlink-gen.o
ovpn-y += peer.o
ovpn-y += pktid.o
ovpn-y += socket.o
ovpn-y += udp.o
+3 −6
Original line number Diff line number Diff line
@@ -48,11 +48,8 @@ struct ovpn_bind *ovpn_bind_from_sockaddr(const struct sockaddr_storage *ss)
 */
void ovpn_bind_reset(struct ovpn_peer *peer, struct ovpn_bind *new)
{
	struct ovpn_bind *old;
	lockdep_assert_held(&peer->lock);

	spin_lock_bh(&peer->lock);
	old = rcu_replace_pointer(peer->bind, new, true);
	spin_unlock_bh(&peer->lock);

	kfree_rcu(old, rcu);
	kfree_rcu(rcu_replace_pointer(peer->bind, new,
				      lockdep_is_held(&peer->lock)), rcu);
}
+148 −0
Original line number Diff line number Diff line
// 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/types.h>
#include <linux/net.h>
#include <linux/netdevice.h>
#include <uapi/linux/ovpn.h>

#include "ovpnpriv.h"
#include "main.h"
#include "pktid.h"
#include "crypto_aead.h"
#include "crypto.h"

static void ovpn_ks_destroy_rcu(struct rcu_head *head)
{
	struct ovpn_crypto_key_slot *ks;

	ks = container_of(head, struct ovpn_crypto_key_slot, rcu);
	ovpn_aead_crypto_key_slot_destroy(ks);
}

void ovpn_crypto_key_slot_release(struct kref *kref)
{
	struct ovpn_crypto_key_slot *ks;

	ks = container_of(kref, struct ovpn_crypto_key_slot, refcount);
	call_rcu(&ks->rcu, ovpn_ks_destroy_rcu);
}

/* can only be invoked when all peer references have been dropped (i.e. RCU
 * release routine)
 */
void ovpn_crypto_state_release(struct ovpn_crypto_state *cs)
{
	struct ovpn_crypto_key_slot *ks;

	ks = rcu_access_pointer(cs->slots[0]);
	if (ks) {
		RCU_INIT_POINTER(cs->slots[0], NULL);
		ovpn_crypto_key_slot_put(ks);
	}

	ks = rcu_access_pointer(cs->slots[1]);
	if (ks) {
		RCU_INIT_POINTER(cs->slots[1], NULL);
		ovpn_crypto_key_slot_put(ks);
	}
}

/* Reset the ovpn_crypto_state object in a way that is atomic
 * to RCU readers.
 */
int ovpn_crypto_state_reset(struct ovpn_crypto_state *cs,
			    const struct ovpn_peer_key_reset *pkr)
{
	struct ovpn_crypto_key_slot *old = NULL, *new;
	u8 idx;

	if (pkr->slot != OVPN_KEY_SLOT_PRIMARY &&
	    pkr->slot != OVPN_KEY_SLOT_SECONDARY)
		return -EINVAL;

	new = ovpn_aead_crypto_key_slot_new(&pkr->key);
	if (IS_ERR(new))
		return PTR_ERR(new);

	spin_lock_bh(&cs->lock);
	idx = cs->primary_idx;
	switch (pkr->slot) {
	case OVPN_KEY_SLOT_PRIMARY:
		old = rcu_replace_pointer(cs->slots[idx], new,
					  lockdep_is_held(&cs->lock));
		break;
	case OVPN_KEY_SLOT_SECONDARY:
		old = rcu_replace_pointer(cs->slots[!idx], new,
					  lockdep_is_held(&cs->lock));
		break;
	}
	spin_unlock_bh(&cs->lock);

	if (old)
		ovpn_crypto_key_slot_put(old);

	return 0;
}

void ovpn_crypto_key_slot_delete(struct ovpn_crypto_state *cs,
				 enum ovpn_key_slot slot)
{
	struct ovpn_crypto_key_slot *ks = NULL;
	u8 idx;

	if (slot != OVPN_KEY_SLOT_PRIMARY &&
	    slot != OVPN_KEY_SLOT_SECONDARY) {
		pr_warn("Invalid slot to release: %u\n", slot);
		return;
	}

	spin_lock_bh(&cs->lock);
	idx = cs->primary_idx;
	switch (slot) {
	case OVPN_KEY_SLOT_PRIMARY:
		ks = rcu_replace_pointer(cs->slots[idx], NULL,
					 lockdep_is_held(&cs->lock));
		break;
	case OVPN_KEY_SLOT_SECONDARY:
		ks = rcu_replace_pointer(cs->slots[!idx], NULL,
					 lockdep_is_held(&cs->lock));
		break;
	}
	spin_unlock_bh(&cs->lock);

	if (!ks) {
		pr_debug("Key slot already released: %u\n", slot);
		return;
	}

	pr_debug("deleting key slot %u, key_id=%u\n", slot, ks->key_id);
	ovpn_crypto_key_slot_put(ks);
}

void ovpn_crypto_key_slots_swap(struct ovpn_crypto_state *cs)
{
	const struct ovpn_crypto_key_slot *old_primary, *old_secondary;
	u8 idx;

	spin_lock_bh(&cs->lock);
	idx = cs->primary_idx;
	old_primary = rcu_dereference_protected(cs->slots[idx],
						lockdep_is_held(&cs->lock));
	old_secondary = rcu_dereference_protected(cs->slots[!idx],
						  lockdep_is_held(&cs->lock));
	/* perform real swap by switching the index of the primary key */
	WRITE_ONCE(cs->primary_idx, !cs->primary_idx);

	pr_debug("key swapped: (old primary) %d <-> (new primary) %d\n",
		 old_primary ? old_primary->key_id : -1,
		 old_secondary ? old_secondary->key_id : -1);

	spin_unlock_bh(&cs->lock);
}
+139 −0
Original line number Diff line number Diff line
/* 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_OVPNCRYPTO_H_
#define _NET_OVPN_OVPNCRYPTO_H_

#include "pktid.h"
#include "proto.h"

/* info needed for both encrypt and decrypt directions */
struct ovpn_key_direction {
	const u8 *cipher_key;
	size_t cipher_key_size;
	const u8 *nonce_tail; /* only needed for GCM modes */
	size_t nonce_tail_size; /* only needed for GCM modes */
};

/* all info for a particular symmetric key (primary or secondary) */
struct ovpn_key_config {
	enum ovpn_cipher_alg cipher_alg;
	u8 key_id;
	struct ovpn_key_direction encrypt;
	struct ovpn_key_direction decrypt;
};

/* used to pass settings from netlink to the crypto engine */
struct ovpn_peer_key_reset {
	enum ovpn_key_slot slot;
	struct ovpn_key_config key;
};

struct ovpn_crypto_key_slot {
	u8 key_id;

	struct crypto_aead *encrypt;
	struct crypto_aead *decrypt;
	u8 nonce_tail_xmit[OVPN_NONCE_TAIL_SIZE];
	u8 nonce_tail_recv[OVPN_NONCE_TAIL_SIZE];

	struct ovpn_pktid_recv pid_recv ____cacheline_aligned_in_smp;
	struct ovpn_pktid_xmit pid_xmit ____cacheline_aligned_in_smp;
	struct kref refcount;
	struct rcu_head rcu;
};

struct ovpn_crypto_state {
	struct ovpn_crypto_key_slot __rcu *slots[2];
	u8 primary_idx;

	/* protects primary and secondary slots */
	spinlock_t lock;
};

static inline bool ovpn_crypto_key_slot_hold(struct ovpn_crypto_key_slot *ks)
{
	return kref_get_unless_zero(&ks->refcount);
}

static inline void ovpn_crypto_state_init(struct ovpn_crypto_state *cs)
{
	RCU_INIT_POINTER(cs->slots[0], NULL);
	RCU_INIT_POINTER(cs->slots[1], NULL);
	cs->primary_idx = 0;
	spin_lock_init(&cs->lock);
}

static inline struct ovpn_crypto_key_slot *
ovpn_crypto_key_id_to_slot(const struct ovpn_crypto_state *cs, u8 key_id)
{
	struct ovpn_crypto_key_slot *ks;
	u8 idx;

	if (unlikely(!cs))
		return NULL;

	rcu_read_lock();
	idx = READ_ONCE(cs->primary_idx);
	ks = rcu_dereference(cs->slots[idx]);
	if (ks && ks->key_id == key_id) {
		if (unlikely(!ovpn_crypto_key_slot_hold(ks)))
			ks = NULL;
		goto out;
	}

	ks = rcu_dereference(cs->slots[!idx]);
	if (ks && ks->key_id == key_id) {
		if (unlikely(!ovpn_crypto_key_slot_hold(ks)))
			ks = NULL;
		goto out;
	}

	/* when both key slots are occupied but no matching key ID is found, ks
	 * has to be reset to NULL to avoid carrying a stale pointer
	 */
	ks = NULL;
out:
	rcu_read_unlock();

	return ks;
}

static inline struct ovpn_crypto_key_slot *
ovpn_crypto_key_slot_primary(const struct ovpn_crypto_state *cs)
{
	struct ovpn_crypto_key_slot *ks;

	rcu_read_lock();
	ks = rcu_dereference(cs->slots[cs->primary_idx]);
	if (unlikely(ks && !ovpn_crypto_key_slot_hold(ks)))
		ks = NULL;
	rcu_read_unlock();

	return ks;
}

void ovpn_crypto_key_slot_release(struct kref *kref);

static inline void ovpn_crypto_key_slot_put(struct ovpn_crypto_key_slot *ks)
{
	kref_put(&ks->refcount, ovpn_crypto_key_slot_release);
}

int ovpn_crypto_state_reset(struct ovpn_crypto_state *cs,
			    const struct ovpn_peer_key_reset *pkr);

void ovpn_crypto_key_slot_delete(struct ovpn_crypto_state *cs,
				 enum ovpn_key_slot slot);

void ovpn_crypto_state_release(struct ovpn_crypto_state *cs);

void ovpn_crypto_key_slots_swap(struct ovpn_crypto_state *cs);

#endif /* _NET_OVPN_OVPNCRYPTO_H_ */
Loading