From: Alexandr Nedvedicky Date: Tue, 21 Apr 2026 12:13:03 +0000 (+0200) Subject: Add test for path challenge flood mitigation X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=68ef88913e321bdb47eee88118ceba18ed2388cd;p=thirdparty%2Fopenssl.git 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 Reviewed-by: Tomas Mraz MergeDate: Mon Jun 8 14:35:21 2026 --- diff --git a/include/internal/quic_channel.h b/include/internal/quic_channel.h index 9abcee458c8..cfe7a6005c8 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 d63b395676c..aaabf5a432e 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 0d59165811a..7475f623c96 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 7961a8bfd70..730b0cf621b 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 5544a9d3db1..19188967213 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), };