]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
Add test for path challenge flood mitigation
authorAlexandr Nedvedicky <sashan@openssl.org>
Tue, 21 Apr 2026 12:13:03 +0000 (14:13 +0200)
committerTomas Mraz <tomas@openssl.foundation>
Thu, 11 Jun 2026 15:08:41 +0000 (17:08 +0200)
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

include/internal/quic_channel.h
ssl/quic/quic_channel.c
ssl/quic/quic_channel_local.h
ssl/quic/quic_rx_depack.c
test/radix/quic_tests.c

index 9abcee458c8acb7717734b3340ecf24f712a53a4..cfe7a6005c82a767095a19119d94722cf3ab3ded 100644 (file)
@@ -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
index d63b395676c1dc16867b922ff6d2cc5e5d1ce5de..aaabf5a432eb71c4f210e336f1c15d3fe96d02d1 100644 (file)
@@ -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;
+}
index 0d59165811adfa3e0b7ce0520c2ef393827a4154..7475f623c961d6be3dcf4a10fe10ef1120b19e61 100644 (file)
@@ -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
index 7961a8bfd701bf402851b84112b96628b1dcf16f..730b0cf621bb0b83c94355ec1959eef9eb1c74df 100644 (file)
@@ -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:
index 5544a9d3db1e6abb3460c3da1d664bc66c25423d..191889672130e3b7491d04154febe049f26d593b 100644 (file)
@@ -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),
 };