diff --git a/include/internal/quic_cfq.h b/include/internal/quic_cfq.h index 0b2a3a4cb2d..96c8d89eb60 100644 --- a/include/internal/quic_cfq.h +++ b/include/internal/quic_cfq.h @@ -149,6 +149,7 @@ QUIC_CFQ_ITEM *ossl_quic_cfq_get_priority_head(const QUIC_CFQ *cfq, QUIC_CFQ_ITEM *ossl_quic_cfq_item_get_priority_next(const QUIC_CFQ_ITEM *item, uint32_t pn_space); +int ossl_quic_cfq_discard_unreliable(QUIC_CFQ *cfq, QUIC_CFQ_ITEM *item); #endif #endif diff --git a/include/internal/quic_channel.h b/include/internal/quic_channel.h index b08e63e148a..d333bc3e5e5 100644 --- a/include/internal/quic_channel.h +++ b/include/internal/quic_channel.h @@ -466,6 +466,9 @@ uint64_t ossl_quic_channel_get_max_idle_timeout_actual(const QUIC_CHANNEL *ch); const QUIC_CONN_ID *scid, const QUIC_CONN_ID *dcid, const QUIC_CONN_ID *odcid); +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/include/internal/quic_fifd.h b/include/internal/quic_fifd.h index 4ea7a2e0d22..afa330cbc4a 100644 --- a/include/internal/quic_fifd.h +++ b/include/internal/quic_fifd.h @@ -83,6 +83,7 @@ int ossl_quic_fifd_pkt_commit(QUIC_FIFD *fifd, QUIC_TXPIM_PKT *pkt); void ossl_quic_fifd_set_qlog_cb(QUIC_FIFD *fifd, QLOG *(*get_qlog_cb)(void *arg), void *arg); +void ossl_quic_fifd_pkt_discard_unreliable(QUIC_FIFD *fifd, QUIC_TXPIM_PKT *tpkt); #endif #endif diff --git a/ssl/quic/quic_cfq.c b/ssl/quic/quic_cfq.c index 3c59234ff0f..16818e55f57 100644 --- a/ssl/quic/quic_cfq.c +++ b/ssl/quic/quic_cfq.c @@ -7,6 +7,7 @@ * https://www.openssl.org/source/license.html */ +#include "internal/quic_channel.h" #include "internal/quic_cfq.h" #include "internal/numbers.h" @@ -307,6 +308,20 @@ void ossl_quic_cfq_mark_lost(QUIC_CFQ *cfq, QUIC_CFQ_ITEM *item, } } +int ossl_quic_cfq_discard_unreliable(QUIC_CFQ *cfq, QUIC_CFQ_ITEM *item) +{ + int discarded; + + if (ossl_quic_cfq_item_is_unreliable(item)) { + ossl_quic_cfq_release(cfq, item); + discarded = 1; + } else { + discarded = 0; + } + + return discarded; +} + /* * Releases a CFQ item. The item may be in either state (NEW or TX) prior to the * call. The QUIC_CFQ_ITEM pointer must not be used following this call. diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c index 631f289b391..9534cbcaf7f 100644 --- a/ssl/quic/quic_channel.c +++ b/ssl/quic/quic_channel.c @@ -2213,6 +2213,12 @@ static void ch_rx_check_forged_pkt_limit(QUIC_CHANNEL *ch) "forgery limit"); } +void ossl_ch_reset_rx_state(QUIC_CHANNEL *ch) +{ + ch->did_crypto_frame = 0; + ch->seen_path_challenge = 0; +} + /* Process queued incoming packets and handle frames, if any. */ static int ch_rx(QUIC_CHANNEL *ch, int channel_only, int *notify_other_threads) { @@ -4060,3 +4066,13 @@ uint64_t ossl_quic_channel_get_max_idle_timeout_actual(const QUIC_CHANNEL *ch) { return ch->max_idle_timeout; } + +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 ae443fccca1..eb082d6cea7 100644 --- a/ssl/quic/quic_channel_local.h +++ b/ssl/quic/quic_channel_local.h @@ -12,6 +12,28 @@ #include "internal/quic_stream_map.h" #include "internal/quic_tls.h" +/* + * This is a part of PATH_CHALLENGE flood [1] mitigation. This limits the + * number of PATH_CHALLENGE frames QUIC stack is willing to process for + * connection. Local QUIC stack creates PATH_RESPONSE frame for PATH_CHALLENGE + * frame it receives from remote peer. The response frame is put Control Frame + * Queue waiting to be dispatched. The PATH_RESPONSE frame is removed from CFQ + * after it is dispatched. The QUIC_PATH_RESPONSE_QLEN limits the number of + * PATH_RESPONSE frames waiting to be dispatched. No new PATH_RESPONSE frames + * are inserted into CFQ if queue limit is exceeded. + * + * QUIC implementations use different limits for PATH_RESPONSE queue lengths: + * quic-go defines maxPathResponses as 256 + * quiche from cloadflare sets DEFAULT_MAX_PATH_CHALLENGE_RX_QUEUE_LEN to 3 + * t-quic from tencent chooses MAX_PATH_CHALS_RECV to be 8 + * + * OpenSSL here introduces QUIC_PATH_RESPONSE_QLEN as 32. + * + * [1] https://www.ietf.org/archive/id/draft-chen-quic-logical-vuln-mitigations-00.txt + * (section 4.2) + */ +#define QUIC_PATH_RESPONSE_QLEN 32 + /* * QUIC Channel Structure * ====================== @@ -457,6 +479,18 @@ struct quic_channel_st { /* Has qlog been requested? */ unsigned int is_tserver_ch : 1; + /* + * RFC 9000 Section 9.2.1 says: + * However, an endpoint SHOULD NOT send multiple + * PATH_CHALLENGE frames in a single packet. + * The counter here allows us to detect multiple presence + * of PATH_CHALLENGE frame in packet. We process only the + * first PATH_CHALLENGE frame found in packet. Remaining PATH_CHALLENGE + * frames are ignored. + * seen_path_challenge flag is always reset before + * ossl_quic_handle_frames() gets called. + */ + unsigned int seen_path_challenge : 1; /* Saved error stack in case permanent error was encountered */ ERR_STATE *err_state; @@ -467,6 +501,15 @@ struct quic_channel_st { /* Title for qlog purposes. We own this copy. */ char *qlog_title; + /* + * number of path responses waiting to be dispatched + * 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_fifd.c b/ssl/quic/quic_fifd.c index 03b8cebd305..e80483b501d 100644 --- a/ssl/quic/quic_fifd.c +++ b/ssl/quic/quic_fifd.c @@ -310,3 +310,46 @@ void ossl_quic_fifd_set_qlog_cb(QUIC_FIFD *fifd, QLOG *(*get_qlog_cb)(void *arg) fifd->get_qlog_cb = get_qlog_cb; fifd->get_qlog_cb_arg = get_qlog_cb_arg; } + +static void txpim_pkt_remove_cfq_item(QUIC_TXPIM_PKT *pkt, QUIC_CFQ_ITEM *cfq_item) +{ + QUIC_CFQ_ITEM *prev = cfq_item->pkt_prev; + + if (prev != NULL) { + prev->pkt_next = cfq_item->pkt_next; + } else { + pkt->retx_head = cfq_item->pkt_next; + } + + if (cfq_item->pkt_next != NULL) + cfq_item->pkt_next->pkt_prev = prev; + + cfq_item->pkt_prev = NULL; + cfq_item->pkt_next = NULL; +} + +void ossl_quic_fifd_pkt_discard_unreliable(QUIC_FIFD *fifd, QUIC_TXPIM_PKT *pkt) +{ + QUIC_CFQ_ITEM *cfq_item, *cfq_next; + + /* + * The packet has been written to network. We can discard frames we don't + * retransmit when loss is detected. + */ + cfq_item = pkt->retx_head; + while (cfq_item != NULL) { + /* + * Discarded items are moved to free list. If item + * got moved to free list we must also remove it from + * cfq list kept in pkt, so ACKM does not find it when + * receives an ACK for pkt. + */ + if (ossl_quic_cfq_discard_unreliable(fifd->cfq, cfq_item)) { + cfq_next = cfq_item->pkt_next; + txpim_pkt_remove_cfq_item(pkt, cfq_item); + cfq_item = cfq_next; + } else { + cfq_item = cfq_item->pkt_next; + } + } +} diff --git a/ssl/quic/quic_rx_depack.c b/ssl/quic/quic_rx_depack.c index 786af9b4c22..7ab59f01a1c 100644 --- a/ssl/quic/quic_rx_depack.c +++ b/ssl/quic/quic_rx_depack.c @@ -931,6 +931,22 @@ static int depack_do_frame_retire_conn_id(PACKET *pkt, static void free_path_response(unsigned char *buf, size_t buf_len, void *arg) { + QUIC_CHANNEL *ch = (QUIC_CHANNEL *)arg; + + assert(ch->path_response_limit > 0); + + 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); } @@ -951,33 +967,41 @@ static int depack_do_frame_path_challenge(PACKET *pkt, return 0; } - /* - * RFC 9000 s. 8.2.2: On receiving a PATH_CHALLENGE frame, an endpoint MUST - * respond by echoing the data contained in the PATH_CHALLENGE frame in a - * PATH_RESPONSE frame. - * - * TODO(QUIC FUTURE): We should try to avoid allocation here in the future. - */ - encoded_len = sizeof(uint64_t) + 1; - if ((encoded = OPENSSL_malloc(encoded_len)) == NULL) - goto err; + if (ch->seen_path_challenge == 0 + && ch->path_response_limit < QUIC_PATH_RESPONSE_QLEN) { + /* + * RFC 9000 s. 8.2.2: On receiving a PATH_CHALLENGE frame, an endpoint + * MUST respond by echoing the data contained in the PATH_CHALLENGE + * frame in a PATH_RESPONSE frame. + * + * TODO(QUIC FUTURE): We should try to avoid allocation here in the + * future. + */ + encoded_len = sizeof(uint64_t) + 1; + if ((encoded = OPENSSL_malloc(encoded_len)) == NULL) + goto err; - if (!WPACKET_init_static_len(&wpkt, encoded, encoded_len, 0)) - goto err; + if (!WPACKET_init_static_len(&wpkt, encoded, encoded_len, 0)) + goto err; - if (!ossl_quic_wire_encode_frame_path_response(&wpkt, frame_data)) { - WPACKET_cleanup(&wpkt); - goto err; - } + if (!ossl_quic_wire_encode_frame_path_response(&wpkt, frame_data)) { + WPACKET_cleanup(&wpkt); + goto err; + } - WPACKET_finish(&wpkt); + WPACKET_finish(&wpkt); - if (!ossl_quic_cfq_add_frame(ch->cfq, 0, QUIC_PN_SPACE_APP, - OSSL_QUIC_FRAME_TYPE_PATH_RESPONSE, - QUIC_CFQ_ITEM_FLAG_UNRELIABLE, - encoded, encoded_len, - free_path_response, NULL)) - goto err; + if (!ossl_quic_cfq_add_frame(ch->cfq, 0, QUIC_PN_SPACE_APP, + OSSL_QUIC_FRAME_TYPE_PATH_RESPONSE, + QUIC_CFQ_ITEM_FLAG_UNRELIABLE, + encoded, encoded_len, + free_path_response, ch)) + goto err; + ch->seen_path_challenge = 1; + ch->path_response_limit++; + } + + ch->path_challenge_rx++; return 1; @@ -1432,7 +1456,7 @@ int ossl_quic_handle_frames(QUIC_CHANNEL *ch, OSSL_QRX_PKT *qpacket) if (ch == NULL) return 0; - ch->did_crypto_frame = 0; + ossl_ch_reset_rx_state(ch); /* Initialize |ackm_data| (and reinitialize |ok|)*/ memset(&ackm_data, 0, sizeof(ackm_data)); diff --git a/ssl/quic/quic_txp.c b/ssl/quic/quic_txp.c index 44aaad868d2..b2565c1a9fe 100644 --- a/ssl/quic/quic_txp.c +++ b/ssl/quic/quic_txp.c @@ -3133,6 +3133,8 @@ static int txp_pkt_commit(OSSL_QUIC_TX_PACKETISER *txp, --probe_info->pto[pn_space]; } + ossl_quic_fifd_pkt_discard_unreliable(&txp->fifd, tpkt); + return rc; } diff --git a/test/radix/quic_tests.c b/test/radix/quic_tests.c index 181ba1226b8..48b1df6c1d4 100644 --- a/test/radix/quic_tests.c +++ b/test/radix/quic_tests.c @@ -294,6 +294,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("mutcbk_inject_frames() not enough space to inject"); + 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("mutcbk_inject_frames() OPENSSL_malloc() failed"); + 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 * ============================================================================ @@ -302,5 +484,6 @@ static SCRIPT_INFO *const scripts[] = { USE(simple_conn) USE(simple_thread) USE(ssl_poll) - USE(check_cwm) + USE(check_cwm) + USE(check_pc_flood) };