Commit 68ef88913e for openssl.org
commit 68ef88913e321bdb47eee88118ceba18ed2388cd
Author: Alexandr Nedvedicky <sashan@openssl.org>
Date: Tue Apr 21 14:13:03 2026 +0200
Add test for path challenge flood mitigation
client injects 16 path challenge frames. Those are received
by server. Only one challenge frame of 16 received triggers
path challenge response. Remaining challenge frames are
discrded/ignored.
Test introduces two counters to channel object:
- path_challenge_rx which is bumped for every patch challenge
frame received
- path_response_tx which is bumped for every path response
frame transmitted
Succesuful test verifies server receives 16 path challenge frames,
but sends just one path response frmae as response.
Reviewed-by: Neil Horman <nhorman@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
MergeDate: Mon Jun 8 14:35:21 2026
diff --git a/include/internal/quic_channel.h b/include/internal/quic_channel.h
index 9abcee458c..cfe7a6005c 100644
--- a/include/internal/quic_channel.h
+++ b/include/internal/quic_channel.h
@@ -545,6 +545,8 @@ void ossl_quic_channel_set_tcause(QUIC_CHANNEL *ch, uint64_t app_error_code,
const char *app_reason);
void ossl_ch_reset_rx_state(QUIC_CHANNEL *ch);
+uint64_t ossl_quic_channel_get_path_challenge_count(const QUIC_CHANNEL *ch);
+uint64_t ossl_quic_channel_get_path_response_count(const QUIC_CHANNEL *ch);
#endif
#endif
diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c
index d63b395676..aaabf5a432 100644
--- a/ssl/quic/quic_channel.c
+++ b/ssl/quic/quic_channel.c
@@ -4361,3 +4361,13 @@ uint64_t ossl_quic_channel_get_active_conn_id_limit_peer_request(const QUIC_CHAN
{
return ch->rx_active_conn_id_limit;
}
+
+uint64_t ossl_quic_channel_get_path_challenge_count(const QUIC_CHANNEL *ch)
+{
+ return ch->path_challenge_rx;
+}
+
+uint64_t ossl_quic_channel_get_path_response_count(const QUIC_CHANNEL *ch)
+{
+ return ch->path_response_tx;
+}
diff --git a/ssl/quic/quic_channel_local.h b/ssl/quic/quic_channel_local.h
index 0d59165811..7475f623c9 100644
--- a/ssl/quic/quic_channel_local.h
+++ b/ssl/quic/quic_channel_local.h
@@ -530,6 +530,10 @@ struct quic_channel_st {
* from control frame queue (CFQ)
*/
unsigned int path_response_limit;
+ /* number of path challenge frames received */
+ unsigned int path_challenge_rx;
+ /* number of path response frames sent */
+ unsigned int path_response_tx;
};
#endif
diff --git a/ssl/quic/quic_rx_depack.c b/ssl/quic/quic_rx_depack.c
index 7961a8bfd7..730b0cf621 100644
--- a/ssl/quic/quic_rx_depack.c
+++ b/ssl/quic/quic_rx_depack.c
@@ -937,6 +937,16 @@ static void free_path_response(unsigned char *buf, size_t buf_len, void *arg)
ch->path_response_limit--;
+ /*
+ * Assume path response frame is being freed on behalf of
+ * finished TX operation. This is for unit testing purposes
+ * only. The counter is also bumped when channel is being
+ * destroyed and CFQ (control frame queue) is freed.
+ * This currently does not matter for check_pc_flood
+ * in test/radix/quic_tests.c.
+ */
+ ch->path_response_tx++;
+
OPENSSL_free(buf);
}
@@ -991,6 +1001,8 @@ static int depack_do_frame_path_challenge(PACKET *pkt,
ch->path_response_limit++;
}
+ ch->path_challenge_rx++;
+
return 1;
err:
diff --git a/test/radix/quic_tests.c b/test/radix/quic_tests.c
index 5544a9d3db..1918896721 100644
--- a/test/radix/quic_tests.c
+++ b/test/radix/quic_tests.c
@@ -311,6 +311,188 @@ DEF_SCRIPT(check_cwm, "check stream obeys cwm")
OP_WRITE_FAIL(C);
}
+struct mutcbk_ctx {
+ QUIC_PKT_HDR mutctx_qhdrin;
+ OSSL_QTX_IOVEC mutctx_iov;
+ const unsigned char *mutctx_inject;
+ size_t mutctx_inject_sz;
+ int mutctx_done;
+};
+
+static int mutcbk_inject_frames(const QUIC_PKT_HDR *hdrin,
+ const OSSL_QTX_IOVEC *iovecin, size_t numin, QUIC_PKT_HDR **hdrout,
+ const OSSL_QTX_IOVEC **iovecout, size_t *numout, void *arg)
+{
+ struct mutcbk_ctx *mutctx = (struct mutcbk_ctx *)arg;
+ size_t i;
+ size_t grow_allowance = 1200; /* QUIC_MIN_INITIAL_DGRAM_LEN */
+ size_t bufsz = 0;
+ char *buf;
+
+ /*
+ * make injection callback a one shot event,
+ * callback is invoked for every packet we
+ * want to modify only one packet here.
+ */
+ if (mutctx->mutctx_done)
+ return 0;
+
+ mutctx->mutctx_done = 1;
+
+ for (i = 0; i < numin; i++)
+ bufsz += iovecin[i].buf_len;
+
+ mutctx->mutctx_iov.buf_len = bufsz; /* keeps old size */
+ grow_allowance -= (bufsz < grow_allowance) ? bufsz : grow_allowance;
+ /* AEAD tag (16 bytes) + long header (14 bytes) */
+ grow_allowance -= (30 < grow_allowance) ? 30 : grow_allowance;
+
+ grow_allowance -= (hdrin->dst_conn_id.id_len < grow_allowance) ? hdrin->dst_conn_id.id_len : grow_allowance;
+ grow_allowance -= (hdrin->src_conn_id.id_len < grow_allowance) ? hdrin->src_conn_id.id_len : grow_allowance;
+
+ if (grow_allowance == 0) {
+ TEST_info("%s not enough space to inject", __func__);
+ return 0;
+ }
+ bufsz += grow_allowance;
+
+ /* discard const */
+ OPENSSL_free((char *)mutctx->mutctx_iov.buf);
+ mutctx->mutctx_iov.buf = OPENSSL_malloc(bufsz);
+ /* discard const */
+ buf = (char *)mutctx->mutctx_iov.buf;
+ if (buf == NULL) {
+ TEST_info("%s OPENSSL_malloc() failed", __func__);
+ return 0;
+ }
+
+ for (i = 0; i < numin; i++) {
+ memcpy(buf, iovecin[i].buf, iovecin[i].buf_len);
+ buf += iovecin[i].buf_len;
+ }
+
+ /* discard const */
+ buf = (char *)mutctx->mutctx_iov.buf;
+ if (mutctx->mutctx_inject != NULL) {
+ memmove(buf + mutctx->mutctx_inject_sz, buf,
+ mutctx->mutctx_iov.buf_len);
+ memcpy(buf, mutctx->mutctx_inject, mutctx->mutctx_inject_sz);
+ }
+ /*
+ * perhaps needed to have not looked at yet
+ */
+ mutctx->mutctx_qhdrin = *hdrin;
+ *hdrout = &mutctx->mutctx_qhdrin;
+ mutctx->mutctx_iov.buf_len += mutctx->mutctx_inject_sz;
+ *iovecout = &mutctx->mutctx_iov;
+ *numout = 1;
+
+ return 1;
+}
+
+static void mutcbk_finish_injecct_frames(void *arg)
+{
+ struct mutcbk_ctx *mutctx = (struct mutcbk_ctx *)arg;
+
+ OPENSSL_free((char *)mutctx->mutctx_iov.buf);
+ mutctx->mutctx_iov.buf = NULL;
+}
+
+/* 16 path challenge frames */
+#define PATH_CHALLENGE_FRAMES \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH" \
+ "\x1a" \
+ "ABCDEFGH"
+
+DEF_FUNC(mount_flood)
+{
+ int ok = 0;
+ SSL *ssl;
+ QUIC_CHANNEL *ch;
+ static struct mutcbk_ctx mutctx = { 0 };
+ static const unsigned char *inject_frames = (const unsigned char *)PATH_CHALLENGE_FRAMES;
+
+ mutctx.mutctx_inject = inject_frames;
+ mutctx.mutctx_inject_sz = sizeof(PATH_CHALLENGE_FRAMES) - 1;
+ REQUIRE_SSL(ssl);
+ ch = ossl_quic_conn_get_channel(ssl);
+ if (!TEST_ptr(ch))
+ goto err;
+
+ if (!TEST_true(ossl_quic_channel_set_mutator(ch, mutcbk_inject_frames,
+ mutcbk_finish_injecct_frames, &mutctx)))
+ goto err;
+ ok = 1;
+err:
+ return ok;
+}
+
+DEF_FUNC(check_flood_stats)
+{
+ int ok = 0;
+ SSL *ssl;
+ QUIC_CHANNEL *ch;
+ uint64_t path_response_count;
+ uint64_t path_challenge_count;
+
+ REQUIRE_SSL(ssl);
+ ch = ossl_quic_conn_get_channel(ssl);
+ if (!TEST_ptr(ch))
+ goto err;
+
+ path_challenge_count = ossl_quic_channel_get_path_challenge_count(ch);
+ path_response_count = ossl_quic_channel_get_path_response_count(ch);
+
+ if (TEST_uint64_t_ne(path_challenge_count, 16))
+ goto err;
+ if (TEST_uint64_t_ne(path_response_count, 1))
+ goto err;
+
+ ok = 1;
+err:
+ return ok;
+}
+
+DEF_SCRIPT(check_pc_flood, "check path challenge flood")
+{
+ OP_SIMPLE_PAIR_CONN();
+ OP_SELECT_SSL(0, C);
+ OP_FUNC(mount_flood);
+ OP_ACCEPT_CONN_WAIT(L, S, 0);
+ OP_WRITE_B(C, "attack");
+ OP_SELECT_SSL(0, S);
+ OP_FUNC(check_flood_stats);
+}
+
/*
* List of Test Scripts
* ============================================================================
@@ -321,4 +503,5 @@ static SCRIPT_INFO *const scripts[] = {
USE(simple_thread),
USE(ssl_poll),
USE(check_cwm),
+ USE(check_pc_flood),
};