From 0ede517cfa73fd3566d2ecd32215b4b12dd1d3b5 Mon Sep 17 00:00:00 2001 From: Hugo Landau Date: Thu, 15 Sep 2022 12:48:50 +0100 Subject: [PATCH] QUIC FIFD Reviewed-by: Tomas Mraz Reviewed-by: Matt Caswell (Merged from https://github.com/openssl/openssl/pull/19206) --- .../quic-design/images/quic-overview.odg | Bin 32160 -> 22501 bytes .../quic-design/images/quic-overview.svg | 196 ++++++---- doc/designs/quic-design/quic-fifm.md | 32 +- doc/designs/quic-design/quic-overview.md | 22 +- include/internal/quic_fifd.h | 56 +++ include/internal/quic_txpim.h | 9 +- ssl/quic/build.info | 2 +- ssl/quic/quic_cfq.c | 6 + ssl/quic/quic_fifd.c | 202 ++++++++++ ssl/quic/quic_txpim.c | 12 +- test/build.info | 6 +- test/quic_fifd_test.c | 358 ++++++++++++++++++ test/recipes/70-test_quic_fifd.t | 19 + 13 files changed, 815 insertions(+), 105 deletions(-) create mode 100644 include/internal/quic_fifd.h create mode 100644 ssl/quic/quic_fifd.c create mode 100644 test/quic_fifd_test.c create mode 100644 test/recipes/70-test_quic_fifd.t diff --git a/doc/designs/quic-design/images/quic-overview.odg b/doc/designs/quic-design/images/quic-overview.odg index a844a0deb7100e1267d6ff42d2c5d930123b848b..c8a055760bb2cdbde439d4814707405b584eb39f 100644 GIT binary patch delta 20966 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 literal 32160 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 - + - + - - + + - - - - - + + + + + - + - + - + - - + + - - - - - - - - + + + + + + + + - - - - - - + + + + + + - - - + + + + - + @@ -126,7 +127,7 @@ - Kernel + Kernel @@ -134,7 +135,7 @@ - SSL API + SSL API @@ -142,7 +143,7 @@ - StreamSend Buffers + StreamSend Buffers @@ -150,15 +151,15 @@ - StreamRead Buffers + StreamRead Buffers - - - - ConnectionState Machine + + + + ConnectionState Machine @@ -166,7 +167,7 @@ - TLS HandshakeRecord Layer + TLS HandshakeRecord Layer @@ -174,7 +175,7 @@ - TX Packetizer + TX Packetizer @@ -182,7 +183,7 @@ - RX Depacketizer + RX Depacketizer @@ -190,7 +191,7 @@ - QUIC WriteRecord Layer + QUIC WriteRecord Layer @@ -198,7 +199,7 @@ - QUIC ReadRecord Layer + QUIC ReadRecord Layer @@ -206,7 +207,7 @@ - ConnectionID Cache + ConnectionID Cache @@ -214,7 +215,7 @@ - DatagramBIO + DatagramBIO @@ -222,7 +223,7 @@ - DatagramBIO + DatagramBIO @@ -230,7 +231,7 @@ - DatagramBIO + DatagramBIO @@ -238,7 +239,7 @@ - UDP + UDP @@ -246,7 +247,7 @@ - Hardware Interfaces + Hardware Interfaces @@ -254,7 +255,7 @@ - UDP + UDP @@ -262,7 +263,7 @@ - UDP + UDP @@ -294,7 +295,7 @@ - DatagramBIO + DatagramBIO @@ -302,7 +303,7 @@ - UDP + UDP @@ -346,7 +347,7 @@ - Path And ConnDemultiplexer + Path And ConnDemultiplexer @@ -428,7 +429,7 @@ - CongestionController + CongestionController @@ -436,7 +437,7 @@ - New Reno + New Reno @@ -462,26 +463,26 @@ - - - - + + + + - - - - Timer AndEvent Queue + + + + Timer AndEvent Queue - - - - + + + + @@ -489,7 +490,7 @@ - Flow Controller AndStatistics Collector + Flow Controller AndStatistics Collector @@ -512,7 +513,7 @@ - ACK Handling AndLoss Detector + ACK Handling AndLoss Detector @@ -524,9 +525,9 @@ - - - + + + @@ -566,18 +567,18 @@ - - - - + + + + - - - - + + + + @@ -603,6 +604,35 @@ + + + + + + Frame-in-FlightManagement + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/designs/quic-design/quic-fifm.md b/doc/designs/quic-design/quic-fifm.md index 1de81318f3b..f03b5d7d368 100644 --- a/doc/designs/quic-design/quic-fifm.md +++ b/doc/designs/quic-design/quic-fifm.md @@ -133,7 +133,7 @@ metadata associated with it: to the `TX` state. If the packet it was sent in is subsequently lost, it is transitioned back to the `NEW` state. -Packets in the `NEW` state participate in a priority queue (the NEW queue) +Frames in the `NEW` state participate in a priority queue (the NEW queue) according to their priority and the CFQ's NEW queue can be iterated in priority order by callers. @@ -304,9 +304,9 @@ allocation made per transmitted packet. The TX packetiser will obtain a `QUIC_TXPIM_PKT` structure from the TXPIM, fill in the structure including the ACK Manager data, and submit it via the FIFD which we introduce below. -The TXPIM does not anything with the `QUIC_TXPIM_PKT` structure itself -other than managing its allocation and manipulation. Constructive -use of the data kept in the TXPIM is made by the FIFD. +The TXPIM does do not anything with the `QUIC_TXPIM_PKT` structure itself other +than managing its allocation and manipulation. Constructive use of the data kept +in the TXPIM is made by the FIFD. ### API @@ -328,10 +328,11 @@ typedef struct quic_txpim_pkt_st { QUIC_FIFD *fifd; /* Regenerate-strategy frames. */ - unsigned int had_handshake_done : 1; - unsigned int had_max_data_frame : 1; - unsigned int had_max_streams_frame : 1; - unsigned int had_ack_frame : 1; + unsigned int had_handshake_done : 1; + unsigned int had_max_data_frame : 1; + unsigned int had_max_streams_bidi_frame : 1; + unsigned int had_max_streams_uni_frame : 1; + unsigned int had_ack_frame : 1; /* Private data follows. */ } QUIC_TXPIM_PKT; @@ -392,6 +393,12 @@ const QUIC_TXPIM_CHUNK *ossl_quic_txpim_pkt_get_chunks(QUIC_TXPIM_PKT *fpkt); * ossl_quic_txpim_pkt_get_chunks(). */ size_t ossl_quic_txpim_pkt_get_num_chunks(QUIC_TXPIM_PKT *fpkt); + +/* + * Returns the number of QUIC_TXPIM_PKTs allocated by the given TXPIM that have + * yet to be returned to the TXPIM. + */ +size_t ossl_quic_txpim_get_in_use(QUIC_TXPIM *txpim); ``` The Frame-in-Flight Dispatcher (FIFD) @@ -432,7 +439,7 @@ simply glues all of these parts together. ### API ```c -typedef struct quic_fifm_st { +typedef struct quic_fifd_st { /* (internals) */ } QUIC_FIFD; @@ -476,9 +483,6 @@ Typical Intended TX Packetiser Usage all CFQ frames are considered of higher priority). For each such frame it places in a packet, it: - - informs the CFQ that the CFQ item has been transmitted, causing a - transition of the CFQ item to the `TX` state; - - calls `ossl_quic_txpim_pkt_add_cfq_item()` on the TXPIM to log the CFQ item as having been transmitted in the given packet, so that the CFQ item can be released or requeued depending on the ultimate fate of the packet. @@ -497,7 +501,9 @@ Typical Intended TX Packetiser Usage - TX Packetiser calls `ossl_quic_fifd_pkt_commit()`. The FIFD takes care of submitting the packet to the ACK Manager and provides its own callback - implementation. + implementation. It also takes care of informing the CFQ that any CFQ items + which were added via `ossl_quic_txpim_pkt_add_cfq_item()` have been + transmitted. In the event of packet loss, ACK or discard, the appropriate QUIC Send Stream, CFQ and regenerate callback calls are made. Regardless of the outcome, the diff --git a/doc/designs/quic-design/quic-overview.md b/doc/designs/quic-design/quic-overview.md index 141abcb9bca..2ef43cefc51 100644 --- a/doc/designs/quic-design/quic-overview.md +++ b/doc/designs/quic-design/quic-overview.md @@ -21,6 +21,13 @@ SSL_read and SSL_write functions. They will be bypassed with a single-copy API for read and write (_not for MVP_). +Frame in Flight Manager +----------------------- + +The frame in flight manager manages the queueing of frames which may need to be +retransmitted if the packets in which they were transmitted were lost. It is +[discussed in more detail here.](./quic-fifm.md) + Connection State Machine ------------------------ @@ -65,12 +72,17 @@ either as data or as events to the subsequent modules based on the frame type. Flow Controller And Statistics Collector is consulted for decisions and to record the statistics of the received stream data. -Flow Controller And Statistics Collector ----------------------------------------- +Flow Controller +--------------- + +This module is consulted by the TX Packetizer and RX Frame Handler for flow +control decisions at both the stream and connection levels. + +Statistics Collector +-------------------- -This module collects various statistics about send and received -stream data. It is also consulted by the TX Packetizer and RX Frame -Handler for flow control decisions. +This module maintains statistics about a connection, most notably the estimated +round trip time to the remote peer. QUIC Write Record Layer ----------------------- diff --git a/include/internal/quic_fifd.h b/include/internal/quic_fifd.h new file mode 100644 index 00000000000..15952e43d89 --- /dev/null +++ b/include/internal/quic_fifd.h @@ -0,0 +1,56 @@ +/* + * Copyright 2022 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the Apache License 2.0 (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#ifndef OSSL_QUIC_FIFD_H +# define OSSL_QUIC_FIFD_H + +# include +# include "internal/quic_types.h" +# include "internal/quic_cfq.h" +# include "internal/quic_ackm.h" +# include "internal/quic_txpim.h" +# include "internal/quic_stream.h" + +/* + * QUIC Frame-in-Flight Dispatcher (FIFD) + * ====================================== + */ +struct quic_fifd_st { + /* Internal data; use the ossl_quic_fifd functions. */ + QUIC_CFQ *cfq; + OSSL_ACKM *ackm; + QUIC_TXPIM *txpim; + QUIC_SSTREAM *(*get_sstream_by_id)(uint64_t stream_id, + void *arg); + void *get_sstream_by_id_arg; + void (*regen_frame)(uint64_t frame_type, + uint64_t stream_id, + void *arg); + void *regen_frame_arg; +}; + +int ossl_quic_fifd_init(QUIC_FIFD *fifd, + QUIC_CFQ *cfq, + OSSL_ACKM *ackm, + QUIC_TXPIM *txpim, + /* stream_id is UINT64_MAX for the crypto stream */ + QUIC_SSTREAM *(*get_sstream_by_id)(uint64_t stream_id, + void *arg), + void *get_sstream_by_id_arg, + /* stream_id is UINT64_MAX if not applicable */ + void (*regen_frame)(uint64_t frame_type, + uint64_t stream_id, + void *arg), + void *regen_frame_arg); + +void ossl_quic_fifd_cleanup(QUIC_FIFD *fifd); /* (no-op) */ + +int ossl_quic_fifd_pkt_commit(QUIC_FIFD *fifd, QUIC_TXPIM_PKT *pkt); + +#endif diff --git a/include/internal/quic_txpim.h b/include/internal/quic_txpim.h index 623004e79da..087d13363fb 100644 --- a/include/internal/quic_txpim.h +++ b/include/internal/quic_txpim.h @@ -35,7 +35,8 @@ typedef struct quic_txpim_pkt_st { /* Regenerate-strategy frames. */ unsigned int had_handshake_done_frame : 1; unsigned int had_max_data_frame : 1; - unsigned int had_max_streams_frame : 1; + unsigned int had_max_streams_bidi_frame : 1; + unsigned int had_max_streams_uni_frame : 1; unsigned int had_ack_frame : 1; /* Private data follows. */ @@ -106,4 +107,10 @@ const QUIC_TXPIM_CHUNK *ossl_quic_txpim_pkt_get_chunks(QUIC_TXPIM_PKT *fpkt); */ size_t ossl_quic_txpim_pkt_get_num_chunks(QUIC_TXPIM_PKT *fpkt); +/* + * Returns the number of QUIC_TXPIM_PKTs allocated by the given TXPIM that have + * yet to be returned to the TXPIM. + */ +size_t ossl_quic_txpim_get_in_use(QUIC_TXPIM *txpim); + #endif diff --git a/ssl/quic/build.info b/ssl/quic/build.info index 1f228cb6d74..45440384e11 100644 --- a/ssl/quic/build.info +++ b/ssl/quic/build.info @@ -5,4 +5,4 @@ SOURCE[$LIBSSL]=cc_dummy.c quic_demux.c quic_record_rx.c SOURCE[$LIBSSL]=quic_record_tx.c quic_record_util.c quic_record_shared.c quic_wire_pkt.c SOURCE[$LIBSSL]=quic_record_rx_wrap.c quic_rx_depack.c SOURCE[$LIBSSL]=quic_fc.c uint_set.c quic_stream.c -SOURCE[$LIBSSL]=quic_cfq.c quic_txpim.c +SOURCE[$LIBSSL]=quic_cfq.c quic_txpim.c quic_fifd.c diff --git a/ssl/quic/quic_cfq.c b/ssl/quic/quic_cfq.c index cdd621458c4..0b0651289a5 100644 --- a/ssl/quic/quic_cfq.c +++ b/ssl/quic/quic_cfq.c @@ -320,6 +320,9 @@ QUIC_CFQ_ITEM *ossl_quic_cfq_get_priority_head(QUIC_CFQ *cfq, uint32_t pn_space) for (; item != NULL && item->pn_space != pn_space; item = item->next); + if (item == NULL) + return NULL; /* ubsan */ + return &item->public; } @@ -335,5 +338,8 @@ QUIC_CFQ_ITEM *ossl_quic_cfq_item_get_priority_next(QUIC_CFQ_ITEM *item, for (; ex != NULL && ex->pn_space != pn_space; ex = ex->next); + if (ex == NULL) + return NULL; /* ubsan */ + return &ex->public; } diff --git a/ssl/quic/quic_fifd.c b/ssl/quic/quic_fifd.c new file mode 100644 index 00000000000..8f548520b1b --- /dev/null +++ b/ssl/quic/quic_fifd.c @@ -0,0 +1,202 @@ +/* + * Copyright 2022 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the Apache License 2.0 (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#include "internal/quic_fifd.h" +#include "internal/quic_wire.h" + +int ossl_quic_fifd_init(QUIC_FIFD *fifd, + QUIC_CFQ *cfq, + OSSL_ACKM *ackm, + QUIC_TXPIM *txpim, + /* stream_id is UINT64_MAX for the crypto stream */ + QUIC_SSTREAM *(*get_sstream_by_id)(uint64_t stream_id, + void *arg), + void *get_sstream_by_id_arg, + /* stream_id is UINT64_MAX if not applicable */ + void (*regen_frame)(uint64_t frame_type, + uint64_t stream_id, + void *arg), + void *regen_frame_arg) +{ + if (cfq == NULL || ackm == NULL || txpim == NULL + || get_sstream_by_id == NULL || regen_frame == NULL) + return 0; + + fifd->cfq = cfq; + fifd->ackm = ackm; + fifd->txpim = txpim; + fifd->get_sstream_by_id = get_sstream_by_id; + fifd->get_sstream_by_id_arg = get_sstream_by_id_arg; + fifd->regen_frame = regen_frame; + fifd->regen_frame_arg = regen_frame_arg; + return 1; +} + +void ossl_quic_fifd_cleanup(QUIC_FIFD *fifd) +{ + /* No-op. */ +} + +static void on_acked(void *arg) +{ + QUIC_TXPIM_PKT *pkt = arg; + QUIC_FIFD *fifd = pkt->fifd; + const QUIC_TXPIM_CHUNK *chunks = ossl_quic_txpim_pkt_get_chunks(pkt); + size_t i, num_chunks = ossl_quic_txpim_pkt_get_num_chunks(pkt); + QUIC_SSTREAM *sstream; + QUIC_CFQ_ITEM *cfq_item, *cfq_item_next; + + /* STREAM and CRYPTO stream chunks, FINs and stream FC frames */ + for (i = 0; i < num_chunks; ++i) { + sstream = fifd->get_sstream_by_id(chunks[i].stream_id, + fifd->get_sstream_by_id_arg); + if (sstream == NULL) + continue; + + if (chunks[i].end >= chunks[i].start) + ossl_quic_sstream_mark_acked(sstream, + chunks[i].start, chunks[i].end); + + if (chunks[i].has_fin && chunks[i].stream_id != UINT64_MAX) + ossl_quic_sstream_mark_acked_fin(sstream); + } + + /* GCR */ + for (cfq_item = pkt->retx_head; cfq_item != NULL; cfq_item = cfq_item_next) { + cfq_item_next = cfq_item->pkt_next; + ossl_quic_cfq_release(fifd->cfq, cfq_item); + } + + ossl_quic_txpim_pkt_release(fifd->txpim, pkt); +} + +static void on_lost(void *arg) +{ + QUIC_TXPIM_PKT *pkt = arg; + QUIC_FIFD *fifd = pkt->fifd; + const QUIC_TXPIM_CHUNK *chunks = ossl_quic_txpim_pkt_get_chunks(pkt); + size_t i, num_chunks = ossl_quic_txpim_pkt_get_num_chunks(pkt); + QUIC_SSTREAM *sstream; + QUIC_CFQ_ITEM *cfq_item, *cfq_item_next; + + /* STREAM and CRYPTO stream chunks, FIN and stream FC frames */ + for (i = 0; i < num_chunks; ++i) { + sstream = fifd->get_sstream_by_id(chunks[i].stream_id, + fifd->get_sstream_by_id_arg); + if (sstream == NULL) + continue; + + if (chunks[i].end >= chunks[i].start) + ossl_quic_sstream_mark_lost(sstream, + chunks[i].start, chunks[i].end); + + if (chunks[i].has_fin && chunks[i].stream_id != UINT64_MAX) + ossl_quic_sstream_mark_lost_fin(sstream); + + /* + * Inform caller that stream needs an FC frame. + * + * Note: We could track whether an FC frame was sent originally for the + * stream to determine if it really needs to be regenerated or not. + * However, if loss has occurred, it's probably better to ensure the + * peer has up-to-date flow control data for the stream. Given that + * these frames are extremely small, we may as well always send it when + * handling loss. + */ + fifd->regen_frame(OSSL_QUIC_FRAME_TYPE_MAX_STREAM_DATA, + chunks[i].stream_id, + fifd->regen_frame_arg); + } + + /* GCR */ + for (cfq_item = pkt->retx_head; cfq_item != NULL; cfq_item = cfq_item_next) { + cfq_item_next = cfq_item->pkt_next; + ossl_quic_cfq_mark_lost(fifd->cfq, cfq_item, UINT32_MAX); + } + + /* Regenerate flag frames */ + if (pkt->had_handshake_done_frame) + fifd->regen_frame(OSSL_QUIC_FRAME_TYPE_HANDSHAKE_DONE, + UINT64_MAX, + fifd->regen_frame_arg); + + if (pkt->had_max_data_frame) + fifd->regen_frame(OSSL_QUIC_FRAME_TYPE_MAX_DATA, + UINT64_MAX, + fifd->regen_frame_arg); + + if (pkt->had_max_streams_bidi_frame) + fifd->regen_frame(OSSL_QUIC_FRAME_TYPE_MAX_STREAMS_BIDI, + UINT64_MAX, + fifd->regen_frame_arg); + + if (pkt->had_max_streams_uni_frame) + fifd->regen_frame(OSSL_QUIC_FRAME_TYPE_MAX_STREAMS_UNI, + UINT64_MAX, + fifd->regen_frame_arg); + + if (pkt->had_ack_frame) + /* + * We always use the ACK_WITH_ECN frame type to represent the ACK frame + * type in our callback; we assume it is the caller's job to decide + * whether it wants to send ECN data or not. + */ + fifd->regen_frame(OSSL_QUIC_FRAME_TYPE_ACK_WITH_ECN, + UINT64_MAX, + fifd->regen_frame_arg); + + ossl_quic_txpim_pkt_release(fifd->txpim, pkt); +} + +static void on_discarded(void *arg) +{ + QUIC_TXPIM_PKT *pkt = arg; + QUIC_FIFD *fifd = pkt->fifd; + QUIC_CFQ_ITEM *cfq_item, *cfq_item_next; + + /* + * Don't need to do anything to SSTREAMs for STREAM and CRYPTO streams, as + * we assume caller will clean them up. + */ + + /* GCR */ + for (cfq_item = pkt->retx_head; cfq_item != NULL; cfq_item = cfq_item_next) { + cfq_item_next = cfq_item->pkt_next; + ossl_quic_cfq_release(fifd->cfq, cfq_item); + } + + ossl_quic_txpim_pkt_release(fifd->txpim, pkt); +} + +int ossl_quic_fifd_pkt_commit(QUIC_FIFD *fifd, QUIC_TXPIM_PKT *pkt) +{ + QUIC_CFQ_ITEM *cfq_item; + + pkt->fifd = fifd; + + pkt->ackm_pkt.on_lost = on_lost; + pkt->ackm_pkt.on_acked = on_acked; + pkt->ackm_pkt.on_discarded = on_discarded; + pkt->ackm_pkt.cb_arg = pkt; + + pkt->ackm_pkt.prev = pkt->ackm_pkt.next + = pkt->ackm_pkt.anext = pkt->ackm_pkt.lnext = NULL; + + /* + * Mark the CFQ items which have been added to this packet as having been + * transmitted. + */ + for (cfq_item = pkt->retx_head; + cfq_item != NULL; + cfq_item = cfq_item->pkt_next) + ossl_quic_cfq_mark_tx(fifd->cfq, cfq_item); + + /* Inform the ACKM. */ + return ossl_ackm_on_tx_packet(fifd->ackm, &pkt->ackm_pkt); +} diff --git a/ssl/quic/quic_txpim.c b/ssl/quic/quic_txpim.c index 38b16a45619..96937587699 100644 --- a/ssl/quic/quic_txpim.c +++ b/ssl/quic/quic_txpim.c @@ -111,7 +111,8 @@ static void txpim_clear(QUIC_TXPIM_PKT_EX *ex) ex->public.fifd = NULL; ex->public.had_handshake_done_frame = 0; ex->public.had_max_data_frame = 0; - ex->public.had_max_streams_frame = 0; + ex->public.had_max_streams_bidi_frame = 0; + ex->public.had_max_streams_uni_frame = 0; ex->public.had_ack_frame = 0; } @@ -202,6 +203,10 @@ const QUIC_TXPIM_CHUNK *ossl_quic_txpim_pkt_get_chunks(QUIC_TXPIM_PKT *fpkt) QUIC_TXPIM_PKT_EX *ex = (QUIC_TXPIM_PKT_EX *)fpkt; if (ex->chunks_need_sort) { + /* + * List of chunks will generally be very small so there is no issue + * simply sorting here. + */ qsort(ex->chunks, ex->num_chunks, sizeof(QUIC_TXPIM_CHUNK), compare); ex->chunks_need_sort = 0; } @@ -215,3 +220,8 @@ size_t ossl_quic_txpim_pkt_get_num_chunks(QUIC_TXPIM_PKT *fpkt) return ex->num_chunks; } + +size_t ossl_quic_txpim_get_in_use(QUIC_TXPIM *txpim) +{ + return txpim->in_use; +} diff --git a/test/build.info b/test/build.info index 728fc111667..370220806d4 100644 --- a/test/build.info +++ b/test/build.info @@ -308,6 +308,10 @@ IF[{- !$disabled{tests} -}] INCLUDE[quic_txpim_test]=../include ../apps/include DEPEND[quic_txpim_test]=../libcrypto.a ../libssl.a libtestutil.a + SOURCE[quic_fifd_test]=quic_fifd_test.c + INCLUDE[quic_fifd_test]=../include ../apps/include + DEPEND[quic_fifd_test]=../libcrypto.a ../libssl.a libtestutil.a + SOURCE[asynctest]=asynctest.c INCLUDE[asynctest]=../include ../apps/include DEPEND[asynctest]=../libcrypto @@ -1032,7 +1036,7 @@ ENDIF ENDIF IF[{- !$disabled{'quic'} -}] - PROGRAMS{noinst}=quicapitest quic_wire_test quic_ackm_test quic_record_test quic_fc_test quic_stream_test quic_cfq_test quic_txpim_test + PROGRAMS{noinst}=quicapitest quic_wire_test quic_ackm_test quic_record_test quic_fc_test quic_stream_test quic_cfq_test quic_txpim_test quic_fifd_test ENDIF SOURCE[quicapitest]=quicapitest.c helpers/ssltestlib.c diff --git a/test/quic_fifd_test.c b/test/quic_fifd_test.c new file mode 100644 index 00000000000..47eb0309301 --- /dev/null +++ b/test/quic_fifd_test.c @@ -0,0 +1,358 @@ +/* + * Copyright 2022 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the Apache License 2.0 (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#include "internal/packet.h" +#include "internal/quic_txpim.h" +#include "internal/quic_fifd.h" +#include "testutil.h" + +static OSSL_TIME cur_time; + +static OSSL_TIME fake_now(void *arg) { + return cur_time; +} + +static void step_time(uint64_t ms) { + cur_time = ossl_time_add(cur_time, ossl_ms2time(ms)); +} + +static QUIC_SSTREAM *(*get_sstream_by_id_p)(uint64_t stream_id, void *arg); + +static QUIC_SSTREAM *get_sstream_by_id(uint64_t stream_id, void *arg) +{ + return get_sstream_by_id_p(stream_id, arg); +} + +static void (*regen_frame_p)(uint64_t frame_type, uint64_t stream_id, void *arg); + +static void regen_frame(uint64_t frame_type, uint64_t stream_id, void *arg) +{ + regen_frame_p(frame_type, stream_id, arg); +} + +typedef struct info_st { + QUIC_FIFD fifd; + OSSL_ACKM *ackm; + QUIC_CFQ *cfq; + QUIC_TXPIM *txpim; + OSSL_STATM statm; + OSSL_CC_DATA *ccdata; + QUIC_SSTREAM *sstream[4]; +} INFO; + +static INFO *cur_info; +static int cb_fail; +static int cfq_freed; + +/* ---------------------------------------------------------------------- + * 1. Test that a submitted packet, on ack, acks all streams inside of it + * Test that a submitted packet, on ack, calls the get by ID function + * correctly + * Test that a submitted packet, on ack, acks all fins inside it + * Test that a submitted packet, on ack, releases the TXPIM packet + */ +static QUIC_SSTREAM *sstream_expect(uint64_t stream_id, void *arg) +{ + if (stream_id == 42 || stream_id == 43) + return cur_info->sstream[stream_id - 42]; + + cb_fail = 1; + return NULL; +} + +static uint64_t regen_frame_type[16]; +static uint64_t regen_stream_id[16]; +static size_t regen_count; + +static void regen_expect(uint64_t frame_type, uint64_t stream_id, void *arg) +{ + regen_frame_type[regen_count] = frame_type; + regen_stream_id[regen_count] = stream_id; + ++regen_count; +} + +static const unsigned char placeholder_data[] = "placeholder"; + +static void cfq_free_cb_(unsigned char *buf, size_t buf_len, void *arg) +{ + if (buf == placeholder_data && buf_len == sizeof(placeholder_data)) + cfq_freed = 1; +} + +#define TEST_KIND_ACK 0 +#define TEST_KIND_LOSS 1 +#define TEST_KIND_DISCARD 2 +#define TEST_KIND_NUM 3 + +static int test_generic(INFO *info, int kind) +{ + int testresult = 0; + size_t i, consumed = 0; + QUIC_TXPIM_PKT *pkt = NULL, *pkt2 = NULL; + OSSL_QUIC_FRAME_STREAM hdr = {0}; + OSSL_QTX_IOVEC iov[2]; + size_t num_iov; + QUIC_TXPIM_CHUNK chunk = {42, 0, 11, 0}; + OSSL_QUIC_FRAME_ACK ack = {0}; + OSSL_QUIC_ACK_RANGE ack_ranges[1] = {0}; + QUIC_CFQ_ITEM *cfq_item = NULL; + uint32_t pn_space = (kind == TEST_KIND_DISCARD) + ? QUIC_PN_SPACE_HANDSHAKE : QUIC_PN_SPACE_APP; + + cur_time = ossl_seconds2time(1000); + regen_count = 0; + + get_sstream_by_id_p = sstream_expect; + regen_frame_p = regen_expect; + + if (!TEST_ptr(pkt = ossl_quic_txpim_pkt_alloc(info->txpim))) + goto err; + + for (i = 0; i < 2; ++i) { + num_iov = OSSL_NELEM(iov); + if (!TEST_true(ossl_quic_sstream_append(info->sstream[i], + (unsigned char *)"Test message", + 12, &consumed)) + || !TEST_size_t_eq(consumed, 12)) + goto err; + + if (i == 1) + ossl_quic_sstream_fin(info->sstream[i]); + + if (!TEST_true(ossl_quic_sstream_get_stream_frame(info->sstream[i], 0, + &hdr, iov, &num_iov)) + || !TEST_int_eq(hdr.is_fin, i == 1) + || !TEST_uint64_t_eq(hdr.offset, 0) + || !TEST_uint64_t_eq(hdr.len, 12) + || !TEST_size_t_eq(ossl_quic_sstream_get_buffer_used(info->sstream[i]), 12) + || !TEST_true(ossl_quic_sstream_mark_transmitted(info->sstream[i], + hdr.offset, + hdr.offset + hdr.len - 1))) + goto err; + + if (i == 1 && !TEST_true(ossl_quic_sstream_mark_transmitted_fin(info->sstream[i], + hdr.offset + hdr.len))) + goto err; + + chunk.has_fin = hdr.is_fin; + chunk.stream_id = 42 + i; + if (!TEST_true(ossl_quic_txpim_pkt_append_chunk(pkt, &chunk))) + goto err; + } + + cfq_freed = 0; + if (!TEST_ptr(cfq_item = ossl_quic_cfq_add_frame(info->cfq, 10, + pn_space, + OSSL_QUIC_FRAME_TYPE_NEW_CONN_ID, + placeholder_data, + sizeof(placeholder_data), + cfq_free_cb_, NULL)) + || !TEST_ptr_eq(cfq_item, ossl_quic_cfq_get_priority_head(info->cfq, pn_space))) + goto err; + + ossl_quic_txpim_pkt_add_cfq_item(pkt, cfq_item); + + pkt->ackm_pkt.pkt_num = 0; + pkt->ackm_pkt.pkt_space = pn_space; + pkt->ackm_pkt.largest_acked = QUIC_PN_INVALID; + pkt->ackm_pkt.num_bytes = 50; + pkt->ackm_pkt.time = cur_time; + pkt->ackm_pkt.is_inflight = 1; + pkt->ackm_pkt.is_ack_eliciting = 1; + if (kind == TEST_KIND_LOSS) { + pkt->had_handshake_done_frame = 1; + pkt->had_max_data_frame = 1; + pkt->had_max_streams_bidi_frame = 1; + pkt->had_max_streams_uni_frame = 1; + pkt->had_ack_frame = 1; + } + + ack_ranges[0].start = 0; + ack_ranges[0].end = 0; + ack.ack_ranges = ack_ranges; + ack.num_ack_ranges = 1; + + if (!TEST_true(ossl_quic_fifd_pkt_commit(&info->fifd, pkt))) + goto err; + + /* CFQ item should have been marked as transmitted */ + if (!TEST_ptr_null(ossl_quic_cfq_get_priority_head(info->cfq, pn_space))) + goto err; + + switch (kind) { + case TEST_KIND_ACK: + if (!TEST_true(ossl_ackm_on_rx_ack_frame(info->ackm, &ack, + pn_space, + cur_time))) + goto err; + + for (i = 0; i < 2; ++i) + if (!TEST_size_t_eq(ossl_quic_sstream_get_buffer_used(info->sstream[i]), 0)) + goto err; + + /* This should fail, which proves the FIN was acked */ + if (!TEST_false(ossl_quic_sstream_mark_lost_fin(info->sstream[1]))) + goto err; + + /* CFQ item must have been released */ + if (!TEST_true(cfq_freed)) + goto err; + + /* No regen calls should have been made */ + if (!TEST_size_t_eq(regen_count, 0)) + goto err; + + break; + + case TEST_KIND_LOSS: + /* Trigger loss detection via packet threshold. */ + if (!TEST_ptr(pkt2 = ossl_quic_txpim_pkt_alloc(info->txpim))) + goto err; + + step_time(10000); + pkt2->ackm_pkt.pkt_num = 50; + pkt2->ackm_pkt.pkt_space = pn_space; + pkt2->ackm_pkt.largest_acked = QUIC_PN_INVALID; + pkt2->ackm_pkt.num_bytes = 50; + pkt2->ackm_pkt.time = cur_time; + pkt2->ackm_pkt.is_inflight = 1; + pkt2->ackm_pkt.is_ack_eliciting = 1; + + ack_ranges[0].start = 50; + ack_ranges[0].end = 50; + ack.ack_ranges = ack_ranges; + ack.num_ack_ranges = 1; + + if (!TEST_true(ossl_quic_fifd_pkt_commit(&info->fifd, pkt2)) + || !TEST_true(ossl_ackm_on_rx_ack_frame(info->ackm, &ack, + pn_space, cur_time))) + goto err; + + for (i = 0; i < 2; ++i) { + num_iov = OSSL_NELEM(iov); + /* + * Stream data we sent must have been marked as lost; check by + * ensuring it is returned again + */ + if (!TEST_true(ossl_quic_sstream_get_stream_frame(info->sstream[i], 0, + &hdr, iov, &num_iov)) + || !TEST_uint64_t_eq(hdr.offset, 0) + || !TEST_uint64_t_eq(hdr.len, 12)) + goto err; + } + + /* FC frame should have regenerated for each stream */ + if (!TEST_size_t_eq(regen_count, 7) + || !TEST_uint64_t_eq(regen_stream_id[0], 42) + || !TEST_uint64_t_eq(regen_frame_type[0], OSSL_QUIC_FRAME_TYPE_MAX_STREAM_DATA) + || !TEST_uint64_t_eq(regen_stream_id[1], 43) + || !TEST_uint64_t_eq(regen_frame_type[1], OSSL_QUIC_FRAME_TYPE_MAX_STREAM_DATA) + || !TEST_uint64_t_eq(regen_frame_type[2], OSSL_QUIC_FRAME_TYPE_HANDSHAKE_DONE) + || !TEST_uint64_t_eq(regen_stream_id[2], UINT64_MAX) + || !TEST_uint64_t_eq(regen_frame_type[3], OSSL_QUIC_FRAME_TYPE_MAX_DATA) + || !TEST_uint64_t_eq(regen_stream_id[3], UINT64_MAX) + || !TEST_uint64_t_eq(regen_frame_type[4], OSSL_QUIC_FRAME_TYPE_MAX_STREAMS_BIDI) + || !TEST_uint64_t_eq(regen_stream_id[4], UINT64_MAX) + || !TEST_uint64_t_eq(regen_frame_type[5], OSSL_QUIC_FRAME_TYPE_MAX_STREAMS_UNI) + || !TEST_uint64_t_eq(regen_stream_id[5], UINT64_MAX) + || !TEST_uint64_t_eq(regen_frame_type[6], OSSL_QUIC_FRAME_TYPE_ACK_WITH_ECN) + || !TEST_uint64_t_eq(regen_stream_id[6], UINT64_MAX)) + goto err; + + /* CFQ item should have been marked as lost */ + if (!TEST_ptr_eq(cfq_item, ossl_quic_cfq_get_priority_head(info->cfq, pn_space))) + goto err; + + /* FIN should have been marked as lost */ + num_iov = OSSL_NELEM(iov); + if (!TEST_true(ossl_quic_sstream_get_stream_frame(info->sstream[1], 10, + &hdr, iov, &num_iov)) + || !TEST_true(hdr.is_fin) + || !TEST_uint64_t_eq(hdr.len, 0)) + goto err; + + break; + + case TEST_KIND_DISCARD: + if (!TEST_true(ossl_ackm_on_pkt_space_discarded(info->ackm, pn_space))) + goto err; + + /* CFQ item must have been released */ + if (!TEST_true(cfq_freed)) + goto err; + + break; + + default: + goto err; + } + + /* TXPIM must have been released */ + if (!TEST_size_t_eq(ossl_quic_txpim_get_in_use(info->txpim), 0)) + goto err; + + testresult = 1; +err: + return testresult; +} + +static int test_fifd(int idx) +{ + int testresult = 0; + INFO info = {0}; + size_t i; + + cur_info = &info; + cb_fail = 0; + + if (!TEST_true(ossl_statm_init(&info.statm)) + || !TEST_ptr(info.ccdata = ossl_cc_dummy_method.new(NULL, NULL, NULL)) + || !TEST_ptr(info.ackm = ossl_ackm_new(fake_now, NULL, + &info.statm, + &ossl_cc_dummy_method, + info.ccdata)) + || !TEST_true(ossl_ackm_on_handshake_confirmed(info.ackm)) + || !TEST_ptr(info.cfq = ossl_quic_cfq_new()) + || !TEST_ptr(info.txpim = ossl_quic_txpim_new()) + || !TEST_true(ossl_quic_fifd_init(&info.fifd, info.cfq, info.ackm, + info.txpim, + get_sstream_by_id, NULL, + regen_frame, NULL))) + goto err; + + for (i = 0; i < OSSL_NELEM(info.sstream); ++i) + if (!TEST_ptr(info.sstream[i] = ossl_quic_sstream_new(1024))) + goto err; + + ossl_statm_update_rtt(&info.statm, ossl_time_zero(), ossl_ms2time(1)); + + if (!TEST_true(test_generic(&info, idx)) + || !TEST_false(cb_fail)) + goto err; + + testresult = 1; +err: + ossl_quic_fifd_cleanup(&info.fifd); + ossl_quic_cfq_free(info.cfq); + ossl_quic_txpim_free(info.txpim); + ossl_ackm_free(info.ackm); + ossl_statm_destroy(&info.statm); + if (info.ccdata != NULL) + ossl_cc_dummy_method.free(info.ccdata); + for (i = 0; i < OSSL_NELEM(info.sstream); ++i) + ossl_quic_sstream_free(info.sstream[i]); + cur_info = NULL; + return testresult; +} + +int setup_tests(void) +{ + ADD_ALL_TESTS(test_fifd, TEST_KIND_NUM); + return 1; +} diff --git a/test/recipes/70-test_quic_fifd.t b/test/recipes/70-test_quic_fifd.t new file mode 100644 index 00000000000..40245523ede --- /dev/null +++ b/test/recipes/70-test_quic_fifd.t @@ -0,0 +1,19 @@ +#! /usr/bin/env perl +# Copyright 2022 The OpenSSL Project Authors. All Rights Reserved. +# +# Licensed under the Apache License 2.0 (the "License"). You may not use +# this file except in compliance with the License. You can obtain a copy +# in the file LICENSE in the source distribution or at +# https://www.openssl.org/source/license.html + +use OpenSSL::Test; +use OpenSSL::Test::Utils; + +setup("test_quic_fifd"); + +plan skip_all => "QUIC protocol is not supported by this OpenSSL build" + if disabled('quic'); + +plan tests => 1; + +ok(run(test(["quic_fifd_test"]))); -- 2.47.3