Commit 94c22bfce0 for bind

commit 94c22bfce04302b3c6f43798a00c0bc4e0fdd179
Author: Aydın Mercan <aydin@isc.org>
Date:   Thu Aug 27 12:02:05 2026 +0300

    Initial definition of QUIC connection state machine

    Add a minimal definition of the `isc_quic_conn_t` state machine for
    usage in other places (e.g. the QUIC router) before having the required
    functionality.

diff --git a/lib/isc/include/isc/quic.h b/lib/isc/include/isc/quic.h
index a0af5b8000..7c45baecdc 100644
--- a/lib/isc/include/isc/quic.h
+++ b/lib/isc/include/isc/quic.h
@@ -17,6 +17,8 @@
 #include <isc/region.h>
 #include <isc/result.h>
 #include <isc/tid.h>
+#include <isc/time.h>
+#include <isc/tls.h>
 #include <isc/types.h>

 /*! \file isc/quic.h */
@@ -52,23 +54,6 @@
  */
 typedef struct isc_quic_router isc_quic_router_t;

-/**
- * \brief
- * Callbacks the router uses to keep a connection alive while it is reachable
- * through the router or has been handed out by a lookup.
- *
- * `conn_ref` must acquire a reference on `conn`; `conn_unref` must release one.
- * They allow the router to hand out a retained connection pointer without
- * racing a concurrent teardown.
- *
- * \warning
- * `conn_unref` may be invoked from an RCU worker thread, because the router
- * releases its own reference from a deferred-reclamation (call_rcu) callback.
- * It must therefore be safe to call from any thread.
- */
-typedef void (*isc_quic_conn_ref_t)(void *conn);
-typedef void (*isc_quic_conn_unref_t)(void *conn);
-
 typedef enum isc_quic_version {
 	/** Invalid QUIC version */
 	ISC_QUIC_VERSION_INVALID = 0,
@@ -85,11 +70,56 @@ typedef enum isc_quic_version {
 	ISC_QUIC_VERSION__MAX = 4,
 } isc_quic_version_t;

+/**
+ * \brief
+ * User-specified callbacks
+ *
+ * All callbacks are optional.
+ */
+struct isc_quic_conn_callbacks {};
+
+/**
+ * \brief
+ * Connection independent QUIC connection options of an endpoint.
+ */
+struct isc_quic_conn_options {
+	/**
+	 * \brief
+	 * TLS context used by the QUIC connection.
+	 */
+	isc_tlsctx_t *tlsctx;
+
+	/**
+	 * \brief
+	 * ALPN value of the TLS handshake.
+	 *
+	 * \warning
+	 * Must be less than 7-bytes, might be increased in the future.
+	 */
+	isc_constregion_t alpn;
+
+	/**
+	 * \brief
+	 * Handshake timeout of the state machine
+	 *
+	 * Must not be `isc_quic_timestamp_invalid`. Zero value disables
+	 * timeout.
+	 */
+	isc_nanosecs_t handshake_timeout;
+
+	/**
+	 * \brief
+	 * Idle timeout of the state machine
+	 *
+	 * Must not be `isc_quic_timestamp_invalid`. Zero value disables
+	 * timeout.
+	 */
+	isc_nanosecs_t idle_timeout;
+};
+
 void
 isc_quic_router_create(isc_mem_t *mctx, size_t cidlen,
-		       isc_quic_conn_ref_t   conn_ref,
-		       isc_quic_conn_unref_t conn_unref,
-		       isc_quic_router_t   **routerp);
+		       isc_quic_router_t **routerp);
 /**<
  * \brief
  * Create a new QUIC CID router.
@@ -137,7 +167,7 @@ isc_quic_router_add_cid(isc_quic_router_t *router, isc_constregion_t cid,

 isc_result_t
 isc_quic_router_get_cid(isc_quic_router_t *router, isc_constregion_t cid,
-			isc_tid_t *tidp, void **connp);
+			isc_tid_t *tidp, isc_quic_conn_t **connp);
 /**<
  * \brief
  * Get the connection associated with a given CID and its thread if `tidp` is
@@ -202,7 +232,7 @@ isc_result_t
 isc_quic_router_get_stateless_reset(
 	isc_quic_router_t *router,
 	const uint8_t token[const restrict ISC_QUIC_STATELESS_TOKEN_LENGTH],
-	isc_tid_t *tidp, void **connp);
+	isc_tid_t *tidp, isc_quic_conn_t **connp);
 /**<
  * \brief
  * Get the connection associated with the given stateless reset token and its
@@ -243,7 +273,7 @@ isc_quic_router_handle_packet(isc_quic_router_t	 *router,
 			      isc_quic_version_t *versionp,
 			      isc_constregion_t	 *dcidp,
 			      isc_constregion_t *scidp, isc_tid_t *tidp,
-			      void **connp);
+			      isc_quic_conn_t **connp);
 /**<
  * \brief
  * Extracts the associated value and relevant information of a given QUIC
@@ -294,3 +324,64 @@ isc_quic_router_handle_packet(isc_quic_router_t	 *router,
  * Unlike ISC_R_UNEXPECTED, the caller MUST NOT send a stateless reset in
  * response (RFC9000, Section 10.3).
  */
+
+isc_result_t
+isc_quic_conn_client_create(isc_mem_t *mctx, isc_quic_router_t *router,
+			    const isc_quic_conn_callbacks_t *callbacks,
+			    void			    *callback_arg,
+			    const isc_quic_conn_options_t   *options,
+			    const char *sni, const isc_sockaddr_t *local,
+			    const isc_sockaddr_t *peer,
+			    isc_quic_conn_t	**connp);
+
+/**<
+ * \brief
+ * Create a new client state machine.
+ *
+ * \par Requires
+ * \li `mctx` is a valid memory context
+ * \li `router` is a valid QUIC router
+ * \li `options != NULL` and is valid
+ * \li `version` is a valid and defined QUIC version
+ * \li `sni` points to a valid SNI string
+ * \li `local != NULL`
+ * \li `peer != NULL`
+ * \li `connp != NULL` and `*connp == NULL`
+ *
+ * \retval ISC_R_SUCCESS on success
+ * \retval ISC_R_NOTIMPLEMENTED if the given QUIC version is not supported
+ * \retval ISC_R_TLSERROR on internal TLS failure
+ * \retval ISC_R_NOMEMORY on allocation failure
+ * \retval ISC_R_FAILURE on unknown failure
+ */
+
+isc_result_t
+isc_quic_conn_server_create(
+	isc_mem_t *mctx, isc_quic_router_t *router,
+	const isc_quic_conn_callbacks_t *callbacks, void *callback_arg,
+	const isc_quic_conn_options_t *options, isc_constregion_t initial_dcid,
+	isc_constregion_t initial_scid, const isc_sockaddr_t *local,
+	const isc_sockaddr_t *peer, isc_quic_conn_t **connp);
+/**<
+ * \brief
+ * Create a new server state machine
+ *
+ * \par Requires
+ * \li `mctx` is a valid memory context
+ * \li `router` is a valid QUIC router
+ * \li `options != NULL` and is valid
+ * \li `initial_version` is a valid and defined QUIC version
+ * \li `initial_dcid.base != NULL` and `initial_dcid.length` ∈ [1, 20]
+ * \li `initial_scid.base != NULL` and `initial_scid.length` ∈ [1, 20]
+ * \li `local != NULL`
+ * \li `peer != NULL`
+ * \li `connp != NULL` and `*connp == NULL`
+ *
+ * \retval ISC_R_SUCCESS on success
+ * \retval ISC_R_NOTIMPLEMENTED if the given QUIC version is not supported
+ * \retval ISC_R_TLSERROR on internal TLS failure
+ * \retval ISC_R_NOMEMORY on allocation failure
+ * \retval ISC_R_FAILURE on unknown failure
+ */
+
+ISC_REFCOUNT_DECL(isc_quic_conn);
diff --git a/lib/isc/include/isc/types.h b/lib/isc/include/isc/types.h
index 9bcd44f8dd..fcbe1e4092 100644
--- a/lib/isc/include/isc/types.h
+++ b/lib/isc/include/isc/types.h
@@ -79,6 +79,12 @@ typedef struct isc_timer      isc_timer_t;	/*%< Timer */
 typedef struct isc_work	      isc_work_t;	/*%< Work offloaded to an
 						 *   external thread */

+#ifdef HAVE_LIBNGTCP2
+typedef struct isc_quic_conn	       isc_quic_conn_t;
+typedef struct isc_quic_conn_callbacks isc_quic_conn_callbacks_t;
+typedef struct isc_quic_conn_options   isc_quic_conn_options_t;
+#endif /* HAVE_LIBNGTCP2 */
+
 #if HAVE_LIBNGHTTP2
 typedef struct isc_nm_http_endpoints isc_nm_http_endpoints_t;
 /*%< HTTP endpoints set */
diff --git a/lib/isc/quic/connection.c b/lib/isc/quic/connection.c
new file mode 100644
index 0000000000..8dd4e9de95
--- /dev/null
+++ b/lib/isc/quic/connection.c
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+#include <stdint.h>
+
+#include <ngtcp2/ngtcp2.h>
+
+#include <isc/attributes.h>
+#include <isc/magic.h>
+#include <isc/mem.h>
+#include <isc/util.h>
+
+#include "quic_p.h" /* IWYU pragma: keep */
+
+#define VALID_CONNECTION(c) ISC_MAGIC_VALID(c, conn_magic)
+
+constexpr uint32_t conn_magic = ISC_MAGIC('Q', 'U', 'I', 'c');
+
+static void
+destroy(isc_quic_conn_t *conn) {
+	isc_mem_t *mctx = conn->mem.user_data;
+
+	isc_quic_router_unref(conn->router);
+
+	isc_mem_put(mctx, conn, sizeof(*conn));
+	isc_mem_unref(mctx);
+}
+
+ISC_REFCOUNT_IMPL(isc_quic_conn, destroy);
+
+isc_result_t
+isc_quic_conn_client_create(
+	isc_mem_t *mctx, isc_quic_router_t *router,
+	const isc_quic_conn_callbacks_t *callbacks, void *callback_arg,
+	const isc_quic_conn_options_t *options ISC_ATTR_UNUSED,
+	const char *sni ISC_ATTR_UNUSED,
+	const isc_sockaddr_t *local ISC_ATTR_UNUSED,
+	const isc_sockaddr_t *peer ISC_ATTR_UNUSED, isc_quic_conn_t **connp) {
+	isc_quic_conn_t *conn;
+
+	REQUIRE(connp != NULL && *connp == NULL);
+
+	conn = isc_mem_get(mctx, sizeof(*conn));
+	*conn = (isc_quic_conn_t){
+		.magic = conn_magic,
+		.references = ISC_REFCOUNT_INITIALIZER(1),
+		.router = isc_quic_router_ref(router),
+		.cb = callbacks,
+		.cbarg = callback_arg,
+		.mem = { .user_data = isc_mem_ref(mctx) },
+	};
+
+	*connp = conn;
+
+	return ISC_R_SUCCESS;
+}
+
+isc_result_t
+isc_quic_conn_server_create(
+	isc_mem_t *mctx, isc_quic_router_t *router,
+	const isc_quic_conn_callbacks_t *callbacks, void *callback_arg,
+	const isc_quic_conn_options_t *options ISC_ATTR_UNUSED,
+	isc_constregion_t initial_dcid ISC_ATTR_UNUSED,
+	isc_constregion_t initial_scid ISC_ATTR_UNUSED,
+	const isc_sockaddr_t *local ISC_ATTR_UNUSED,
+	const isc_sockaddr_t *peer ISC_ATTR_UNUSED, isc_quic_conn_t **connp) {
+	isc_quic_conn_t *conn;
+
+	REQUIRE(connp != NULL && *connp == NULL);
+
+	conn = isc_mem_get(mctx, sizeof(*conn));
+	*conn = (isc_quic_conn_t){
+		.magic = conn_magic,
+		.references = ISC_REFCOUNT_INITIALIZER(1),
+		.router = isc_quic_router_ref(router),
+		.cb = callbacks,
+		.cbarg = callback_arg,
+		.mem = { .user_data = isc_mem_ref(mctx) },
+	};
+
+	*connp = conn;
+
+	return ISC_R_SUCCESS;
+}
diff --git a/lib/isc/quic/meson.build b/lib/isc/quic/meson.build
index 18a216ae73..2223bd501f 100644
--- a/lib/isc/quic/meson.build
+++ b/lib/isc/quic/meson.build
@@ -12,6 +12,7 @@
 isc_srcset.add(
     ngtcp2_dep,
     files(
+        'connection.c',
         'router.c',
     ),
 )
diff --git a/lib/isc/quic/quic_p.h b/lib/isc/quic/quic_p.h
new file mode 100644
index 0000000000..4f16c5480a
--- /dev/null
+++ b/lib/isc/quic/quic_p.h
@@ -0,0 +1,34 @@
+
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <ngtcp2/ngtcp2.h>
+
+#include <isc/quic.h>
+#include <isc/types.h>
+
+struct isc_quic_conn {
+	uint32_t magic;
+	isc_refcount_t references;
+	isc_quic_router_t *router;
+
+	const isc_quic_conn_callbacks_t *cb;
+	void *cbarg;
+
+	/* isc_mem_t resides inside `mem.user_data` */
+	ngtcp2_mem mem;
+};
diff --git a/lib/isc/quic/router.c b/lib/isc/quic/router.c
index 4858d6570c..e694a91a9b 100644
--- a/lib/isc/quic/router.c
+++ b/lib/isc/quic/router.c
@@ -31,8 +31,7 @@ struct cid_entry {
 	struct cds_lfht_node node;
 	struct rcu_head rcu_head;
 	isc_mem_t *mctx;
-	isc_quic_conn_unref_t unref;
-	void *value;
+	isc_quic_conn_t *value;
 	isc_tid_t tid;
 	size_t length;
 	uint8_t data[] ISC_ATTR_COUNTED_BY(length);
@@ -42,9 +41,8 @@ struct reset_entry_t {
 	struct cds_lfht_node node;
 	struct rcu_head rcu_head;
 	isc_mem_t *mctx;
-	isc_quic_conn_unref_t unref;
 	isc_tid_t tid;
-	void *value;
+	isc_quic_conn_t *value;
 	uint8_t token[ISC_QUIC_STATELESS_TOKEN_LENGTH];
 };

@@ -53,8 +51,6 @@ struct isc_quic_router {
 	isc_refcount_t references;
 	isc_mem_t *mctx;
 	size_t cidlen;
-	isc_quic_conn_ref_t conn_ref;
-	isc_quic_conn_unref_t conn_unref;
 	struct cds_lfht *cid_ht;
 	struct cds_lfht *reset_ht;
 };
@@ -94,7 +90,7 @@ cid_match(struct cds_lfht_node *node, const void *key) {
 static void
 cid_free(struct rcu_head *rcu_head) {
 	cid_entry_t *entry = caa_container_of(rcu_head, cid_entry_t, rcu_head);
-	entry->unref(entry->value);
+	isc_quic_conn_unref(entry->value);
 	isc_mem_putanddetach(&entry->mctx, entry,
 			     STRUCT_FLEX_SIZE(entry, data, entry->length));
 }
@@ -110,7 +106,7 @@ static void
 reset_free(struct rcu_head *rcu_head) {
 	reset_entry_t *entry = caa_container_of(rcu_head, reset_entry_t,
 						rcu_head);
-	entry->unref(entry->value);
+	isc_quic_conn_unref(entry->value);
 	isc_mem_putanddetach(&entry->mctx, entry, sizeof(*entry));
 }

@@ -141,15 +137,12 @@ ISC_REFCOUNT_IMPL(isc_quic_router, destroy);

 void
 isc_quic_router_create(isc_mem_t *mctx, size_t cidlen,
-		       isc_quic_conn_ref_t conn_ref,
-		       isc_quic_conn_unref_t conn_unref,
 		       isc_quic_router_t **routerp) {
 	isc_quic_router_t *router;

 	REQUIRE(routerp != NULL && *routerp == NULL);
 	REQUIRE(cidlen >= ISC_QUIC_CID_MIN_LENGTH &&
 		cidlen <= ISC_QUIC_CID_MAX_LENGTH);
-	REQUIRE(conn_ref != NULL && conn_unref != NULL);

 	router = isc_mem_get(mctx, sizeof(*router));
 	*router = (isc_quic_router_t){
@@ -157,8 +150,6 @@ isc_quic_router_create(isc_mem_t *mctx, size_t cidlen,
 		.references = ISC_REFCOUNT_INITIALIZER(1),
 		.mctx = isc_mem_ref(mctx),
 		.cidlen = cidlen,
-		.conn_ref = conn_ref,
-		.conn_unref = conn_unref,
 		.cid_ht = cds_lfht_new(
 			ht_init_size, ht_min_size, 0,
 			CDS_LFHT_AUTO_RESIZE | CDS_LFHT_ACCOUNTING, NULL),
@@ -191,14 +182,13 @@ isc_quic_router_add_cid(isc_quic_router_t *router, isc_constregion_t cid,
 	*entry = (cid_entry_t){
 		.value = conn,
 		.mctx = isc_mem_ref(router->mctx),
-		.unref = router->conn_unref,
 		.tid = tid,
 		.length = cid.length,
 	};
 	memmove(entry->data, cid.base, cid.length);

 	/* The routing table holds a reference for as long as it stores conn. */
-	router->conn_ref(conn);
+	isc_quic_conn_ref(conn);

 	rcu_read_lock();
 	node = cds_lfht_add_unique(router->cid_ht, hash, cid_match, &cid,
@@ -206,7 +196,7 @@ isc_quic_router_add_cid(isc_quic_router_t *router, isc_constregion_t cid,
 	rcu_read_unlock();

 	if (node != &entry->node) {
-		router->conn_unref(conn);
+		isc_quic_conn_unref(conn);
 		isc_mem_putanddetach(&entry->mctx, entry,
 				     STRUCT_FLEX_SIZE(entry, data, cid.length));
 		return ISC_R_EXISTS;
@@ -217,7 +207,7 @@ isc_quic_router_add_cid(isc_quic_router_t *router, isc_constregion_t cid,

 isc_result_t
 isc_quic_router_get_cid(isc_quic_router_t *router, isc_constregion_t cid,
-			isc_tid_t *tidp, void **connp) {
+			isc_tid_t *tidp, isc_quic_conn_t **connp) {
 	struct cds_lfht_node *node;
 	struct cds_lfht_iter iter;
 	isc_result_t result;
@@ -239,7 +229,7 @@ isc_quic_router_get_cid(isc_quic_router_t *router, isc_constregion_t cid,
 	} else {
 		result = ISC_R_SUCCESS;
 		if (connp != NULL) {
-			router->conn_ref(entry->value);
+			isc_quic_conn_ref(entry->value);
 			*connp = entry->value;
 		}
 		SET_IF_NOT_NULL(tidp, entry->tid);
@@ -300,13 +290,12 @@ isc_quic_router_add_stateless_reset(
 	*entry = (reset_entry_t){
 		.value = conn,
 		.mctx = isc_mem_ref(router->mctx),
-		.unref = router->conn_unref,
 		.tid = tid,
 	};
 	memmove(entry->token, token, ISC_QUIC_STATELESS_TOKEN_LENGTH);

 	/* The routing table holds a reference for as long as it stores conn. */
-	router->conn_ref(conn);
+	isc_quic_conn_ref(conn);

 	rcu_read_lock();
 	node = cds_lfht_add_unique(router->reset_ht, hash, reset_match, token,
@@ -314,7 +303,7 @@ isc_quic_router_add_stateless_reset(
 	rcu_read_unlock();

 	if (node != &entry->node) {
-		router->conn_unref(conn);
+		isc_quic_conn_unref(conn);
 		isc_mem_putanddetach(&entry->mctx, entry, sizeof(*entry));
 		return ISC_R_EXISTS;
 	}
@@ -326,7 +315,7 @@ isc_result_t
 isc_quic_router_get_stateless_reset(
 	isc_quic_router_t *router,
 	const uint8_t token[const restrict ISC_QUIC_STATELESS_TOKEN_LENGTH],
-	isc_tid_t *tidp, void **connp) {
+	isc_tid_t *tidp, isc_quic_conn_t **connp) {
 	struct cds_lfht_node *node;
 	struct cds_lfht_iter iter;
 	reset_entry_t *entry;
@@ -348,7 +337,7 @@ isc_quic_router_get_stateless_reset(
 		result = ISC_R_SUCCESS;
 		SET_IF_NOT_NULL(tidp, entry->tid);
 		if (connp != NULL) {
-			router->conn_ref(entry->value);
+			isc_quic_conn_ref(entry->value);
 			*connp = entry->value;
 		}
 	}
@@ -396,7 +385,7 @@ isc_quic_router_handle_packet(isc_quic_router_t *router,
 			      isc_quic_version_t *versionp,
 			      isc_constregion_t *dcidp,
 			      isc_constregion_t *scidp, isc_tid_t *tidp,
-			      void **connp) {
+			      isc_quic_conn_t **connp) {
 	isc_constregion_t dcid, scid;
 	ngtcp2_version_cid version;
 	const uint8_t *token;
diff --git a/tests/isc/quic_test.c b/tests/isc/quic_test.c
index c6791eff88..79304231f6 100644
--- a/tests/isc/quic_test.c
+++ b/tests/isc/quic_test.c
@@ -32,25 +32,6 @@

 constexpr isc_tid_t connection_tid = 3;

-/*
- * The tests use a sentinel pointer as the "connection"; these callbacks let
- * them assert that the router balances every reference it takes and hands out.
- */
-static atomic_int conn_refs = 0;
-
-static void
-test_conn_ref(void *conn) {
-	(void)conn;
-	(void)atomic_fetch_add(&conn_refs, 1);
-}
-
-static void
-test_conn_unref(void *conn) {
-	/* May run on an RCU worker thread, hence the atomic counter. */
-	(void)conn;
-	(void)atomic_fetch_sub(&conn_refs, 1);
-}
-
 constexpr uint8_t client_dcid[] = {
 	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
 };
@@ -160,204 +141,233 @@ constexpr uint8_t short_header_packet[] = {

 ISC_RUN_TEST_IMPL(isc_quic_router_cid) {
 	isc_quic_router_t *router = NULL;
+	isc_quic_conn_t *conn, *found;
 	isc_constregion_t cid;
 	isc_result_t result;
 	isc_tid_t tid = ISC_TID_UNKNOWN;
-	void *value = NULL;

-	isc_quic_router_create(isc_g_mctx, sizeof(client_dcid), test_conn_ref,
-			       test_conn_unref, &router);
+	isc_quic_conn_options_t opts = {};
+
+	isc_quic_router_create(isc_g_mctx, sizeof(client_dcid), &router);
+
+	conn = NULL;
+	result = isc_quic_conn_client_create(isc_g_mctx, router, NULL, NULL,
+					     &opts, NULL, NULL, NULL, &conn);
+	assert_int_equal(result, ISC_R_SUCCESS);

 	cid = (isc_constregion_t){ sizeof(client_dcid), client_dcid };
-	result = isc_quic_router_add_cid(router, cid, connection_tid, router);
+	result = isc_quic_router_add_cid(router, cid, connection_tid, conn);
 	assert_int_equal(result, ISC_R_SUCCESS);

 	cid = (isc_constregion_t){ sizeof(client_dcid), client_dcid };
-	result = isc_quic_router_add_cid(router, cid, connection_tid, router);
+	result = isc_quic_router_add_cid(router, cid, connection_tid, conn);
 	assert_int_equal(result, ISC_R_EXISTS);

+	found = NULL;
 	cid = (isc_constregion_t){ sizeof(client_dcid), client_dcid };
-	result = isc_quic_router_get_cid(router, cid, &tid, &value);
+	result = isc_quic_router_get_cid(router, cid, &tid, &found);
 	assert_int_equal(result, ISC_R_SUCCESS);
-	assert_ptr_equal(value, router);
+	assert_ptr_equal(found, conn);
 	assert_int_equal(tid, connection_tid);
-	test_conn_unref(value);
+	isc_quic_conn_unref(found);

-	value = NULL;
+	found = NULL;
 	cid = (isc_constregion_t){ sizeof(unknown_cid), unknown_cid };
-	result = isc_quic_router_get_cid(router, cid, NULL, &value);
+	result = isc_quic_router_get_cid(router, cid, NULL, &found);
 	assert_int_equal(result, ISC_R_NOTFOUND);

 	cid = (isc_constregion_t){ sizeof(unknown_cid), unknown_cid };
-	result = isc_quic_router_add_cid(router, cid, connection_tid, router);
+	result = isc_quic_router_add_cid(router, cid, connection_tid, conn);
+	assert_int_equal(result, ISC_R_SUCCESS);
+	result = isc_quic_router_del_cid(router, cid);
 	assert_int_equal(result, ISC_R_SUCCESS);

-	isc_quic_router_detach(&router);
+	cid = (isc_constregion_t){ sizeof(client_dcid), client_dcid };
+	result = isc_quic_router_del_cid(router, cid);
+	assert_int_equal(result, ISC_R_SUCCESS);
+	result = isc_quic_router_del_cid(router, cid);
+	assert_int_equal(result, ISC_R_NOTFOUND);

-	/* Flush deferred frees, then check the router balanced every ref. */
-	rcu_barrier();
-	assert_int_equal(atomic_load(&conn_refs), 0);
+	isc_quic_router_detach(&router);
+	isc_quic_conn_detach(&conn);
 }

 ISC_RUN_TEST_IMPL(isc_quic_router_stateless_reset) {
 	uint8_t token[ISC_QUIC_STATELESS_TOKEN_LENGTH];
 	isc_quic_router_t *router = NULL;
+	isc_quic_conn_t *conn, *found;
 	isc_result_t result;
 	isc_tid_t tid = ISC_TID_UNKNOWN;
-	void *value = NULL;

-	isc_quic_router_create(isc_g_mctx, sizeof(client_dcid), test_conn_ref,
-			       test_conn_unref, &router);
+	isc_quic_conn_options_t opts = {};
+
+	isc_quic_router_create(isc_g_mctx, sizeof(client_dcid), &router);
+
+	conn = NULL;
+	result = isc_quic_conn_client_create(isc_g_mctx, router, NULL, NULL,
+					     &opts, NULL, NULL, NULL, &conn);
+	assert_int_equal(result, ISC_R_SUCCESS);

 	isc_random_buf(token, sizeof(token));

+	found = NULL;
 	result = isc_quic_router_get_stateless_reset(router, token, NULL,
-						     &value);
+						     &found);
 	assert_int_equal(result, ISC_R_NOTFOUND);

 	result = isc_quic_router_add_stateless_reset(router, token,
-						     connection_tid, router);
+						     connection_tid, conn);
 	assert_int_equal(result, ISC_R_SUCCESS);

 	result = isc_quic_router_add_stateless_reset(router, token,
-						     connection_tid, router);
+						     connection_tid, conn);
 	assert_int_equal(result, ISC_R_EXISTS);

+	found = NULL;
 	result = isc_quic_router_get_stateless_reset(router, token, &tid,
-						     &value);
+						     &found);
 	assert_int_equal(result, ISC_R_SUCCESS);
-	assert_ptr_equal(value, router);
+	assert_ptr_equal(found, conn);
 	assert_int_equal(tid, connection_tid);
-	test_conn_unref(value);
+	isc_quic_conn_unref(found);

 	result = isc_quic_router_del_stateless_reset(router, token);
 	assert_int_equal(result, ISC_R_SUCCESS);

+	found = NULL;
 	result = isc_quic_router_get_stateless_reset(router, token, NULL,
-						     &value);
+						     &found);
 	assert_int_equal(result, ISC_R_NOTFOUND);

 	result = isc_quic_router_del_stateless_reset(router, token);
 	assert_int_equal(result, ISC_R_NOTFOUND);

 	isc_quic_router_detach(&router);
-
-	/* Flush deferred frees, then check the router balanced every ref. */
-	rcu_barrier();
-	assert_int_equal(atomic_load(&conn_refs), 0);
+	isc_quic_conn_detach(&conn);
 }

 ISC_RUN_TEST_IMPL(isc_quic_router_packet) {
 	isc_quic_router_t *router = NULL;
+	isc_quic_conn_t *conn, *found;
 	isc_quic_version_t version;
 	isc_constregion_t dcid, scid;
 	isc_result_t result;
 	isc_tid_t tid = ISC_TID_UNKNOWN;
-	void *value = NULL;

-	isc_quic_router_create(isc_g_mctx, sizeof(client_dcid), test_conn_ref,
-			       test_conn_unref, &router);
+	isc_quic_conn_options_t opts = {};
+
+	isc_quic_router_create(isc_g_mctx, sizeof(client_dcid), &router);
+
+	conn = NULL;
+	result = isc_quic_conn_client_create(isc_g_mctx, router, NULL, NULL,
+					     &opts, NULL, NULL, NULL, &conn);
+	assert_int_equal(result, ISC_R_SUCCESS);

 	/* Initial CRYPTO frames MUST be 1200 bytes */
+	found = NULL;
 	result = isc_quic_router_handle_packet(
 		router, (isc_constregion_t){ 1199, initial_frame_v1 }, &version,
-		&dcid, &scid, NULL, &value);
+		&dcid, &scid, NULL, &found);
 	assert_int_equal(result, ISC_R_IGNORE);
 	assert_int_equal(version, ISC_QUIC_VERSION_V1);
+	assert_ptr_equal(found, NULL);

 	/* Use reserved version 0x0A0A0A0A */
 	result = isc_quic_router_handle_packet(
 		router,
 		(isc_constregion_t){ 1200, initial_frame_reserved_version },
-		&version, &dcid, &scid, NULL, &value);
+		&version, &dcid, &scid, NULL, &found);
 	assert_int_equal(result, ISC_R_NOTIMPLEMENTED);
 	assert_int_equal(version, ISC_QUIC_VERSION_UNKNOWN);
+	assert_ptr_equal(found, NULL);

 	result = isc_quic_router_handle_packet(
 		router, (isc_constregion_t){ 1200, initial_frame_v1 }, &version,
-		&dcid, &scid, NULL, &value);
+		&dcid, &scid, NULL, &found);
 	assert_int_equal(result, ISC_R_NOTFOUND);
 	assert_int_equal(version, ISC_QUIC_VERSION_V1);
 	assert_int_equal(dcid.length, sizeof(client_dcid));
 	assert_memory_equal(dcid.base, client_dcid, dcid.length);
+	assert_ptr_equal(found, NULL);

 	/* A 1-RTT packet for an unknown connection: caller MAY send a reset */
 	result = isc_quic_router_handle_packet(
 		router,
 		(isc_constregion_t){ sizeof(short_header_packet),
 				     short_header_packet },
-		NULL, &dcid, &scid, NULL, &value);
+		NULL, &dcid, &scid, NULL, &found);
 	assert_int_equal(result, ISC_R_UNEXPECTED);
-	assert_null(value);
+	assert_null(found);
 	assert_int_equal(dcid.length, sizeof(client_dcid));
 	assert_memory_equal(dcid.base, client_dcid, dcid.length);

-	result = isc_quic_router_add_cid(router, dcid, connection_tid, router);
+	result = isc_quic_router_add_cid(router, dcid, connection_tid, conn);
 	assert_int_equal(result, ISC_R_SUCCESS);

 	result = isc_quic_router_handle_packet(
 		router, (isc_constregion_t){ 1200, initial_frame_v1 }, &version,
-		&dcid, &scid, NULL, &value);
+		&dcid, &scid, NULL, &found);
 	assert_int_equal(result, ISC_R_SUCCESS);
 	assert_int_equal(version, ISC_QUIC_VERSION_V1);
-	assert_ptr_equal(value, router);
-	test_conn_unref(value);
+	assert_ptr_equal(found, conn);
+	isc_quic_conn_unref(found);

-	value = NULL;
+	found = NULL;
 	result = isc_quic_router_handle_packet(
-		router, (isc_constregion_t){ 1200, initial_frame_v1 }, &version,
-		&dcid, &scid, NULL, &value);
+		router, (isc_constregion_t){ 500, initial_frame_v1 }, &version,
+		&dcid, &scid, NULL, &found);
 	assert_int_equal(result, ISC_R_SUCCESS);
-	assert_ptr_equal(value, router);
-	test_conn_unref(value);
+	assert_ptr_equal(found, conn);
+	isc_quic_conn_unref(found);

-	value = NULL;
+	found = NULL;
 	result = isc_quic_router_handle_packet(
 		router,
 		(isc_constregion_t){ sizeof(short_header_packet),
 				     short_header_packet },
-		NULL, &dcid, &scid, &tid, &value);
+		NULL, &dcid, &scid, &tid, &found);
 	assert_int_equal(result, ISC_R_SUCCESS);
-	assert_ptr_equal(value, router);
+	assert_ptr_equal(found, conn);
 	assert_int_equal(tid, connection_tid);
 	assert_int_equal(dcid.length, sizeof(client_dcid));
 	assert_memory_equal(dcid.base, client_dcid, dcid.length);
-	test_conn_unref(value);
+	isc_quic_conn_unref(found);

 	result = isc_quic_router_del_cid(router, dcid);
 	assert_int_equal(result, ISC_R_SUCCESS);

-	value = NULL;
+	found = NULL;
 	result = isc_quic_router_handle_packet(
 		router, (isc_constregion_t){ 1200, initial_frame_v1 }, &version,
-		&dcid, &scid, NULL, &value);
+		&dcid, &scid, NULL, &found);
 	assert_int_equal(result, ISC_R_NOTFOUND);

 	result = isc_quic_router_handle_packet(
 		router,
 		(isc_constregion_t){ sizeof(short_header_packet),
 				     short_header_packet },
-		NULL, &dcid, &scid, NULL, &value);
+		NULL, &dcid, &scid, NULL, &found);
 	assert_int_equal(result, ISC_R_UNEXPECTED);
-	assert_null(value);
+	assert_null(found);

 	result = isc_quic_router_add_stateless_reset(
-		router, stateless_reset_token, connection_tid, router);
+		router, stateless_reset_token, connection_tid, conn);
 	assert_int_equal(result, ISC_R_SUCCESS);

 	result = isc_quic_router_handle_packet(
 		router,
 		(isc_constregion_t){ sizeof(stateless_reset_packet),
 				     stateless_reset_packet },
-		NULL, &dcid, &scid, NULL, &value);
+		NULL, &dcid, &scid, NULL, &found);
 	assert_int_equal(result, ISC_R_CONNECTIONRESET);
-	test_conn_unref(value);
+	isc_quic_conn_unref(found);

-	isc_quic_router_detach(&router);
+	result = isc_quic_router_del_stateless_reset(router,
+						     stateless_reset_token);
+	assert_int_equal(result, ISC_R_SUCCESS);

-	/* Flush deferred frees, then check the router balanced every ref. */
-	rcu_barrier();
-	assert_int_equal(atomic_load(&conn_refs), 0);
+	isc_quic_conn_detach(&conn);
+	isc_quic_router_detach(&router);
 }

 ISC_TEST_LIST_START