From: Hugo Landau Date: Fri, 3 Nov 2023 16:27:35 +0000 (+0000) Subject: QUIC LCIDM: Add LCIDM X-Git-Tag: openssl-3.3.0-alpha1~526 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8489a0a1f246e2f8bb1d98fc550b7a0fd341ef51;p=thirdparty%2Fopenssl.git QUIC LCIDM: Add LCIDM Reviewed-by: Neil Horman Reviewed-by: Matt Caswell (Merged from https://github.com/openssl/openssl/pull/22673) --- diff --git a/include/internal/quic_lcidm.h b/include/internal/quic_lcidm.h new file mode 100644 index 00000000000..a9f59bdf196 --- /dev/null +++ b/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 diff --git a/ssl/quic/build.info b/ssl/quic/build.info index b8d871848c1..e8016dae91f 100644 --- a/ssl/quic/build.info +++ b/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 diff --git a/ssl/quic/quic_lcidm.c b/ssl/quic/quic_lcidm.c new file mode 100644 index 00000000000..8427eb4d415 --- /dev/null +++ b/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 +#include +#include + +/* + * 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; +}