Commit 60f5f4e0a7 for bind
commit 60f5f4e0a7d847a50250288f8ba7de0c1fe3644c
Author: Aydın Mercan <aydin@isc.org>
Date: Sat Jul 4 11:11:21 2026 +0300
Add QUIC specific routing mechanisms
QUIC is a protocol that is agnostic of the source address/port and uses
connection IDs and stateless reset tokens to distinguish sessions.
The thread-safe `isc_quic_router_t` structure keeps track of this
information and also has functionality for inspecting packets to find
the appropriate action alongside the associated connection if it exists.
diff --git a/lib/isc/include/isc/quic.h b/lib/isc/include/isc/quic.h
new file mode 100644
index 0000000000..a0af5b8000
--- /dev/null
+++ b/lib/isc/include/isc/quic.h
@@ -0,0 +1,296 @@
+/*
+ * 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 <isc/refcount.h>
+#include <isc/region.h>
+#include <isc/result.h>
+#include <isc/tid.h>
+#include <isc/types.h>
+
+/*! \file isc/quic.h */
+
+/**
+ * \brief
+ * Minimum allowed length of a CID.
+ *
+ * Specified in RFC9000, Section 17.2.
+ */
+#define ISC_QUIC_CID_MIN_LENGTH 1
+
+/**
+ * \brief
+ * Maximum allowed length of a CID.
+ *
+ * Specified in RFC9000, Section 17.2.
+ */
+#define ISC_QUIC_CID_MAX_LENGTH 20
+
+/**
+ * \brief
+ * Length of the stateless reset token.
+ *
+ * Specified in RFC9000, Section 10.3.
+ */
+#define ISC_QUIC_STATELESS_TOKEN_LENGTH 16
+
+/**
+ * \brief
+ * Thread-safe data structure for associating CID's and stateless reset tokens
+ * to QUIC connections.
+ */
+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,
+
+ /** Unknown QUIC version */
+ ISC_QUIC_VERSION_UNKNOWN = 1,
+
+ /** RFC9000 */
+ ISC_QUIC_VERSION_V1 = 2,
+
+ /** RFC9369 */
+ ISC_QUIC_VERSION_V2 = 3,
+
+ ISC_QUIC_VERSION__MAX = 4,
+} isc_quic_version_t;
+
+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);
+/**<
+ * \brief
+ * Create a new QUIC CID router.
+ *
+ * `cidlen` is the length of the Connection IDs this endpoint issues. It is
+ * needed to route incoming Short header (1-RTT) packets, whose Destination
+ * Connection ID length is not carried on the wire (RFC9000, Section 5.2).
+ *
+ * `conn_ref` and `conn_unref` acquire and release a reference on a connection.
+ * The router holds a reference for as long as a connection is reachable and
+ * takes an extra one on every value it hands out (see isc_quic_router_get_cid()
+ * and isc_quic_router_get_stateless_reset()).
+ *
+ * \par Requires:
+ * \li `mctx` is a valid memory context.
+ * \li `cidlen` ∈ [1, 20]
+ * \li `conn_ref != NULL` and `conn_unref != NULL`
+ * \li `routerp != NULL` and `*routerp == NULL`
+ */
+
+ISC_REFCOUNT_DECL(isc_quic_router);
+
+isc_result_t
+isc_quic_router_add_cid(isc_quic_router_t *router, isc_constregion_t cid,
+ isc_tid_t tid, void *conn);
+/**<
+ * \brief
+ * Associate a connection with the given CID.
+ *
+ * The CID will be copied and thus the parameter free from any lifetime
+ * requirements.
+ *
+ * `tid` is stored alongside the connection and returned by
+ * isc_quic_router_get_cid(); it identifies the loop that owns `conn`.
+ *
+ * \par Requires:
+ * \li `router` is a valid router.
+ * \li `cid.base != NULL` and `cid.length` ∈ [1, 20]
+ * \li `tid >= 0`
+ * \li `conn != NULL`
+ *
+ * \retval ISC_R_SUCCESS on success
+ * \retval ISC_R_EXISTS if a connection exists for the given CID
+ */
+
+isc_result_t
+isc_quic_router_get_cid(isc_quic_router_t *router, isc_constregion_t cid,
+ isc_tid_t *tidp, void **connp);
+/**<
+ * \brief
+ * Get the connection associated with a given CID and its thread if `tidp` is
+ * not NULL.
+ *
+ * \note
+ * On success `*connp` carries an extra reference on the connection, taken with
+ * the router's `conn_ref` callback; the caller must release it with the
+ * matching `conn_unref` once done. This keeps the connection alive across the
+ * handoff to its owning `tid`, closing the race with a concurrent teardown.
+ *
+ * \par Requires:
+ * \li `router` is a valid router.
+ * \li `cid.base != NULL` and `cid.length` ∈ [1, 20]
+ *
+ * \retval ISC_R_SUCCESS on success
+ * \retval ISC_R_NOTFOUND if no connection exists for the given CID.
+ */
+
+isc_result_t
+isc_quic_router_del_cid(isc_quic_router_t *router, isc_constregion_t cid);
+/**<
+ * \brief
+ * Remove the connection associated with the given CID.
+ *
+ * \par Requires:
+ * \li `router` is a valid router.
+ * \li `cid.base != NULL` and `cid.length` ∈ [1, 20]
+ *
+ * \retval ISC_R_SUCCESS on success
+ * \retval ISC_R_NOTFOUND if no connection exists for the given CID.
+ */
+
+isc_result_t
+isc_quic_router_add_stateless_reset(
+ isc_quic_router_t *router,
+ const uint8_t token[restrict ISC_QUIC_STATELESS_TOKEN_LENGTH],
+ isc_tid_t tid, void *conn);
+/**<
+ * \brief
+ * Associate a connection with the given stateless reset token.
+ *
+ * `tid` is stored alongside the connection and returned by
+ * isc_quic_router_get_stateless_reset(); it identifies the loop that owns
+ * `conn`.
+ *
+ * The token will be copied and thus the parameter free from any lifetime
+ * requirements.
+ *
+ * \par Requires:
+ * \li `router` is a valid router.
+ * \li `token != NULL`
+ * \li `tid >= 0`
+ * \li `conn != NULL`
+ *
+ * \retval ISC_R_SUCCESS on success
+ * \retval ISC_R_EXISTS if a connection is already associated with the given
+ * token.
+ */
+
+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);
+/**<
+ * \brief
+ * Get the connection associated with the given stateless reset token and its
+ * thread if `tidp` is not NULL.
+ *
+ * \note
+ * On success `*connp` carries an extra reference on the connection (see
+ * isc_quic_router_get_cid()); the caller must release it with the router's
+ * `conn_unref` callback.
+ *
+ * \par Requires:
+ * \li `router` is a valid router.
+ * \li `token` != NULL
+ *
+ * \retval ISC_R_SUCCESS on success
+ * \retval ISC_R_NOTFOUND if no connection exists for the given token.
+ */
+
+isc_result_t
+isc_quic_router_del_stateless_reset(
+ isc_quic_router_t *router,
+ const uint8_t token[const restrict ISC_QUIC_STATELESS_TOKEN_LENGTH]);
+/**<
+ * \brief
+ * Remove the connection associated with the given stateless reset token.
+ *
+ * \par Requires:
+ * \li `router` is a valid router.
+ * \li `token != NULL`
+ *
+ * \retval ISC_R_SUCCESS on success
+ * \retval ISC_R_NOTFOUND if no connection exists for the given token.
+ */
+
+isc_result_t
+isc_quic_router_handle_packet(isc_quic_router_t *router,
+ isc_constregion_t packet,
+ isc_quic_version_t *versionp,
+ isc_constregion_t *dcidp,
+ isc_constregion_t *scidp, isc_tid_t *tidp,
+ void **connp);
+/**<
+ * \brief
+ * Extracts the associated value and relevant information of a given QUIC
+ * packet.
+ *
+ * \par Requires:
+ * \li `router` is a valid router.
+ * \li `packet.base != NULL` and `packet.length != 0`
+ * \li `dcidp != NULL`
+ * \li `scidp != NULL`
+ * \li `connp != NULL` and `*connp == NULL`
+ *
+ * It checks for the following
+ * \li Check if the packet header is valid. If so, extract relevant information.
+ * \li If the DCID is associated with a value, return it.
+ * \li Otherwise, check if the packet version is unknown. If so, also check if
+ * it has a stateless reset token.
+ * \li Check if it can be accepted as a handshake.
+ *
+ * \warning
+ * The router will **NOT** automatically remove the CID on stateless reset.
+ *
+ * \note
+ * On return, `*dcidp` and `*scidp` alias into `packet.base`; they are only
+ * valid for as long as `packet` is, and are left unset on the
+ * ISC_R_INVALIDPROTO path (where the header could not be decoded).
+ *
+ * \note
+ * When a connection is returned via `*connp` (ISC_R_SUCCESS or
+ * ISC_R_CONNECTIONRESET), it carries an extra reference the caller must release
+ * with the router's `conn_unref` callback (see isc_quic_router_get_cid()).
+ *
+ * \retval ISC_R_SUCCESS when an associated connection for the packet is found
+ * \retval ISC_R_CONNECTIONRESET when a stateless reset packet for a session has
+ * been found.
+ * \retval ISC_R_NOTFOUND when the packet should be accepted as the handshake of
+ * a new connection.
+ * \retval ISC_R_NOTIMPLEMENTED when the version is unsupported and a Version
+ * Negotiation packet should be sent; `*dcidp` and `*scidp` are populated with
+ * the peer's connection IDs (which may exceed ISC_QUIC_CID_MAX_LENGTH).
+ * \retval ISC_R_INVALIDPROTO on packet misformat
+ * \retval ISC_R_UNEXPECTED when a Short header (1-RTT) packet is not associated
+ * with a connection and is not a known stateless reset. The caller MAY respond
+ * with a stateless reset (RFC9000, Section 10.3).
+ * \retval ISC_R_IGNORE when the packet should be dropped silently: a Long
+ * header packet that cannot be accepted as a handshake (e.g. Handshake or
+ * 0-RTT for an unknown connection) or a received Version Negotiation packet.
+ * Unlike ISC_R_UNEXPECTED, the caller MUST NOT send a stateless reset in
+ * response (RFC9000, Section 10.3).
+ */
diff --git a/lib/isc/meson.build b/lib/isc/meson.build
index dc8fb420c0..7717832a03 100644
--- a/lib/isc/meson.build
+++ b/lib/isc/meson.build
@@ -23,6 +23,7 @@ isc_inc_p += include_directories('.')
subdir('crypto')
subdir('netmgr')
subdir('ossl_wrap')
+subdir('quic', if_found: [ngtcp2_dep])
isc_srcset.add(
m_dep,
diff --git a/lib/isc/quic/meson.build b/lib/isc/quic/meson.build
new file mode 100644
index 0000000000..18a216ae73
--- /dev/null
+++ b/lib/isc/quic/meson.build
@@ -0,0 +1,17 @@
+# 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.
+
+isc_srcset.add(
+ ngtcp2_dep,
+ files(
+ 'router.c',
+ ),
+)
diff --git a/lib/isc/quic/router.c b/lib/isc/quic/router.c
new file mode 100644
index 0000000000..4858d6570c
--- /dev/null
+++ b/lib/isc/quic/router.c
@@ -0,0 +1,517 @@
+/*
+ * 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/hash.h>
+#include <isc/magic.h>
+#include <isc/mem.h>
+#include <isc/quic.h>
+#include <isc/safe.h>
+#include <isc/urcu.h>
+#include <isc/util.h>
+
+typedef struct cid_entry cid_entry_t;
+typedef struct reset_entry_t reset_entry_t;
+
+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_tid_t tid;
+ size_t length;
+ uint8_t data[] ISC_ATTR_COUNTED_BY(length);
+};
+
+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;
+ uint8_t token[ISC_QUIC_STATELESS_TOKEN_LENGTH];
+};
+
+struct isc_quic_router {
+ uint32_t magic;
+ 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;
+};
+
+constexpr uint32_t router_magic = ISC_MAGIC('Q', 'U', 'I', 'r');
+
+/*
+ * Header Form bit, set on Long header packets and clear on Short header
+ * (1-RTT) packets (RFC9000, Section 17). ngtcp2 only exposes this in a
+ * private header, so it is defined locally.
+ */
+constexpr uint8_t quic_long_header_bit = 0x80;
+constexpr unsigned long ht_init_size = (1 << 4); /* Must be power of 2 */
+constexpr unsigned long ht_min_size = (1 << 4); /* Must be power of 2 */
+
+STATIC_ASSERT(ISC_QUIC_CID_MIN_LENGTH == NGTCP2_MIN_CIDLEN,
+ "QUIC minimum CID length mismatches with the definition "
+ "inside ngtcp2");
+
+STATIC_ASSERT(ISC_QUIC_CID_MAX_LENGTH == NGTCP2_MAX_CIDLEN,
+ "QUIC maximum CID length mismatches with the definition "
+ "inside ngtcp2");
+
+STATIC_ASSERT(ISC_QUIC_STATELESS_TOKEN_LENGTH ==
+ NGTCP2_STATELESS_RESET_TOKENLEN,
+ "QUIC stateless token length mismatches with the definition "
+ "inside ngtcp2");
+
+static int
+cid_match(struct cds_lfht_node *node, const void *key) {
+ cid_entry_t *entry = caa_container_of(node, cid_entry_t, node);
+ const isc_constregion_t *data = key;
+ return (entry->length == data->length) &&
+ (memcmp(entry->data, data->base, entry->length) == 0);
+}
+
+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_mem_putanddetach(&entry->mctx, entry,
+ STRUCT_FLEX_SIZE(entry, data, entry->length));
+}
+
+static int
+reset_match(struct cds_lfht_node *node, const void *key) {
+ reset_entry_t *entry = caa_container_of(node, reset_entry_t, node);
+ return isc_safe_memequal(entry->token, key,
+ ISC_QUIC_STATELESS_TOKEN_LENGTH);
+}
+
+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_mem_putanddetach(&entry->mctx, entry, sizeof(*entry));
+}
+
+static void
+destroy(isc_quic_router_t *router) {
+ struct cds_lfht_iter iter;
+ reset_entry_t *r;
+ cid_entry_t *c;
+
+ cds_lfht_for_each_entry(router->cid_ht, &iter, c, node) {
+ INSIST(!cds_lfht_del(router->cid_ht, &c->node));
+ call_rcu(&c->rcu_head, cid_free);
+ }
+ RUNTIME_CHECK(!cds_lfht_destroy(router->cid_ht, NULL));
+
+ cds_lfht_for_each_entry(router->reset_ht, &iter, r, node) {
+ INSIST(!cds_lfht_del(router->reset_ht, &r->node));
+ call_rcu(&r->rcu_head, reset_free);
+ }
+ RUNTIME_CHECK(!cds_lfht_destroy(router->reset_ht, NULL));
+
+ router->magic = 0;
+
+ isc_mem_putanddetach(&router->mctx, router, sizeof(*router));
+}
+
+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){
+ .magic = router_magic,
+ .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),
+ .reset_ht = cds_lfht_new(
+ ht_init_size, ht_min_size, 0,
+ CDS_LFHT_AUTO_RESIZE | CDS_LFHT_ACCOUNTING, NULL),
+ };
+ INSIST(router->cid_ht != NULL);
+ INSIST(router->reset_ht != NULL);
+
+ *routerp = router;
+}
+
+isc_result_t
+isc_quic_router_add_cid(isc_quic_router_t *router, isc_constregion_t cid,
+ isc_tid_t tid, void *conn) {
+ struct cds_lfht_node *node;
+ cid_entry_t *entry;
+ uint32_t hash;
+
+ REQUIRE(router != NULL && router->magic == router_magic);
+ REQUIRE(cid.base != NULL && cid.length >= ISC_QUIC_CID_MIN_LENGTH &&
+ cid.length <= ISC_QUIC_CID_MAX_LENGTH);
+ REQUIRE(tid >= 0);
+
+ hash = isc_hash32(cid.base, cid.length, true);
+
+ entry = isc_mem_get(router->mctx,
+ STRUCT_FLEX_SIZE(entry, data, cid.length));
+ *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);
+
+ rcu_read_lock();
+ node = cds_lfht_add_unique(router->cid_ht, hash, cid_match, &cid,
+ &entry->node);
+ rcu_read_unlock();
+
+ if (node != &entry->node) {
+ router->conn_unref(conn);
+ isc_mem_putanddetach(&entry->mctx, entry,
+ STRUCT_FLEX_SIZE(entry, data, cid.length));
+ return ISC_R_EXISTS;
+ }
+
+ return ISC_R_SUCCESS;
+}
+
+isc_result_t
+isc_quic_router_get_cid(isc_quic_router_t *router, isc_constregion_t cid,
+ isc_tid_t *tidp, void **connp) {
+ struct cds_lfht_node *node;
+ struct cds_lfht_iter iter;
+ isc_result_t result;
+ cid_entry_t *entry;
+ uint32_t hash;
+
+ REQUIRE(router != NULL && router->magic == router_magic);
+ REQUIRE(cid.base != NULL && cid.length >= ISC_QUIC_CID_MIN_LENGTH &&
+ cid.length <= ISC_QUIC_CID_MAX_LENGTH);
+
+ hash = isc_hash32(cid.base, cid.length, true);
+
+ rcu_read_lock();
+ cds_lfht_lookup(router->cid_ht, hash, cid_match, &cid, &iter);
+ node = cds_lfht_iter_get_node(&iter);
+ entry = cds_lfht_entry(node, cid_entry_t, node);
+ if (entry == NULL) {
+ result = ISC_R_NOTFOUND;
+ } else {
+ result = ISC_R_SUCCESS;
+ if (connp != NULL) {
+ router->conn_ref(entry->value);
+ *connp = entry->value;
+ }
+ SET_IF_NOT_NULL(tidp, entry->tid);
+ }
+ rcu_read_unlock();
+
+ return result;
+}
+
+isc_result_t
+isc_quic_router_del_cid(isc_quic_router_t *router, isc_constregion_t cid) {
+ struct cds_lfht_node *node;
+ struct cds_lfht_iter iter;
+ isc_result_t result;
+ cid_entry_t *entry;
+ uint32_t hash;
+
+ REQUIRE(router != NULL && router->magic == router_magic);
+ REQUIRE(cid.base != NULL && cid.length >= ISC_QUIC_CID_MIN_LENGTH &&
+ cid.length <= ISC_QUIC_CID_MAX_LENGTH);
+
+ hash = isc_hash32(cid.base, cid.length, true);
+
+ rcu_read_lock();
+ cds_lfht_lookup(router->cid_ht, hash, cid_match, &cid, &iter);
+ node = cds_lfht_iter_get_node(&iter);
+ if (node == NULL) {
+ result = ISC_R_NOTFOUND;
+ } else if (cds_lfht_del(router->cid_ht, node) != 0) {
+ /* Another thread removed the same entry first. */
+ result = ISC_R_NOTFOUND;
+ } else {
+ entry = caa_container_of(node, cid_entry_t, node);
+ call_rcu(&entry->rcu_head, cid_free);
+ result = ISC_R_SUCCESS;
+ }
+ rcu_read_unlock();
+
+ return result;
+}
+
+isc_result_t
+isc_quic_router_add_stateless_reset(
+ isc_quic_router_t *router,
+ const uint8_t token[restrict ISC_QUIC_STATELESS_TOKEN_LENGTH],
+ isc_tid_t tid, void *conn) {
+ struct cds_lfht_node *node;
+ reset_entry_t *entry;
+ uint32_t hash;
+
+ REQUIRE(router != NULL && router->magic == router_magic);
+ REQUIRE(token != NULL);
+ REQUIRE(tid >= 0);
+
+ hash = isc_hash32(token, ISC_QUIC_STATELESS_TOKEN_LENGTH, true);
+
+ entry = isc_mem_get(router->mctx, sizeof(*entry));
+ *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);
+
+ rcu_read_lock();
+ node = cds_lfht_add_unique(router->reset_ht, hash, reset_match, token,
+ &entry->node);
+ rcu_read_unlock();
+
+ if (node != &entry->node) {
+ router->conn_unref(conn);
+ isc_mem_putanddetach(&entry->mctx, entry, sizeof(*entry));
+ return ISC_R_EXISTS;
+ }
+
+ return ISC_R_SUCCESS;
+}
+
+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) {
+ struct cds_lfht_node *node;
+ struct cds_lfht_iter iter;
+ reset_entry_t *entry;
+ isc_result_t result;
+ uint32_t hash;
+
+ REQUIRE(router != NULL && router->magic == router_magic);
+ REQUIRE(token != NULL);
+
+ hash = isc_hash32(token, NGTCP2_STATELESS_RESET_TOKENLEN, true);
+
+ rcu_read_lock();
+ cds_lfht_lookup(router->reset_ht, hash, reset_match, token, &iter);
+ node = cds_lfht_iter_get_node(&iter);
+ entry = cds_lfht_entry(node, reset_entry_t, node);
+ if (entry == NULL) {
+ result = ISC_R_NOTFOUND;
+ } else {
+ result = ISC_R_SUCCESS;
+ SET_IF_NOT_NULL(tidp, entry->tid);
+ if (connp != NULL) {
+ router->conn_ref(entry->value);
+ *connp = entry->value;
+ }
+ }
+ rcu_read_unlock();
+
+ return result;
+}
+
+isc_result_t
+isc_quic_router_del_stateless_reset(
+ isc_quic_router_t *router,
+ const uint8_t token[const restrict ISC_QUIC_STATELESS_TOKEN_LENGTH]) {
+ struct cds_lfht_node *node;
+ struct cds_lfht_iter iter;
+ isc_result_t result;
+ reset_entry_t *entry;
+ uint32_t hash;
+
+ REQUIRE(router != NULL && router->magic == router_magic);
+ REQUIRE(token != NULL);
+
+ hash = isc_hash32(token, NGTCP2_STATELESS_RESET_TOKENLEN, true);
+
+ rcu_read_lock();
+ cds_lfht_lookup(router->reset_ht, hash, reset_match, token, &iter);
+ node = cds_lfht_iter_get_node(&iter);
+ if (node == NULL) {
+ result = ISC_R_NOTFOUND;
+ } else if (cds_lfht_del(router->reset_ht, node) != 0) {
+ /* Another thread removed the same entry first. */
+ result = ISC_R_NOTFOUND;
+ } else {
+ entry = caa_container_of(node, reset_entry_t, node);
+ call_rcu(&entry->rcu_head, reset_free);
+ result = ISC_R_SUCCESS;
+ }
+ rcu_read_unlock();
+
+ return result;
+}
+
+isc_result_t
+isc_quic_router_handle_packet(isc_quic_router_t *router,
+ isc_constregion_t packet,
+ isc_quic_version_t *versionp,
+ isc_constregion_t *dcidp,
+ isc_constregion_t *scidp, isc_tid_t *tidp,
+ void **connp) {
+ isc_constregion_t dcid, scid;
+ ngtcp2_version_cid version;
+ const uint8_t *token;
+ int r;
+
+ REQUIRE(router != NULL && router->magic == router_magic);
+ REQUIRE(packet.base != NULL && packet.length != 0);
+ REQUIRE(dcidp != NULL && scidp != NULL);
+ REQUIRE(connp != NULL && *connp == NULL);
+
+ r = ngtcp2_pkt_decode_version_cid(&version, packet.base, packet.length,
+ router->cidlen);
+ switch (r) {
+ case 0:
+ break;
+ case NGTCP2_ERR_VERSION_NEGOTIATION:
+ SET_IF_NOT_NULL(versionp, ISC_QUIC_VERSION_UNKNOWN);
+ /*
+ * ngtcp2 populates all fields of `version` on this path so
+ * the caller can build the Version Negotiation response.
+ */
+ *dcidp = (isc_constregion_t){ version.dcidlen, version.dcid };
+ *scidp = (isc_constregion_t){ version.scidlen, version.scid };
+ return ISC_R_NOTIMPLEMENTED;
+ case NGTCP2_ERR_INVALID_ARGUMENT:
+ default:
+ /*
+ * The default is technically not necessary since ngtcp2 prefers
+ * to add a new function version instead but this is an easy
+ * reassurance.
+ */
+ SET_IF_NOT_NULL(versionp, ISC_QUIC_VERSION_INVALID);
+ return ISC_R_INVALIDPROTO;
+ }
+
+ switch (version.version) {
+ case NGTCP2_PROTO_VER_V1:
+ SET_IF_NOT_NULL(versionp, ISC_QUIC_VERSION_V1);
+ break;
+ case NGTCP2_PROTO_VER_V2:
+ SET_IF_NOT_NULL(versionp, ISC_QUIC_VERSION_V2);
+ break;
+ default:
+ SET_IF_NOT_NULL(versionp, ISC_QUIC_VERSION_UNKNOWN);
+ break;
+ }
+
+ dcid = (isc_constregion_t){ version.dcidlen, version.dcid };
+ scid = (isc_constregion_t){ version.scidlen, version.scid };
+
+ *dcidp = dcid;
+ *scidp = scid;
+
+ if (version.version == 0) {
+ /*
+ * A zero version is reported both for a Short header (1-RTT)
+ * packet and for a received Version Negotiation packet (Long
+ * header). The Header Form bit (RFC9000, Section 17) tells them
+ * apart: only a Short header packet can be a Stateless Reset.
+ */
+ if ((((const uint8_t *)packet.base)[0] &
+ quic_long_header_bit) != 0)
+ {
+ return ISC_R_IGNORE;
+ }
+
+ /*
+ * A Stateless Reset is at least
+ * NGTCP2_MIN_STATELESS_RESET_RANDLEN + the token length long
+ * (RFC9000, Section 10.3).
+ */
+ if (packet.length < NGTCP2_MIN_STATELESS_RESET_RANDLEN +
+ ISC_QUIC_STATELESS_TOKEN_LENGTH)
+ {
+ return ISC_R_IGNORE;
+ }
+
+ token = (const uint8_t *)packet.base + packet.length -
+ ISC_QUIC_STATELESS_TOKEN_LENGTH;
+
+ if (isc_quic_router_get_stateless_reset(router, token, tidp,
+ connp) == ISC_R_SUCCESS)
+ {
+ return ISC_R_CONNECTIONRESET;
+ }
+
+ /* Not a Stateless Reset: fall through to the CID lookup. */
+ }
+
+ /*
+ * The DCID length is taken from the wire and, for a supported version,
+ * is only bounded above by ngtcp2. Guard the lookup so an out-of-range
+ * (e.g. zero-length) DCID does not trip the REQUIRE in get_cid().
+ */
+ if (dcid.length >= ISC_QUIC_CID_MIN_LENGTH &&
+ dcid.length <= ISC_QUIC_CID_MAX_LENGTH &&
+ isc_quic_router_get_cid(router, dcid, tidp, connp) == ISC_R_SUCCESS)
+ {
+ return ISC_R_SUCCESS;
+ }
+
+ if (version.version == 0) {
+ /*
+ * A Short header (1-RTT) packet for a connection we do not
+ * know, and not a known Stateless Reset either: the caller
+ * MAY answer with a Stateless Reset (RFC9000, Section 10.3).
+ * ngtcp2_accept() only ever accepts a Long header Initial,
+ * so there is nothing further to try.
+ */
+ return ISC_R_UNEXPECTED;
+ }
+
+ if (ngtcp2_accept(NULL, packet.base, packet.length) != 0) {
+ return ISC_R_IGNORE;
+ }
+
+ return ISC_R_NOTFOUND;
+}
diff --git a/tests/isc/meson.build b/tests/isc/meson.build
index 5966a51380..aa47457556 100644
--- a/tests/isc/meson.build
+++ b/tests/isc/meson.build
@@ -66,7 +66,7 @@ flaky_isc_test = [
is_el8 = run_command('grep', '-q', '-F', 'platform:el8', '/etc/os-release', check: false).returncode() == 0
-_all_isc_test = isc_test + ['doh']
+_all_isc_test = isc_test + ['doh', 'quic']
run_command(
check_test_registration,
meson.current_source_dir(),
@@ -78,6 +78,10 @@ if config.has('HAVE_LIBNGHTTP2') and not is_el8
isc_test += 'doh'
endif
+if config.has('HAVE_LIBNGTCP2')
+ isc_test += 'quic'
+endif
+
foreach unit : isc_test
test_bin = executable(
unit,
diff --git a/tests/isc/quic_test.c b/tests/isc/quic_test.c
new file mode 100644
index 0000000000..c6791eff88
--- /dev/null
+++ b/tests/isc/quic_test.c
@@ -0,0 +1,369 @@
+/*
+ * 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 <inttypes.h>
+#include <sched.h> /* IWYU pragma: keep */
+#include <setjmp.h> /* IWYU pragma: keep */
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdlib.h> /* IWYU pragma: keep */
+#include <unistd.h>
+
+#define UNIT_TESTING
+#include <cmocka.h>
+
+#include <isc/atomic.h>
+#include <isc/lib.h>
+#include <isc/quic.h>
+#include <isc/random.h>
+#include <isc/urcu.h>
+
+#include <tests/isc.h>
+
+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,
+};
+
+constexpr uint8_t unknown_cid[] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
+};
+
+/* Adapted from https://quic.xargs.org/ */
+constexpr uint8_t initial_frame_v1[1200] = {
+ /*
+ * Long Header Packet Byte (RFC9000, Section 17.2):
+ * 11000000
+ * ││├┘├┘├┘
+ * │││ │ └── Packet number field length (1-byte)
+ * │││ └──── Reserved Bits (RFC9000, Section 17.2.2)
+ * ││└────── Long Packet Type (RFC9000, Section 17.2, Table 5)
+ * │└─────── Fixed Bit
+ * └──────── Header Form
+ */
+ 0xc0,
+ /* QUIC version v1 */
+ 0x00, 0x00, 0x00, 0x01,
+ /* 8-bytes of DCID to follow */
+ 0x08,
+ /* DCID */
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ /* 5-bytes of SCID to follow */
+ 0x05,
+ /* SCID */
+ 0xF0, 0xF1, 0xF2, 0xF3, 0xF4,
+ /* 0-bytes of token to follow */
+ 0x00,
+ /* 259-bytes of packet to follow */
+ 0x41, 0x03,
+ /* Packet number */
+ 0x00
+ /* The rest is zero-padded packet information that won't be inspected */
+};
+
+constexpr uint8_t initial_frame_reserved_version[1200] = {
+ /*
+ * Long Header Packet Byte (RFC9000, Section 17.2):
+ * 11000000
+ * ││├┘├┘├┘
+ * │││ │ └── Packet number field length (1-byte)
+ * │││ └──── Reserved Bits (RFC9000, Section 17.2.2)
+ * ││└────── Long Packet Type (RFC9000, Section 17.2, Table 5)
+ * │└─────── Fixed Bit
+ * └──────── Header Form
+ */
+ 0xC0,
+ /* QUIC reserved version */
+ 0x0A, 0x0A, 0x0A, 0x0A,
+ /* 8-bytes of DCID to follow */
+ 0x08,
+ /* DCID */
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ /* 5-bytes of SCID to follow */
+ 0x05,
+ /* SCID */
+ 0xF0, 0xF1, 0xF2, 0xF3, 0xF4,
+ /* 0-bytes of token to follow */
+ 0x00,
+ /* 259-bytes of packet to follow */
+ 0x41, 0x03,
+ /* Packet number */
+ 0x00
+ /* The rest is zero-padded packet information that won't be inspected */
+};
+
+constexpr uint8_t stateless_reset_token[ISC_QUIC_STATELESS_TOKEN_LENGTH] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+};
+
+constexpr uint8_t stateless_reset_packet[] = {
+ 0x40, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0x00, 0x01, 0x02, 0x03,
+ 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+};
+
+/*
+ * A 1-RTT packet for the connection identified by client_dcid. Its last 16
+ * bytes deliberately differ from stateless_reset_token.
+ */
+constexpr uint8_t short_header_packet[] = {
+ /*
+ * Short Header Packet Byte (RFC9000, Section 17.3.1):
+ * 01000000
+ * │││├┘│├┘
+ * ││││ │└── Packet Number Length (1-byte)
+ * ││││ └─── Key Phase
+ * │││└───── Reserved Bits
+ * ││└────── Spin Bit
+ * │└─────── Fixed Bit
+ * └──────── Header Form
+ */
+ 0x40,
+ /* DCID (always router->cidlen bytes in a Short header) */
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ /* Packet number */
+ 0x00,
+ /* Protected payload including the AEAD tag; never inspected */
+ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB,
+ 0xAC, 0xAD, 0xAE, 0xAF
+};
+
+ISC_RUN_TEST_IMPL(isc_quic_router_cid) {
+ isc_quic_router_t *router = NULL;
+ 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);
+
+ cid = (isc_constregion_t){ sizeof(client_dcid), client_dcid };
+ result = isc_quic_router_add_cid(router, cid, connection_tid, router);
+ 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);
+ assert_int_equal(result, ISC_R_EXISTS);
+
+ cid = (isc_constregion_t){ sizeof(client_dcid), client_dcid };
+ result = isc_quic_router_get_cid(router, cid, &tid, &value);
+ assert_int_equal(result, ISC_R_SUCCESS);
+ assert_ptr_equal(value, router);
+ assert_int_equal(tid, connection_tid);
+ test_conn_unref(value);
+
+ value = NULL;
+ cid = (isc_constregion_t){ sizeof(unknown_cid), unknown_cid };
+ result = isc_quic_router_get_cid(router, cid, NULL, &value);
+ 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);
+ assert_int_equal(result, ISC_R_SUCCESS);
+
+ 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_RUN_TEST_IMPL(isc_quic_router_stateless_reset) {
+ uint8_t token[ISC_QUIC_STATELESS_TOKEN_LENGTH];
+ isc_quic_router_t *router = NULL;
+ 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_random_buf(token, sizeof(token));
+
+ result = isc_quic_router_get_stateless_reset(router, token, NULL,
+ &value);
+ assert_int_equal(result, ISC_R_NOTFOUND);
+
+ result = isc_quic_router_add_stateless_reset(router, token,
+ connection_tid, router);
+ assert_int_equal(result, ISC_R_SUCCESS);
+
+ result = isc_quic_router_add_stateless_reset(router, token,
+ connection_tid, router);
+ assert_int_equal(result, ISC_R_EXISTS);
+
+ result = isc_quic_router_get_stateless_reset(router, token, &tid,
+ &value);
+ assert_int_equal(result, ISC_R_SUCCESS);
+ assert_ptr_equal(value, router);
+ assert_int_equal(tid, connection_tid);
+ test_conn_unref(value);
+
+ result = isc_quic_router_del_stateless_reset(router, token);
+ assert_int_equal(result, ISC_R_SUCCESS);
+
+ result = isc_quic_router_get_stateless_reset(router, token, NULL,
+ &value);
+ 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_RUN_TEST_IMPL(isc_quic_router_packet) {
+ isc_quic_router_t *router = NULL;
+ 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);
+
+ /* Initial CRYPTO frames MUST be 1200 bytes */
+ result = isc_quic_router_handle_packet(
+ router, (isc_constregion_t){ 1199, initial_frame_v1 }, &version,
+ &dcid, &scid, NULL, &value);
+ assert_int_equal(result, ISC_R_IGNORE);
+ assert_int_equal(version, ISC_QUIC_VERSION_V1);
+
+ /* Use reserved version 0x0A0A0A0A */
+ result = isc_quic_router_handle_packet(
+ router,
+ (isc_constregion_t){ 1200, initial_frame_reserved_version },
+ &version, &dcid, &scid, NULL, &value);
+ assert_int_equal(result, ISC_R_NOTIMPLEMENTED);
+ assert_int_equal(version, ISC_QUIC_VERSION_UNKNOWN);
+
+ result = isc_quic_router_handle_packet(
+ router, (isc_constregion_t){ 1200, initial_frame_v1 }, &version,
+ &dcid, &scid, NULL, &value);
+ 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);
+
+ /* 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);
+ assert_int_equal(result, ISC_R_UNEXPECTED);
+ assert_null(value);
+ 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);
+ 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);
+ assert_int_equal(result, ISC_R_SUCCESS);
+ assert_int_equal(version, ISC_QUIC_VERSION_V1);
+ assert_ptr_equal(value, router);
+ test_conn_unref(value);
+
+ value = NULL;
+ result = isc_quic_router_handle_packet(
+ router, (isc_constregion_t){ 1200, initial_frame_v1 }, &version,
+ &dcid, &scid, NULL, &value);
+ assert_int_equal(result, ISC_R_SUCCESS);
+ assert_ptr_equal(value, router);
+ test_conn_unref(value);
+
+ value = NULL;
+ result = isc_quic_router_handle_packet(
+ router,
+ (isc_constregion_t){ sizeof(short_header_packet),
+ short_header_packet },
+ NULL, &dcid, &scid, &tid, &value);
+ assert_int_equal(result, ISC_R_SUCCESS);
+ assert_ptr_equal(value, router);
+ 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);
+
+ result = isc_quic_router_del_cid(router, dcid);
+ assert_int_equal(result, ISC_R_SUCCESS);
+
+ value = NULL;
+ result = isc_quic_router_handle_packet(
+ router, (isc_constregion_t){ 1200, initial_frame_v1 }, &version,
+ &dcid, &scid, NULL, &value);
+ 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);
+ assert_int_equal(result, ISC_R_UNEXPECTED);
+ assert_null(value);
+
+ result = isc_quic_router_add_stateless_reset(
+ router, stateless_reset_token, connection_tid, router);
+ 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);
+ assert_int_equal(result, ISC_R_CONNECTIONRESET);
+ test_conn_unref(value);
+
+ 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_TEST_LIST_START
+ISC_TEST_ENTRY(isc_quic_router_cid)
+ISC_TEST_ENTRY(isc_quic_router_stateless_reset)
+ISC_TEST_ENTRY(isc_quic_router_packet)
+ISC_TEST_LIST_END
+
+ISC_TEST_MAIN