Browse Source

QUIC LCIDM: Add LCIDM

Reviewed-by: Neil Horman <nhorman@openssl.org>
Reviewed-by: Matt Caswell <matt@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/22673)
Hugo Landau 1 year ago
parent
commit
8489a0a1f2
3 changed files with 669 additions and 0 deletions
  1. 214 0
      include/internal/quic_lcidm.h
  2. 1 0
      ssl/quic/build.info
  3. 454 0
      ssl/quic/quic_lcidm.c

+ 214 - 0
include/internal/quic_lcidm.h

@@ -0,0 +1,214 @@
+/*
+* Copyright 2023 The OpenSSL Project Authors. All Rights Reserved.
+*
+* Licensed under the Apache License 2.0 (the "License").  You may not use
+* this file except in compliance with the License.  You can obtain a copy
+* in the file LICENSE in the source distribution or at
+* https://www.openssl.org/source/license.html
+*/
+
+#ifndef OSSL_INTERNAL_QUIC_LCIDM_H
+# define OSSL_INTERNAL_QUIC_LCIDM_H
+# pragma once
+
+# include "internal/e_os.h"
+# include "internal/time.h"
+# include "internal/quic_types.h"
+# include "internal/quic_wire.h"
+
+# ifndef OPENSSL_NO_QUIC
+
+/*
+ * QUIC Local Connection ID Manager
+ * ================================
+ *
+ * This manages connection IDs for the RX side, which is to say that it issues
+ * local CIDs (LCIDs) to a peer which that peer can then use to address us via a
+ * packet DCID. This is as opposed to CID management for the TX side, which
+ * determines which CIDs we use to transmit based on remote CIDs (RCIDs) the
+ * peer sent to us.
+ *
+ * An opaque pointer can be associated with each LCID. Pointer identity
+ * (equality) is used to distinguish distinct connections.
+ *
+ * LCIDs fall into three categories:
+ *
+ *   1. A client's Initial ODCID                       (1)
+ *   2. A server's Initial SCID                        (1)
+ *   3. A CID issued via a NEW_CONNECTION_ID frame     (n)
+ *   4. A server's Retry SCID                          (0..1)
+ *
+ * (1) is enrolled using ossl_quic_lcidm_enrol_odcid() and retired by the time
+ * of handshake completion at the latest. It is needed in case the first
+ * response packet from a server is lost and the client keeps using its Initial
+ * ODCID. There is never more than one of these, and no sequence number is
+ * associated with this temporary LCID.
+ *
+ * (2) is created when a server responds to a new connection request, and is
+ * generated by the server as the preferred DCID for traffic directed towards
+ * it. A client should switch to using this as soon as it receives a valid
+ * packet from the server. This LCID has a sequence number of 0.
+ *
+ * (3) is created when we issue a NEW_CONNECTION_ID frame. Arbitrarily many of
+ * these can exist.
+ *
+ * (4) is a special case. When a server issues a retry it generates a new SCID
+ * much as it does for (2). However since retries are supposed to be stateless,
+ * we don't actually register it as an LCID. When the client subsequently
+ * replies with an Initial packet with token in response to the Retry, the
+ * server will handle this as a new connection attempt due to not recognising
+ * the DCID, which is what we want anyway. (The Retry SCID is subsequently
+ * validated as matching the new Initial ODCID via attestation in the encrypted
+ * contents of the opaque retry token.) Thus, the LCIDM is not actually involved
+ * at all here.
+ *
+ * Retirement is as follows:
+ *
+ * (1) is retired automatically when we know it won't be needed anymore. This is
+ * when the handshake is completed at the latest, and could potentially be
+ * earlier.
+ *
+ * Both (2) and (3) are retired normally via RETIRE_CONNECTION_ID frames, as it
+ * has a sequence number of 0.
+ */
+typedef struct quic_lcidm_st QUIC_LCIDM;
+
+/*
+ * Creates a new LCIDM. lcid_len is the length to use for LCIDs in bytes, which
+ * may be zero.
+ *
+ * Returns NULL on failure.
+ */
+QUIC_LCIDM *ossl_quic_lcidm_new(OSSL_LIB_CTX *libctx, size_t lcid_len);
+
+/* Frees a LCIDM. */
+void ossl_quic_lcidm_free(QUIC_LCIDM *lcidm);
+
+/* Gets the local CID length this LCIDM was configured to use. */
+size_t ossl_quic_lcidm_get_lcid_len(const QUIC_LCIDM *lcidm);
+
+/*
+ * Determines the number of active LCIDs (i.e,. LCIDs which can be used for
+ * reception) currently associated with the given opaque pointer.
+ */
+size_t ossl_quic_lcidm_get_num_active_lcid(const QUIC_LCIDM *lcidm,
+                                           void *opaque);
+
+/*
+ * Enrol an Initial ODCID sent by the peer. This is the DCID in the first
+ * Initial packet sent by a client. When we receive a client's first Initial
+ * packet, we immediately respond with our own SCID (generated using
+ * ossl_quic_lcidm_generate_initial) to tell the client to switch to using that,
+ * so ideally the ODCID will only be used for a single packet. However since
+ * that response might be lost, we also need to accept additional packets using
+ * the ODCID and need to make sure they get routed to the same connection and
+ * not interpreted as another new connection attempt. Thus before the CID
+ * switchover is confirmed, we also have to handle incoming packets addressed to
+ * the ODCID. This function is used to temporarily enroll the ODCID for a
+ * connection. Such a LCID is considered to have a sequence number of
+ * LCIDM_ODCID_SEQ_NUM internally for our purposes.
+ *
+ * Note that this is the *only* circumstance where we recognise an LCID we did
+ * not generate ourselves.
+ *
+ * This function may only be called once for a given connection.
+ * Returns 1 on success or 0 on failure.
+ */
+int ossl_quic_lcidm_enrol_odcid(QUIC_LCIDM *lcidm, void *opaque,
+                                const QUIC_CONN_ID *initial_odcid);
+
+/*
+ * Retire a previously enrolled ODCID for a connection. This is generally done
+ * when we know the peer won't be using it any more (when the handshake is
+ * completed at the absolute latest, possibly earlier).
+ *
+ * Returns 1 if there was an enrolled ODCID which was retired and 0 if there was
+ * not or on other failure.
+ */
+int ossl_quic_lcidm_retire_odcid(QUIC_LCIDM *lcidm, void *opaque);
+
+/*
+ * Create the first LCID for a given opaque pointer. The generated LCID is
+ * written to *initial_lcid and associated with the given opaque pointer.
+ *
+ * After this function returns successfully, the caller can for example
+ * register the new LCID with a DEMUX.
+ *
+ * May not be called more than once for a given opaque pointer value.
+ */
+int ossl_quic_lcidm_generate_initial(QUIC_LCIDM *lcidm,
+                                     void *opaque,
+                                     QUIC_CONN_ID *initial_lcid);
+
+/*
+ * Create a subsequent LCID for a given opaque pointer. The information needed
+ * for a NEW_CONN_ID frame informing the peer of the new LCID, including the
+ * LCID itself, is written to *ncid_frame.
+ *
+ * ncid_frame->stateless_reset is not initialised and the caller is responsible
+ * for setting it.
+ *
+ * After this function returns successfully, the caller can for example
+ * register the new LCID with a DEMUX and queue the NEW_CONN_ID frame.
+ */
+int ossl_quic_lcidm_generate(QUIC_LCIDM *lcidm,
+                             void *opaque,
+                             OSSL_QUIC_FRAME_NEW_CONN_ID *ncid_frame);
+
+/*
+ * Retire up to one LCID for a given opaque pointer value. Called repeatedly to
+ * handle a RETIRE_CONN_ID frame.
+ *
+ * If containing_pkt_dcid is non-NULL, this function enforces the requirement
+ * that a CID not be retired by a packet using that CID as the DCID. If
+ * containing_pkt_dcid is NULL, this check is skipped.
+ *
+ * If a LCID is retired as a result of a call to this function, the LCID which
+ * was retired is written to *retired_lcid, the sequence number of the LCID is
+ * written to *retired_seq_num and *did_retire is set to 1. Otherwise,
+ * *did_retire is set to 0. This enables a caller to e.g. unregister the LCID
+ * from a DEMUX. A caller should call this function repeatedly until the
+ * function returns with *did_retire set to 0.
+ *
+ * This call is likely to cause the value returned by
+ * ossl_quic_lcidm_get_num_active_lcid() to go down. A caller may wish to call
+ * ossl_quic_lcidm_generate() repeatedly to bring the number of active LCIDs
+ * back up to some threshold in response after calling this function.
+ *
+ * Returns 1 on success and 0 on failure. If arguments are valid but zero LCIDs
+ * are retired, this is considered a success condition.
+ */
+int ossl_quic_lcidm_retire(QUIC_LCIDM *lcidm,
+                           void *opaque,
+                           uint64_t retire_prior_to,
+                           const QUIC_CONN_ID *containing_pkt_dcid,
+                           QUIC_CONN_ID *retired_lcid,
+                           uint64_t *retired_seq_num,
+                           int *did_retire);
+
+/*
+ * Cull all LCIDM state relating to a given opaque pointer value. This is useful
+ * if connection state is spontaneously freed. The caller is responsible for
+ * e.g. DEMUX state updates.
+ */
+int ossl_quic_lcidm_cull(QUIC_LCIDM *lcidm, void *opaque);
+
+/*
+ * Lookup a LCID. If the LCID is found, writes the associated opaque pointer to
+ * *opaque and the associated sequence number to *seq_num. Returns 1 on success
+ * and 0 if an entry is not found. An output argument may be set to NULL if its
+ * value is not required.
+ *
+ * If the LCID is for an Initial ODCID, *seq_num is set to
+ * LCIDM_ODCID_SEQ_NUM.
+ */
+#define LCIDM_ODCID_SEQ_NUM     UINT64_MAX
+
+int ossl_quic_lcidm_lookup(QUIC_LCIDM *lcidm,
+                           const QUIC_CONN_ID *lcid,
+                           uint64_t *seq_num,
+                           void **opaque);
+
+# endif
+
+#endif

+ 1 - 0
ssl/quic/build.info

@@ -15,3 +15,4 @@ SOURCE[$LIBSSL]=quic_tls.c
 SOURCE[$LIBSSL]=quic_thread_assist.c
 SOURCE[$LIBSSL]=quic_trace.c
 SOURCE[$LIBSSL]=quic_srtm.c quic_srt_gen.c
+SOURCE[$LIBSSL]=quic_lcidm.c

+ 454 - 0
ssl/quic/quic_lcidm.c

@@ -0,0 +1,454 @@
+/*
+ * Copyright 2023 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include "internal/quic_lcidm.h"
+#include "internal/quic_types.h"
+#include "internal/quic_vlint.h"
+#include "internal/common.h"
+#include <openssl/lhash.h>
+#include <openssl/rand.h>
+#include <openssl/err.h>
+
+/*
+ * QUIC Local Connection ID Manager
+ * ================================
+ */
+
+typedef struct quic_lcidm_conn_st QUIC_LCIDM_CONN;
+
+enum {
+    LCID_TYPE_ODCID,        /* This LCID is the ODCID from the peer */
+    LCID_TYPE_INITIAL,      /* This is our Initial SCID */
+    LCID_TYPE_NCID          /* This LCID was issued via a NCID frame */
+};
+
+typedef struct quic_lcid_st {
+    QUIC_CONN_ID                cid;
+    uint64_t                    seq_num;
+
+    /* Back-pointer to the owning QUIC_LCIDM_CONN structure. */
+    QUIC_LCIDM_CONN             *conn;
+
+    /* LCID_TYPE_* */
+    unsigned int                type                : 2;
+} QUIC_LCID;
+
+DEFINE_LHASH_OF_EX(QUIC_LCID);
+DEFINE_LHASH_OF_EX(QUIC_LCIDM_CONN);
+
+struct quic_lcidm_conn_st {
+    size_t              num_active_lcid;
+    LHASH_OF(QUIC_LCID) *lcids;
+    void                *opaque;
+    QUIC_LCID           *odcid_lcid;
+    uint64_t            next_seq_num;
+
+    /* Have we enrolled an ODCID? */
+    unsigned int        done_odcid          : 1;
+};
+
+struct quic_lcidm_st {
+    OSSL_LIB_CTX                *libctx;
+    LHASH_OF(QUIC_LCID)         *lcids; /* (QUIC_CONN_ID) -> (QUIC_LCID *)  */
+    LHASH_OF(QUIC_LCIDM_CONN)   *conns; /* (void *opaque) -> (QUIC_LCIDM_CONN *) */
+    size_t                      lcid_len; /* Length in bytes for all LCIDs */
+};
+
+static unsigned long bin_hash(const unsigned char *buf, size_t buf_len)
+{
+    unsigned long hash = 0;
+    size_t i;
+
+    for (i = 0; i < buf_len; ++i)
+        hash ^= ((unsigned long)buf[i]) << (8 * (i % sizeof(unsigned long)));
+
+    return hash;
+}
+
+static unsigned long lcid_hash(const QUIC_LCID *lcid)
+{
+    return bin_hash(lcid->cid.id, lcid->cid.id_len);
+}
+
+static int lcid_comp(const QUIC_LCID *a, const QUIC_LCID *b)
+{
+    return !ossl_quic_conn_id_eq(&a->cid, &b->cid);
+}
+
+static unsigned long lcidm_conn_hash(const QUIC_LCIDM_CONN *conn)
+{
+    return (unsigned long)(uintptr_t)conn->opaque;
+}
+
+static int lcidm_conn_comp(const QUIC_LCIDM_CONN *a, const QUIC_LCIDM_CONN *b)
+{
+    return a->opaque != b->opaque;
+}
+
+QUIC_LCIDM *ossl_quic_lcidm_new(OSSL_LIB_CTX *libctx, size_t lcid_len)
+{
+    QUIC_LCIDM *lcidm = NULL;
+
+    if (lcid_len > QUIC_MAX_CONN_ID_LEN)
+        goto err;
+
+    if ((lcidm = OPENSSL_zalloc(sizeof(*lcidm))) == NULL)
+        goto err;
+
+    if ((lcidm->lcids = lh_QUIC_LCID_new(lcid_hash, lcid_comp)) == NULL)
+        goto err;
+
+    if ((lcidm->conns = lh_QUIC_LCIDM_CONN_new(lcidm_conn_hash,
+                                               lcidm_conn_comp)) == NULL)
+        goto err;
+
+    lcidm->libctx   = libctx;
+    lcidm->lcid_len = lcid_len;
+    return lcidm;
+
+err:
+    if (lcidm != NULL) {
+        lh_QUIC_LCID_free(lcidm->lcids);
+        lh_QUIC_LCIDM_CONN_free(lcidm->conns);
+        OPENSSL_free(lcidm);
+    }
+    return NULL;
+}
+
+static void lcidm_delete_conn(QUIC_LCIDM *lcidm, QUIC_LCIDM_CONN *conn);
+
+static void lcidm_delete_conn_(QUIC_LCIDM_CONN *conn, void *arg)
+{
+    lcidm_delete_conn((QUIC_LCIDM *)arg, conn);
+}
+
+void ossl_quic_lcidm_free(QUIC_LCIDM *lcidm)
+{
+    if (lcidm == NULL)
+        return;
+
+    lh_QUIC_LCIDM_CONN_doall_arg(lcidm->conns, lcidm_delete_conn_, lcidm);
+
+    lh_QUIC_LCID_free(lcidm->lcids);
+    lh_QUIC_LCIDM_CONN_free(lcidm->conns);
+    OPENSSL_free(lcidm);
+}
+
+static QUIC_LCID *lcidm_get_lcid(const QUIC_LCIDM *lcidm, const QUIC_CONN_ID *lcid)
+{
+    QUIC_LCID key;
+
+    key.cid = *lcid;
+
+    return lh_QUIC_LCID_retrieve(lcidm->lcids, &key);
+}
+
+static QUIC_LCIDM_CONN *lcidm_get_conn(const QUIC_LCIDM *lcidm, void *opaque)
+{
+    QUIC_LCIDM_CONN key;
+
+    key.opaque = opaque;
+
+    return lh_QUIC_LCIDM_CONN_retrieve(lcidm->conns, &key);
+}
+
+static QUIC_LCIDM_CONN *lcidm_upsert_conn(const QUIC_LCIDM *lcidm, void *opaque)
+{
+    QUIC_LCIDM_CONN *conn = lcidm_get_conn(lcidm, opaque);
+
+    if (conn != NULL)
+        return conn;
+
+    if ((conn = OPENSSL_zalloc(sizeof(*conn))) == NULL)
+        return NULL;
+
+    if ((conn->lcids = lh_QUIC_LCID_new(lcid_hash, lcid_comp)) == NULL) {
+        OPENSSL_free(conn);
+        return NULL;
+    }
+
+    conn->opaque = opaque;
+    lh_QUIC_LCIDM_CONN_insert(lcidm->conns, conn);
+    return conn;
+}
+
+static void lcidm_delete_conn_lcid(QUIC_LCIDM *lcidm, QUIC_LCID *lcid)
+{
+    lh_QUIC_LCID_delete(lcidm->lcids, lcid);
+    lh_QUIC_LCID_delete(lcid->conn->lcids, lcid);
+    --lcid->conn->num_active_lcid;
+    OPENSSL_free(lcid);
+}
+
+/* doall_arg wrapper */
+static void lcidm_delete_conn_lcid_(QUIC_LCID *lcid, void *arg)
+{
+    lcidm_delete_conn_lcid((QUIC_LCIDM *)arg, lcid);
+}
+
+static void lcidm_delete_conn(QUIC_LCIDM *lcidm, QUIC_LCIDM_CONN *conn)
+{
+    lh_QUIC_LCID_doall_arg(conn->lcids, lcidm_delete_conn_lcid_, lcidm);
+    lh_QUIC_LCIDM_CONN_delete(lcidm->conns, conn);
+    lh_QUIC_LCID_free(conn->lcids);
+    OPENSSL_free(conn);
+}
+
+static QUIC_LCID *lcidm_conn_new_lcid(QUIC_LCIDM *lcidm, QUIC_LCIDM_CONN *conn,
+                                      const QUIC_CONN_ID *lcid)
+{
+    QUIC_LCID *lcid_obj;
+
+    if ((lcid_obj = OPENSSL_zalloc(sizeof(*lcid_obj))) == NULL)
+        return NULL;
+
+    lcid_obj->cid = *lcid;
+    lcid_obj->conn = conn;
+    lh_QUIC_LCID_insert(conn->lcids, lcid_obj);
+    lh_QUIC_LCID_insert(lcidm->lcids, lcid_obj);
+    ++conn->num_active_lcid;
+    return lcid_obj;
+}
+
+size_t ossl_quic_lcidm_get_lcid_len(const QUIC_LCIDM *lcidm)
+{
+    return lcidm->lcid_len;
+}
+
+size_t ossl_quic_lcidm_get_num_active_lcid(const QUIC_LCIDM *lcidm,
+                                           void *opaque)
+{
+    QUIC_LCIDM_CONN *conn;
+
+    conn = lcidm_get_conn(lcidm, opaque);
+    if (conn == NULL)
+        return 0;
+
+    return conn->num_active_lcid;
+}
+
+static int gen_rand_conn_id(OSSL_LIB_CTX *libctx, size_t len, QUIC_CONN_ID *cid)
+{
+    if (len > QUIC_MAX_CONN_ID_LEN)
+        return 0;
+
+    cid->id_len = (unsigned char)len;
+
+    if (RAND_bytes_ex(libctx, cid->id, len, len * 8) != 1) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_RAND_LIB);
+        cid->id_len = 0;
+        return 0;
+    }
+
+    return 1;
+}
+
+static int lcidm_generate_cid(QUIC_LCIDM *lcidm,
+                              QUIC_CONN_ID *cid)
+{
+    return gen_rand_conn_id(lcidm->libctx, lcidm->lcid_len, cid);
+}
+
+static int lcidm_generate(QUIC_LCIDM *lcidm,
+                          void *opaque,
+                          unsigned int type,
+                          QUIC_CONN_ID *lcid_out,
+                          uint64_t *seq_num)
+{
+    QUIC_LCIDM_CONN *conn;
+    QUIC_LCID key, *lcid_obj;
+
+    if ((conn = lcidm_upsert_conn(lcidm, opaque)) == NULL)
+        return 0;
+
+    if ((type == LCID_TYPE_INITIAL && conn->next_seq_num > 0)
+        || conn->next_seq_num > OSSL_QUIC_VLINT_MAX)
+        return 0;
+
+    if (!lcidm_generate_cid(lcidm, lcid_out))
+        return 0;
+
+    key.cid = *lcid_out;
+    if (lh_QUIC_LCID_retrieve(lcidm->lcids, &key) != NULL)
+        return 0;
+
+    if ((lcid_obj = lcidm_conn_new_lcid(lcidm, conn, lcid_out)) == NULL)
+        return 0;
+
+    lcid_obj->seq_num   = conn->next_seq_num;
+    lcid_obj->type      = type;
+
+    if (seq_num != NULL)
+        *seq_num = lcid_obj->seq_num;
+
+    ++conn->next_seq_num;
+    return 1;
+}
+
+int ossl_quic_lcidm_enrol_odcid(QUIC_LCIDM *lcidm,
+                                void *opaque,
+                                const QUIC_CONN_ID *initial_odcid)
+{
+    QUIC_LCIDM_CONN *conn;
+    QUIC_LCID key, *lcid_obj;
+
+    if (initial_odcid == NULL)
+        return 0;
+
+    if ((conn = lcidm_upsert_conn(lcidm, opaque)) == NULL)
+        return 0;
+
+    if (conn->done_odcid)
+        return 0;
+
+    key.cid = *initial_odcid;
+    if (lh_QUIC_LCID_retrieve(lcidm->lcids, &key) != NULL)
+        return 0;
+
+    if ((lcid_obj = lcidm_conn_new_lcid(lcidm, conn, initial_odcid)) == NULL)
+        return 0;
+
+    lcid_obj->seq_num   = LCIDM_ODCID_SEQ_NUM;
+    lcid_obj->type      = LCID_TYPE_ODCID;
+
+    conn->odcid_lcid    = lcid_obj;
+    conn->done_odcid    = 1;
+    return 1;
+}
+
+int ossl_quic_lcidm_generate_initial(QUIC_LCIDM *lcidm,
+                                     void *opaque,
+                                     QUIC_CONN_ID *initial_lcid)
+{
+    return lcidm_generate(lcidm, opaque, LCID_TYPE_INITIAL,
+                          initial_lcid, NULL);
+}
+
+int ossl_quic_lcidm_generate(QUIC_LCIDM *lcidm,
+                             void *opaque,
+                             OSSL_QUIC_FRAME_NEW_CONN_ID *ncid_frame)
+{
+    ncid_frame->seq_num         = 0;
+    ncid_frame->retire_prior_to = 0;
+
+    return lcidm_generate(lcidm, opaque, LCID_TYPE_NCID,
+                          &ncid_frame->conn_id,
+                          &ncid_frame->seq_num);
+}
+
+int ossl_quic_lcidm_retire_odcid(QUIC_LCIDM *lcidm, void *opaque)
+{
+    QUIC_LCIDM_CONN *conn;
+
+    if ((conn = lcidm_upsert_conn(lcidm, opaque)) == NULL)
+        return 0;
+
+    if (conn->odcid_lcid == NULL)
+        return 0;
+
+    lcidm_delete_conn_lcid(lcidm, conn->odcid_lcid);
+    conn->odcid_lcid = NULL;
+    return 1;
+}
+
+struct retire_args {
+    QUIC_LCID           *earliest_seq_num_lcid;
+    uint64_t            earliest_seq_num, retire_prior_to;
+};
+
+static void retire_for_conn(QUIC_LCID *lcid, void *arg)
+{
+    struct retire_args *args = arg;
+
+    /* ODCID LCID cannot be retired via this API */
+    if (lcid->type == LCID_TYPE_ODCID
+        || lcid->seq_num >= args->retire_prior_to)
+        return;
+
+    if (lcid->seq_num < args->earliest_seq_num) {
+        args->earliest_seq_num = lcid->seq_num;
+        args->earliest_seq_num_lcid = lcid;
+    }
+}
+
+int ossl_quic_lcidm_retire(QUIC_LCIDM *lcidm,
+                           void *opaque,
+                           uint64_t retire_prior_to,
+                           const QUIC_CONN_ID *containing_pkt_dcid,
+                           QUIC_CONN_ID *retired_lcid,
+                           uint64_t *retired_seq_num,
+                           int *did_retire)
+{
+    QUIC_LCIDM_CONN key, *conn;
+    struct retire_args args = {0};
+
+    key.opaque = opaque;
+
+    if (did_retire == NULL)
+        return 0;
+
+    *did_retire = 0;
+    if ((conn = lh_QUIC_LCIDM_CONN_retrieve(lcidm->conns, &key)) == NULL)
+        return 1;
+
+    args.retire_prior_to    = retire_prior_to;
+    args.earliest_seq_num   = UINT64_MAX;
+    lh_QUIC_LCID_doall_arg(conn->lcids, retire_for_conn, &args);
+    if (args.earliest_seq_num_lcid == NULL)
+        return 1;
+
+    if (containing_pkt_dcid != NULL
+        && ossl_quic_conn_id_eq(&args.earliest_seq_num_lcid->cid,
+                                containing_pkt_dcid))
+        return 0;
+
+    *did_retire = 1;
+    if (retired_lcid != NULL)
+        *retired_lcid = args.earliest_seq_num_lcid->cid;
+    if (retired_seq_num != NULL)
+        *retired_seq_num = args.earliest_seq_num_lcid->seq_num;
+
+    lcidm_delete_conn_lcid(lcidm, args.earliest_seq_num_lcid);
+    return 1;
+}
+
+int ossl_quic_lcidm_cull(QUIC_LCIDM *lcidm, void *opaque)
+{
+    QUIC_LCIDM_CONN key, *conn;
+
+    key.opaque = opaque;
+
+    if ((conn = lh_QUIC_LCIDM_CONN_retrieve(lcidm->conns, &key)) == NULL)
+        return 0;
+
+    lcidm_delete_conn(lcidm, conn);
+    return 1;
+}
+
+int ossl_quic_lcidm_lookup(QUIC_LCIDM *lcidm,
+                           const QUIC_CONN_ID *lcid,
+                           uint64_t *seq_num,
+                           void **opaque)
+{
+    QUIC_LCID *lcid_obj;
+
+    if (lcid == NULL)
+        return 0;
+
+    if ((lcid_obj = lcidm_get_lcid(lcidm, lcid)) == NULL)
+        return 0;
+
+    if (seq_num != NULL)
+        *seq_num        = lcid_obj->seq_num;
+
+    if (opaque != NULL)
+        *opaque         = lcid_obj->conn->opaque;
+
+    return 1;
+}