QUIC Flow Control

Reviewed-by: Paul Dale <pauli@openssl.org>
Reviewed-by: Matt Caswell <matt@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/19040)
This commit is contained in:
Hugo Landau 2022-08-22 15:32:16 +01:00
parent 28a5aa0cbd
commit 508e087c4c
9 changed files with 1596 additions and 3 deletions

View File

@ -0,0 +1,272 @@
Flow Control
============
Introduction to QUIC Flow Control
---------------------------------
QUIC flow control acts at both connection and stream levels. At any time,
transmission of stream data could be prevented by connection-level flow control,
by stream-level flow control, or both. Flow control uses a credit-based model in
which the relevant flow control limit is expressed as the maximum number of
bytes allowed to be sent on a stream, or across all streams, since the beginning
of the stream or connection. This limit may be periodically bumped.
It is important to note that both connection and stream-level flow control
relate only to the transmission of QUIC stream data. QUIC flow control at stream
level counts the total number of logical bytes sent on a given stream. Note that
this does not count retransmissions; thus, if a byte is sent, lost, and sent
again, this still only counts as one byte for the purposes of flow control. Note
that the total number of logical bytes sent on a given stream is equivalent to
the current “length” of the stream. In essence, the relevant quantity is
`max(offset + len)` for all STREAM frames `(offset, len)` we have ever sent for
the stream.
(It is essential that this be determined correctly, as deadlock may occur if we
believe we have exhausted our flow control credit whereas the peer believes we
have not, as the peer may wait indefinitely for us to send more data before
advancing us more flow control credit.)
QUIC flow control at connection level is based on the sum of all the logical
bytes transmitted across all streams since the start of the connection.
Connection-level flow control is controlled by the `MAX_DATA` frame;
stream-level flow control is controlled by the `MAX_STREAM_DATA` frame.
The `DATA_BLOCKED` and `STREAM_DATA_BLOCKED` frames defined by RFC 9000 are less
important than they first appear, as peers are not allowed to rely on them. (For
example, a peer is not allowed to wait until we send `DATA_BLOCKED` to increase
our connection-level credit, and a conformant QUIC implementation can choose to
never generate either of these frame types.) These frames rather serve two
purposes: to enhance flow control performance, and as a debugging aid.
However, their implementation is not critical.
Note that it follows from the above that the CRYPTO-frame stream is not subject
to flow control.
Note that flow control and congestion control are completely separate
mechanisms. In a given circumstance, either or both mechanisms may restrict our
ability to transmit application data.
Consider the following diagram:
RWM SWM SWM' CWM CWM'
| | | | |
| |<-- credit| -->| |
| <-|- threshold -|----->| |
----------------->
window size
We introduce the following terminology:
- **Controlled bytes** refers to any byte which counts for purposes of flow
control. A controlled byte is any byte of application data in a STREAM frame
payload, the first time it is sent (retransmissions do not count).
- (RX side only) **Retirement**, which refers to where we dequeue one or more
controlled bytes from a QUIC stream and hand them to the application, meaning
we are no longer responsible for them.
Retirement is an important factor in our RX flow control design, as we want
peers to transmit not just at the rate that our QUIC implementation can
process incoming data, but also at a rate the application can handle.
- (RX side only) The **Retired Watermark** (RWM), the total number of retired
controlled bytes since the beginning of the connection or stream.
- The **Spent Watermark** (SWM), which is the number of controlled bytes we have
sent (for the TX side) or received (for the RX side). This represents the
amount of flow control budget which has been spent. It is a monotonic value
and never decreases. On the RX side, such bytes have not necessarily been
retired yet.
- The **Credit Watermark** (CWM), which is the number of bytes which have
been authorized for transmission so far. This count is a cumulative count
since the start of the connection or stream and thus is also monotonic.
- The available **credit**, which is always simply the difference between
the SWM and the CWM.
- (RX side only) The **threshold**, which is how close we let the RWM
get to the CWM before we choose to extend the peer more credit by bumping the
CWM. The threshold is relative to (i.e., subtracted from) the CWM.
- (RX side only) The **window size**, which is the amount by which we or a peer
choose to bump the CWM each time, as we reach or exceed the threshold. The new
CWM is calculated as the SWM plus the window size (note that it added to the
SWM, not the old CWM.)
Note that:
- If the available credit is zero, the TX side is blocked due to a lack of
credit.
- If any circumstance occurs which would cause the SWM to exceed the CWM,
a flow control protocol violation has occurred and the connection
should be terminated.
Connection-Level Flow Control - TX Side
---------------------------------------
TX side flow control is exceptionally simple. It can be modelled as the
following state machine:
---> event: On TX (numBytes)
---> event: On TX Window Updated (numBytes)
<--- event: On TX Blocked
Get TX Window() -> numBytes
The On TX event is passed to the state machine whenever we send a packet.
`numBytes` is the total number of controlled bytes we sent in the packet (i.e.,
the number of bytes of STREAM frame payload which are not retransmissions). This
value is added to the TX-side SWM value. Note that this may be zero, though
there is no need to pass the event in this case.
The On TX Window Updated event is passed to the state machine whenever we have
our CWM increased. In other words, it is passed whenever we receive a `MAX_DATA`
frame, with the integer value contained in that frame (or when we receive the
`initial_max_data` transport parameter).
The On TX Window Updated event expresses the CWM (that is, the cumulative
number of controlled bytes we are allowed to send since the start of the
connection), thus it is monotonic and may never regress. If an On TX Window
Update event is passed to the state machine with a value lower than that passed
in any previous such event, it indicates a peer protocol error or a local
programming error.
The Get TX Window function returns our credit value (that is, it returns the
number of controlled bytes we are allowed to send). This value is reduced by the
On TX event and increased by the On TX Window Updated event. In fact, it is
simply the difference between the last On TX Window Updated value and the sum of
the `numBytes` arguments of all On TX events so far; it is that simple.
The On TX Blocked event is emitted at the time of any edge transition where the
value which would be returned by the Get TX Window function changes from
non-zero to zero. This always occurs during processing of an On TX event. (This
event is intended to assist in deciding when to generate `DATA_BLOCKED`
frames.)
We must not exceed the flow control limits, else the peer may terminate the
connection with an error.
An initial connection-level credit is communicated by the peer in the
`initial_max_data` transport parameter. All other credits occur as a result of a
`MAX_DATA` frame.
Stream-Level Flow Control - TX Side
-----------------------------------
Stream-level flow control works exactly the same as connection-level flow
control for the TX side.
The On TX Window Updated event occurs in response to the `MAX_STREAM_DATA`
frame, or based on the relevant transport parameter
(`initial_max_stream_data_bidi_local`, `initial_max_stream_data_bidi_remote`,
`initial_max_stream_data_uni`).
The On TX Blocked event can be used to decide when to generate
`STREAM_DATA_BLOCKED` frames.
Note that the number of controlled bytes we can send in a stream is limited by
both connection and stream-level flow control; thus the number of controlled
bytes we can send is the lesser value of the values returned by the Get TX
Window function on the connection-level and stream-level state machines,
respectively.
Connection-Level Flow Control - RX Side
---------------------------------------
---> event: On RX Controlled Bytes (numBytes) [internal event]
---> event: On Retire Controlled Bytes (numBytes)
<--- event: Increase Window (numBytes)
<--- event: Flow Control Error
RX side connection-level flow control provides an indication of when to generate
`MAX_DATA` frames to bump the peer's connection-level transmission credit. It is
somewhat more involved than the TX side.
The state machine receives On RX Controlled Bytes events from stream-level flow
controllers. Callers do not pass the event themselves. The event is generated by
a stream-level flow controller whenever we receive any controlled bytes.
`numBytes` is the number of controlled bytes we received. (This event is
generated by stream-level flow control as retransmitted stream data must be
counted only once, and the stream-level flow control is therefore in the best
position to determine how many controlled bytes (i.e., new, non-retransmitted
stream payload bytes) have been received).
If we receive more controlled bytes than we authorized, the state machine emits
the Flow Control Error event. The connection should be terminated with a
protocol error in this case.
The state machine emits the Increase Window event when it thinks that the peer
should be advanced more flow control credit (i.e., when the CWM should be
bumped). `numBytes` is the new CWM value, and is monotonic with regard to all
previous Increase Window events emitted by the state machine.
The state machine is passed the On Retire Controlled bytes event when one or
more controlled bytes are dequeued from any stream and passed to the
application.
The state machine uses the cadence of the On Retire Controlled Bytes events it
receives to determine when to increase the flow control window. Thus, the On
Retire Controlled Bytes event should be sent to the state machine when
processing of the received controlled bytes has been *completed* (i.e., passed
to the application).
Stream-Level Flow Control - RX Side
-----------------------------------
RX-side stream-level flow control works similarly to RX-side connection-level
flow control. There are a few differences:
- There is no On RX Controlled Bytes event.
- The On Retire Controlled Bytes event may optionally pass the same event
to a connection-level flow controller (an implementation decision), as these
events should always occur at the same time.
- An additional event is added, which replaces the On RX Controlled Bytes event:
---> event: On RX Stream Frame (offsetPlusLength, isFin)
This event should be passed to the state machine when a STREAM frame is
received. The `offsetPlusLength` argument is the sum of the offset field of
the STREAM frame and the length of the frame's payload in bytes. The isFin
argument should specify whether the STREAM frame had the FIN flag set.
This event is used to generate the internal On RX Controlled Bytes event to
the connection-level flow controller. It is also used by stream-level flow
control to determine if flow control limits are violated by the peer.
The state machine handles `offsetPlusLength` monotonically and ignores the
event if a previous such event already had an equal or greater value. The
reason this event is used instead of a `On RX (numBytes)` style event is that
this API can be monotonic and thus easier to use (the caller does not need to
remember if they have already counted a specific controlled byte in a STREAM
frame, which may after all duplicate some of the controlled bytes in a
previous STREAM frame).
RX Window Sizing
----------------
For RX flow control we must determine our window size. This is the value we add
to the peer's current SWM to determine the new CWM each time as RWM reaches the
threshold. The window size should be adapted dynamically according to network
conditions.
Many implementations choose to have a mechanism for increasing the window size
but not decreasing it, a simple approach which we adopt here.
The common algorithm is a so-called auto-tuning approach in which the rate of
window consumption (i.e., the rate at which RWM approaches CWM after CWM is
bumped) is measured and compared to the measured connection RTT. If the time it
takes to consume one window size exceeds a fixed multiple of the RTT, the window
size is doubled, up to an implementation-chosen maximum window size.
Auto-tuning occurs in 'epochs'. At the end of each auto-tuning epoch, a decision
is made on whether to double the window size, and a new auto-tuning epoch is
started.
For more information on auto-tuning, see [Flow control in
QUIC](https://docs.google.com/document/d/1F2YfdDXKpy20WVKJueEf4abn_LVZHhMUMS5gX6Pgjl4/edit#heading=h.hcm2y5x4qmqt)
and [QUIC Flow
Control](https://docs.google.com/document/d/1SExkMmGiz8VYzV3s9E35JQlJ73vhzCekKkDi85F1qCE/edit#).

View File

@ -0,0 +1,38 @@
/*
* 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_ERROR_H
# define OSSL_QUIC_ERROR_H
# include <openssl/ssl.h>
/* RFC 9000 Section 20.1 */
# define QUIC_ERR_NO_ERROR 0x00
# define QUIC_ERR_INTERNAL_ERROR 0x01
# define QUIC_ERR_CONNECTION_REFUSED 0x02
# define QUIC_ERR_FLOW_CONTROL_ERROR 0x03
# define QUIC_ERR_STREAM_LIMIT_ERROR 0x04
# define QUIC_ERR_STREAM_STATE_ERROR 0x05
# define QUIC_ERR_FINAL_SIZE_ERROR 0x06
# define QUIC_ERR_FRAME_ENCODING_ERROR 0x07
# define QUIC_ERR_TRANSPORT_PARAMETER_ERROR 0x08
# define QUIC_ERR_CONNECTION_ID_LIMIT_ERROR 0x09
# define QUIC_ERR_PROTOCOL_VIOLATION 0x0A
# define QUIC_ERR_INVALID_TOKEN 0x0B
# define QUIC_ERR_APPLICATION_ERROR 0x0C
# define QUIC_ERR_CRYPTO_BUFFER_EXCEEDED 0x0D
# define QUIC_ERR_KEY_UPDATE_ERROR 0x0E
# define QUIC_ERR_AEAD_LIMIT_REACHED 0x0F
# define QUIC_ERR_NO_VIABLE_PATH 0x10
/* Inclusive range for handshake-specific errors. */
# define QUIC_ERR_CRYPTO_ERR_BEGIN 0x0100
# define QUUC_ERR_CRYPTO_ERR_END 0x01FF
#endif

256
include/internal/quic_fc.h Normal file
View File

@ -0,0 +1,256 @@
/*
* 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_FC_H
# define OSSL_QUIC_FC_H
# include <openssl/ssl.h>
# include "internal/time.h"
/*
* TX Flow Controller (TXFC)
* =========================
*
* For discussion, see doc/designs/quic-design/quic-fc.md.
*/
typedef struct quic_txfc_st QUIC_TXFC;
struct quic_txfc_st {
QUIC_TXFC *parent; /* stream-level iff non-NULL */
uint64_t swm, cwm;
char has_become_blocked;
};
/*
* Initialises a TX flow controller. conn_txfc should be non-NULL and point to
* the connection-level flow controller if the TXFC is for stream-level flow
* control, and NULL otherwise.
*/
int ossl_quic_txfc_init(QUIC_TXFC *txfc, QUIC_TXFC *conn_txfc);
/*
* Gets the parent (i.e., connection-level) TX flow controller. Returns NULL if
* called on a connection-level TX flow controller.
*/
QUIC_TXFC *ossl_quic_txfc_get_parent(QUIC_TXFC *txfc);
/*
* Bump the credit watermark (CWM) value. This is the 'On TX Window Updated'
* operation. This function is a no-op if it has already been called with an
* equal or higher CWM value.
*
* It returns 1 iff the call resulted in the CWM being bumped and 0 if it was
* not increased because it has already been called with an equal or higher CWM
* value. This is not an error per se but may indicate a local programming error
* or a protocol error in a remote peer.
*/
int ossl_quic_txfc_bump_cwm(QUIC_TXFC *txfc, uint64_t cwm);
/*
* Get the number of bytes by which we are in credit. This is the number of
* controlled bytes we are allowed to send. (Thus if this function returns 0, we
* are currently blocked.)
*
* If called on a stream-level TXFC, ossl_quic_txfc_get_credit is called on
* the connection-level TXFC as well, and the lesser of the two values is
* returned.
*/
uint64_t ossl_quic_txfc_get_credit(QUIC_TXFC *txfc);
/*
* Like ossl_quic_txfc_get_credit(), but when called on a stream-level TXFC,
* retrieves only the stream-level credit value and does not clamp it based on
* connection-level flow control.
*/
uint64_t ossl_quic_txfc_get_credit_local(QUIC_TXFC *txfc);
/*
* Consume num_bytes of credit. This is the 'On TX' operation. This should be
* called when we transmit any controlled bytes. Calling this with an argument
* of 0 is a no-op.
*
* We must never transmit more controlled bytes than we are in credit for (see
* the return value of ossl_quic_txfc_get_credit()). If you call this function
* with num_bytes greater than our current credit, this function consumes the
* remainder of the credit and returns 0. This indicates a serious programming
* error on the caller's part. Otherwise, the function returns 1.
*
* If called on a stream-level TXFC, ossl_quic_txfc_consume_credit() is called
* on the connection-level TXFC also. If the call to that function on the
* connection-level TXFC returns zero, this function will also return zero.
*/
int ossl_quic_txfc_consume_credit(QUIC_TXFC *txfc, uint64_t num_bytes);
/*
* Like ossl_quic_txfc_consume_credit(), but when called on a stream-level TXFC,
* consumes only from the stream-level credit and does not inform the
* connection-level TXFC.
*/
int ossl_quic_txfc_consume_credit_local(QUIC_TXFC *txfc, uint64_t num_bytes);
/*
* This flag is provided for convenience. A caller is not required to use it. It
* is a boolean flag set whenever our credit drops to zero. If clear is 1, the
* flag is cleared. The old value of the flag is returned. Callers may use this
* to determine if they need to send a DATA_BLOCKED or STREAM_DATA_BLOCKED
* frame, which should contain the value returned by ossl_quic_txfc_get_cwm().
*/
int ossl_quic_txfc_has_become_blocked(QUIC_TXFC *txfc, int clear);
/*
* Get the current CWM value. This is mainly only needed when generating a
* DATA_BLOCKED or STREAM_DATA_BLOCKED frame, or for diagnostic purposes.
*/
uint64_t ossl_quic_txfc_get_cwm(QUIC_TXFC *txfc);
/*
* Get the current spent watermark (SWM) value. This is purely for diagnostic
* use and should not be needed in normal circumstances.
*/
uint64_t ossl_quic_txfc_get_swm(QUIC_TXFC *txfc);
/*
* RX Flow Controller (RXFC)
* =========================
*/
typedef struct quic_rxfc_st QUIC_RXFC;
struct quic_rxfc_st {
/*
* swm is the sent/received watermark, which tracks how much we have
* received from the peer. rwm is the retired watermark, which tracks how
* much has been passed to the application. esrwm is the rwm value at which
* the current auto-tuning epoch started. hwm is the highest stream length
* (STREAM frame offset + payload length) we have seen from a STREAM frame
* yet.
*/
uint64_t cwm, swm, rwm, esrwm, hwm, cur_window_size, max_window_size;
OSSL_TIME epoch_start;
OSSL_TIME (*now)(void *arg);
void *now_arg;
QUIC_RXFC *parent;
unsigned char error_code, has_cwm_changed, is_fin;
};
/*
* Initialises an RX flow controller. conn_rxfc should be non-NULL and point to
* a connection-level RXFC if the RXFC is for stream-level flow control, and
* NULL otherwise. initial_window_size and max_window_size specify the initial
* and absolute maximum window sizes, respectively. Window size values are
* expressed in bytes and determine how much credit the RXFC extends to the peer
* to transmit more data at a time.
*/
int ossl_quic_rxfc_init(QUIC_RXFC *rxfc, QUIC_RXFC *conn_rxfc,
uint64_t initial_window_size,
uint64_t max_window_size,
OSSL_TIME (*now)(void *arg),
void *now_arg);
/*
* Gets the parent (i.e., connection-level) RXFC. Returns NULL if called on a
* connection-level RXFC.
*/
QUIC_RXFC *ossl_quic_rxfc_get_parent(QUIC_RXFC *rxfc);
/*
* Changes the current maximum window size value.
*/
void ossl_quic_rxfc_set_max_window_size(QUIC_RXFC *rxfc,
size_t max_window_size);
/*
* To be called whenever a STREAM frame is received.
*
* end is the value (offset + len), where offset is the offset field of the
* STREAM frame and len is the length of the STREAM frame's payload in bytes.
*
* is_fin should be 1 if the STREAM frame had the FIN flag set and 0 otherwise.
*
* conn_rxfc should point to a connection-level RXFC, which will have its state
* updated correctly by the stream-level RXFC.
*
* This function may be used on a stream-level RXFC only.
*
* You should check ossl_quic_rxfc_has_error() on both connection-level and
* stream-level RXFCs after calling this function, as an incoming STREAM frame
* may cause flow control limits to be exceeded by an errant peer. This
* function still returns 1 in this case, as this is not a caller error.
*
* Returns 1 on success or 0 on failure.
*/
int ossl_quic_rxfc_on_rx_stream_frame(QUIC_RXFC *rxfc,
uint64_t end, int is_fin);
/*
* To be called whenever controlled bytes are retired, i.e. when bytes are
* dequeued from a QUIC stream and passed to the application. num_bytes
* is the number of bytes which were passed to the application.
*
* You should call this only on a stream-level RXFC. This function will update
* the connection-level RXFC automatically.
*
* rtt should be the current best understanding of the RTT to the peer, as
* offered by the Statistics Manager.
*
* You should check ossl_quic_rxfc_has_cwm_changed() after calling this
* function, as it may have caused the RXFC to decide to grant more flow control
* credit to the peer.
*
* Returns 1 on success and 0 on failure.
*/
int ossl_quic_rxfc_on_retire(QUIC_RXFC *rxfc,
uint64_t num_bytes,
OSSL_TIME rtt);
/*
* Returns the current CWM which the RXFC thinks the peer should have.
*
* Note that the RXFC will increase this value in response to events, at which
* time a MAX_DATA or MAX_STREAM_DATA frame must be generated. Use
* ossl_quic_rxfc_has_cwm_changed() to detect this condition.
*
* This value increases monotonically.
*/
uint64_t ossl_quic_rxfc_get_cwm(QUIC_RXFC *rxfc);
/*
* Returns the current SWM. This is the total number of bytes the peer has
* transmitted to us. This is intended for diagnostic use only; you should
* not need it.
*/
uint64_t ossl_quic_rxfc_get_swm(QUIC_RXFC *rxfc);
/*
* Returns the current RWM. This is the total number of bytes that has been
* retired. This is intended for diagnostic use only; you should not need it.
*/
uint64_t ossl_quic_rxfc_get_rwm(QUIC_RXFC *rxfc);
/*
* Returns the CWM changed flag. If clear is 1, the flag is cleared and the old
* value is returned.
*/
int ossl_quic_rxfc_has_cwm_changed(QUIC_RXFC *rxfc, int clear);
/*
* Returns a QUIC_ERR_* error code if a flow control error has been detected.
* Otherwise, returns QUIC_ERR_NO_ERROR. If clear is 1, the error is cleared
* and the old value is returned.
*
* May return one of the following values:
*
* QUIC_ERR_FLOW_CONTROL_ERROR:
* This indicates a flow control protocol violation by the remote peer; the
* connection should be terminated in this event.
* QUIC_ERR_FINAL_SIZE:
* The peer attempted to change the stream length after ending the stream.
*/
int ossl_quic_rxfc_get_error(QUIC_RXFC *rxfc, int clear);
#endif

View File

@ -1,3 +1,3 @@
$LIBSSL=../../libssl
SOURCE[$LIBSSL]=quic_method.c quic_impl.c quic_wire.c quic_ackm.c quic_statm.c cc_dummy.c quic_demux.c quic_record_rx.c quic_record_rx_wrap.c quic_record_tx.c quic_record_util.c quic_record_shared.c quic_wire_pkt.c quic_rx_depack.c
SOURCE[$LIBSSL]=quic_method.c quic_impl.c quic_wire.c quic_ackm.c quic_statm.c cc_dummy.c quic_demux.c quic_record_rx.c quic_record_rx_wrap.c quic_record_tx.c quic_record_util.c quic_record_shared.c quic_wire_pkt.c quic_rx_depack.c quic_fc.c

372
ssl/quic/quic_fc.c Normal file
View File

@ -0,0 +1,372 @@
/*
* 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_fc.h"
#include "internal/quic_error.h"
#include "internal/common.h"
#include "internal/safe_math.h"
#include <assert.h>
OSSL_SAFE_MATH_UNSIGNED(uint64_t, uint64_t)
/*
* TX Flow Controller (TXFC)
* =========================
*/
int ossl_quic_txfc_init(QUIC_TXFC *txfc, QUIC_TXFC *conn_txfc)
{
if (conn_txfc != NULL && conn_txfc->parent != NULL)
return 0;
txfc->swm = 0;
txfc->cwm = 0;
txfc->parent = conn_txfc;
txfc->has_become_blocked = 0;
return 1;
}
QUIC_TXFC *ossl_quic_txfc_get_parent(QUIC_TXFC *txfc)
{
return txfc->parent;
}
int ossl_quic_txfc_bump_cwm(QUIC_TXFC *txfc, uint64_t cwm)
{
if (cwm <= txfc->cwm)
return 0;
txfc->cwm = cwm;
return 1;
}
uint64_t ossl_quic_txfc_get_credit_local(QUIC_TXFC *txfc)
{
assert(txfc->swm <= txfc->cwm);
return txfc->cwm - txfc->swm;
}
uint64_t ossl_quic_txfc_get_credit(QUIC_TXFC *txfc)
{
uint64_t r, conn_r;
r = ossl_quic_txfc_get_credit_local(txfc);
if (txfc->parent != NULL) {
assert(txfc->parent->parent == NULL);
conn_r = ossl_quic_txfc_get_credit_local(txfc->parent);
if (conn_r < r)
r = conn_r;
}
return r;
}
int ossl_quic_txfc_consume_credit_local(QUIC_TXFC *txfc, uint64_t num_bytes)
{
int ok = 1;
uint64_t credit = ossl_quic_txfc_get_credit_local(txfc);
if (num_bytes > credit) {
ok = 0;
num_bytes = credit;
}
if (num_bytes > 0 && num_bytes == credit)
txfc->has_become_blocked = 1;
txfc->swm += num_bytes;
return ok;
}
int ossl_quic_txfc_consume_credit(QUIC_TXFC *txfc, uint64_t num_bytes)
{
int ok = ossl_quic_txfc_consume_credit_local(txfc, num_bytes);
if (txfc->parent != NULL) {
assert(txfc->parent->parent == NULL);
if (!ossl_quic_txfc_consume_credit_local(txfc->parent, num_bytes))
return 0;
}
return ok;
}
int ossl_quic_txfc_has_become_blocked(QUIC_TXFC *txfc, int clear)
{
int r = txfc->has_become_blocked;
if (clear)
txfc->has_become_blocked = 0;
return r;
}
uint64_t ossl_quic_txfc_get_cwm(QUIC_TXFC *txfc)
{
return txfc->cwm;
}
uint64_t ossl_quic_txfc_get_swm(QUIC_TXFC *txfc)
{
return txfc->swm;
}
/*
* RX Flow Controller (RXFC)
* =========================
*/
int ossl_quic_rxfc_init(QUIC_RXFC *rxfc, QUIC_RXFC *conn_rxfc,
uint64_t initial_window_size,
uint64_t max_window_size,
OSSL_TIME (*now)(void *now_arg),
void *now_arg)
{
if (conn_rxfc != NULL && conn_rxfc->parent != NULL)
return 0;
rxfc->swm = 0;
rxfc->cwm = initial_window_size;
rxfc->rwm = 0;
rxfc->esrwm = 0;
rxfc->hwm = 0;
rxfc->cur_window_size = initial_window_size;
rxfc->max_window_size = max_window_size;
rxfc->parent = conn_rxfc;
rxfc->error_code = 0;
rxfc->has_cwm_changed = 0;
rxfc->epoch_start = ossl_time_zero();
rxfc->now = now;
rxfc->now_arg = now_arg;
rxfc->is_fin = 0;
return 1;
}
QUIC_RXFC *ossl_quic_rxfc_get_parent(QUIC_RXFC *rxfc)
{
return rxfc->parent;
}
void ossl_quic_rxfc_set_max_window_size(QUIC_RXFC *rxfc,
size_t max_window_size)
{
rxfc->max_window_size = max_window_size;
}
static void rxfc_start_epoch(QUIC_RXFC *rxfc)
{
rxfc->epoch_start = rxfc->now(rxfc->now_arg);
rxfc->esrwm = rxfc->rwm;
}
static int on_rx_controlled_bytes(QUIC_RXFC *rxfc, uint64_t num_bytes)
{
int ok = 1;
uint64_t credit = rxfc->cwm - rxfc->swm;
if (num_bytes > credit) {
ok = 0;
num_bytes = credit;
rxfc->error_code = QUIC_ERR_FLOW_CONTROL_ERROR;
}
rxfc->swm += num_bytes;
return ok;
}
int ossl_quic_rxfc_on_rx_stream_frame(QUIC_RXFC *rxfc, uint64_t end, int is_fin)
{
uint64_t delta;
if (rxfc->parent == NULL)
return 0;
if (rxfc->is_fin && ((is_fin && rxfc->hwm != end) || end > rxfc->hwm)) {
/* Stream size cannot change after the stream is finished */
rxfc->error_code = QUIC_ERR_FINAL_SIZE_ERROR;
return 1; /* not a caller error */
}
if (is_fin)
rxfc->is_fin = 1;
if (end > rxfc->hwm) {
delta = end - rxfc->hwm;
rxfc->hwm = end;
on_rx_controlled_bytes(rxfc, delta); /* result ignored */
on_rx_controlled_bytes(rxfc->parent, delta); /* result ignored */
} else if (end < rxfc->hwm && is_fin) {
rxfc->error_code = QUIC_ERR_FINAL_SIZE_ERROR;
return 1; /* not a caller error */
}
return 1;
}
/* threshold = 3/4 */
#define WINDOW_THRESHOLD_NUM 3
#define WINDOW_THRESHOLD_DEN 4
static int rxfc_cwm_bump_desired(QUIC_RXFC *rxfc)
{
int err = 0;
uint64_t window_rem = rxfc->cwm - rxfc->rwm;
uint64_t threshold
= safe_mul_uint64_t(rxfc->cur_window_size,
WINDOW_THRESHOLD_NUM, &err) / WINDOW_THRESHOLD_DEN;
if (err)
/*
* Extremely large window should never occur, but if it does, just use
* 1/2 as the threshold.
*/
threshold = rxfc->cur_window_size / 2;
return window_rem <= threshold;
}
static int rxfc_should_bump_window_size(QUIC_RXFC *rxfc, OSSL_TIME rtt)
{
/*
* dt: time since start of epoch
* b: bytes of window consumed since start of epoch
* dw: proportion of window consumed since start of epoch
* T_window: time it will take to use up the entire window, based on dt, dw
* RTT: The current estimated RTT.
*
* b = rwm - esrwm
* dw = b / window_size
* T_window = dt / dw
* T_window = dt / (b / window_size)
* T_window = (dt * window_size) / b
*
* We bump the window size if T_window < 4 * RTT.
*
* We leave the division by b on the LHS to reduce the risk of overflowing
* our 64-bit nanosecond representation, which will afford plenty of
* precision left over after the division anyway.
*/
uint64_t b = rxfc->rwm - rxfc->esrwm;
OSSL_TIME now, dt, t_window;
if (b == 0)
return 0;
now = rxfc->now(rxfc->now_arg);
dt = ossl_time_subtract(now, rxfc->epoch_start);
t_window = ossl_time_muldiv(dt, rxfc->cur_window_size, b);
return ossl_time_compare(t_window, ossl_time_multiply(rtt, 4)) < 0;
}
static void rxfc_adjust_window_size(QUIC_RXFC *rxfc, uint64_t min_window_size,
OSSL_TIME rtt)
{
/* Are we sending updates too often? */
uint64_t new_window_size;
new_window_size = rxfc->cur_window_size;
if (rxfc_should_bump_window_size(rxfc, rtt))
new_window_size *= 2;
if (new_window_size < min_window_size)
new_window_size = min_window_size;
if (new_window_size > rxfc->max_window_size) /* takes precedence over min size */
new_window_size = rxfc->max_window_size;
rxfc->cur_window_size = new_window_size;
rxfc_start_epoch(rxfc);
}
static void rxfc_update_cwm(QUIC_RXFC *rxfc, uint64_t min_window_size,
OSSL_TIME rtt)
{
uint64_t new_cwm;
if (!rxfc_cwm_bump_desired(rxfc))
return;
rxfc_adjust_window_size(rxfc, min_window_size, rtt);
new_cwm = rxfc->rwm + rxfc->cur_window_size;
if (new_cwm > rxfc->cwm) {
rxfc->cwm = new_cwm;
rxfc->has_cwm_changed = 1;
}
}
static int rxfc_on_retire(QUIC_RXFC *rxfc, uint64_t num_bytes,
uint64_t min_window_size,
OSSL_TIME rtt)
{
if (ossl_time_is_zero(rxfc->epoch_start))
/* This happens when we retire our first ever bytes. */
rxfc_start_epoch(rxfc);
rxfc->rwm += num_bytes;
rxfc_update_cwm(rxfc, min_window_size, rtt);
return 1;
}
int ossl_quic_rxfc_on_retire(QUIC_RXFC *rxfc,
uint64_t num_bytes,
OSSL_TIME rtt)
{
if (rxfc->parent == NULL)
return 0;
if (num_bytes == 0)
return 1;
if (rxfc->rwm + num_bytes > rxfc->swm)
/* Impossible for us to retire more bytes than we have received. */
return 0;
rxfc_on_retire(rxfc, num_bytes, 0, rtt);
rxfc_on_retire(rxfc->parent, num_bytes, rxfc->cur_window_size, rtt);
return 1;
}
uint64_t ossl_quic_rxfc_get_cwm(QUIC_RXFC *rxfc)
{
return rxfc->cwm;
}
uint64_t ossl_quic_rxfc_get_swm(QUIC_RXFC *rxfc)
{
return rxfc->swm;
}
uint64_t ossl_quic_rxfc_get_rwm(QUIC_RXFC *rxfc)
{
return rxfc->rwm;
}
int ossl_quic_rxfc_has_cwm_changed(QUIC_RXFC *rxfc, int clear)
{
int r = rxfc->has_cwm_changed;
if (clear)
rxfc->has_cwm_changed = 0;
return r;
}
int ossl_quic_rxfc_get_error(QUIC_RXFC *rxfc, int clear)
{
int r = rxfc->error_code;
if (clear)
rxfc->error_code = 0;
return r;
}

View File

@ -287,6 +287,10 @@ IF[{- !$disabled{tests} -}]
INCLUDE[quic_record_test]=../include ../apps/include
DEPEND[quic_record_test]=../libcrypto.a ../libssl.a libtestutil.a
SOURCE[quic_fc_test]=quic_fc_test.c
INCLUDE[quic_fc_test]=../include ../apps/include
DEPEND[quic_fc_test]=../libcrypto.a ../libssl.a libtestutil.a
SOURCE[asynctest]=asynctest.c
INCLUDE[asynctest]=../include ../apps/include
DEPEND[asynctest]=../libcrypto
@ -991,7 +995,7 @@ ENDIF
ENDIF
IF[{- !$disabled{'quic'} -}]
PROGRAMS{noinst}=quicapitest quic_wire_test quic_ackm_test quic_record_test
PROGRAMS{noinst}=quicapitest quic_wire_test quic_ackm_test quic_record_test quic_fc_test
ENDIF
SOURCE[quicapitest]=quicapitest.c helpers/ssltestlib.c

632
test/quic_fc_test.c Normal file
View File

@ -0,0 +1,632 @@
/*
* 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_fc.h"
#include "internal/quic_error.h"
#include "testutil.h"
static int test_txfc(int is_stream)
{
int testresult = 0;
QUIC_TXFC conn_txfc, stream_txfc, *txfc, *parent_txfc;
if (!TEST_true(ossl_quic_txfc_init(&conn_txfc, 0)))
goto err;
if (is_stream && !TEST_true(ossl_quic_txfc_init(&stream_txfc, &conn_txfc)))
goto err;
txfc = is_stream ? &stream_txfc : &conn_txfc;
parent_txfc = is_stream ? &conn_txfc : NULL;
if (!TEST_true(ossl_quic_txfc_bump_cwm(txfc, 2000)))
goto err;
if (is_stream && !TEST_true(ossl_quic_txfc_bump_cwm(parent_txfc, 2000)))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_swm(txfc), 0))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_cwm(txfc), 2000))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_credit_local(txfc), 2000))
goto err;
if (is_stream && !TEST_uint64_t_eq(ossl_quic_txfc_get_credit(txfc),
2000))
goto err;
if (!TEST_false(ossl_quic_txfc_has_become_blocked(txfc, 0)))
goto err;
if (!TEST_true(ossl_quic_txfc_consume_credit(txfc, 500)))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_credit_local(txfc), 1500))
goto err;
if (is_stream && !TEST_uint64_t_eq(ossl_quic_txfc_get_credit(txfc),
1500))
goto err;
if (!TEST_false(ossl_quic_txfc_has_become_blocked(txfc, 0)))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_swm(txfc), 500))
goto err;
if (!TEST_true(ossl_quic_txfc_consume_credit(txfc, 100)))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_swm(txfc), 600))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_credit_local(txfc), 1400))
goto err;
if (is_stream && !TEST_uint64_t_eq(ossl_quic_txfc_get_credit(txfc),
1400))
goto err;
if (!TEST_false(ossl_quic_txfc_has_become_blocked(txfc, 0)))
goto err;
if (!TEST_true(ossl_quic_txfc_consume_credit(txfc, 1400)))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_credit_local(txfc), 0))
goto err;
if (is_stream && !TEST_uint64_t_eq(ossl_quic_txfc_get_credit(txfc),
0))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_swm(txfc), 2000))
goto err;
if (!TEST_true(ossl_quic_txfc_has_become_blocked(txfc, 0)))
goto err;
if (!TEST_true(ossl_quic_txfc_has_become_blocked(txfc, 0)))
goto err;
if (!TEST_true(ossl_quic_txfc_has_become_blocked(txfc, 1)))
goto err;
if (!TEST_false(ossl_quic_txfc_has_become_blocked(txfc, 0)))
goto err;
if (!TEST_false(ossl_quic_txfc_has_become_blocked(txfc, 0)))
goto err;
if (!TEST_false(ossl_quic_txfc_consume_credit(txfc, 1)))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_cwm(txfc), 2000))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_swm(txfc), 2000))
goto err;
if (!TEST_false(ossl_quic_txfc_bump_cwm(txfc, 2000)))
goto err;
if (!TEST_true(ossl_quic_txfc_bump_cwm(txfc, 2500)))
goto err;
if (is_stream && !TEST_true(ossl_quic_txfc_bump_cwm(parent_txfc, 2400)))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_cwm(txfc), 2500))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_swm(txfc), 2000))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_credit_local(txfc), 500))
goto err;
if (is_stream)
ossl_quic_txfc_has_become_blocked(parent_txfc, 1);
if (is_stream) {
if (!TEST_true(ossl_quic_txfc_consume_credit(txfc, 399)))
goto err;
if (!TEST_false(ossl_quic_txfc_has_become_blocked(txfc, 0)))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_txfc_get_credit(txfc), 1))
goto err;
if (!TEST_true(ossl_quic_txfc_consume_credit(txfc, 1)))
goto err;
if (!TEST_true(ossl_quic_txfc_has_become_blocked(parent_txfc, 0)))
goto err;
if (!TEST_true(ossl_quic_txfc_has_become_blocked(parent_txfc, 1)))
goto err;
if (!TEST_false(ossl_quic_txfc_has_become_blocked(parent_txfc, 0)))
goto err;
} else {
if (!TEST_true(ossl_quic_txfc_consume_credit(txfc, 499)))
goto err;
if (!TEST_false(ossl_quic_txfc_has_become_blocked(txfc, 0)))
goto err;
if (is_stream && !TEST_false(ossl_quic_txfc_has_become_blocked(parent_txfc, 0)))
goto err;
if (!TEST_true(ossl_quic_txfc_consume_credit(txfc, 1)))
goto err;
if (!TEST_true(ossl_quic_txfc_has_become_blocked(txfc, 0)))
goto err;
if (!TEST_true(ossl_quic_txfc_has_become_blocked(txfc, 1)))
goto err;
if (!TEST_false(ossl_quic_txfc_has_become_blocked(txfc, 0)))
goto err;
}
testresult = 1;
err:
return testresult;
}
static OSSL_TIME cur_time;
static OSSL_TIME fake_now(void *arg)
{
return cur_time;
}
#define RX_OPC_END 0
#define RX_OPC_INIT_CONN 1 /* arg0=initial window, arg1=max window */
#define RX_OPC_INIT_STREAM 2 /* arg0=initial window, arg1=max window */
#define RX_OPC_RX 3 /* arg0=end, arg1=is_fin */
#define RX_OPC_RETIRE 4 /* arg0=num_bytes, arg1=rtt in OSSL_TIME ticks, expect_fail */
#define RX_OPC_CHECK_CWM_CONN 5 /* arg0=expected */
#define RX_OPC_CHECK_CWM_STREAM 6 /* arg0=expected */
#define RX_OPC_CHECK_SWM_CONN 7 /* arg0=expected */
#define RX_OPC_CHECK_SWM_STREAM 8 /* arg0=expected */
#define RX_OPC_CHECK_RWM_CONN 9 /* arg0=expected */
#define RX_OPC_CHECK_RWM_STREAM 10 /* arg0=expected */
#define RX_OPC_CHECK_CHANGED_CONN 11 /* arg0=expected, arg1=clear */
#define RX_OPC_CHECK_CHANGED_STREAM 12 /* arg0=expected, arg1=clear */
#define RX_OPC_CHECK_ERROR_CONN 13 /* arg0=expected, arg1=clear */
#define RX_OPC_CHECK_ERROR_STREAM 14 /* arg0=expected, arg1=clear */
#define RX_OPC_STEP_TIME 15 /* arg0=OSSL_TIME ticks to advance */
#define RX_OPC_MSG 16
struct rx_test_op {
unsigned char op;
size_t stream_idx;
uint64_t arg0, arg1;
unsigned char expect_fail;
const char *msg;
};
#define RX_OP_END \
{ RX_OPC_END }
#define RX_OP_INIT_CONN(init_window_size, max_window_size) \
{ RX_OPC_INIT_CONN, 0, (init_window_size), (max_window_size) },
#define RX_OP_INIT_STREAM(stream_idx, init_window_size, max_window_size) \
{ RX_OPC_INIT_STREAM, (stream_idx), (init_window_size), (max_window_size) },
#define RX_OP_RX(stream_idx, end, is_fin) \
{ RX_OPC_RX, (stream_idx), (end), (is_fin) },
#define RX_OP_RETIRE(stream_idx, num_bytes, rtt, expect_fail) \
{ RX_OPC_RETIRE, (stream_idx), (num_bytes), (rtt), (expect_fail) },
#define RX_OP_CHECK_CWM_CONN(expected) \
{ RX_OPC_CHECK_CWM_CONN, 0, (expected) },
#define RX_OP_CHECK_CWM_STREAM(stream_id, expected) \
{ RX_OPC_CHECK_CWM_STREAM, (stream_id), (expected) },
#define RX_OP_CHECK_SWM_CONN(expected) \
{ RX_OPC_CHECK_SWM_CONN, 0, (expected) },
#define RX_OP_CHECK_SWM_STREAM(stream_id, expected) \
{ RX_OPC_CHECK_SWM_STREAM, (stream_id), (expected) },
#define RX_OP_CHECK_RWM_CONN(expected) \
{ RX_OPC_CHECK_RWM_CONN, 0, (expected) },
#define RX_OP_CHECK_RWM_STREAM(stream_id, expected) \
{ RX_OPC_CHECK_RWM_STREAM, (stream_id), (expected) },
#define RX_OP_CHECK_CHANGED_CONN(expected, clear) \
{ RX_OPC_CHECK_CHANGED_CONN, 0, (expected), (clear) },
#define RX_OP_CHECK_CHANGED_STREAM(stream_id, expected, clear) \
{ RX_OPC_CHECK_CHANGED_STREAM, (stream_id), (expected), (clear) },
#define RX_OP_CHECK_ERROR_CONN(expected, clear) \
{ RX_OPC_CHECK_ERROR_CONN, 0, (expected), (clear) },
#define RX_OP_CHECK_ERROR_STREAM(stream_id, expected, clear) \
{ RX_OPC_CHECK_ERROR_STREAM, (stream_id), (expected), (clear) },
#define RX_OP_STEP_TIME(t) \
{ RX_OPC_STEP_TIME, 0, (t) },
#define RX_OP_MSG(msg) \
{ RX_OPC_MSG, 0, 0, 0, 0, (msg) },
#define RX_OP_INIT(init_window_size, max_window_size) \
RX_OP_INIT_CONN(init_window_size, max_window_size) \
RX_OP_INIT_STREAM(0, init_window_size, max_window_size)
#define RX_OP_CHECK_CWM(expected) \
RX_OP_CHECK_CWM_CONN(expected) \
RX_OP_CHECK_CWM_STREAM(0, expected)
#define RX_OP_CHECK_SWM(expected) \
RX_OP_CHECK_SWM_CONN(expected) \
RX_OP_CHECK_SWM_STREAM(0, expected)
#define RX_OP_CHECK_RWM(expected) \
RX_OP_CHECK_RWM_CONN(expected) \
RX_OP_CHECK_RWM_STREAM(0, expected)
#define RX_OP_CHECK_CHANGED(expected, clear) \
RX_OP_CHECK_CHANGED_CONN(expected, clear) \
RX_OP_CHECK_CHANGED_STREAM(0, expected, clear)
#define RX_OP_CHECK_ERROR(expected, clear) \
RX_OP_CHECK_ERROR_CONN(expected, clear) \
RX_OP_CHECK_ERROR_STREAM(0, expected, clear)
#define INIT_WINDOW_SIZE (1 * 1024 * 1024)
#define INIT_S_WINDOW_SIZE (384 * 1024)
/* 1. Basic RXFC Tests (stream window == connection window) */
static const struct rx_test_op rx_script_1[] = {
RX_OP_STEP_TIME(1000 * OSSL_TIME_MS)
RX_OP_INIT(INIT_WINDOW_SIZE, 10 * INIT_WINDOW_SIZE)
/* Check initial state. */
RX_OP_CHECK_CWM(INIT_WINDOW_SIZE)
RX_OP_CHECK_ERROR(0, 0)
RX_OP_CHECK_CHANGED(0, 0)
/* We cannot retire what we have not received. */
RX_OP_RETIRE(0, 1, 0, 1)
/* Zero bytes is a no-op and always valid. */
RX_OP_RETIRE(0, 0, 0, 0)
/* Consume some window. */
RX_OP_RX(0, 50, 0)
/* CWM has not changed. */
RX_OP_CHECK_CWM(INIT_WINDOW_SIZE)
RX_OP_CHECK_SWM(50)
/* RX, Partial retire */
RX_OP_RX(0, 60, 0)
RX_OP_CHECK_SWM(60)
RX_OP_RETIRE(0, 20, 50 * OSSL_TIME_MS, 0)
RX_OP_CHECK_RWM(20)
RX_OP_CHECK_SWM(60)
RX_OP_CHECK_CWM(INIT_WINDOW_SIZE)
RX_OP_CHECK_CHANGED(0, 0)
RX_OP_CHECK_ERROR(0, 0)
/* Fully retired */
RX_OP_RETIRE(0, 41, 0, 1)
RX_OP_RETIRE(0, 40, 0, 0)
RX_OP_CHECK_SWM(60)
RX_OP_CHECK_RWM(60)
RX_OP_CHECK_CWM(INIT_WINDOW_SIZE)
RX_OP_CHECK_CHANGED(0, 0)
RX_OP_CHECK_ERROR(0, 0)
/* Exhaustion of window - we do not enlarge the window this epoch */
RX_OP_STEP_TIME(201 * OSSL_TIME_MS)
RX_OP_RX(0, INIT_WINDOW_SIZE, 0)
RX_OP_RETIRE(0, INIT_WINDOW_SIZE - 60, 50 * OSSL_TIME_MS, 0)
RX_OP_CHECK_SWM(INIT_WINDOW_SIZE)
RX_OP_CHECK_CHANGED(1, 0)
RX_OP_CHECK_CHANGED(1, 1)
RX_OP_CHECK_CHANGED(0, 0)
RX_OP_CHECK_ERROR(0, 0)
RX_OP_CHECK_CWM(INIT_WINDOW_SIZE * 2)
/* Second epoch - we still do not enlarge the window this epoch */
RX_OP_RX(0, INIT_WINDOW_SIZE + 1, 0)
RX_OP_STEP_TIME(201 * OSSL_TIME_MS)
RX_OP_RX(0, INIT_WINDOW_SIZE * 2, 0)
RX_OP_RETIRE(0, INIT_WINDOW_SIZE, 50 * OSSL_TIME_MS, 0)
RX_OP_CHECK_SWM(INIT_WINDOW_SIZE * 2)
RX_OP_CHECK_CHANGED(1, 0)
RX_OP_CHECK_CHANGED(1, 1)
RX_OP_CHECK_CHANGED(0, 0)
RX_OP_CHECK_ERROR(0, 0)
RX_OP_CHECK_CWM(INIT_WINDOW_SIZE * 3)
/* Third epoch - we enlarge the window */
RX_OP_RX(0, INIT_WINDOW_SIZE * 2 + 1, 0)
RX_OP_STEP_TIME(199 * OSSL_TIME_MS)
RX_OP_RX(0, INIT_WINDOW_SIZE * 3, 0)
RX_OP_RETIRE(0, INIT_WINDOW_SIZE, 50 * OSSL_TIME_MS, 0)
RX_OP_CHECK_SWM(INIT_WINDOW_SIZE * 3)
RX_OP_CHECK_CHANGED(1, 0)
RX_OP_CHECK_CHANGED(1, 1)
RX_OP_CHECK_CHANGED(0, 0)
RX_OP_CHECK_ERROR(0, 0)
RX_OP_CHECK_CWM(INIT_WINDOW_SIZE * 5)
/* Fourth epoch - peer violates flow control */
RX_OP_RX(0, INIT_WINDOW_SIZE * 5 - 5, 0)
RX_OP_STEP_TIME(250 * OSSL_TIME_MS)
RX_OP_RX(0, INIT_WINDOW_SIZE * 5 + 1, 0)
RX_OP_CHECK_SWM(INIT_WINDOW_SIZE * 5)
RX_OP_CHECK_ERROR(QUIC_ERR_FLOW_CONTROL_ERROR, 0)
RX_OP_CHECK_ERROR(QUIC_ERR_FLOW_CONTROL_ERROR, 1)
RX_OP_CHECK_ERROR(0, 0)
RX_OP_CHECK_CWM(INIT_WINDOW_SIZE * 5)
/*
* No window expansion due to flow control violation; window expansion is
* triggered by retirement only.
*/
RX_OP_CHECK_CHANGED(0, 0)
RX_OP_END
};
/* 2. Interaction between connection and stream-level flow control */
static const struct rx_test_op rx_script_2[] = {
RX_OP_STEP_TIME(1000 * OSSL_TIME_MS)
RX_OP_INIT_CONN(INIT_WINDOW_SIZE, 10 * INIT_WINDOW_SIZE)
RX_OP_INIT_STREAM(0, INIT_S_WINDOW_SIZE, 30 * INIT_S_WINDOW_SIZE)
RX_OP_INIT_STREAM(1, INIT_S_WINDOW_SIZE, 30 * INIT_S_WINDOW_SIZE)
RX_OP_RX(0, 10, 0)
RX_OP_CHECK_CWM_CONN(INIT_WINDOW_SIZE)
RX_OP_CHECK_CWM_STREAM(0, INIT_S_WINDOW_SIZE)
RX_OP_CHECK_CWM_STREAM(1, INIT_S_WINDOW_SIZE)
RX_OP_CHECK_SWM_CONN(10)
RX_OP_CHECK_SWM_STREAM(0, 10)
RX_OP_CHECK_SWM_STREAM(1, 0)
RX_OP_CHECK_RWM_CONN(0)
RX_OP_CHECK_RWM_STREAM(0, 0)
RX_OP_CHECK_RWM_STREAM(1, 0)
RX_OP_RX(1, 42, 0)
RX_OP_RX(1, 42, 0) /* monotonic; equal or lower values ignored */
RX_OP_RX(1, 35, 0)
RX_OP_CHECK_CWM_CONN(INIT_WINDOW_SIZE)
RX_OP_CHECK_CWM_STREAM(0, INIT_S_WINDOW_SIZE)
RX_OP_CHECK_CWM_STREAM(1, INIT_S_WINDOW_SIZE)
RX_OP_CHECK_SWM_CONN(52)
RX_OP_CHECK_SWM_STREAM(0, 10)
RX_OP_CHECK_SWM_STREAM(1, 42)
RX_OP_CHECK_RWM_CONN(0)
RX_OP_CHECK_RWM_STREAM(0, 0)
RX_OP_CHECK_RWM_STREAM(1, 0)
RX_OP_RETIRE(0, 10, 50 * OSSL_TIME_MS, 0)
RX_OP_CHECK_RWM_CONN(10)
RX_OP_CHECK_RWM_STREAM(0, 10)
RX_OP_CHECK_CWM_CONN(INIT_WINDOW_SIZE)
RX_OP_CHECK_CWM_STREAM(0, INIT_S_WINDOW_SIZE)
RX_OP_CHECK_CWM_STREAM(1, INIT_S_WINDOW_SIZE)
RX_OP_RETIRE(1, 42, 50 * OSSL_TIME_MS, 0)
RX_OP_CHECK_RWM_CONN(52)
RX_OP_CHECK_RWM_STREAM(1, 42)
RX_OP_CHECK_CWM_CONN(INIT_WINDOW_SIZE)
RX_OP_CHECK_CWM_STREAM(0, INIT_S_WINDOW_SIZE)
RX_OP_CHECK_CWM_STREAM(1, INIT_S_WINDOW_SIZE)
RX_OP_CHECK_CHANGED_CONN(0, 0)
/* FC limited by stream but not connection */
RX_OP_STEP_TIME(1000 * OSSL_TIME_MS)
RX_OP_RX(0, INIT_S_WINDOW_SIZE, 0)
RX_OP_CHECK_SWM_CONN(INIT_S_WINDOW_SIZE + 42)
RX_OP_CHECK_SWM_STREAM(0, INIT_S_WINDOW_SIZE)
RX_OP_CHECK_SWM_STREAM(1, 42)
RX_OP_CHECK_CWM_CONN(INIT_WINDOW_SIZE)
RX_OP_CHECK_CWM_STREAM(0, INIT_S_WINDOW_SIZE)
/* We bump CWM when more than 1/4 of the window has been retired */
RX_OP_RETIRE(0, INIT_S_WINDOW_SIZE - 10, 50 * OSSL_TIME_MS, 0)
RX_OP_CHECK_CWM_STREAM(0, INIT_S_WINDOW_SIZE * 2)
RX_OP_CHECK_CHANGED_STREAM(0, 1, 0)
RX_OP_CHECK_CHANGED_STREAM(0, 1, 1)
RX_OP_CHECK_CHANGED_STREAM(0, 0, 0)
/*
* This is more than 1/4 of the connection window, so CWM will
* be bumped here too.
*/
RX_OP_CHECK_CWM_CONN(INIT_S_WINDOW_SIZE + INIT_WINDOW_SIZE + 42)
RX_OP_CHECK_RWM_CONN(INIT_S_WINDOW_SIZE + 42)
RX_OP_CHECK_RWM_STREAM(0, INIT_S_WINDOW_SIZE)
RX_OP_CHECK_RWM_STREAM(1, 42)
RX_OP_CHECK_CHANGED_CONN(1, 0)
RX_OP_CHECK_CHANGED_CONN(1, 1)
RX_OP_CHECK_CHANGED_CONN(0, 0)
RX_OP_CHECK_ERROR_CONN(0, 0)
RX_OP_CHECK_ERROR_STREAM(0, 0, 0)
RX_OP_CHECK_ERROR_STREAM(1, 0, 0)
/* Test exceeding limit at stream level. */
RX_OP_RX(0, INIT_S_WINDOW_SIZE * 2 + 1, 0)
RX_OP_CHECK_ERROR_STREAM(0, QUIC_ERR_FLOW_CONTROL_ERROR, 0)
RX_OP_CHECK_ERROR_STREAM(0, QUIC_ERR_FLOW_CONTROL_ERROR, 1)
RX_OP_CHECK_ERROR_STREAM(0, 0, 0)
RX_OP_CHECK_ERROR_CONN(0, 0) /* doesn't affect conn */
/* Test exceeding limit at connection level. */
RX_OP_RX(0, INIT_WINDOW_SIZE * 2, 0)
RX_OP_CHECK_ERROR_CONN(QUIC_ERR_FLOW_CONTROL_ERROR, 0)
RX_OP_CHECK_ERROR_CONN(QUIC_ERR_FLOW_CONTROL_ERROR, 1)
RX_OP_CHECK_ERROR_CONN(0, 0)
RX_OP_END
};
static const struct rx_test_op *rx_scripts[] = {
rx_script_1,
rx_script_2
};
static int run_rxfc_script(const struct rx_test_op *script)
{
#define MAX_STREAMS 3
int testresult = 0;
const struct rx_test_op *op = script;
QUIC_RXFC conn_rxfc, stream_rxfc[MAX_STREAMS];
char stream_init_done[MAX_STREAMS] = {0};
int conn_init_done = 0;
cur_time = ossl_time_zero();
for (; op->op != RX_OPC_END; ++op) {
switch (op->op) {
case RX_OPC_INIT_CONN:
if (!TEST_true(ossl_quic_rxfc_init(&conn_rxfc, 0,
op->arg0, op->arg1,
fake_now, NULL)))
goto err;
conn_init_done = 1;
break;
case RX_OPC_INIT_STREAM:
if (!TEST_size_t_lt(op->stream_idx, OSSL_NELEM(stream_rxfc)))
goto err;
if (!TEST_true(ossl_quic_rxfc_init(&stream_rxfc[op->stream_idx],
&conn_rxfc,
op->arg0, op->arg1,
fake_now, NULL)))
goto err;
stream_init_done[op->stream_idx] = 1;
break;
case RX_OPC_RX:
if (!TEST_true(conn_init_done && op->stream_idx < OSSL_NELEM(stream_rxfc)
&& stream_init_done[op->stream_idx]))
goto err;
if (!TEST_true(ossl_quic_rxfc_on_rx_stream_frame(&stream_rxfc[op->stream_idx],
op->arg0,
(int)op->arg1)))
goto err;
break;
case RX_OPC_RETIRE:
if (!TEST_true(conn_init_done && op->stream_idx < OSSL_NELEM(stream_rxfc)
&& stream_init_done[op->stream_idx]))
goto err;
if (!TEST_int_eq(ossl_quic_rxfc_on_retire(&stream_rxfc[op->stream_idx],
op->arg0,
ossl_ticks2time(op->arg1)),
!op->expect_fail))
goto err;
break;
case RX_OPC_CHECK_CWM_CONN:
if (!TEST_true(conn_init_done))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_rxfc_get_cwm(&conn_rxfc),
op->arg0))
goto err;
break;
case RX_OPC_CHECK_CWM_STREAM:
if (!TEST_true(op->stream_idx < OSSL_NELEM(stream_rxfc)
&& stream_init_done[op->stream_idx]))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_rxfc_get_cwm(&stream_rxfc[op->stream_idx]),
op->arg0))
goto err;
break;
case RX_OPC_CHECK_SWM_CONN:
if (!TEST_true(conn_init_done))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_rxfc_get_swm(&conn_rxfc),
op->arg0))
goto err;
break;
case RX_OPC_CHECK_SWM_STREAM:
if (!TEST_true(op->stream_idx < OSSL_NELEM(stream_rxfc)
&& stream_init_done[op->stream_idx]))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_rxfc_get_swm(&stream_rxfc[op->stream_idx]),
op->arg0))
goto err;
break;
case RX_OPC_CHECK_RWM_CONN:
if (!TEST_true(conn_init_done))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_rxfc_get_rwm(&conn_rxfc),
op->arg0))
goto err;
break;
case RX_OPC_CHECK_RWM_STREAM:
if (!TEST_true(op->stream_idx < OSSL_NELEM(stream_rxfc)
&& stream_init_done[op->stream_idx]))
goto err;
if (!TEST_uint64_t_eq(ossl_quic_rxfc_get_rwm(&stream_rxfc[op->stream_idx]),
op->arg0))
goto err;
break;
case RX_OPC_CHECK_CHANGED_CONN:
if (!TEST_true(conn_init_done))
goto err;
if (!TEST_int_eq(ossl_quic_rxfc_has_cwm_changed(&conn_rxfc,
(int)op->arg1),
(int)op->arg0))
goto err;
break;
case RX_OPC_CHECK_CHANGED_STREAM:
if (!TEST_true(op->stream_idx < OSSL_NELEM(stream_rxfc)
&& stream_init_done[op->stream_idx]))
goto err;
if (!TEST_int_eq(ossl_quic_rxfc_has_cwm_changed(&stream_rxfc[op->stream_idx],
(int)op->arg1),
(int)op->arg0))
goto err;
break;
case RX_OPC_CHECK_ERROR_CONN:
if (!TEST_true(conn_init_done))
goto err;
if (!TEST_int_eq(ossl_quic_rxfc_get_error(&conn_rxfc,
(int)op->arg1),
(int)op->arg0))
goto err;
break;
case RX_OPC_CHECK_ERROR_STREAM:
if (!TEST_true(op->stream_idx < OSSL_NELEM(stream_rxfc)
&& stream_init_done[op->stream_idx]))
goto err;
if (!TEST_int_eq(ossl_quic_rxfc_get_error(&stream_rxfc[op->stream_idx],
(int)op->arg1),
(int)op->arg0))
goto err;
break;
case RX_OPC_STEP_TIME:
cur_time = ossl_time_add(cur_time, ossl_ticks2time(op->arg0));
break;
case RX_OPC_MSG:
fprintf(stderr, "# %s\n", op->msg);
break;
default:
goto err;
}
}
testresult = 1;
err:
return testresult;
}
static int test_rxfc(int idx)
{
return run_rxfc_script(rx_scripts[idx]);
}
int setup_tests(void)
{
ADD_ALL_TESTS(test_txfc, 2);
ADD_ALL_TESTS(test_rxfc, OSSL_NELEM(rx_scripts));
return 1;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2015-2018 The OpenSSL Project Authors. All Rights Reserved.
* 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

View File

@ -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_fc");
plan skip_all => "QUIC protocol is not supported by this OpenSSL build"
if disabled('quic');
plan tests => 1;
ok(run(test(["quic_fc_test"])));