Add a library that implements the per-packet part of IPsec ESP
(RFC 4303) with AES-GCM (RFC 4106), so that the userspace datapath can
encrypt and decrypt tunnel traffic itself.  A follow-up patch uses it
for an 'esp' tunnel port type.

A security association (SA) holds the key, the salt and the sequence
number state for one direction.  SAs are kept in a global SA database,
a cmap, where datapath threads look them up without locks: an outbound
SA by its SPI and the peer's address, which are both in the header the
datapath pushes, and an inbound SA by its SPI alone.

esp_seal() and esp_open() are safe to call concurrently for the same SA:
 - Outbound sequence numbers are taken with an atomic increment.  The
   sequence number must not cycle, so packets are refused once it is
   exhausted.  Extended (64-bit) Sequence Numbers are supported.
 - The explicit IV is the sequence number XORed with a per-SA value from
   the system's entropy source, so a manually keyed SA that restarts at
   sequence number 1 does not reuse a nonce.
 - The anti-replay window (up to 4096 packets, 64 by default) is
   checked before decryption and updated after authentication, under a
   per-SA mutex.  For ESN the high half of the sequence number is
   inferred as in RFC 4303 appendix A.

Encryption uses OpenSSL's EVP interface, which uses AES-NI where
available.  Each thread caches a few cipher contexts with expanded keys,
so a packet costs no allocation and no key expansion.  Without OpenSSL,
SAs cannot be created.

The unit test checks decryption against vectors produced by an
independent AES-GCM implementation, with and without ESN, as well as
round trips for all key sizes, tampering, replay and sequence number
wraparound.

Assisted-by: claude-opus-5-5, Claude Code
Signed-off-by: Thilak Raj Surendra Babu <[email protected]>
---
 lib/automake.mk   |   2 +
 lib/esp.c         | 685 ++++++++++++++++++++++++++++++++++++++++++++++
 lib/esp.h         |  97 +++++++
 lib/packets.h     |   4 +-
 tests/automake.mk |   1 +
 tests/library.at  |   6 +
 tests/test-esp.c  | 387 ++++++++++++++++++++++++++
 7 files changed, 1180 insertions(+), 2 deletions(-)
 create mode 100644 lib/esp.c
 create mode 100644 lib/esp.h
 create mode 100644 tests/test-esp.c

diff --git a/lib/automake.mk b/lib/automake.mk
index 2dfca79da..1e3a800fe 100644
--- a/lib/automake.mk
+++ b/lib/automake.mk
@@ -104,6 +104,8 @@ lib_libopenvswitch_la_SOURCES = \
        lib/dynamic-string.c \
        lib/entropy.c \
        lib/entropy.h \
+       lib/esp.c \
+       lib/esp.h \
        lib/fat-rwlock.c \
        lib/fat-rwlock.h \
        lib/fatal-signal.c \
diff --git a/lib/esp.c b/lib/esp.c
new file mode 100644
index 000000000..a6a364356
--- /dev/null
+++ b/lib/esp.c
@@ -0,0 +1,685 @@
+/*
+ * Copyright (c) 2026 Thilak Raj Surendra Babu
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+#include "esp.h"
+
+#include <errno.h>
+#include <string.h>
+
+#ifdef HAVE_OPENSSL
+#include <openssl/crypto.h>
+#include <openssl/evp.h>
+#endif
+
+#include "byte-order.h"
+#include "cmap.h"
+#include "coverage.h"
+#include "entropy.h"
+#include "hash.h"
+#include "ovs-atomic.h"
+#include "ovs-rcu.h"
+#include "ovs-thread.h"
+#include "util.h"
+#include "openvswitch/vlog.h"
+
+VLOG_DEFINE_THIS_MODULE(esp);
+
+#ifdef HAVE_OPENSSL
+COVERAGE_DEFINE(esp_tx_seq_exhausted);
+COVERAGE_DEFINE(esp_tx_crypto_error);
+COVERAGE_DEFINE(esp_rx_malformed);
+COVERAGE_DEFINE(esp_rx_no_sa);
+COVERAGE_DEFINE(esp_rx_replay);
+COVERAGE_DEFINE(esp_rx_auth_failed);
+#endif
+
+struct esp_sa {
+    struct cmap_node node;          /* In 'esp_sad', for inbound SAs. */
+    struct esp_sa_params params;
+    uint64_t id;                    /* Unique, never 0. */
+#ifdef HAVE_OPENSSL
+    const EVP_CIPHER *cipher;
+#endif
+
+    /* Outbound.  The explicit IV of a packet is its sequence number XORed
+     * with 'iv_salt', which is random per SA and comes from the system's
+     * entropy source, so that a manually keyed SA that restarts its sequence
+     * numbers does not reuse IVs. */
+    atomic_uint64_t seq;            /* Last sequence number sent. */
+    uint64_t iv_salt;
+
+    /* Inbound anti-replay window (RFC 4303 section 3.4.3 and appendix A).
+     * Bit (N % replay_size) of 'replay_bitmap' is set if sequence number N
+     * has been received, for N in (replay_top - replay_size, replay_top]. */
+    struct ovs_mutex replay_mutex;
+    uint64_t replay_top OVS_GUARDED;
+    uint32_t replay_size;           /* In packets, multiple of 64. */
+    uint64_t *replay_bitmap;        /* Its contents are guarded. */
+};
+
+#ifdef HAVE_OPENSSL
+#define OVS_UNUSED_WITHOUT_OPENSSL
+#else
+#define OVS_UNUSED_WITHOUT_OPENSSL OVS_UNUSED
+#endif
+
+/* SA database, indexed by SPI and destination.  Only written by the main
+ * thread. */
+static struct cmap esp_sad = CMAP_INITIALIZER;
+
+bool
+esp_is_supported(void)
+{
+#ifdef HAVE_OPENSSL
+    return true;
+#else
+    return false;
+#endif
+}
+
+/* Parses 's', an AES-GCM key followed by a 4-byte salt as hexadecimal digits
+ * with an optional "0x" prefix, into 'params'.  This is the format used by
+ * "ip xfrm state ... aead 'rfc4106(gcm(aes))' <key> 128".
+ *
+ * Returns NULL if successful, otherwise a malloc()'d error message. */
+char *
+esp_parse_key(const char *s, struct esp_sa_params *params)
+{
+    size_t n_digits, key_len;
+
+    if (!strncmp(s, "0x", 2) || !strncmp(s, "0X", 2)) {
+        s += 2;
+    }
+
+    n_digits = strlen(s);
+    key_len = n_digits / 2;
+    if (n_digits % 2
+        || (key_len != 16 + ESP_SALT_LEN && key_len != 24 + ESP_SALT_LEN
+            && key_len != 32 + ESP_SALT_LEN)) {
+        return xasprintf("key must be 20, 28 or 36 bytes (an AES-128, -192 "
+                         "or -256 key followed by a 4-byte salt), "
+                         "got %"PRIuSIZE" hex digits", n_digits);
+    }
+
+    for (size_t i = 0; i < key_len; i++) {
+        bool ok;
+
+        params->key[i] = hexits_value(&s[2 * i], 2, &ok);
+        if (!ok) {
+            return xstrdup("key contains a non-hexadecimal digit");
+        }
+    }
+    params->key_len = key_len;
+
+    return NULL;
+}
+
+bool
+esp_sa_params_equal(const struct esp_sa_params *a,
+                    const struct esp_sa_params *b)
+{
+    return (a->spi == b->spi
+            && ipv6_addr_equals(&a->dst, &b->dst)
+            && a->key_len == b->key_len
+            && !memcmp(a->key, b->key, a->key_len)
+            && a->esn == b->esn
+            && a->replay_window == b->replay_window);
+}
+
+/* Returns a new SA for 'params', or NULL if ESP is not supported or 'params'
+ * is invalid. */
+struct esp_sa *
+esp_sa_create(const struct esp_sa_params *params OVS_UNUSED_WITHOUT_OPENSSL)
+{
+#ifdef HAVE_OPENSSL
+    static atomic_uint64_t next_id = 1;
+    const EVP_CIPHER *cipher;
+    struct esp_sa *sa;
+
+    switch (params->key_len - ESP_SALT_LEN) {
+    case 16:
+        cipher = EVP_aes_128_gcm();
+        break;
+    case 24:
+        cipher = EVP_aes_192_gcm();
+        break;
+    case 32:
+        cipher = EVP_aes_256_gcm();
+        break;
+    default:
+        return NULL;
+    }
+
+    /* An inbound SA needs the anti-replay window to infer the high half of
+     * extended sequence numbers. */
+    bool inbound = !ipv6_addr_is_set(&params->dst);
+    if (params->replay_window > ESP_MAX_REPLAY_WINDOW
+        || (inbound && params->esn && !params->replay_window)) {
+        return NULL;
+    }
+
+    sa = xzalloc(sizeof *sa);
+    sa->params = *params;
+    atomic_add_relaxed(&next_id, 1, &sa->id);
+    sa->cipher = cipher;
+    atomic_init(&sa->seq, 0);
+    get_entropy_or_die(&sa->iv_salt, sizeof sa->iv_salt);
+
+    ovs_mutex_init(&sa->replay_mutex);
+    sa->replay_size = ROUND_UP(params->replay_window, 64);
+    if (sa->replay_size) {
+        sa->replay_bitmap = xzalloc(sa->replay_size / 8);
+    }
+
+    return sa;
+#else
+    return NULL;
+#endif
+}
+
+void
+esp_sa_destroy(struct esp_sa *sa)
+{
+    if (sa) {
+        ovs_mutex_destroy(&sa->replay_mutex);
+        free(sa->replay_bitmap);
+#ifdef HAVE_OPENSSL
+        OPENSSL_cleanse(sa->params.key, sizeof sa->params.key);
+#endif
+        free(sa);
+    }
+}
+
+/* Destroys 'sa' once no thread can be using it anymore. */
+void
+esp_sa_destroy_postponed(struct esp_sa *sa)
+{
+    if (sa) {
+        ovsrcu_postpone(esp_sa_destroy, sa);
+    }
+}
+
+const struct esp_sa_params *
+esp_sa_get_params(const struct esp_sa *sa)
+{
+    return &sa->params;
+}
+
+/* Makes the next packet sent on outbound SA 'sa' use sequence number 'seq'.
+ * For testing only. */
+void
+esp_sa_set_next_seq(struct esp_sa *sa, uint64_t seq)
+{
+    atomic_store_relaxed(&sa->seq, seq - 1);
+}
+
+/* SA database. */
+
+static uint32_t
+esp_sad_hash(ovs_be32 spi, const struct in6_addr *dst)
+{
+    return hash_bytes(dst, sizeof *dst, (OVS_FORCE uint32_t) spi);
+}
+
+/* Adds 'sa' to the SAD.  Returns 0 if successful, EEXIST if an SA with the
+ * same SPI and destination is already present. */
+int
+esp_sad_insert(struct esp_sa *sa)
+{
+    const struct esp_sa_params *p = &sa->params;
+
+    if (esp_sad_lookup(p->spi, &p->dst)) {
+        return EEXIST;
+    }
+    cmap_insert(&esp_sad, &sa->node, esp_sad_hash(p->spi, &p->dst));
+    return 0;
+}
+
+/* Replaces 'old', which must be in the SAD, by 'new'.  Their SPIs and
+ * destinations must be the same.  Packets processed meanwhile use one or the
+ * other. */
+void
+esp_sad_replace(struct esp_sa *old, struct esp_sa *new)
+{
+    const struct esp_sa_params *p = &new->params;
+
+    ovs_assert(old->params.spi == p->spi
+               && ipv6_addr_equals(&old->params.dst, &p->dst));
+    cmap_replace(&esp_sad, &old->node, &new->node,
+                 esp_sad_hash(p->spi, &p->dst));
+}
+
+void
+esp_sad_remove(struct esp_sa *sa)
+{
+    const struct esp_sa_params *p = &sa->params;
+
+    cmap_remove(&esp_sad, &sa->node, esp_sad_hash(p->spi, &p->dst));
+}
+
+/* Returns the SA with 'spi' and 'dst' from the SAD, or NULL.  'dst' is the
+ * peer's address for an outbound SA, all-zeros for an inbound SA. */
+struct esp_sa *
+esp_sad_lookup(ovs_be32 spi, const struct in6_addr *dst)
+{
+    struct esp_sa *sa;
+
+    CMAP_FOR_EACH_WITH_HASH (sa, node, esp_sad_hash(spi, dst), &esp_sad) {
+        if (sa->params.spi == spi && ipv6_addr_equals(&sa->params.dst, dst)) {
+            return sa;
+        }
+    }
+    return NULL;
+}
+
+#ifdef HAVE_OPENSSL
+/* Anti-replay window. */
+
+/* Determines the full sequence number of a packet whose transmitted (low 32)
+ * bits are 'seq_lo', per RFC 4303 appendix A.2.1.  Returns false if it would
+ * be outside the 64-bit sequence number space. */
+static bool
+esp_replay_infer_seq(const struct esp_sa *sa, uint32_t seq_lo,
+                     uint64_t *seqp)
+    OVS_REQUIRES(sa->replay_mutex)
+{
+    uint32_t top_lo = sa->replay_top;
+    uint32_t top_hi = sa->replay_top >> 32;
+    uint32_t bottom = top_lo - sa->replay_size + 1;
+    uint32_t seq_hi;
+
+    if (!sa->params.esn) {
+        *seqp = seq_lo;
+        return true;
+    }
+
+    if (top_lo >= sa->replay_size - 1) {
+        /* The window lies within one 2**32 subspace. */
+        if (seq_lo >= bottom) {
+            seq_hi = top_hi;
+        } else if (top_hi == UINT32_MAX) {
+            return false;
+        } else {
+            seq_hi = top_hi + 1;
+        }
+    } else {
+        /* The window spans two subspaces. */
+        if (seq_lo >= bottom) {
+            if (!top_hi) {
+                return false;
+            }
+            seq_hi = top_hi - 1;
+        } else {
+            seq_hi = top_hi;
+        }
+    }
+
+    *seqp = ((uint64_t) seq_hi << 32) | seq_lo;
+    return true;
+}
+
+static bool
+esp_replay_bit(const struct esp_sa *sa, uint64_t seq)
+    OVS_REQUIRES(sa->replay_mutex)
+{
+    uint32_t bit = seq % sa->replay_size;
+
+    return sa->replay_bitmap[bit / 64] & (UINT64_C(1) << (bit % 64));
+}
+
+static bool
+esp_replay_check__(const struct esp_sa *sa, uint64_t seq)
+    OVS_REQUIRES(sa->replay_mutex)
+{
+    if (!seq) {
+        return false;
+    } else if (seq > sa->replay_top) {
+        return true;
+    } else if (sa->replay_top - seq >= sa->replay_size) {
+        return false;
+    } else {
+        return !esp_replay_bit(sa, seq);
+    }
+}
+
+/* Checks, before authenticating it, whether a packet with transmitted
+ * sequence number 'seq_lo' may be accepted.  If so, stores its full sequence
+ * number in '*seqp' and returns true. */
+static bool
+esp_replay_check(struct esp_sa *sa, uint32_t seq_lo, uint64_t *seqp)
+{
+    bool ok;
+
+    if (!sa->replay_size) {
+        *seqp = seq_lo;
+        return true;
+    }
+
+    ovs_mutex_lock(&sa->replay_mutex);
+    ok = (esp_replay_infer_seq(sa, seq_lo, seqp)
+          && esp_replay_check__(sa, *seqp));
+    ovs_mutex_unlock(&sa->replay_mutex);
+
+    return ok;
+}
+
+/* Records that the authenticated packet with sequence number 'seq' has been
+ * received.  Returns false if another thread received it meanwhile. */
+static bool
+esp_replay_update(struct esp_sa *sa, uint64_t seq)
+{
+    uint32_t bit;
+    bool ok;
+
+    if (!sa->replay_size) {
+        return true;
+    }
+
+    ovs_mutex_lock(&sa->replay_mutex);
+    ok = esp_replay_check__(sa, seq);
+    if (ok) {
+        if (seq > sa->replay_top) {
+            uint64_t diff = seq - sa->replay_top;
+
+            if (diff >= sa->replay_size) {
+                memset(sa->replay_bitmap, 0, sa->replay_size / 8);
+            } else {
+                for (uint64_t s = sa->replay_top + 1; s < seq; s++) {
+                    bit = s % sa->replay_size;
+                    sa->replay_bitmap[bit / 64] &=
+                        ~(UINT64_C(1) << (bit % 64));
+                }
+            }
+            sa->replay_top = seq;
+        }
+        bit = seq % sa->replay_size;
+        sa->replay_bitmap[bit / 64] |= UINT64_C(1) << (bit % 64);
+    }
+    ovs_mutex_unlock(&sa->replay_mutex);
+
+    return ok;
+}
+
+#endif /* HAVE_OPENSSL */
+
+/* Packet processing. */
+
+/* Returns the number of bytes that must follow a payload of 'payload_len'
+ * bytes: padding to a 4-byte boundary, the ESP trailer and the ICV. */
+size_t
+esp_trailer_len(size_t payload_len)
+{
+    size_t pad_len = (4 - (payload_len + ESP_TRAILER_LEN) % 4) % 4;
+
+    return pad_len + ESP_TRAILER_LEN + ESP_ICV_LEN;
+}
+
+#ifdef HAVE_OPENSSL
+/* Each thread keeps a few cipher contexts with their keys already expanded,
+ * indexed by SA and direction.  A collision only costs a key expansion. */
+#define ESP_CTX_CACHE_SIZE 16
+
+struct esp_ctx_slot {
+    uint64_t sa_id;                 /* 0 if unused. */
+    bool enc;
+    EVP_CIPHER_CTX *ctx;
+};
+
+struct esp_ctx_cache {
+    struct esp_ctx_slot slots[ESP_CTX_CACHE_SIZE];
+};
+
+static ovsthread_key_t esp_ctx_cache_key;
+
+static void
+esp_ctx_cache_destroy(void *cache_)
+{
+    struct esp_ctx_cache *cache = cache_;
+
+    for (size_t i = 0; i < ESP_CTX_CACHE_SIZE; i++) {
+        EVP_CIPHER_CTX_free(cache->slots[i].ctx);
+    }
+    free(cache);
+}
+
+static EVP_CIPHER_CTX *
+esp_get_ctx(const struct esp_sa *sa, bool enc)
+{
+    static struct ovsthread_once once = OVSTHREAD_ONCE_INITIALIZER;
+    struct esp_ctx_cache *cache;
+    struct esp_ctx_slot *slot;
+
+    if (ovsthread_once_start(&once)) {
+        ovsthread_key_create(&esp_ctx_cache_key, esp_ctx_cache_destroy);
+        ovsthread_once_done(&once);
+    }
+
+    cache = ovsthread_getspecific(esp_ctx_cache_key);
+    if (!cache) {
+        cache = xzalloc(sizeof *cache);
+        ovsthread_setspecific(esp_ctx_cache_key, cache);
+    }
+
+    slot = &cache->slots[(sa->id * 2 + enc) % ESP_CTX_CACHE_SIZE];
+
+    if (!slot->ctx) {
+        slot->ctx = EVP_CIPHER_CTX_new();
+        if (!slot->ctx) {
+            return NULL;
+        }
+    }
+    if (slot->sa_id != sa->id || slot->enc != enc) {
+        slot->sa_id = 0;
+        if (!EVP_CipherInit_ex(slot->ctx, sa->cipher, NULL, sa->params.key,
+                               NULL, enc)) {
+            return NULL;
+        }
+        slot->sa_id = sa->id;
+        slot->enc = enc;
+    }
+    return slot->ctx;
+}
+
+/* Builds the AES-GCM nonce and additional authenticated data for a packet
+ * with ESP header 'esp' and full sequence number 'seq' (RFC 4106 section 5).
+ * Returns the length of the AAD. */
+static size_t
+esp_gcm_prepare(const struct esp_sa *sa, const struct esp_header *esp,
+                uint64_t seq, uint8_t nonce[ESP_SALT_LEN + ESP_IV_LEN],
+                uint8_t aad[12])
+{
+    ovs_be32 spi = get_16aligned_be32(&esp->spi);
+    ovs_be32 seq_hi = htonl(seq >> 32);
+    ovs_be32 seq_lo = htonl(seq);
+
+    memcpy(nonce, &sa->params.key[sa->params.key_len - ESP_SALT_LEN],
+           ESP_SALT_LEN);
+    memcpy(nonce + ESP_SALT_LEN, esp + 1, ESP_IV_LEN);
+
+    memcpy(aad, &spi, 4);
+    if (sa->params.esn) {
+        memcpy(aad + 4, &seq_hi, 4);
+        memcpy(aad + 8, &seq_lo, 4);
+        return 12;
+    } else {
+        memcpy(aad + 4, &seq_lo, 4);
+        return 8;
+    }
+}
+#endif
+
+/* Encrypts a packet for outbound SA 'sa'.
+ *
+ * 'esp' must be followed by ESP_IV_LEN bytes of space for the IV, then by
+ * 'payload_len' bytes of payload whose type is 'next_hdr' (an IP protocol
+ * number), then by esp_trailer_len(payload_len) bytes of space.  Fills in the
+ * ESP header, IV, padding, trailer and ICV, and encrypts the payload, padding
+ * and trailer in place.
+ *
+ * Returns 0 if successful, otherwise a positive errno value. */
+int
+esp_seal(struct esp_sa *sa OVS_UNUSED_WITHOUT_OPENSSL,
+         struct esp_header *esp OVS_UNUSED_WITHOUT_OPENSSL,
+         size_t payload_len OVS_UNUSED_WITHOUT_OPENSSL,
+         uint8_t next_hdr OVS_UNUSED_WITHOUT_OPENSSL)
+{
+#ifdef HAVE_OPENSSL
+    uint8_t *payload = (uint8_t *) esp + ESP_PREFIX_LEN;
+    size_t ct_len = esp_trailer_len(payload_len) - ESP_ICV_LEN + payload_len;
+    size_t pad_len = ct_len - payload_len - ESP_TRAILER_LEN;
+    uint8_t nonce[ESP_SALT_LEN + ESP_IV_LEN];
+    uint8_t *trailer = payload + payload_len;
+    EVP_CIPHER_CTX *ctx;
+    uint8_t aad[12];
+    size_t aad_len;
+    uint64_t seq;
+    ovs_be64 iv;
+    int len;
+
+    atomic_add_relaxed(&sa->seq, 1, &seq);
+    seq++;
+    if (OVS_UNLIKELY(sa->params.esn ? !seq : seq > UINT32_MAX)) {
+        /* RFC 4303 section 3.3.3: the sequence number must not cycle.  With
+         * ESN, this only catches the wrap to zero, not later numbers, but
+         * sending 2**64 packets is out of reach. */
+        COVERAGE_INC(esp_tx_seq_exhausted);
+        return ERANGE;
+    }
+
+    for (size_t i = 0; i < pad_len; i++) {
+        trailer[i] = i + 1;
+    }
+    trailer[pad_len] = pad_len;
+    trailer[pad_len + 1] = next_hdr;
+
+    put_16aligned_be32(&esp->spi, sa->params.spi);
+    put_16aligned_be32(&esp->seq_no, htonl(seq));
+    iv = htonll(seq ^ sa->iv_salt);
+    memcpy(esp + 1, &iv, ESP_IV_LEN);
+
+    aad_len = esp_gcm_prepare(sa, esp, seq, nonce, aad);
+
+    ctx = esp_get_ctx(sa, true);
+    if (OVS_UNLIKELY(!ctx
+                     || !EVP_CipherInit_ex(ctx, NULL, NULL, NULL, nonce, 1)
+                     || !EVP_CipherUpdate(ctx, NULL, &len, aad, aad_len)
+                     || !EVP_CipherUpdate(ctx, payload, &len, payload, ct_len)
+                     || !EVP_CipherFinal_ex(ctx, payload + len, &len)
+                     || !EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG,
+                                             ESP_ICV_LEN,
+                                             payload + ct_len))) {
+        COVERAGE_INC(esp_tx_crypto_error);
+        return EIO;
+    }
+    return 0;
+#else
+    return EOPNOTSUPP;
+#endif
+}
+
+/* Authenticates and decrypts the 'len'-byte ESP packet that starts at 'esp',
+ * using the inbound SA for its SPI, and checks it against the SA's
+ * anti-replay window.
+ *
+ * If successful, stores the SPI in '*spi', the protocol of the payload in
+ * '*next_hdr' and its length in '*payload_len', and returns 0.  The payload
+ * starts ESP_PREFIX_LEN bytes after 'esp'.  Otherwise, returns a positive
+ * errno value. */
+int
+esp_open(struct esp_header *esp OVS_UNUSED_WITHOUT_OPENSSL,
+         size_t len OVS_UNUSED_WITHOUT_OPENSSL,
+         ovs_be32 *spi OVS_UNUSED_WITHOUT_OPENSSL,
+         uint8_t *next_hdr OVS_UNUSED_WITHOUT_OPENSSL,
+         size_t *payload_len OVS_UNUSED_WITHOUT_OPENSSL)
+{
+#ifdef HAVE_OPENSSL
+    uint8_t *payload = (uint8_t *) esp + ESP_PREFIX_LEN;
+    uint8_t nonce[ESP_SALT_LEN + ESP_IV_LEN];
+    EVP_CIPHER_CTX *ctx;
+    uint8_t *trailer;
+    struct esp_sa *sa;
+    uint8_t *pad;
+    uint8_t aad[12];
+    size_t aad_len;
+    size_t ct_len;
+    size_t pad_len;
+    uint32_t seq_lo;
+    uint64_t seq;
+    int out_len;
+
+    if (OVS_UNLIKELY(len < ESP_PREFIX_LEN + ESP_TRAILER_LEN + ESP_ICV_LEN)) {
+        COVERAGE_INC(esp_rx_malformed);
+        return EINVAL;
+    }
+    ct_len = len - ESP_PREFIX_LEN - ESP_ICV_LEN;
+
+    *spi = get_16aligned_be32(&esp->spi);
+    sa = esp_sad_lookup(*spi, &in6addr_any);
+    if (OVS_UNLIKELY(!sa)) {
+        COVERAGE_INC(esp_rx_no_sa);
+        return ENOENT;
+    }
+
+    seq_lo = ntohl(get_16aligned_be32(&esp->seq_no));
+    if (OVS_UNLIKELY(!esp_replay_check(sa, seq_lo, &seq))) {
+        COVERAGE_INC(esp_rx_replay);
+        return EALREADY;
+    }
+
+    aad_len = esp_gcm_prepare(sa, esp, seq, nonce, aad);
+
+    ctx = esp_get_ctx(sa, false);
+    if (OVS_UNLIKELY(!ctx
+                     || !EVP_CipherInit_ex(ctx, NULL, NULL, NULL, nonce, 0)
+                     || !EVP_CipherUpdate(ctx, NULL, &out_len, aad, aad_len)
+                     || !EVP_CipherUpdate(ctx, payload, &out_len, payload,
+                                          ct_len)
+                     || !EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG,
+                                             ESP_ICV_LEN, payload + ct_len)
+                     || EVP_CipherFinal_ex(ctx, payload + out_len,
+                                           &out_len) <= 0)) {
+        COVERAGE_INC(esp_rx_auth_failed);
+        return EBADMSG;
+    }
+
+    if (OVS_UNLIKELY(!esp_replay_update(sa, seq))) {
+        COVERAGE_INC(esp_rx_replay);
+        return EALREADY;
+    }
+
+    trailer = payload + ct_len - ESP_TRAILER_LEN;
+    pad_len = trailer[0];
+    if (OVS_UNLIKELY(pad_len + ESP_TRAILER_LEN > ct_len)) {
+        COVERAGE_INC(esp_rx_malformed);
+        return EINVAL;
+    }
+    /* RFC 4303 section 2.4: the padding bytes are 1, 2, 3, ... */
+    pad = trailer - pad_len;
+    for (size_t i = 0; i < pad_len; i++) {
+        if (OVS_UNLIKELY(pad[i] != i + 1)) {
+            COVERAGE_INC(esp_rx_malformed);
+            return EINVAL;
+        }
+    }
+
+    *next_hdr = trailer[1];
+    *payload_len = ct_len - pad_len - ESP_TRAILER_LEN;
+    return 0;
+#else
+    return EOPNOTSUPP;
+#endif
+}
diff --git a/lib/esp.h b/lib/esp.h
new file mode 100644
index 000000000..be4558c30
--- /dev/null
+++ b/lib/esp.h
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2026 Thilak Raj Surendra Babu
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ESP_H
+#define ESP_H 1
+
+/* IPsec Encapsulating Security Payload (ESP, RFC 4303) with AES-GCM
+ * (RFC 4106), for tunnels in the userspace datapath.
+ *
+ * A Security Association (SA) holds the keys and sequence number state for
+ * one direction of a tunnel.  SAs are published in a global SA database
+ * (SAD), where datapath threads look them up:
+ *
+ *   - An outbound SA by its SPI and the address of the peer, which are both
+ *     in the tunnel header that the datapath pushes.
+ *
+ *   - An inbound SA by its SPI alone, as this host chose the SPI.  Received
+ *     packets are authenticated and decrypted before it is known which tunnel
+ *     port they belong to.
+ *
+ * esp_seal() and esp_open() may be called from any thread, including PMD
+ * threads, concurrently for the same SA.  All other functions must be called
+ * from a single thread (the main thread in ovs-vswitchd).  An SA that has
+ * been removed from the SAD or is no longer published to other threads must
+ * be freed with esp_sa_destroy_postponed(). */
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include "compiler.h"
+#include "openvswitch/types.h"
+#include "packets.h"
+
+/* ESP_HEADER_LEN, struct esp_header and ESP_TRAILER_LEN are in packets.h. */
+#define ESP_IV_LEN      8
+#define ESP_ICV_LEN     16
+#define ESP_SALT_LEN    4
+
+/* Bytes between the start of the ESP header and the encrypted payload. */
+#define ESP_PREFIX_LEN  (ESP_HEADER_LEN + ESP_IV_LEN)
+
+/* An AES-256 key followed by the 4-byte salt, as in RFC 4106. */
+#define ESP_MAX_KEY_LEN (32 + ESP_SALT_LEN)
+
+#define ESP_DEFAULT_REPLAY_WINDOW 64
+#define ESP_MAX_REPLAY_WINDOW     4096
+
+struct esp_sa_params {
+    ovs_be32 spi;
+    struct in6_addr dst;            /* Outbound: peer address.  Inbound: 0. */
+    uint8_t key[ESP_MAX_KEY_LEN];   /* AES key followed by the salt. */
+    uint8_t key_len;                /* Including the salt. */
+    bool esn;                       /* Extended (64-bit) Sequence Numbers. */
+    uint16_t replay_window;         /* Inbound only, in packets; 0 = off,
+                                     * which is invalid with 'esn'. */
+};
+
+bool esp_is_supported(void);
+
+char *esp_parse_key(const char *, struct esp_sa_params *)
+    OVS_WARN_UNUSED_RESULT;
+bool esp_sa_params_equal(const struct esp_sa_params *,
+                         const struct esp_sa_params *);
+
+struct esp_sa *esp_sa_create(const struct esp_sa_params *);
+void esp_sa_destroy(struct esp_sa *);
+void esp_sa_destroy_postponed(struct esp_sa *);
+const struct esp_sa_params *esp_sa_get_params(const struct esp_sa *);
+
+int esp_sad_insert(struct esp_sa *);
+void esp_sad_replace(struct esp_sa *old, struct esp_sa *new);
+void esp_sad_remove(struct esp_sa *);
+struct esp_sa *esp_sad_lookup(ovs_be32 spi, const struct in6_addr *dst);
+
+size_t esp_trailer_len(size_t payload_len);
+int esp_seal(struct esp_sa *, struct esp_header *, size_t payload_len,
+             uint8_t next_hdr);
+int esp_open(struct esp_header *, size_t len, ovs_be32 *spi,
+             uint8_t *next_hdr, size_t *payload_len);
+
+/* For testing only. */
+void esp_sa_set_next_seq(struct esp_sa *, uint64_t seq);
+
+#endif /* esp.h */
diff --git a/lib/packets.h b/lib/packets.h
index ea0bda840..b9d770581 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -878,8 +878,8 @@ BUILD_ASSERT_DECL(UDP_HEADER_LEN == sizeof(struct 
udp_header));
 
 #define ESP_HEADER_LEN 8
 struct esp_header {
-    ovs_be32 spi;
-    ovs_be32 seq_no;
+    ovs_16aligned_be32 spi;
+    ovs_16aligned_be32 seq_no;
 };
 BUILD_ASSERT_DECL(ESP_HEADER_LEN == sizeof(struct esp_header));
 
diff --git a/tests/automake.mk b/tests/automake.mk
index 2f3e4f7ef..4dc3754b1 100644
--- a/tests/automake.mk
+++ b/tests/automake.mk
@@ -496,6 +496,7 @@ tests_ovstest_SOURCES = \
        tests/test-reconnect.c \
        tests/test-rstp.c \
        tests/test-sflow.c \
+       tests/test-esp.c \
        tests/test-sha1.c \
        tests/test-skiplist.c \
        tests/test-stp.c \
diff --git a/tests/library.at b/tests/library.at
index 80ebe6ed8..dc6ec3000 100644
--- a/tests/library.at
+++ b/tests/library.at
@@ -63,6 +63,12 @@ AT_CHECK([ovstest test-sha1], [0], [....................
 ])
 AT_CLEANUP
 
+AT_SETUP([IPsec ESP])
+AT_KEYWORDS([esp ipsec])
+AT_SKIP_IF([test "$HAVE_OPENSSL" = no])
+AT_CHECK([ovstest test-esp])
+AT_CLEANUP
+
 AT_SETUP([test skiplist])
 AT_KEYWORDS([skiplist])
 AT_CHECK([ovstest test-skiplist], [0], [skiplist insert
diff --git a/tests/test-esp.c b/tests/test-esp.c
new file mode 100644
index 000000000..e5fd9013d
--- /dev/null
+++ b/tests/test-esp.c
@@ -0,0 +1,387 @@
+/*
+ * Copyright (c) 2026 Thilak Raj Surendra Babu
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+#include "esp.h"
+#include <errno.h>
+#include <string.h>
+#include "byte-order.h"
+#include "ovstest.h"
+#include "random.h"
+#include "util.h"
+
+#define TEST_KEY "101112131415161718191a1b1c1d1e1fcafebabe"
+#define TEST_PAYLOAD "OVS ESP known answer test vector"
+
+/* Produced with the 'cryptography' Python module from TEST_KEY, IV
+ * 0x0011223344556677, payload TEST_PAYLOAD and next header 4.
+ *  - SPI 0x1000, sequence number 7.
+ *  - SPI 0x1001, extended sequence number 0x100000005. */
+static const char *kat_seq7 =
+    "00001000000000070011223344556677"
+    "6b646566a4cb5ffb8ae9320d838a07cc83405ce68e7003193ade723b8a3b01b8"
+    "2246cdb5397447f5217f7a2a0edea54dcab9d890";
+static const char *kat_esn =
+    "00001001000000050011223344556677"
+    "6b646566a4cb5ffb8ae9320d838a07cc83405ce68e7003193ade723b8a3b01b8"
+    "2246cdb5d03e0fb3b624b02969575ee723f923a5";
+
+struct test_pkt {
+    uint8_t buf[256];
+    size_t len;
+};
+
+static struct esp_sa_params
+make_params(uint32_t spi, const char *key, bool esn, uint16_t window)
+{
+    struct esp_sa_params params;
+    char *error;
+
+    memset(&params, 0, sizeof params);
+    params.spi = htonl(spi);
+    params.esn = esn;
+    params.replay_window = window;
+    error = esp_parse_key(key, &params);
+    ovs_assert(!error);
+    return params;
+}
+
+static void
+from_hex(struct test_pkt *pkt, const char *hex)
+{
+    pkt->len = strlen(hex) / 2;
+    ovs_assert(pkt->len && pkt->len <= sizeof pkt->buf);
+    for (size_t i = 0; i < pkt->len; i++) {
+        bool ok;
+
+        pkt->buf[i] = hexits_value(&hex[2 * i], 2, &ok);
+        ovs_assert(ok);
+    }
+}
+
+static struct esp_header *
+pkt_esp(struct test_pkt *pkt)
+{
+    return ALIGNED_CAST(struct esp_header *, pkt->buf);
+}
+
+static int
+seal(struct esp_sa *sa, struct test_pkt *pkt, const void *payload,
+     size_t payload_len)
+{
+    memcpy(pkt->buf + ESP_PREFIX_LEN, payload, payload_len);
+    pkt->len = ESP_PREFIX_LEN + payload_len + esp_trailer_len(payload_len);
+    ovs_assert(pkt->len <= sizeof pkt->buf);
+    return esp_seal(sa, pkt_esp(pkt), payload_len, IPPROTO_IPIP);
+}
+
+static int
+open_pkt(struct test_pkt *pkt, size_t *payload_len)
+{
+    uint8_t next_hdr;
+    ovs_be32 spi;
+
+    return esp_open(pkt_esp(pkt), pkt->len, &spi, &next_hdr, payload_len);
+}
+
+/* Seals a packet with sequence number 'seq' on 'out' and returns the result
+ * of opening it. */
+static int
+send_seq(struct esp_sa *out, uint64_t seq)
+{
+    struct test_pkt pkt;
+    size_t len;
+
+    esp_sa_set_next_seq(out, seq);
+    ovs_assert(!seal(out, &pkt, "x", 1));
+    return open_pkt(&pkt, &len);
+}
+
+static void
+test_parse_key(void)
+{
+    struct esp_sa_params params;
+    char *error;
+
+    error = esp_parse_key("0x" TEST_KEY, &params);
+    ovs_assert(!error);
+    ovs_assert(params.key_len == 20);
+    ovs_assert(params.key[0] == 0x10 && params.key[19] == 0xbe);
+
+    error = esp_parse_key(TEST_KEY TEST_KEY, &params);
+    ovs_assert(error);
+    free(error);
+
+    error = esp_parse_key("101112131415161718191a1b1c1d1e1fcafebabz", &params);
+    ovs_assert(error);
+    free(error);
+
+    error = esp_parse_key("", &params);
+    ovs_assert(error);
+    free(error);
+}
+
+static void
+test_known_answer(void)
+{
+    struct esp_sa_params params = make_params(0x1000, TEST_KEY, false, 64);
+    struct esp_sa *sa = esp_sa_create(&params);
+    struct test_pkt pkt;
+    uint8_t next_hdr;
+    size_t len;
+    ovs_be32 spi;
+
+    ovs_assert(!esp_sad_insert(sa));
+
+    /* Corrupted ICV, payload, or authenticated sequence number.  These must
+     * not advance the anti-replay window. */
+    from_hex(&pkt, kat_seq7);
+    pkt.buf[pkt.len - 1] ^= 1;
+    ovs_assert(open_pkt(&pkt, &len) == EBADMSG);
+    from_hex(&pkt, kat_seq7);
+    pkt.buf[ESP_PREFIX_LEN] ^= 1;
+    ovs_assert(open_pkt(&pkt, &len) == EBADMSG);
+    from_hex(&pkt, kat_seq7);
+    pkt.buf[7] = 8;
+    ovs_assert(open_pkt(&pkt, &len) == EBADMSG);
+
+    /* Unknown SPI and truncated packet. */
+    from_hex(&pkt, kat_seq7);
+    pkt.buf[3] = 0x99;
+    ovs_assert(open_pkt(&pkt, &len) == ENOENT);
+    from_hex(&pkt, kat_seq7);
+    pkt.len = ESP_PREFIX_LEN + ESP_ICV_LEN + 1;
+    ovs_assert(open_pkt(&pkt, &len) == EINVAL);
+
+    from_hex(&pkt, kat_seq7);
+    ovs_assert(!esp_open(pkt_esp(&pkt), pkt.len, &spi, &next_hdr, &len));
+    ovs_assert(spi == htonl(0x1000));
+    ovs_assert(next_hdr == 4);
+    ovs_assert(len == strlen(TEST_PAYLOAD));
+    ovs_assert(!memcmp(pkt.buf + ESP_PREFIX_LEN, TEST_PAYLOAD, len));
+
+    /* Replayed. */
+    from_hex(&pkt, kat_seq7);
+    ovs_assert(open_pkt(&pkt, &len) == EALREADY);
+
+    esp_sad_remove(sa);
+    esp_sa_destroy(sa);
+}
+
+static void
+test_round_trip(void)
+{
+    static const char *keys[] = {
+        TEST_KEY,
+        "000102030405060708090a0b0c0d0e0f1011121314151617deadbeef",
+        ("000102030405060708090a0b0c0d0e0f"
+         "101112131415161718191a1b1c1d1e1fdeadbeef"),
+    };
+
+    for (size_t k = 0; k < ARRAY_SIZE(keys); k++) {
+        for (int esn = 0; esn < 2; esn++) {
+            struct esp_sa_params params = make_params(0x2000, keys[k],
+                                                      esn, 64);
+            struct esp_sa *out = esp_sa_create(&params);
+            struct esp_sa *in = esp_sa_create(&params);
+            uint8_t payload[100];
+
+            ovs_assert(out && in);
+            ovs_assert(!esp_sad_insert(in));
+            for (size_t i = 0; i < sizeof payload; i++) {
+                payload[i] = i * 7;
+            }
+
+            for (size_t n = 0; n <= sizeof payload; n++) {
+                struct test_pkt pkt;
+                size_t len;
+
+                ovs_assert(!seal(out, &pkt, payload, n));
+                ovs_assert((pkt.len - ESP_PREFIX_LEN - ESP_ICV_LEN) % 4 == 0);
+                if (n >= 16) {
+                    ovs_assert(memcmp(pkt.buf + ESP_PREFIX_LEN, payload, n));
+                }
+                ovs_assert(!open_pkt(&pkt, &len));
+                ovs_assert(len == n);
+                ovs_assert(!memcmp(pkt.buf + ESP_PREFIX_LEN, payload, n));
+            }
+
+            esp_sad_remove(in);
+            esp_sa_destroy(in);
+            esp_sa_destroy(out);
+        }
+    }
+}
+
+static void
+test_replay_window(void)
+{
+    struct esp_sa_params params = make_params(0x3000, TEST_KEY, false, 64);
+    struct esp_sa *out = esp_sa_create(&params);
+    struct esp_sa *in = esp_sa_create(&params);
+
+    ovs_assert(!esp_sad_insert(in));
+
+    ovs_assert(send_seq(out, 0) == EALREADY);   /* 0 is never valid. */
+    ovs_assert(!send_seq(out, 200));
+    ovs_assert(!send_seq(out, 137));            /* Oldest in window. */
+    ovs_assert(send_seq(out, 136) == EALREADY); /* Too old. */
+    ovs_assert(send_seq(out, 137) == EALREADY); /* Duplicate. */
+    ovs_assert(!send_seq(out, 199));
+    ovs_assert(!send_seq(out, 263));            /* Slides by 63. */
+    ovs_assert(send_seq(out, 199) == EALREADY);
+    ovs_assert(!send_seq(out, 200 + 64));
+    ovs_assert(!send_seq(out, 10000));          /* Slides beyond the window. */
+    ovs_assert(!send_seq(out, 9999));
+
+    /* Without ESN the sequence number must not cycle. */
+    ovs_assert(!send_seq(out, UINT32_MAX));
+    struct test_pkt pkt;
+    ovs_assert(seal(out, &pkt, "x", 1) == ERANGE);
+
+    esp_sad_remove(in);
+    esp_sa_destroy(in);
+    esp_sa_destroy(out);
+
+    /* Anti-replay disabled. */
+    params = make_params(0x3001, TEST_KEY, false, 0);
+    out = esp_sa_create(&params);
+    in = esp_sa_create(&params);
+    ovs_assert(!esp_sad_insert(in));
+    ovs_assert(!send_seq(out, 5));
+    ovs_assert(!send_seq(out, 5));
+    esp_sad_remove(in);
+    esp_sa_destroy(in);
+    esp_sa_destroy(out);
+}
+
+static void
+test_esn(void)
+{
+    struct esp_sa_params params = make_params(0x1001, TEST_KEY, true, 64);
+    struct esp_sa *out = esp_sa_create(&params);
+    struct esp_sa *in = esp_sa_create(&params);
+    const uint64_t wrap = UINT64_C(1) << 32;
+    struct test_pkt pkt;
+    size_t len;
+
+    ovs_assert(!esp_sad_insert(in));
+
+    /* A receiver cannot follow a jump of nearly 2**32. */
+    ovs_assert(send_seq(out, wrap - 10) == EALREADY);
+
+    ovs_assert(!send_seq(out, wrap / 2));
+    ovs_assert(!send_seq(out, wrap - 16));
+    ovs_assert(!send_seq(out, wrap - 1));
+    ovs_assert(!send_seq(out, wrap + 1));       /* Low 32 bits wrap. */
+    ovs_assert(!send_seq(out, wrap - 2));       /* Still in the window. */
+    ovs_assert(send_seq(out, wrap - 1) == EALREADY);
+
+    /* The known-answer packet has sequence number 2**32 + 5. */
+    from_hex(&pkt, kat_esn);
+    ovs_assert(!open_pkt(&pkt, &len));
+    ovs_assert(len == strlen(TEST_PAYLOAD));
+    ovs_assert(!memcmp(pkt.buf + ESP_PREFIX_LEN, TEST_PAYLOAD, len));
+
+    /* Too far ahead: authenticated as 2**32 + 7, which fails. */
+    ovs_assert(send_seq(out, 3 * wrap + 7) == EBADMSG);
+
+    esp_sad_remove(in);
+    esp_sa_destroy(in);
+    esp_sa_destroy(out);
+
+    /* ESN requires an anti-replay window. */
+    params.replay_window = 0;
+    ovs_assert(!esp_sa_create(&params));
+}
+
+static void
+test_sad(void)
+{
+    struct esp_sa_params p1 = make_params(0x4000, TEST_KEY, false, 64);
+    struct esp_sa_params p2 = make_params(0x4000, TEST_KEY, true, 64);
+    struct esp_sa_params p3 = make_params(0x4000, TEST_KEY, false, 0);
+    const ovs_be32 spi = htonl(0x4000);
+    struct esp_sa *sa1, *sa2, *sa3;
+    struct in6_addr peer;
+
+    /* An outbound SA with the same SPI, for a peer. */
+    in6_addr_set_mapped_ipv4(&peer, htonl(0x01010101));
+    p3.dst = peer;
+
+    sa1 = esp_sa_create(&p1);
+    sa2 = esp_sa_create(&p2);
+    sa3 = esp_sa_create(&p3);
+
+    ovs_assert(!esp_sa_params_equal(&p1, &p2));
+    ovs_assert(!esp_sad_lookup(spi, &in6addr_any));
+    ovs_assert(!esp_sad_insert(sa1));
+    ovs_assert(esp_sad_insert(sa2) == EEXIST);
+    ovs_assert(!esp_sad_insert(sa3));
+    ovs_assert(esp_sad_lookup(spi, &in6addr_any) == sa1);
+    ovs_assert(esp_sad_lookup(spi, &peer) == sa3);
+    esp_sad_replace(sa1, sa2);
+    ovs_assert(esp_sad_lookup(spi, &in6addr_any) == sa2);
+    esp_sad_remove(sa2);
+    ovs_assert(!esp_sad_lookup(spi, &in6addr_any));
+    ovs_assert(esp_sad_lookup(spi, &peer) == sa3);
+    esp_sad_remove(sa3);
+    ovs_assert(!esp_sad_lookup(spi, &peer));
+
+    esp_sa_destroy(sa1);
+    esp_sa_destroy(sa2);
+    esp_sa_destroy(sa3);
+}
+
+/* A manually keyed SA restarts its sequence numbers with the same key when
+ * ovs-vswitchd restarts, so its IVs must not depend on OVS's seeded generator:
+ * two SAs from the same seed must still use different IVs. */
+static void
+test_iv_salt(void)
+{
+    struct esp_sa_params params = make_params(0x1000, TEST_KEY, false, 0);
+    uint8_t ivs[2][ESP_IV_LEN];
+
+    for (size_t i = 0; i < ARRAY_SIZE(ivs); i++) {
+        struct test_pkt pkt;
+        struct esp_sa *sa;
+
+        random_set_seed(1);
+        sa = esp_sa_create(&params);
+        ovs_assert(!seal(sa, &pkt, TEST_PAYLOAD, strlen(TEST_PAYLOAD)));
+        memcpy(ivs[i], pkt.buf + ESP_HEADER_LEN, ESP_IV_LEN);
+        esp_sa_destroy(sa);
+    }
+    ovs_assert(memcmp(ivs[0], ivs[1], ESP_IV_LEN));
+}
+
+static void
+test_esp_main(int argc OVS_UNUSED, char *argv[] OVS_UNUSED)
+{
+    if (!esp_is_supported()) {
+        exit(77);
+    }
+
+    test_parse_key();
+    test_known_answer();
+    test_round_trip();
+    test_replay_window();
+    test_esn();
+    test_sad();
+    test_iv_salt();
+}
+
+OVSTEST_REGISTER("test-esp", test_esp_main);
-- 
2.43.0

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to