@@ -10,6 +10,7 @@
#ifndef _NET_OVPN_OVPNCRYPTO_H_
#define _NET_OVPN_OVPNCRYPTO_H_
+#include "crypto_limits.h"
#include "pktid.h"
#include "proto.h"
@@ -37,6 +38,8 @@ struct ovpn_peer_key_reset {
struct ovpn_crypto_key_slot {
u8 key_id;
+ enum ovpn_cipher_alg cipher_alg;
+ struct ovpn_limit usage_limit;
struct crypto_aead *encrypt;
struct crypto_aead *decrypt;
@@ -44,7 +47,9 @@ struct ovpn_crypto_key_slot {
u8 nonce_tail_recv[OVPN_NONCE_TAIL_SIZE];
struct ovpn_pktid_recv pid_recv ____cacheline_aligned_in_smp;
+ struct ovpn_key_usage usage_recv;
struct ovpn_pktid_xmit pid_xmit ____cacheline_aligned_in_smp;
+ struct ovpn_key_usage usage_xmit;
struct kref refcount;
struct rcu_head rcu;
};
@@ -139,6 +139,7 @@ int ovpn_aead_encrypt(struct ovpn_peer *peer, struct ovpn_crypto_key_slot *ks,
struct sk_buff *skb)
{
const unsigned int tag_size = crypto_aead_authsize(ks->encrypt);
+ unsigned int plaintext_len;
struct aead_request *req;
struct sk_buff *trailer;
struct scatterlist *sg;
@@ -149,6 +150,7 @@ int ovpn_aead_encrypt(struct ovpn_peer *peer, struct ovpn_crypto_key_slot *ks,
ovpn_skb_cb(skb)->peer = peer;
ovpn_skb_cb(skb)->ks = ks;
+ plaintext_len = skb->len;
/* Sample AEAD header format:
* 48000001 00000005 7e7046bd 444a7e28 cc6387b1 64a4d6c1 380275a...
@@ -205,7 +207,12 @@ int ovpn_aead_encrypt(struct ovpn_peer *peer, struct ovpn_crypto_key_slot *ks,
/* obtain packet ID, which is used both as a first
* 4 bytes of nonce and last 4 bytes of associated data.
*/
- ret = ovpn_pktid_xmit_next(&ks->pid_xmit, &pktid);
+ ret = ovpn_pktid_xmit_next(&ks->pid_xmit, &ks->usage_xmit,
+ &ks->usage_limit,
+ ovpn_aead_limit_blocks(ks->cipher_alg,
+ OVPN_AAD_SIZE,
+ plaintext_len),
+ &pktid);
if (unlikely(ret < 0))
return ret;
if (unlikely(ret > 0))
@@ -425,6 +432,10 @@ ovpn_aead_crypto_key_slot_new(const struct ovpn_key_config *kc)
ks->decrypt = NULL;
kref_init(&ks->refcount);
ks->key_id = kc->key_id;
+ ks->cipher_alg = kc->cipher_alg;
+ ovpn_key_usage_limit_init(&ks->usage_limit, kc->cipher_alg);
+ ovpn_key_usage_init(&ks->usage_xmit);
+ ovpn_key_usage_init(&ks->usage_recv);
ks->encrypt = ovpn_aead_init("encrypt", alg_name,
kc->encrypt.cipher_key,
new file mode 100644
@@ -0,0 +1,125 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/* OpenVPN data channel offload
+ *
+ * Copyright (C) 2026 OpenVPN, Inc.
+ *
+ * Author: Ralf Lici <ralf@mandelbit.com>
+ * Antonio Quartulli <antonio@openvpn.net>
+ */
+
+#ifndef _NET_OVPN_CRYPTO_LIMITS_H_
+#define _NET_OVPN_CRYPTO_LIMITS_H_
+
+#include <crypto/aes.h>
+#include <linux/atomic.h>
+#include <linux/limits.h>
+#include <linux/math.h>
+#include <linux/types.h>
+#include <uapi/linux/ovpn.h>
+
+/* use the OpenVPN/SP 800-38D AES-GCM invocation limit */
+#define OVPN_AES_GCM_USAGE_LIMIT ((1ULL << 36) - 1)
+
+/* notify userspace at 7/8 of the AES-GCM hard limit */
+#define OVPN_AES_GCM_USAGE_NOTIFY (OVPN_AES_GCM_USAGE_LIMIT / 8 * 7)
+
+#define OVPN_KEY_USAGE_NOTIFY_BIT 0
+
+struct ovpn_limit {
+ u64 soft;
+ u64 hard;
+};
+
+struct ovpn_key_usage {
+ atomic64_t blocks;
+ unsigned long flags;
+};
+
+static inline void ovpn_key_usage_init(struct ovpn_key_usage *usage)
+{
+ atomic64_set(&usage->blocks, 0);
+ usage->flags = 0;
+}
+
+static inline void
+ovpn_key_usage_limit_init(struct ovpn_limit *limit,
+ enum ovpn_cipher_alg cipher_alg)
+{
+ limit->soft = U64_MAX;
+ limit->hard = U64_MAX;
+
+ switch (cipher_alg) {
+ case OVPN_CIPHER_ALG_AES_GCM:
+ limit->soft = OVPN_AES_GCM_USAGE_NOTIFY;
+ limit->hard = OVPN_AES_GCM_USAGE_LIMIT;
+ break;
+ case OVPN_CIPHER_ALG_CHACHA20_POLY1305:
+ default:
+ break;
+ }
+}
+
+static inline bool ovpn_key_usage_over_limit(u64 limit, u64 pktid, u64 blocks)
+{
+ return pktid > limit || blocks > limit - pktid;
+}
+
+static inline bool ovpn_key_usage_notify_once(struct ovpn_key_usage *usage)
+{
+ return !test_and_set_bit(OVPN_KEY_USAGE_NOTIFY_BIT, &usage->flags);
+}
+
+static inline int
+ovpn_key_usage_xmit(struct ovpn_key_usage *usage,
+ const struct ovpn_limit *limit,
+ u64 pktid, u64 blocks, bool pktid_notify)
+{
+ int ret = 0;
+ u64 total;
+
+ total = atomic64_add_return(blocks, &usage->blocks);
+
+ /* tx must stop before the hard limit is crossed */
+ if (unlikely(ovpn_key_usage_over_limit(limit->hard, pktid, total)))
+ return -ERANGE;
+
+ /* soft limits ask userspace to rekey before tx must stop */
+ if (unlikely(pktid_notify ||
+ ovpn_key_usage_over_limit(limit->soft, pktid, total)) &&
+ ovpn_key_usage_notify_once(usage))
+ ret = 1;
+
+ return ret;
+}
+
+static inline bool
+ovpn_key_usage_recv(struct ovpn_key_usage *usage,
+ const struct ovpn_limit *limit,
+ u64 pktid, u64 blocks)
+{
+ u64 total;
+
+ total = atomic64_add_return(blocks, &usage->blocks);
+
+ /* rx threshold crossings are reported once without dropping
+ * the packet
+ */
+ return unlikely(ovpn_key_usage_over_limit(limit->soft, pktid, total)) &&
+ ovpn_key_usage_notify_once(usage);
+}
+
+static inline u64 ovpn_aead_limit_blocks(enum ovpn_cipher_alg cipher_alg,
+ unsigned int aad,
+ unsigned int bytes)
+{
+ switch (cipher_alg) {
+ case OVPN_CIPHER_ALG_AES_GCM:
+ return DIV_ROUND_UP_ULL(aad, AES_BLOCK_SIZE) +
+ DIV_ROUND_UP_ULL(bytes, AES_BLOCK_SIZE);
+ case OVPN_CIPHER_ALG_CHACHA20_POLY1305:
+ default:
+ return 0;
+ }
+}
+
+#endif /* _NET_OVPN_CRYPTO_LIMITS_H_ */
@@ -112,6 +112,7 @@ void ovpn_decrypt_post(void *data, int ret)
struct sk_buff *skb = data;
struct ovpn_socket *sock;
struct ovpn_peer *peer;
+ u64 aead_blocks;
__be16 proto;
__be32 *pid;
@@ -141,6 +142,16 @@ void ovpn_decrypt_post(void *data, int ret)
goto drop;
}
+ aead_blocks = ovpn_aead_limit_blocks(ks->cipher_alg,
+ OVPN_OPCODE_SIZE +
+ OVPN_NONCE_WIRE_SIZE,
+ skb->len - payload_offset);
+ if (unlikely(ovpn_pktid_recv_update_aead(&ks->pid_recv,
+ &ks->usage_recv,
+ &ks->usage_limit,
+ aead_blocks)))
+ ovpn_nl_key_swap_notify(peer, ks->key_id);
+
/* keep track of last received authenticated packet for keepalive */
WRITE_ONCE(peer->last_recv, ktime_get_real_seconds());
@@ -10,6 +10,7 @@
#ifndef _NET_OVPN_OVPNPKTID_H_
#define _NET_OVPN_OVPNPKTID_H_
+#include "crypto_limits.h"
#include "proto.h"
/* If no packets received for this length of time, set a backtrack floor
@@ -58,35 +59,75 @@ struct ovpn_pktid_recv {
/**
* ovpn_pktid_xmit_next - allocate a transmit packet ID
* @pid: transmit packet ID state
+ * @usage: key usage state
+ * @limit: key usage limits
+ * @aead_blocks: AEAD usage blocks consumed by this packet
* @pktid: location where the generated packet ID is stored
*
* The returned packet ID becomes part of the AEAD nonce, so the helper rejects
- * the packet before the 32-bit packet-ID space wraps.
+ * the packet before the 32-bit packet-ID space wraps. It also reserves this
+ * packet's AEAD usage against the key and rejects the packet before the hard
+ * AES-GCM usage limit would be exceeded.
*
- * The packet-ID soft threshold does not reject the packet. It returns 1 once
- * so the caller can notify userspace to rekey while packet-ID space remains.
+ * Soft thresholds do not reject the packet. They return 1 once so the caller
+ * can notify userspace to rekey while packet-ID space remains and before the
+ * hard AES-GCM usage limit is reached.
*
* Return: 1 if userspace should be notified, 0 if no notification is needed,
* or a negative error code otherwise.
*/
-static inline int ovpn_pktid_xmit_next(struct ovpn_pktid_xmit *pid, u32 *pktid)
+static inline int ovpn_pktid_xmit_next(struct ovpn_pktid_xmit *pid,
+ struct ovpn_key_usage *usage,
+ const struct ovpn_limit *limit,
+ u64 aead_blocks, u32 *pktid)
{
const u32 seq_num = atomic_fetch_add_unless(&pid->seq_num, 1, 0);
- int ret = 0;
+ bool pktid_notify;
+ int ret;
/* packet IDs are used to create cipher IVs and must not wrap */
if (unlikely(!seq_num))
return -ERANGE;
- /* notify userspace before the packet ID space is close to wrapping */
- if (unlikely(seq_num == PKTID_XMIT_REKEY_NOTIFY))
- ret = 1;
+ pktid_notify = seq_num >= PKTID_XMIT_REKEY_NOTIFY;
+ ret = ovpn_key_usage_xmit(usage, limit, seq_num, aead_blocks,
+ pktid_notify);
+ if (unlikely(ret < 0))
+ return ret;
*pktid = seq_num;
return ret;
}
+/**
+ * ovpn_pktid_recv_update_aead - account receive-side AEAD usage
+ * @pr: receive packet ID state
+ * @usage: key usage state
+ * @limit: key usage limits
+ * @aead_blocks: AEAD usage blocks consumed by this packet
+ *
+ * RX AEAD accounting is only informational for the local userspace process.
+ * The peer's packets that already passed authentication and replay checks are
+ * not dropped because the peer crossed a local usage threshold.
+ *
+ * Return: true if userspace should be notified, false otherwise.
+ */
+static inline bool
+ovpn_pktid_recv_update_aead(struct ovpn_pktid_recv *pr,
+ struct ovpn_key_usage *usage,
+ const struct ovpn_limit *limit,
+ u64 aead_blocks)
+{
+ bool ret;
+
+ spin_lock_bh(&pr->lock);
+ ret = ovpn_key_usage_recv(usage, limit, pr->id, aead_blocks);
+ spin_unlock_bh(&pr->lock);
+
+ return ret;
+}
+
/* Write 12-byte AEAD IV to dest */
static inline void ovpn_pktid_aead_write(const u32 pktid,
const u8 nt[],