diff --git a/deps/ngtcp2/ngtcp2.gyp b/deps/ngtcp2/ngtcp2.gyp index 2901eff484ac55..08de0fca860529 100644 --- a/deps/ngtcp2/ngtcp2.gyp +++ b/deps/ngtcp2/ngtcp2.gyp @@ -27,6 +27,7 @@ 'ngtcp2/lib/ngtcp2_objalloc.c', 'ngtcp2/lib/ngtcp2_opl.c', 'ngtcp2/lib/ngtcp2_path.c', + 'ngtcp2/lib/ngtcp2_pcg.c', 'ngtcp2/lib/ngtcp2_pkt.c', 'ngtcp2/lib/ngtcp2_pmtud.c', 'ngtcp2/lib/ngtcp2_ppe.c', @@ -34,6 +35,7 @@ 'ngtcp2/lib/ngtcp2_pv.c', 'ngtcp2/lib/ngtcp2_qlog.c', 'ngtcp2/lib/ngtcp2_range.c', + 'ngtcp2/lib/ngtcp2_ratelim.c', 'ngtcp2/lib/ngtcp2_ringbuf.c', 'ngtcp2/lib/ngtcp2_rob.c', 'ngtcp2/lib/ngtcp2_rst.c', diff --git a/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/ngtcp2.h b/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/ngtcp2.h index b0cc867cde0b20..d58a6f1240d4a4 100644 --- a/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/ngtcp2.h +++ b/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/ngtcp2.h @@ -1704,7 +1704,8 @@ typedef enum ngtcp2_token_type { #define NGTCP2_SETTINGS_V1 1 #define NGTCP2_SETTINGS_V2 2 -#define NGTCP2_SETTINGS_VERSION NGTCP2_SETTINGS_V2 +#define NGTCP2_SETTINGS_V3 3 +#define NGTCP2_SETTINGS_VERSION NGTCP2_SETTINGS_V3 /** * @struct @@ -1917,6 +1918,23 @@ typedef struct ngtcp2_settings { * field has been available since v1.4.0. */ size_t pmtud_probeslen; + /* The following fields have been added since NGTCP2_SETTINGS_V3. */ + /** + * :member:`glitch_ratelim_burst` is the maximum number of tokens + * available to "glitch" rate limiter. "glitch" is a suspicious + * activity from a remote endpoint. If detected, certain amount of + * tokens are consumed. If no tokens are available to consume, the + * connection is closed. The rate of token generation is specified + * by :member:`glitch_ratelim_rate`. This field has been available + * since v1.15.0. + */ + uint64_t glitch_ratelim_burst; + /** + * :member:`glitch_ratelim_rate` is the number of tokens generated + * per second. See :member:`glitch_ratelim_burst` for "glitch" rate + * limiter. This field has been available since v1.15.0. + */ + uint64_t glitch_ratelim_rate; } ngtcp2_settings; /** @@ -3000,9 +3018,8 @@ typedef int (*ngtcp2_begin_path_validation)(ngtcp2_conn *conn, uint32_t flags, * an application the outcome of path validation. |flags| is zero or * more of :macro:`NGTCP2_PATH_VALIDATION_FLAG_* * `. |path| is the path that was - * validated. |old_path| is the path that is previously used before a - * local endpoint has migrated to |path| if |old_path| is not NULL. - * If |res| is + * validated. |fallback_path|, if not NULL, is the path that is used + * if the path validation failed. If |res| is * :enum:`ngtcp2_path_validation_result.NGTCP2_PATH_VALIDATION_RESULT_SUCCESS`, * the path validation succeeded. If |res| is * :enum:`ngtcp2_path_validation_result.NGTCP2_PATH_VALIDATION_RESULT_FAILURE`, @@ -3014,7 +3031,7 @@ typedef int (*ngtcp2_begin_path_validation)(ngtcp2_conn *conn, uint32_t flags, */ typedef int (*ngtcp2_path_validation)(ngtcp2_conn *conn, uint32_t flags, const ngtcp2_path *path, - const ngtcp2_path *old_path, + const ngtcp2_path *fallback_path, ngtcp2_path_validation_result res, void *user_data); @@ -5598,6 +5615,77 @@ NGTCP2_EXTERN size_t ngtcp2_conn_get_send_quantum(ngtcp2_conn *conn); NGTCP2_EXTERN size_t ngtcp2_conn_get_stream_loss_count(ngtcp2_conn *conn, int64_t stream_id); +/** + * @functypedef + * + * :type:`ngtcp2_write_pkt` is a callback function to write a single + * packet in the buffer pointed by |dest| of length |destlen|. The + * implementation should use `ngtcp2_conn_write_pkt`, + * `ngtcp2_conn_writev_stream`, `ngtcp2_conn_writev_datagram`, or + * their variants to write the packet. |path|, |pi|, |dest|, + * |destlen|, and |ts| should be directly passed to those functions. + * If the callback succeeds, it should return the number of bytes + * written to the buffer. In general, this callback function should + * return the value that the above mentioned functions returned except + * for the following error codes: + * + * - :macro:`NGTCP2_ERR_STREAM_DATA_BLOCKED` + * - :macro:`NGTCP2_ERR_STREAM_SHUT_WR` + * - :macro:`NGTCP2_ERR_STREAM_NOT_FOUND` + * + * Those error codes should be handled by an application. If any + * error occurred outside those functions, return + * :macro:`NGTCP2_ERR_CALLBACK_FAILURE`. If no packet is produced, + * return 0. + * + * Because GSO requires that the aggregated packets have the same + * length, :macro:`NGTCP2_WRITE_STREAM_FLAG_PADDING` (or + * :macro:`NGTCP2_WRITE_DATAGRAM_FLAG_PADDING` if + * `ngtcp2_conn_writev_datagram` is used) is recommended. + * + * This callback function has been available since v1.15.0. + */ +typedef ngtcp2_ssize (*ngtcp2_write_pkt)(ngtcp2_conn *conn, ngtcp2_path *path, + ngtcp2_pkt_info *pi, uint8_t *dest, + size_t destlen, ngtcp2_tstamp ts, + void *user_data); + +/** + * @function + * + * `ngtcp2_conn_write_aggregate_pkt` is a helper function to write + * multiple packets in the provided buffer, which is suitable to be + * sent at once in GSO. This function returns the number of bytes + * written to the buffer pointed by |buf| of length |buflen|. + * |buflen| must be at least + * `ngtcp2_conn_get_path_max_tx_udp_payload_size(conn) + * ` bytes long. It is + * recommended to pass the buffer at least + * `ngtcp2_conn_get_max_tx_udp_payload_size(conn) + * ` bytes in order to send a + * PMTUD packet. This function only writes multiple packets if the + * first packet is `ngtcp2_conn_get_path_max_tx_udp_payload_size(conn) + * ` bytes long. The + * application can adjust the length of the buffer to limit the number + * of packets to aggregate. If this function returns positive + * integer, all packets share the same :type:`ngtcp2_path` and + * :type:`ngtcp2_pkt_info` values, and they are assigned to the + * objects pointed by |path| and |pi| respectively. The length of all + * packets other than the last packet is assigned to |*pgsolen|. The + * length of last packet is equal to or less than |*pgsolen|. + * |write_pkt| must write a single packet. After all packets are + * written, this function calls `ngtcp2_conn_update_pkt_tx_time`. + * + * This function returns the number of bytes written to the buffer, or + * a negative error code returned by |write_pkt|. + * + * This function has been available since v1.15.0. + */ +NGTCP2_EXTERN ngtcp2_ssize ngtcp2_conn_write_aggregate_pkt_versioned( + ngtcp2_conn *conn, ngtcp2_path *path, int pkt_info_version, + ngtcp2_pkt_info *pi, uint8_t *buf, size_t buflen, size_t *pgsolen, + ngtcp2_write_pkt write_pkt, ngtcp2_tstamp ts); + /** * @function * @@ -5680,15 +5768,19 @@ NGTCP2_EXTERN void ngtcp2_path_storage_zero(ngtcp2_path_storage *ps); * values. First this function fills |settings| with 0, and set the * default value to the following fields: * - * * :type:`cc_algo ` = + * * :member:`cc_algo ` = * :enum:`ngtcp2_cc_algo.NGTCP2_CC_ALGO_CUBIC` - * * :type:`initial_rtt ` = + * * :member:`initial_rtt ` = * :macro:`NGTCP2_DEFAULT_INITIAL_RTT` - * * :type:`ack_thresh ` = 2 - * * :type:`max_tx_udp_payload_size + * * :member:`ack_thresh ` = 2 + * * :member:`max_tx_udp_payload_size * ` = 1452 - * * :type:`handshake_timeout ` = + * * :member:`handshake_timeout ` = * ``UINT64_MAX`` + * * :member:`glitch_ratelim_burst + * ` = 1000 + * * :member:`glitch_ratelim_rate + * ` = 33 */ NGTCP2_EXTERN void ngtcp2_settings_default_versioned(int settings_version, ngtcp2_settings *settings); @@ -5700,15 +5792,15 @@ NGTCP2_EXTERN void ngtcp2_settings_default_versioned(int settings_version, * default values. First this function fills |params| with 0, and set * the default value to the following fields: * - * * :type:`max_udp_payload_size + * * :member:`max_udp_payload_size * ` = * :macro:`NGTCP2_DEFAULT_MAX_RECV_UDP_PAYLOAD_SIZE` - * * :type:`ack_delay_exponent + * * :member:`ack_delay_exponent * ` = * :macro:`NGTCP2_DEFAULT_ACK_DELAY_EXPONENT` - * * :type:`max_ack_delay ` = + * * :member:`max_ack_delay ` = * :macro:`NGTCP2_DEFAULT_MAX_ACK_DELAY` - * * :type:`active_connection_id_limit + * * :member:`active_connection_id_limit * ` = * :macro:`NGTCP2_DEFAULT_ACTIVE_CONNECTION_ID_LIMIT` */ @@ -5980,6 +6072,17 @@ NGTCP2_EXTERN uint32_t ngtcp2_select_version(const uint32_t *preferred_versions, #define ngtcp2_conn_get_conn_info(CONN, CINFO) \ ngtcp2_conn_get_conn_info_versioned((CONN), NGTCP2_CONN_INFO_VERSION, (CINFO)) +/* + * `ngtcp2_conn_write_aggregate_pkt` is a wrapper around + * `ngtcp2_conn_write_aggregate_pkt_versioned` to set the correct + * struct version. + */ +#define ngtcp2_conn_write_aggregate_pkt(CONN, PATH, PI, BUF, BUFLEN, PGSOLEN, \ + WRITE_PKT, TS) \ + ngtcp2_conn_write_aggregate_pkt_versioned( \ + (CONN), (PATH), NGTCP2_PKT_INFO_VERSION, (PI), (BUF), (BUFLEN), (PGSOLEN), \ + (WRITE_PKT), (TS)) + /* * `ngtcp2_settings_default` is a wrapper around * `ngtcp2_settings_default_versioned` to set the correct struct diff --git a/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/version.h b/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/version.h index 533f6d5ef6c55d..e70a095ceb1300 100644 --- a/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/version.h +++ b/deps/ngtcp2/ngtcp2/lib/includes/ngtcp2/version.h @@ -36,7 +36,7 @@ * * Version number of the ngtcp2 library release. */ -#define NGTCP2_VERSION "1.14.0" +#define NGTCP2_VERSION "1.15.1" /** * @macro @@ -46,6 +46,6 @@ * number, 8 bits for minor and 8 bits for patch. Version 1.2.3 * becomes 0x010203. */ -#define NGTCP2_VERSION_NUM 0x010e00 +#define NGTCP2_VERSION_NUM 0x010f01 #endif /* !defined(NGTCP2_VERSION_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_acktr.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_acktr.c index 776dc0c2c3ef1a..59bc621ef46b28 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_acktr.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_acktr.c @@ -316,9 +316,9 @@ void ngtcp2_acktr_recv_ack(ngtcp2_acktr *acktr, const ngtcp2_ack *fr) { } void ngtcp2_acktr_commit_ack(ngtcp2_acktr *acktr) { - acktr->flags &= (uint16_t) ~(NGTCP2_ACKTR_FLAG_ACTIVE_ACK | - NGTCP2_ACKTR_FLAG_IMMEDIATE_ACK | - NGTCP2_ACKTR_FLAG_CANCEL_TIMER); + acktr->flags &= + (uint16_t)~(NGTCP2_ACKTR_FLAG_ACTIVE_ACK | NGTCP2_ACKTR_FLAG_IMMEDIATE_ACK | + NGTCP2_ACKTR_FLAG_CANCEL_TIMER); acktr->first_unacked_ts = UINT64_MAX; acktr->rx_npkt = 0; } diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.c index a2ffeb6188a57e..44be1e189b8835 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.c @@ -33,6 +33,7 @@ #include "ngtcp2_rcvry.h" #include "ngtcp2_rst.h" #include "ngtcp2_conn_stat.h" +#include "ngtcp2_pcg.h" #define NGTCP2_BBR_MAX_BW_FILTERLEN 2 @@ -906,15 +907,9 @@ static int bbr_is_time_to_probe_bw(ngtcp2_cc_bbr *bbr, ngtcp2_conn_stat *cstat, } static void bbr_pick_probe_wait(ngtcp2_cc_bbr *bbr) { - uint8_t rand; - - bbr->rand(&rand, 1, &bbr->rand_ctx); - - bbr->rounds_since_bw_probe = (uint64_t)(rand / 128); - - bbr->rand(&rand, 1, &bbr->rand_ctx); - - bbr->bw_probe_wait = 2 * NGTCP2_SECONDS + NGTCP2_SECONDS * rand / 255; + bbr->rounds_since_bw_probe = ngtcp2_pcg32_rand_n(bbr->pcg, 2); + bbr->bw_probe_wait = + 2 * NGTCP2_SECONDS + ngtcp2_pcg32_rand_n(bbr->pcg, NGTCP2_SECONDS + 1); } static int bbr_is_reno_coexistence_probe_time(ngtcp2_cc_bbr *bbr, @@ -1388,8 +1383,7 @@ static void bbr_cc_reset(ngtcp2_cc *cc, ngtcp2_conn_stat *cstat, void ngtcp2_cc_bbr_init(ngtcp2_cc_bbr *bbr, ngtcp2_log *log, ngtcp2_conn_stat *cstat, ngtcp2_rst *rst, - ngtcp2_tstamp initial_ts, ngtcp2_rand rand, - const ngtcp2_rand_ctx *rand_ctx) { + ngtcp2_tstamp initial_ts, ngtcp2_pcg32 *pcg) { *bbr = (ngtcp2_cc_bbr){ .cc = { @@ -1403,8 +1397,7 @@ void ngtcp2_cc_bbr_init(ngtcp2_cc_bbr *bbr, ngtcp2_log *log, .reset = bbr_cc_reset, }, .rst = rst, - .rand = rand, - .rand_ctx = *rand_ctx, + .pcg = pcg, .initial_cwnd = cstat->cwnd, }; diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.h index e823711a500654..0499924f58264f 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_bbr.h @@ -35,6 +35,7 @@ #include "ngtcp2_window_filter.h" typedef struct ngtcp2_rst ngtcp2_rst; +typedef struct ngtcp2_pcg32 ngtcp2_pcg32; typedef enum ngtcp2_bbr_state { NGTCP2_BBR_STATE_STARTUP, @@ -62,8 +63,7 @@ typedef struct ngtcp2_cc_bbr { uint64_t initial_cwnd; ngtcp2_rst *rst; - ngtcp2_rand rand; - ngtcp2_rand_ctx rand_ctx; + ngtcp2_pcg32 *pcg; /* max_bw_filter for tracking the maximum recent delivery rate samples for estimating max_bw. */ @@ -136,7 +136,6 @@ typedef struct ngtcp2_cc_bbr { void ngtcp2_cc_bbr_init(ngtcp2_cc_bbr *bbr, ngtcp2_log *log, ngtcp2_conn_stat *cstat, ngtcp2_rst *rst, - ngtcp2_tstamp initial_ts, ngtcp2_rand rand, - const ngtcp2_rand_ctx *rand_ctx); + ngtcp2_tstamp initial_ts, ngtcp2_pcg32 *pcg); #endif /* !defined(NGTCP2_BBR_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_buf.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_buf.c index 75326d6b76b9ca..bf4273f816a810 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_buf.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_buf.c @@ -36,6 +36,12 @@ size_t ngtcp2_buf_cap(const ngtcp2_buf *buf) { return (size_t)(buf->end - buf->begin); } +void ngtcp2_buf_trunc(ngtcp2_buf *buf, size_t len) { + if (ngtcp2_buf_len(buf) > len) { + buf->last = buf->pos + len; + } +} + int ngtcp2_buf_chain_new(ngtcp2_buf_chain **pbufchain, size_t len, const ngtcp2_mem *mem) { *pbufchain = ngtcp2_mem_malloc(mem, sizeof(ngtcp2_buf_chain) + len); diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_buf.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_buf.h index b2bdafc3875260..b59ac9a54b34e6 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_buf.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_buf.h @@ -70,9 +70,7 @@ static inline size_t ngtcp2_buf_left(const ngtcp2_buf *buf) { * ngtcp2_buf_len returns the number of bytes left to read. In other * words, it returns buf->last - buf->pos. */ -static inline size_t ngtcp2_buf_len(const ngtcp2_buf *buf) { - return (size_t)(buf->last - buf->pos); -} +#define ngtcp2_buf_len(BUF) (size_t)((BUF)->last - (BUF)->pos) /* * ngtcp2_buf_cap returns the capacity of the buffer. In other words, @@ -80,6 +78,13 @@ static inline size_t ngtcp2_buf_len(const ngtcp2_buf *buf) { */ size_t ngtcp2_buf_cap(const ngtcp2_buf *buf); +/* + * ngtcp2_buf_trunc truncates the number of bytes to read to at most + * |len|. In other words, it sets buf->last = buf->pos + len if + * ngtcp2_buf_len(buf) > len. + */ +void ngtcp2_buf_trunc(ngtcp2_buf *buf, size_t len); + /* * ngtcp2_buf_chain is a linked list of ngtcp2_buf. */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.c index 393c2281f1ae13..d97ba6a71b2529 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.c @@ -288,7 +288,7 @@ static int conn_call_begin_path_validation(ngtcp2_conn *conn, const ngtcp2_pv *pv) { int rv; uint32_t flags = NGTCP2_PATH_VALIDATION_FLAG_NONE; - const ngtcp2_path *old_path = NULL; + const ngtcp2_path *fallback_path = NULL; if (!pv || !conn->callbacks.begin_path_validation) { return 0; @@ -299,11 +299,11 @@ static int conn_call_begin_path_validation(ngtcp2_conn *conn, } if (pv->flags & NGTCP2_PV_FLAG_FALLBACK_PRESENT) { - old_path = &pv->fallback_dcid.ps.path; + fallback_path = &pv->fallback_dcid.ps.path; } rv = conn->callbacks.begin_path_validation(conn, flags, &pv->dcid.ps.path, - old_path, conn->user_data); + fallback_path, conn->user_data); if (rv != 0) { return NGTCP2_ERR_CALLBACK_FAILURE; } @@ -315,7 +315,7 @@ static int conn_call_path_validation(ngtcp2_conn *conn, const ngtcp2_pv *pv, ngtcp2_path_validation_result res) { int rv; uint32_t flags = NGTCP2_PATH_VALIDATION_FLAG_NONE; - const ngtcp2_path *old_path = NULL; + const ngtcp2_path *fallback_path = NULL; if (!conn->callbacks.path_validation) { return 0; @@ -326,17 +326,17 @@ static int conn_call_path_validation(ngtcp2_conn *conn, const ngtcp2_pv *pv, } if (pv->flags & NGTCP2_PV_FLAG_FALLBACK_PRESENT) { - old_path = &pv->fallback_dcid.ps.path; + fallback_path = &pv->fallback_dcid.ps.path; } - if (conn->server && old_path && - (ngtcp2_addr_cmp(&pv->dcid.ps.path.remote, &old_path->remote) & + if (conn->server && fallback_path && + (ngtcp2_addr_cmp(&pv->dcid.ps.path.remote, &fallback_path->remote) & (NGTCP2_ADDR_CMP_FLAG_ADDR | NGTCP2_ADDR_CMP_FLAG_FAMILY))) { flags |= NGTCP2_PATH_VALIDATION_FLAG_NEW_TOKEN; } - rv = conn->callbacks.path_validation(conn, flags, &pv->dcid.ps.path, old_path, - res, conn->user_data); + rv = conn->callbacks.path_validation(conn, flags, &pv->dcid.ps.path, + fallback_path, res, conn->user_data); if (rv != 0) { return NGTCP2_ERR_CALLBACK_FAILURE; } @@ -1061,13 +1061,28 @@ conn_set_local_transport_params(ngtcp2_conn *conn, } static void conn_update_skip_pkt(ngtcp2_conn *conn, ngtcp2_pktns *pktns) { - uint8_t gap; + const int64_t min_gap = 3; + uint8_t r; + int64_t gap; - conn->callbacks.rand(&gap, 1, &conn->local.settings.rand_ctx); + assert(INT64_MAX != pktns->tx.skip_pkt.next_pkt_num); - pktns->tx.skip_pkt.next_pkt_num = - pktns->tx.last_pkt_num + 3 + - (int64_t)gap * (1ll << pktns->tx.skip_pkt.exponent++); + conn->callbacks.rand(&r, 1, &conn->local.settings.rand_ctx); + + if (1ll << pktns->tx.skip_pkt.exponent > + (NGTCP2_MAX_PKT_NUM - min_gap) / ((int64_t)r + 1)) { + pktns->tx.skip_pkt.next_pkt_num = INT64_MAX; + return; + } + + gap = ((int64_t)r + 1) * (1ll << pktns->tx.skip_pkt.exponent++) + min_gap; + + if (pktns->tx.last_pkt_num > NGTCP2_MAX_PKT_NUM - gap) { + pktns->tx.skip_pkt.next_pkt_num = INT64_MAX; + return; + } + + pktns->tx.skip_pkt.next_pkt_num = pktns->tx.last_pkt_num + gap; ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, "next skip pkn=%" PRId64, pktns->tx.skip_pkt.next_pkt_num); @@ -1138,7 +1153,7 @@ static int conn_new(ngtcp2_conn **pconn, const ngtcp2_cid *dcid, ngtcp2_settings settingsbuf; ngtcp2_transport_params paramsbuf; ngtcp2_callbacks callbacksbuf; - uint64_t map_seed; + uint64_t seed; (void)settings_version; settings = @@ -1248,8 +1263,11 @@ static int conn_new(ngtcp2_conn **pconn, const ngtcp2_cid *dcid, ngtcp2_pq_init(&(*pconn)->scid.used, retired_ts_less, mem); - callbacks->rand((uint8_t *)&map_seed, sizeof(map_seed), &settings->rand_ctx); - ngtcp2_map_init(&(*pconn)->strms, map_seed, mem); + callbacks->rand((uint8_t *)&seed, sizeof(seed), &settings->rand_ctx); + ngtcp2_map_init(&(*pconn)->strms, seed, mem); + + callbacks->rand((uint8_t *)&seed, sizeof(seed), &settings->rand_ctx); + ngtcp2_pcg32_init(&(*pconn)->pcg, seed); ngtcp2_pq_init(&(*pconn)->tx.strmq, cycle_less, mem); @@ -1325,8 +1343,7 @@ static int conn_new(ngtcp2_conn **pconn, const ngtcp2_cid *dcid, break; case NGTCP2_CC_ALGO_BBR: ngtcp2_cc_bbr_init(&(*pconn)->bbr, &(*pconn)->log, &(*pconn)->cstat, - &(*pconn)->rst, settings->initial_ts, callbacks->rand, - &settings->rand_ctx); + &(*pconn)->rst, settings->initial_ts, &(*pconn)->pcg); break; default: @@ -1335,6 +1352,9 @@ static int conn_new(ngtcp2_conn **pconn, const ngtcp2_cid *dcid, ngtcp2_static_ringbuf_path_history_init(&(*pconn)->path_history); + ngtcp2_ratelim_init(&(*pconn)->glitch_rlim, settings->glitch_ratelim_burst, + settings->glitch_ratelim_rate, settings->initial_ts); + (*pconn)->callbacks = *callbacks; rv = pktns_new(&(*pconn)->in_pktns, NGTCP2_PKTNS_ID_INITIAL, &(*pconn)->rst, @@ -1524,6 +1544,7 @@ int ngtcp2_conn_client_new_versioned( (*pconn)->state = NGTCP2_CS_CLIENT_INITIAL; (*pconn)->local.bidi.next_stream_id = 0; (*pconn)->local.uni.next_stream_id = 2; + (*pconn)->flags |= NGTCP2_CONN_FLAG_CRUMBLE_INITIAL_CRYPTO; rv = ngtcp2_conn_commit_local_transport_params(*pconn); if (rv != 0) { @@ -2127,6 +2148,230 @@ static uint8_t conn_pkt_flags_short(ngtcp2_conn *conn) { : NGTCP2_PKT_FLAG_NONE)); } +/* + * conn_cut_crypto_frame splits (*pfrc)->fr.stream by removing + * |removed_data| from (*pfrc)->fr.stream.data[0]. + * (*pfrc)->fr.stream.data[0] must contain |removed_data|, and + * (*pfrc)->fr.stream.datacnt >= 1. New ngtcp2_frame_chain object + * that contains |removed_data| is created, and pushed to + * |crypto_strm| via ngtcp2_strm_streamfrq_push. Because + * (*pfrc)->fr.stream.datacnt cannot be changed, if it is not 1, new + * ngtcp2_frame_chain object is created to contain the data before + * |removed_data|. Then *pfrc is deleted, and the newly created + * object is assigned to *pfrc instead. If there are data following + * the removed part of data, new ngtcp2_frame_chain object is created + * for it, and (*pfrc)->next points to the object. + * + * This function returns 0 if it succeeds, or one of the following + * negative error codes: + * + * NGTCP2_ERR_NOMEM + * Out of memory + */ +static int conn_cut_crypto_frame(ngtcp2_conn *conn, ngtcp2_frame_chain **pfrc, + ngtcp2_strm *crypto_strm, + const ngtcp2_vec *removed_data) { + ngtcp2_vec *data = (*pfrc)->fr.stream.data; + size_t datacnt = (*pfrc)->fr.stream.datacnt; + size_t ndatacnt; + ngtcp2_frame_chain *left_frc, *right_frc = NULL, *removed_frc; + size_t offset; + int rv; + + assert(datacnt); + assert(data[0].base < removed_data->base); + assert(ngtcp2_vec_end(removed_data) <= ngtcp2_vec_end(&data[0])); + + offset = (size_t)(removed_data->base - data->base); + + rv = ngtcp2_frame_chain_stream_datacnt_objalloc_new( + &removed_frc, 1, &conn->frc_objalloc, conn->mem); + if (rv != 0) { + return rv; + } + + /* ngtcp2_frame_chain for the removed data */ + removed_frc->fr.stream.type = NGTCP2_FRAME_CRYPTO; + removed_frc->fr.stream.offset = (*pfrc)->fr.stream.offset + offset; + removed_frc->fr.stream.datacnt = 1; + removed_frc->fr.stream.data[0] = (ngtcp2_vec){ + .base = data->base + offset, + .len = removed_data->len, + }; + + rv = ngtcp2_strm_streamfrq_push(crypto_strm, removed_frc); + if (rv != 0) { + ngtcp2_frame_chain_objalloc_del(removed_frc, &conn->frc_objalloc, + conn->mem); + return rv; + } + + if (data[0].len == offset + removed_data->len) { + ndatacnt = datacnt - 1; + } else { + ndatacnt = datacnt; + } + + if (ndatacnt) { + /* ngtcp2_frame_chain after the removed data */ + rv = ngtcp2_frame_chain_stream_datacnt_objalloc_new( + &right_frc, ndatacnt, &conn->frc_objalloc, conn->mem); + if (rv != 0) { + return rv; + } + + right_frc->fr.stream.type = NGTCP2_FRAME_CRYPTO; + right_frc->fr.stream.offset = + removed_frc->fr.stream.offset + removed_frc->fr.stream.data->len; + right_frc->fr.stream.datacnt = 0; + ngtcp2_vec_split(right_frc->fr.stream.data, &right_frc->fr.stream.datacnt, + data, &datacnt, offset + removed_data->len, ndatacnt); + + assert(ndatacnt == right_frc->fr.stream.datacnt); + assert(1 == datacnt); + } + + /* We cannot change (*pfrc)->fr.stream.datacnt. If it changes, + create new ngtcp2_frame_chain. */ + if ((*pfrc)->fr.stream.datacnt == 1) { + (*pfrc)->fr.stream.data[0].len = offset; + (*pfrc)->next = right_frc; + return 0; + } + + rv = ngtcp2_frame_chain_stream_datacnt_objalloc_new( + &left_frc, 1, &conn->frc_objalloc, conn->mem); + if (rv != 0) { + ngtcp2_frame_chain_objalloc_del(right_frc, &conn->frc_objalloc, conn->mem); + return rv; + } + + left_frc->fr.stream.type = NGTCP2_FRAME_CRYPTO; + left_frc->fr.stream.offset = (*pfrc)->fr.stream.offset; + left_frc->fr.stream.datacnt = 1; + left_frc->fr.stream.data[0] = (ngtcp2_vec){ + .base = data[0].base, + .len = offset, + }; + left_frc->next = right_frc; + + ngtcp2_frame_chain_objalloc_del(*pfrc, &conn->frc_objalloc, conn->mem); + *pfrc = left_frc; + + return 0; +} + +/* + * conn_crumble_initial_crypto splits CRYPTO frame (*pfrc)->fr.stream + * into pieces and adds PADDING and PING frames, and reorder those + * frames. Those frames are encoded in the buffer pointed by |data| + * and |offsets|. |data| is the pointer to the array of ngtcp2_vec of + * at least NGTCP2_MAX_STREAM_DATACNT. |offsets| contains the CRYPTO + * offset of the corresponding ngtcp2_vec in |data|, and it also + * should have the capacity at least NGTCP2_MAX_STREAM_DATACNT + * uint64_t. |left| is the number of bytes available for the current + * packet. |crypto_offset| is the next smallest CRYPTO offset. + * |crypto_strm| is the CRYPTO stream. + * + * This function returns the number of objects written to |data| and + * |offsets|, or one of the following negative error codes: + * + * NGTCP2_ERR_NOMEM + * Out of memory + */ +static ngtcp2_ssize +conn_crumble_initial_crypto(ngtcp2_conn *conn, ngtcp2_frame_chain **pfrc, + ngtcp2_vec *data, uint64_t *offsets, + ngtcp2_strm *crypto_strm, size_t left, + uint64_t crypto_offset) { + ngtcp2_vec server_name; + ngtcp2_vec removed_data; + size_t max_add_frames = 10; + size_t single_crypto_overhead = + 1 + ngtcp2_put_uvarintlen(crypto_offset + left - 1) + + ngtcp2_put_uvarintlen(left); + size_t total_crypto_overhead = single_crypto_overhead * max_add_frames; + size_t datacnt; + size_t i; + int rv; + + if (left <= total_crypto_overhead) { + return 0; + } + + left -= total_crypto_overhead; + + left = ngtcp2_pkt_crypto_max_datalen(crypto_offset, left, left); + if (left == (size_t)-1) { + return 0; + } + + rv = ngtcp2_strm_streamfrq_pop(crypto_strm, pfrc, left); + if (rv != 0) { + assert(ngtcp2_err_is_fatal(rv)); + return rv; + } + + if (*pfrc == NULL) { + return 0; + } + + assert(crypto_offset == (*pfrc)->fr.stream.offset); + + ngtcp2_vec_copy(data, (*pfrc)->fr.stream.data, (*pfrc)->fr.stream.datacnt); + datacnt = (*pfrc)->fr.stream.datacnt; + + offsets[0] = (*pfrc)->fr.stream.offset; + + for (i = 1; i < datacnt; ++i) { + offsets[i] = offsets[i - 1] + data[i - 1].len; + } + + if (datacnt < NGTCP2_MAX_STREAM_DATACNT && + ngtcp2_pkt_find_server_name(&server_name, data) && server_name.len > 1) { + if (ngtcp2_strm_streamfrq_empty(crypto_strm) || + ngtcp2_strm_streamfrq_unacked_offset(crypto_strm) == (uint64_t)-1) { + datacnt = ngtcp2_pkt_split_vec_at( + data, datacnt, offsets, + (size_t)(server_name.base - data[0].base) + server_name.len / 2); + } else { + /* If we have another data to send (most likely in the another + packet), remove the part of SNI from this packet. */ + datacnt = ngtcp2_pkt_remove_vec_partial( + &removed_data, data, datacnt, offsets, &conn->pcg, &server_name); + + rv = conn_cut_crypto_frame(conn, pfrc, crypto_strm, &removed_data); + if (rv != 0) { + ngtcp2_frame_chain_objalloc_del(*pfrc, &conn->frc_objalloc, conn->mem); + return rv; + } + + /* Add the length of removed data to total_crypto_overhead so + that we can use them for inter CRYPTO frames padding. */ + total_crypto_overhead += removed_data.len; + } + } + + if (datacnt < max_add_frames + 1) { + max_add_frames -= datacnt - 1; + + datacnt = ngtcp2_pkt_split_vec_rand(data, datacnt, offsets, &conn->pcg, + max_add_frames); + } + + for (i = 1; i < datacnt; ++i) { + total_crypto_overhead -= 1 + ngtcp2_put_uvarintlen(offsets[i]) + + ngtcp2_put_uvarintlen(data[i].len); + } + + datacnt = ngtcp2_pkt_append_ping_and_padding(data, datacnt, &conn->pcg, + total_crypto_overhead); + + ngtcp2_pkt_permutate_vec(data, datacnt, offsets, &conn->pcg); + + return (ngtcp2_ssize)datacnt; +} + static size_t conn_min_pktlen(ngtcp2_conn *conn); /* @@ -2253,38 +2498,94 @@ conn_write_handshake_pkt(ngtcp2_conn *conn, ngtcp2_pkt_info *pi, uint8_t *dest, destlen >= NGTCP2_MAX_UDP_PAYLOAD_SIZE) { build_pkt: for (; !ngtcp2_strm_streamfrq_empty(&pktns->crypto.strm);) { - left = ngtcp2_ppe_left(&ppe); - crypto_offset = ngtcp2_strm_streamfrq_unacked_offset(&pktns->crypto.strm); if (crypto_offset == (uint64_t)-1) { ngtcp2_strm_streamfrq_clear(&pktns->crypto.strm); break; } - left = ngtcp2_pkt_crypto_max_datalen(crypto_offset, left, left); - if (left == (size_t)-1) { + left = ngtcp2_ppe_left(&ppe); + if (left == 0) { break; } - rv = ngtcp2_strm_streamfrq_pop(&pktns->crypto.strm, &nfrc, left); - if (rv != 0) { - assert(ngtcp2_err_is_fatal(rv)); - ngtcp2_frame_chain_list_objalloc_del(frq, &conn->frc_objalloc, - conn->mem); - return rv; - } + if (type == NGTCP2_PKT_INITIAL && + (conn->flags & NGTCP2_CONN_FLAG_CRUMBLE_INITIAL_CRYPTO)) { + ngtcp2_vec data[NGTCP2_MAX_STREAM_DATACNT]; + uint64_t offsets[NGTCP2_MAX_STREAM_DATACNT]; + ngtcp2_ssize datacnt; + size_t i; - if (nfrc == NULL) { - break; - } + datacnt = conn_crumble_initial_crypto( + conn, &nfrc, data, offsets, &pktns->crypto.strm, left, crypto_offset); + if (datacnt < 0) { + assert(ngtcp2_err_is_fatal((int)datacnt)); + ngtcp2_frame_chain_list_objalloc_del(frq, &conn->frc_objalloc, + conn->mem); - rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd, &nfrc->fr); - if (rv != 0) { - ngtcp2_unreachable(); + return datacnt; + } + + if (datacnt == 0) { + break; + } + + for (i = 0; i < (size_t)datacnt; ++i) { + if (data[i].base == NULL) { + if (data[i].len == 0) { + lfr.ping.type = NGTCP2_FRAME_PING; + } else { + lfr.padding = (ngtcp2_padding){ + .type = NGTCP2_FRAME_PADDING, + .len = data[i].len, + }; + } + } else { + lfr.stream = (ngtcp2_stream){ + .type = NGTCP2_FRAME_CRYPTO, + .offset = offsets[i], + .datacnt = 1, + .data[0] = data[i], + }; + } + + rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd, &lfr); + if (rv != 0) { + ngtcp2_unreachable(); + } + } + } else { + left = ngtcp2_pkt_crypto_max_datalen(crypto_offset, left, left); + if (left == (size_t)-1) { + break; + } + + rv = ngtcp2_strm_streamfrq_pop(&pktns->crypto.strm, &nfrc, left); + if (rv != 0) { + assert(ngtcp2_err_is_fatal(rv)); + ngtcp2_frame_chain_list_objalloc_del(frq, &conn->frc_objalloc, + conn->mem); + return rv; + } + + if (nfrc == NULL) { + break; + } + + rv = + conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd, &nfrc->fr); + if (rv != 0) { + ngtcp2_unreachable(); + } } *pfrc = nfrc; - pfrc = &(*pfrc)->next; + + for (; nfrc->next;) { + nfrc = nfrc->next; + } + + pfrc = &nfrc->next; pkt_empty = 0; rtb_entry_flags |= NGTCP2_RTB_ENTRY_FLAG_ACK_ELICITING | @@ -5270,9 +5571,12 @@ static void assign_recved_ack_delay_unscaled(ngtcp2_ack *fr, * Stream ID exceeds allowed limit. * NGTCP2_ERR_NOMEM * Out of memory. + * NGTCP2_ERR_INTERNAL + * Suspicious remote endpoint activity exceeded threshold. */ static int conn_recv_max_stream_data(ngtcp2_conn *conn, - const ngtcp2_max_stream_data *fr) { + const ngtcp2_max_stream_data *fr, + ngtcp2_tstamp ts) { ngtcp2_strm *strm; ngtcp2_idtr *idtr; int local_stream = conn_local_stream(conn, fr->stream_id); @@ -5302,6 +5606,10 @@ static int conn_recv_max_stream_data(ngtcp2_conn *conn, if (strm == NULL) { if (local_stream) { /* Stream has been closed. */ + if (ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } @@ -5312,6 +5620,10 @@ static int conn_recv_max_stream_data(ngtcp2_conn *conn, } assert(rv == NGTCP2_ERR_STREAM_IN_USE); /* Stream has been closed. */ + if (ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } @@ -5331,19 +5643,29 @@ static int conn_recv_max_stream_data(ngtcp2_conn *conn, } } - if (strm->tx.max_offset < fr->max_stream_data) { - strm->tx.max_offset = fr->max_stream_data; - - /* Don't call callback if stream is half-closed local */ - if (strm->flags & NGTCP2_STRM_FLAG_SHUT_WR) { - return 0; + if (strm->tx.max_offset >= fr->max_stream_data) { + if (ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; } - rv = conn_call_extend_max_stream_data(conn, strm, fr->stream_id, - fr->max_stream_data); - if (rv != 0) { - return rv; + return 0; + } + + strm->tx.max_offset = fr->max_stream_data; + + /* Don't call callback if stream is half-closed local */ + if (strm->flags & NGTCP2_STRM_FLAG_SHUT_WR) { + if (ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; } + + return 0; + } + + rv = conn_call_extend_max_stream_data(conn, strm, fr->stream_id, + fr->max_stream_data); + if (rv != 0) { + return rv; } return 0; @@ -5976,7 +6298,8 @@ static int conn_verify_fixed_bit(ngtcp2_conn *conn, ngtcp2_pkt_hd *hd) { static int conn_recv_crypto(ngtcp2_conn *conn, ngtcp2_encryption_level encryption_level, - ngtcp2_strm *strm, const ngtcp2_stream *fr); + ngtcp2_strm *strm, const ngtcp2_stream *fr, + ngtcp2_tstamp ts); static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, const ngtcp2_pkt_info *pi, const uint8_t *pkt, @@ -6015,6 +6338,8 @@ static int conn_process_buffered_protected_pkt(ngtcp2_conn *conn, * TLS stack reported error. * NGTCP2_ERR_PROTO * Generic QUIC protocol error. + * NGTCP2_ERR_INTERNAL + * Suspicious remote endpoint activity exceeded threshold. * * In addition to the above error codes, error codes returned from * conn_recv_pkt are also returned. @@ -6525,7 +6850,7 @@ conn_recv_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, conn->negotiated_version); } - rv = conn_recv_crypto(conn, encryption_level, crypto, &fr->stream); + rv = conn_recv_crypto(conn, encryption_level, crypto, &fr->stream, ts); if (rv != 0) { return rv; } @@ -6814,15 +7139,24 @@ static int conn_emit_pending_stream_data(ngtcp2_conn *conn, ngtcp2_strm *strm, * The end offset exceeds the maximum value. * NGTCP2_ERR_CALLBACK_FAILURE * User-defined callback function failed. + * NGTCP2_ERR_INTERNAL + * Suspicious remote endpoint activity exceeded threshold. */ static int conn_recv_crypto(ngtcp2_conn *conn, ngtcp2_encryption_level encryption_level, - ngtcp2_strm *crypto, const ngtcp2_stream *fr) { + ngtcp2_strm *crypto, const ngtcp2_stream *fr, + ngtcp2_tstamp ts) { uint64_t fr_end_offset; uint64_t rx_offset; int rv; + ngtcp2_ssize nwrite; if (fr->datacnt == 0) { + if (encryption_level != NGTCP2_ENCRYPTION_LEVEL_INITIAL && + ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } @@ -6835,6 +7169,11 @@ static int conn_recv_crypto(ngtcp2_conn *conn, rx_offset = ngtcp2_strm_rx_offset(crypto); if (fr_end_offset <= rx_offset) { + if (encryption_level != NGTCP2_ENCRYPTION_LEVEL_INITIAL && + ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + if (conn->server && !(conn->flags & NGTCP2_CONN_FLAG_HANDSHAKE_EARLY_RETRANSMIT) && encryption_level == NGTCP2_ENCRYPTION_LEVEL_INITIAL) { @@ -6892,8 +7231,18 @@ static int conn_recv_crypto(ngtcp2_conn *conn, return NGTCP2_ERR_CRYPTO_BUFFER_EXCEEDED; } - return ngtcp2_strm_recv_reordering(crypto, fr->data[0].base, fr->data[0].len, - fr->offset); + nwrite = ngtcp2_strm_recv_reordering(crypto, fr->data[0].base, + fr->data[0].len, fr->offset); + if (nwrite < 0) { + return (int)nwrite; + } + + if (encryption_level != NGTCP2_ENCRYPTION_LEVEL_INITIAL && nwrite == 0 && + ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + + return 0; } /* @@ -6927,8 +7276,11 @@ static int conn_max_data_violated(ngtcp2_conn *conn, uint64_t datalen) { * NGTCP2_ERR_FINAL_SIZE * STREAM frame has strictly larger end offset than it is * permitted. + * NGTCP2_ERR_INTERNAL + * Suspicious remote endpoint activity exceeded threshold. */ -static int conn_recv_stream(ngtcp2_conn *conn, const ngtcp2_stream *fr) { +static int conn_recv_stream(ngtcp2_conn *conn, const ngtcp2_stream *fr, + ngtcp2_tstamp ts) { int rv; ngtcp2_strm *strm; ngtcp2_idtr *idtr; @@ -6937,6 +7289,8 @@ static int conn_recv_stream(ngtcp2_conn *conn, const ngtcp2_stream *fr) { int bidi; uint64_t datalen = ngtcp2_vec_len(fr->data, fr->datacnt); uint32_t sdflags = NGTCP2_STREAM_DATA_FLAG_NONE; + ngtcp2_ssize nwrite; + int new_strm = 0; local_stream = conn_local_stream(conn, fr->stream_id); bidi = bidi_stream(fr->stream_id); @@ -6970,8 +7324,11 @@ static int conn_recv_stream(ngtcp2_conn *conn, const ngtcp2_stream *fr) { strm = ngtcp2_conn_find_stream(conn, fr->stream_id); if (strm == NULL) { if (local_stream) { - /* TODO The stream has been closed. This should be responded - with RESET_STREAM, or simply ignored. */ + /* The stream has been closed. */ + if (ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } @@ -6981,8 +7338,11 @@ static int conn_recv_stream(ngtcp2_conn *conn, const ngtcp2_stream *fr) { return rv; } assert(rv == NGTCP2_ERR_STREAM_IN_USE); - /* TODO The stream has been closed. This should be responded - with RESET_STREAM, or simply ignored. */ + /* The stream has been closed. */ + if (ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } @@ -6997,6 +7357,8 @@ static int conn_recv_stream(ngtcp2_conn *conn, const ngtcp2_stream *fr) { return rv; } + new_strm = 1; + if (!bidi) { ngtcp2_strm_shutdown(strm, NGTCP2_STRM_FLAG_SHUT_WR); strm->flags |= NGTCP2_STRM_FLAG_FIN_ACKED; @@ -7037,10 +7399,18 @@ static int conn_recv_stream(ngtcp2_conn *conn, const ngtcp2_stream *fr) { } if (strm->flags & NGTCP2_STRM_FLAG_RESET_STREAM_RECVED) { + if (ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } if (rx_offset == fr_end_offset) { + if (!new_strm && ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } } else if (strm->rx.last_offset > fr_end_offset) { @@ -7060,10 +7430,18 @@ static int conn_recv_stream(ngtcp2_conn *conn, const ngtcp2_stream *fr) { ngtcp2_max_uint64(strm->rx.last_offset, fr_end_offset); if (fr_end_offset <= rx_offset) { + if (!new_strm && ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } if (strm->flags & NGTCP2_STRM_FLAG_RESET_STREAM_RECVED) { + if (ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } } @@ -7111,10 +7489,14 @@ static int conn_recv_stream(ngtcp2_conn *conn, const ngtcp2_stream *fr) { return rv; } } else if (fr->datacnt && !(strm->flags & NGTCP2_STRM_FLAG_STOP_SENDING)) { - rv = ngtcp2_strm_recv_reordering(strm, fr->data[0].base, fr->data[0].len, - fr->offset); - if (rv != 0) { - return rv; + nwrite = ngtcp2_strm_recv_reordering(strm, fr->data[0].base, + fr->data[0].len, fr->offset); + if (nwrite < 0) { + return (int)nwrite; + } + + if (nwrite == 0 && ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; } } return ngtcp2_conn_close_stream_if_shut_rdwr(conn, strm); @@ -7211,9 +7593,12 @@ handle_max_remote_streams_extension(uint64_t *punsent_max_remote_streams, * NGTCP2_MAX_VARINT. * NGTCP2_ERR_FINAL_SIZE * The final offset is strictly larger than it is permitted. + * NGTCP2_ERR_INTERNAL + * Suspicious remote endpoint activity exceeded threshold. */ static int conn_recv_reset_stream(ngtcp2_conn *conn, - const ngtcp2_reset_stream *fr) { + const ngtcp2_reset_stream *fr, + ngtcp2_tstamp ts) { ngtcp2_strm *strm; int local_stream = conn_local_stream(conn, fr->stream_id); int bidi = bidi_stream(fr->stream_id); @@ -7251,6 +7636,10 @@ static int conn_recv_reset_stream(ngtcp2_conn *conn, strm = ngtcp2_conn_find_stream(conn, fr->stream_id); if (strm == NULL) { if (local_stream) { + if (ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } @@ -7260,6 +7649,11 @@ static int conn_recv_reset_stream(ngtcp2_conn *conn, return rv; } assert(rv == NGTCP2_ERR_STREAM_IN_USE); + + if (ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } @@ -7294,6 +7688,10 @@ static int conn_recv_reset_stream(ngtcp2_conn *conn, } if (strm->flags & NGTCP2_STRM_FLAG_RESET_STREAM_RECVED) { + if (ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } @@ -7349,9 +7747,12 @@ static int conn_recv_reset_stream(ngtcp2_conn *conn, * Out of memory. * NGTCP2_ERR_CALLBACK_FAILURE * User-defined callback function failed. + * NGTCP2_ERR_INTERNAL + * Suspicious remote endpoint activity exceeded threshold. */ static int conn_recv_stop_sending(ngtcp2_conn *conn, - const ngtcp2_stop_sending *fr) { + const ngtcp2_stop_sending *fr, + ngtcp2_tstamp ts) { int rv; ngtcp2_strm *strm; ngtcp2_idtr *idtr; @@ -7380,6 +7781,10 @@ static int conn_recv_stop_sending(ngtcp2_conn *conn, strm = ngtcp2_conn_find_stream(conn, fr->stream_id); if (strm == NULL) { if (local_stream) { + if (ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } rv = ngtcp2_idtr_open(idtr, fr->stream_id); @@ -7388,6 +7793,11 @@ static int conn_recv_stop_sending(ngtcp2_conn *conn, return rv; } assert(rv == NGTCP2_ERR_STREAM_IN_USE); + + if (ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } @@ -7410,6 +7820,10 @@ static int conn_recv_stop_sending(ngtcp2_conn *conn, } if (strm->flags & NGTCP2_STRM_FLAG_STOP_SENDING_RECVED) { + if (ngtcp2_ratelim_drain(&conn->glitch_rlim, 1, ts) != 0) { + return NGTCP2_ERR_INTERNAL; + } + return 0; } @@ -8671,6 +9085,8 @@ conn_recv_delayed_handshake_pkt(ngtcp2_conn *conn, const ngtcp2_pkt_info *pi, * Flow control limit is violated. * NGTCP2_ERR_FINAL_SIZE * Frame has strictly larger end offset than it is permitted. + * NGTCP2_ERR_INTERNAL + * Suspicious remote endpoint activity exceeded threshold. */ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, const ngtcp2_pkt_info *pi, const uint8_t *pkt, @@ -9074,7 +9490,7 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, ++num_ack_processed; break; case NGTCP2_FRAME_STREAM: - rv = conn_recv_stream(conn, &fr->stream); + rv = conn_recv_stream(conn, &fr->stream, ts); if (rv != 0) { return rv; } @@ -9082,28 +9498,28 @@ static ngtcp2_ssize conn_recv_pkt(ngtcp2_conn *conn, const ngtcp2_path *path, break; case NGTCP2_FRAME_CRYPTO: rv = conn_recv_crypto(conn, NGTCP2_ENCRYPTION_LEVEL_1RTT, - &pktns->crypto.strm, &fr->stream); + &pktns->crypto.strm, &fr->stream, ts); if (rv != 0) { return rv; } non_probing_pkt = 1; break; case NGTCP2_FRAME_RESET_STREAM: - rv = conn_recv_reset_stream(conn, &fr->reset_stream); + rv = conn_recv_reset_stream(conn, &fr->reset_stream, ts); if (rv != 0) { return rv; } non_probing_pkt = 1; break; case NGTCP2_FRAME_STOP_SENDING: - rv = conn_recv_stop_sending(conn, &fr->stop_sending); + rv = conn_recv_stop_sending(conn, &fr->stop_sending, ts); if (rv != 0) { return rv; } non_probing_pkt = 1; break; case NGTCP2_FRAME_MAX_STREAM_DATA: - rv = conn_recv_max_stream_data(conn, &fr->max_stream_data); + rv = conn_recv_max_stream_data(conn, &fr->max_stream_data, ts); if (rv != 0) { return rv; } @@ -11463,7 +11879,11 @@ conn_write_vmsg_wrapper(ngtcp2_conn *conn, ngtcp2_path *path, } else if ((cstat->cwnd >= cstat->ssthresh || cstat->bytes_in_flight * 2 < cstat->cwnd) && nwrite == 0 && conn_pacing_pkt_tx_allowed(conn, ts) && - (conn->flags & NGTCP2_CONN_FLAG_HANDSHAKE_COMPLETED)) { + (conn->flags & NGTCP2_CONN_FLAG_HANDSHAKE_COMPLETED) && + /* Because NGTCP2_CONN_FLAG_AGGREGATE_PKTS is set after a + packet is produced, if it is set, we are sure that we + are not app-limited. */ + !(conn->flags & NGTCP2_CONN_FLAG_AGGREGATE_PKTS)) { conn->rst.app_limited = conn->rst.delivered + cstat->bytes_in_flight; if (conn->rst.app_limited == 0) { @@ -11811,7 +12231,7 @@ ngtcp2_ssize ngtcp2_conn_write_vmsg(ngtcp2_conn *conn, ngtcp2_path *path, if (!conn->pktns.rtb.probe_pkt_left && conn_cwnd_is_zero(conn)) { destlen = 0; } else { - if (res == 0) { + if (res == 0 && !(conn->flags & NGTCP2_CONN_FLAG_AGGREGATE_PKTS)) { nwrite = conn_write_path_response(conn, path, pi, dest, origdestlen, ts); if (nwrite) { @@ -13462,6 +13882,91 @@ void ngtcp2_conn_add_path_history(ngtcp2_conn *conn, const ngtcp2_dcid *dcid, ent->ts = ts; } +ngtcp2_ssize ngtcp2_conn_write_aggregate_pkt_versioned( + ngtcp2_conn *conn, ngtcp2_path *path, int pkt_info_version, + ngtcp2_pkt_info *pi, uint8_t *buf, size_t buflen, size_t *pgsolen, + ngtcp2_write_pkt write_pkt, ngtcp2_tstamp ts) { + size_t max_udp_payloadlen = ngtcp2_conn_get_max_tx_udp_payload_size(conn); + size_t path_max_udp_payloadlen = + ngtcp2_conn_get_path_max_tx_udp_payload_size(conn); + ngtcp2_ssize nwrite; + uint8_t *wbuf = buf; + size_t wbuflen; + ngtcp2_ecn_state ecn_state; + int first_pkt; + ngtcp2_pkt_info pi_discard; + ngtcp2_path_storage path_discard; + (void)pkt_info_version; + + assert(buflen >= path_max_udp_payloadlen); + + buflen = + ngtcp2_min_size(buflen, ngtcp2_max_size(ngtcp2_conn_get_send_quantum(conn), + path_max_udp_payloadlen)); + + for (;;) { + ecn_state = conn->tx.ecn.state; + + wbuflen = buflen >= max_udp_payloadlen ? max_udp_payloadlen + : path_max_udp_payloadlen; + + nwrite = write_pkt(conn, path, pi, wbuf, wbuflen, ts, conn->user_data); + if (nwrite < 0) { + break; + } + + if (nwrite == 0) { + nwrite = wbuf - buf; + break; + } + + first_pkt = buf == wbuf; + wbuf += nwrite; + buflen -= (size_t)nwrite; + + if (first_pkt) { + assert(!(conn->flags & NGTCP2_CONN_FLAG_AGGREGATE_PKTS)); + + *pgsolen = (size_t)nwrite; + + if ((size_t)nwrite != path_max_udp_payloadlen || + buflen < path_max_udp_payloadlen || ecn_state != conn->tx.ecn.state) { + nwrite = wbuf - buf; + break; + } + + /* All aggregated packets should share the same path and pi. + Pass the placeholder values to the callback because they + might be overwritten by later calls, especially pi is set to + empty when no packet is produced. */ + if (path) { + ngtcp2_path_storage_zero(&path_discard); + path = &path_discard.path; + } + + if (pi) { + pi = &pi_discard; + } + + conn->flags |= NGTCP2_CONN_FLAG_AGGREGATE_PKTS; + + continue; + } + + if (buflen < path_max_udp_payloadlen || (size_t)nwrite < *pgsolen || + ecn_state != conn->tx.ecn.state) { + nwrite = wbuf - buf; + break; + } + } + + conn->flags &= ~NGTCP2_CONN_FLAG_AGGREGATE_PKTS; + + ngtcp2_conn_update_pkt_tx_time(conn, ts); + + return nwrite; +} + const ngtcp2_path_history_entry * ngtcp2_conn_find_path_history(ngtcp2_conn *conn, const ngtcp2_path *path, ngtcp2_tstamp ts) { diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.h index 5979d39654b839..2d607d379fc310 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_conn.h @@ -52,6 +52,8 @@ #include "ngtcp2_rst.h" #include "ngtcp2_conn_stat.h" #include "ngtcp2_dcidtr.h" +#include "ngtcp2_pcg.h" +#include "ngtcp2_ratelim.h" typedef enum { /* Client specific handshake states */ @@ -200,6 +202,14 @@ void ngtcp2_path_challenge_entry_init(ngtcp2_path_challenge_entry *pcent, /* NGTCP2_CONN_FLAG_KEY_UPDATE_INITIATOR is set when the local endpoint has initiated key update. */ #define NGTCP2_CONN_FLAG_KEY_UPDATE_INITIATOR 0x10000u +/* NGTCP2_CONN_FLAG_AGGREGATE_PKTS is set when + ngtcp2_conn_writev_stream is called inside the callback invoked by + ngtcp2_conn_write_aggregate_pkt. */ +#define NGTCP2_CONN_FLAG_AGGREGATE_PKTS 0x20000u +/* NGTCP2_CONN_FLAG_CRUMBLE_INITIAL_CRYPTO, if set, crumbles an + Initial CRYPTO frame into pieces as a countermeasure against Deep + Packet Inspection. */ +#define NGTCP2_CONN_FLAG_CRUMBLE_INITIAL_CRYPTO 0x40000u typedef struct ngtcp2_pktns { struct { @@ -641,12 +651,17 @@ struct ngtcp2_conn { successfully. The path is added to this history when a local endpoint migrates to the another path. */ ngtcp2_static_ringbuf_path_history path_history; + /* glitch_rlim is the rate limit of glitches that can be tolerated. + If more than those glitches are detected, a connection is + closed. */ + ngtcp2_ratelim glitch_rlim; const ngtcp2_mem *mem; /* idle_ts is the time instant when idle timer started. */ ngtcp2_tstamp idle_ts; /* handshake_confirmed_ts is the time instant when handshake is confirmed. For server, it is confirmed when completed. */ ngtcp2_tstamp handshake_confirmed_ts; + ngtcp2_pcg32 pcg; void *user_data; uint32_t client_chosen_version; uint32_t negotiated_version; diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_frame_chain.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_frame_chain.h index e7b33632529cd4..01f07cfa4d95ed 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_frame_chain.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_frame_chain.h @@ -90,10 +90,6 @@ ngtcp2_objalloc_decl(frame_chain, ngtcp2_frame_chain, oplent) int ngtcp2_bind_frame_chains(ngtcp2_frame_chain *a, ngtcp2_frame_chain *b, const ngtcp2_mem *mem); -/* NGTCP2_MAX_STREAM_DATACNT is the maximum number of ngtcp2_vec that - a ngtcp2_stream can include. */ -#define NGTCP2_MAX_STREAM_DATACNT 256 - /* * ngtcp2_frame_chain_objalloc_new allocates ngtcp2_frame_chain using * |objalloc|. diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pcg.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pcg.c new file mode 100644 index 00000000000000..9d0eb57e0db940 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pcg.c @@ -0,0 +1,88 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "ngtcp2_pcg.h" + +#include + +/* + * PCG implementation from + * https://github.com/imneme/pcg-c/blob/83252d9c23df9c82ecb42210afed61a7b42402d7/include/pcg_variants.h + * + * PCG Random Number Generation for C. + * + * Copyright 2014-2019 Melissa O'Neill , + * and the PCG Project contributors. + * + * SPDX-License-Identifier: (Apache-2.0 OR MIT) + * + * Licensed under the Apache License, Version 2.0 (provided in + * LICENSE-APACHE.txt and at http://www.apache.org/licenses/LICENSE-2.0) + * or under the MIT license (provided in LICENSE-MIT.txt and at + * http://opensource.org/licenses/MIT), at your option. This file may not + * be copied, modified, or distributed except according to those terms. + * + * Distributed on an "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See your chosen license for details. + * + * For additional information about the PCG random number generation scheme, + * visit http://www.pcg-random.org/. + */ + +#define NGTCP2_PCG_DEFAULT_MULTIPLIER_64 6364136223846793005ULL +#define NGTCP2_PCG_DEFAULT_INCREMENT_64 1442695040888963407ULL + +static void pcg_oneseq_64_step_r(ngtcp2_pcg32 *pcg) { + pcg->state = pcg->state * NGTCP2_PCG_DEFAULT_MULTIPLIER_64 + + NGTCP2_PCG_DEFAULT_INCREMENT_64; +} + +void ngtcp2_pcg32_init(ngtcp2_pcg32 *pcg, uint64_t seed) { + pcg->state = 0; + pcg_oneseq_64_step_r(pcg); + pcg->state += seed; + pcg_oneseq_64_step_r(pcg); +} + +static uint32_t pcg_rotr_32(uint32_t value, unsigned int rot) { + return (value >> rot) | (value << ((-rot) & 31)); +} + +static uint32_t pcg_output_xsh_rr_64_32(uint64_t state) { + return pcg_rotr_32((uint32_t)(((state >> 18u) ^ state) >> 27u), + (unsigned int)(state >> 59u)); +} + +uint32_t ngtcp2_pcg32_rand(ngtcp2_pcg32 *pcg) { + uint64_t oldstate = pcg->state; + + pcg_oneseq_64_step_r(pcg); + + return pcg_output_xsh_rr_64_32(oldstate); +} + +uint32_t ngtcp2_pcg32_rand_n(ngtcp2_pcg32 *pcg, uint32_t n) { + assert(n); + return (uint32_t)(((uint64_t)ngtcp2_pcg32_rand(pcg) * n) >> 32); +} diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pcg.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pcg.h new file mode 100644 index 00000000000000..a637183efc181e --- /dev/null +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pcg.h @@ -0,0 +1,54 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NGTCP2_PCG_H +#define NGTCP2_PCG_H + +#ifdef HAVE_CONFIG_H +# include +#endif /* defined(HAVE_CONFIG_H) */ + +#include + +typedef struct ngtcp2_pcg32 { + uint64_t state; +} ngtcp2_pcg32; + +/* + * ngtcp2_pcg32_init initializes |pcg| with |seed|. + */ +void ngtcp2_pcg32_init(ngtcp2_pcg32 *pcg, uint64_t seed); + +/* + * ngtcp2_pcg32_rand returns a random value in [0, UINT32_MAX]. + */ +uint32_t ngtcp2_pcg32_rand(ngtcp2_pcg32 *pcg); + +/* + * ngtcp2_pcg32_rand_n returns a random value in [0, n). |n| must not + * be zero. + */ +uint32_t ngtcp2_pcg32_rand_n(ngtcp2_pcg32 *pcg, uint32_t n); + +#endif /* !defined(NGTCP2_PCG_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.c index af8a059d08f7bb..d63dc932e1bfb4 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.c @@ -33,7 +33,9 @@ #include "ngtcp2_cid.h" #include "ngtcp2_mem.h" #include "ngtcp2_vec.h" +#include "ngtcp2_buf.h" #include "ngtcp2_unreachable.h" +#include "ngtcp2_pcg.h" int ngtcp2_pkt_chain_new(ngtcp2_pkt_chain **ppc, const ngtcp2_path *path, const ngtcp2_pkt_info *pi, const uint8_t *pkt, @@ -2571,3 +2573,294 @@ int ngtcp2_pkt_verify_reserved_bits(uint8_t c) { return (c & NGTCP2_SHORT_RESERVED_BIT_MASK) == 0 ? 0 : NGTCP2_ERR_PROTO; } + +size_t ngtcp2_pkt_split_vec_rand(ngtcp2_vec *data, size_t datacnt, + uint64_t *offsets, ngtcp2_pcg32 *pcg, + size_t max_add) { + ngtcp2_vec *v; + size_t idx; + size_t len; + + for (; max_add; --max_add) { + idx = ngtcp2_pcg32_rand_n(pcg, (uint32_t)datacnt); + assert(idx < datacnt); + + v = &data[idx]; + + if (v->len <= 1) { + continue; + } + + len = v->len / 2; + + ngtcp2_vec_split_at(&data[datacnt], v, len); + + offsets[datacnt] = offsets[idx] + len; + + ++datacnt; + } + + return datacnt; +} + +size_t ngtcp2_pkt_split_vec_at(ngtcp2_vec *data, size_t datacnt, + uint64_t *offsets, size_t at) { + assert(at < data[0].len); + + ngtcp2_vec_split_at(&data[datacnt], &data[0], at); + + offsets[datacnt] = offsets[0] + at; + + return datacnt + 1; +} + +static int pkt_tls_skip8(ngtcp2_buf *buf) { + size_t len; + + if (ngtcp2_buf_len(buf) < 1) { + return -1; + } + + len = *buf->pos++; + + if (ngtcp2_buf_len(buf) < len) { + return -1; + } + + buf->pos += len; + + return 0; +} + +static int pkt_tls_skip16(ngtcp2_buf *buf) { + uint16_t len; + + if (ngtcp2_buf_len(buf) < sizeof(len)) { + return -1; + } + + buf->pos = (uint8_t *)ngtcp2_get_uint16be(&len, buf->pos); + + if (ngtcp2_buf_len(buf) < len) { + return -1; + } + + buf->pos += len; + + return 0; +} + +int ngtcp2_pkt_find_server_name(ngtcp2_vec *server_name, const ngtcp2_vec *v) { + ngtcp2_buf buf; + uint32_t msglen; + uint16_t len; + uint16_t legacy_ver; + uint16_t ext_type; + + assert(v->len); + + ngtcp2_buf_init(&buf, v->base, v->len); + buf.last += v->len; + + /* Handshake msg_type and length */ + if (ngtcp2_buf_len(&buf) < 1 + 3) { + return 0; + } + + /* Keep parsing only when msg_type is client_hello(1). */ + if (*buf.pos++ != 1) { + return 0; + } + + buf.pos = (uint8_t *)ngtcp2_get_uint24be(&msglen, buf.pos); + + /* Truncate the buffer to msglen */ + ngtcp2_buf_trunc(&buf, msglen); + + /* legacy_version(0x0303) */ + if (ngtcp2_buf_len(&buf) < sizeof(uint16_t)) { + return 0; + } + + buf.pos = (uint8_t *)ngtcp2_get_uint16be(&legacy_ver, buf.pos); + if (legacy_ver != 0x0303) { + return 0; + } + + /* random */ + if (ngtcp2_buf_len(&buf) < 32) { + return 0; + } + + buf.pos += 32; + + /* legacy_session_id */ + if (pkt_tls_skip8(&buf) != 0) { + return 0; + } + + /* cipher_suites */ + if (pkt_tls_skip16(&buf) != 0) { + return 0; + } + + /* legacy_compression_methods */ + if (pkt_tls_skip8(&buf) != 0) { + return 0; + } + + /* extensions */ + if (ngtcp2_buf_len(&buf) < sizeof(uint16_t)) { + return 0; + } + + buf.pos = (uint8_t *)ngtcp2_get_uint16be(&len, buf.pos); + + /* Truncate the buffer to extensions length */ + ngtcp2_buf_trunc(&buf, len); + + for (;;) { + /* Verify that extension_type and length of extension_data are + available */ + if (ngtcp2_buf_len(&buf) < sizeof(uint16_t) * 2) { + return 0; + } + + /* extension_type */ + buf.pos = (uint8_t *)ngtcp2_get_uint16be(&ext_type, buf.pos); + if (ext_type != 0) { + /* extension_data */ + if (pkt_tls_skip16(&buf) != 0) { + return 0; + } + + continue; + } + + /* Server Name Indication extension(0) */ + + /* extension_data */ + buf.pos = (uint8_t *)ngtcp2_get_uint16be(&len, buf.pos); + if (ngtcp2_buf_len(&buf) < len || len < 2) { + return 0; + } + + /* Truncate the buffer to extension_data length */ + ngtcp2_buf_trunc(&buf, len); + + /* server_name_list */ + buf.pos = (uint8_t *)ngtcp2_get_uint16be(&len, buf.pos); + if (ngtcp2_buf_len(&buf) < len || len < 1 + 2) { + return 0; + } + + /* We deliberately do not check server_name_list length + 2 == + extension_data length. They most likely match, and even if + not, no problem at all. */ + + /* Truncate the buffer to server_name_list length */ + ngtcp2_buf_trunc(&buf, len); + + /* name_type */ + if (*buf.pos++ != 0) { + return 0; + } + + /* name */ + buf.pos = (uint8_t *)ngtcp2_get_uint16be(&len, buf.pos); + if (ngtcp2_buf_len(&buf) < len) { + return 0; + } + + server_name->base = buf.pos; + server_name->len = len; + + return 1; + } +} + +size_t ngtcp2_pkt_append_ping_and_padding(ngtcp2_vec *data, size_t datacnt, + ngtcp2_pcg32 *pcg, size_t n) { + uint32_t k; + + for (; n && datacnt < NGTCP2_MAX_STREAM_DATACNT;) { + k = ngtcp2_pcg32_rand_n(pcg, (uint32_t)n + 1); + if (k == 0) { + /* PING */ + data[datacnt] = (ngtcp2_vec){ + .base = NULL, + .len = 0, + }; + + ++k; + } else { + /* PADDING of k length */ + data[datacnt] = (ngtcp2_vec){ + .base = NULL, + .len = k, + }; + } + + ++datacnt; + n -= k; + } + + return datacnt; +} + +void ngtcp2_pkt_permutate_vec(ngtcp2_vec *data, size_t datacnt, + uint64_t *offsets, ngtcp2_pcg32 *pcg) { + size_t i, j; + ngtcp2_vec v; + uint64_t o; + + if (datacnt < 2) { + return; + } + + for (i = datacnt - 1; i > 0; --i) { + j = ngtcp2_pcg32_rand_n(pcg, (uint32_t)i); + + if (i == j) { + continue; + } + + v = data[i]; + data[i] = data[j]; + data[j] = v; + + o = offsets[i]; + offsets[i] = offsets[j]; + offsets[j] = o; + } +} + +size_t ngtcp2_pkt_remove_vec_partial(ngtcp2_vec *removed_data, ngtcp2_vec *data, + size_t datacnt, uint64_t *offsets, + ngtcp2_pcg32 *pcg, + const ngtcp2_vec *part) { + ngtcp2_vec *v = &data[0]; + size_t len; + + assert(datacnt); + assert(v->base < part->base); + assert(ngtcp2_vec_end(part) <= ngtcp2_vec_end(v)); + + len = (size_t)(part->base - v->base) + part->len / 2; + + ngtcp2_vec_split_at(removed_data, v, len); + + if (removed_data->len == 1) { + return datacnt; + } + + len = 1 + ngtcp2_pcg32_rand_n( + pcg, (uint32_t)ngtcp2_min_size(30, removed_data->len - 1)); + assert(len < removed_data->len); + + ngtcp2_vec_split_at(&data[datacnt], removed_data, len); + + offsets[datacnt] = offsets[0] + v->len + removed_data->len; + + return datacnt + 1; +} diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.h index ec72708e0426e7..ed358dc48d5cae 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_pkt.h @@ -144,6 +144,12 @@ length of data to send is larger than this limit. */ #define NGTCP2_MIN_STREAM_DATALEN 256 +/* NGTCP2_MAX_STREAM_DATACNT is the maximum number of ngtcp2_vec that + a ngtcp2_stream can include. */ +#define NGTCP2_MAX_STREAM_DATACNT 256 + +typedef struct ngtcp2_pcg32 ngtcp2_pcg32; + typedef struct ngtcp2_pkt_retry { ngtcp2_cid odcid; uint8_t *token; @@ -1234,4 +1240,81 @@ uint8_t ngtcp2_pkt_versioned_type(uint32_t version, uint32_t pkt_type); */ uint8_t ngtcp2_pkt_get_type_long(uint32_t version, uint8_t c); +/* + * ngtcp2_pkt_split_vec_rand appends ngtcp2_vec at most |max_add| + * times to the array pointed by |data| of length |datacnt| by + * splitting the existing ngtcp2_vec into two. Which ngtcp2_vec to + * split is chosen randomly. |offsets| contains the offset of each + * ngtcp2_vec pointed by |data|. |offsets| is also updated. The + * arrays must have the capacity at least |datacnt| + |max_add|. + * |pcg| is a random number generator. + * + * This function returns |datacnt| plus the number of ngtcp2_vec that + * are appended. + */ +size_t ngtcp2_pkt_split_vec_rand(ngtcp2_vec *data, size_t datacnt, + uint64_t *offsets, ngtcp2_pcg32 *pcg, + size_t max_add); + +/* + * ngtcp2_pkt_split_vec_at splits data[0] at offset |at|, and the + * right side of ngtcp2_vec is assigned to data[datacnt]. Similarly, + * offsets[0] + |at| is assigned to offsets[datacnt]. |data| must + * point to the array of ngtcp2_vec of length |datacnt|, and |datacnt| + * must be greater than 0. |at| must be strictly less than data->len. + * + * This function returns |datacnt| + 1. + */ +size_t ngtcp2_pkt_split_vec_at(ngtcp2_vec *data, size_t datacnt, + uint64_t *offsets, size_t at); + +/* + * ngtcp2_pkt_find_server_name searches TLS Server Name Indication + * extension in |v|. If it is found, assign the portion of server + * name to the object pointed by |server_name|, and returns nonzero. + * Otherwise, it returns 0. If |v| contains the extension partially, + * the function returns 0. |v| must not be empty. + */ +int ngtcp2_pkt_find_server_name(ngtcp2_vec *server_name, const ngtcp2_vec *v); + +/* + * ngtcp2_pkt_append_ping_and_padding appends PING and PADDING frames + * to the array pointed by |data| of length |datacnt|. The capacity + * of array must be at least NGTCP2_MAX_STREAM_DATACNT. |n| is the + * number of bytes available for serialized PING and PADDING frames. + * |pcg| is a random number generator. Which frames to add is + * determined randomly. + * + * The special encoding of PING and PADDING frames into ngtcp2_vec: + * + * - .base is NULL. + * - If .len is 0, it represents PING. Otherwise, PADDING of .len + * length. + * + * This function returns |datacnt| plus the number of frames added. + */ +size_t ngtcp2_pkt_append_ping_and_padding(ngtcp2_vec *data, size_t datacnt, + ngtcp2_pcg32 *pcg, size_t n); + +/* + * ngtcp2_pkt_permutate_vec permutates |data| and |offsets|, both have + * the |datacnt| elements. |pcg| is a random number generator. + */ +void ngtcp2_pkt_permutate_vec(ngtcp2_vec *data, size_t datacnt, + uint64_t *offsets, ngtcp2_pcg32 *pcg); + +/* + * ngtcp2_pkt_remove_vec_partial removes the portion of data that + * contains part of |part| from data[0]. This function does not + * remove whole range of |part|. The length of removed data is chosen + * randomly. The removed portion of data is assigned to the object + * pointed by |removed_data|. If there is data located after the + * removed data, it will be assigned to data[datacnt]. + * offsets[datacnt] is also updated, and the function returns + * |datacnt| + 1. Otherwise, this function returns |datacnt|. + */ +size_t ngtcp2_pkt_remove_vec_partial(ngtcp2_vec *removed_data, ngtcp2_vec *data, + size_t datacnt, uint64_t *offsets, + ngtcp2_pcg32 *pcg, const ngtcp2_vec *part); + #endif /* !defined(NGTCP2_PKT_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ratelim.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ratelim.c new file mode 100644 index 00000000000000..efedc3daa7600f --- /dev/null +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ratelim.c @@ -0,0 +1,77 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * Copyright (c) 2023 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "ngtcp2_ratelim.h" + +#include + +#include "ngtcp2_macro.h" + +void ngtcp2_ratelim_init(ngtcp2_ratelim *rlim, uint64_t burst, uint64_t rate, + ngtcp2_tstamp ts) { + *rlim = (ngtcp2_ratelim){ + .burst = burst, + .rate = rate, + .tokens = burst, + .ts = ts, + }; +} + +/* ratelim_update updates rlim->tokens with the current |ts|. */ +static void ratelim_update(ngtcp2_ratelim *rlim, ngtcp2_tstamp ts) { + uint64_t d, gain; + + assert(ts >= rlim->ts); + + if (ts == rlim->ts) { + return; + } + + d = ts - rlim->ts; + rlim->ts = ts; + + gain = rlim->rate * d + rlim->carry; + + rlim->tokens += gain / NGTCP2_SECONDS; + + if (rlim->tokens < rlim->burst) { + rlim->carry = gain % NGTCP2_SECONDS; + } else { + rlim->tokens = rlim->burst; + rlim->carry = 0; + } +} + +int ngtcp2_ratelim_drain(ngtcp2_ratelim *rlim, uint64_t n, ngtcp2_tstamp ts) { + ratelim_update(rlim, ts); + + if (rlim->tokens < n) { + return -1; + } + + rlim->tokens -= n; + + return 0; +} diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_ratelim.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ratelim.h new file mode 100644 index 00000000000000..14485c562d4e32 --- /dev/null +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_ratelim.h @@ -0,0 +1,59 @@ +/* + * ngtcp2 + * + * Copyright (c) 2025 ngtcp2 contributors + * Copyright (c) 2023 nghttp2 contributors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#ifndef NGTCP2_RATELIM_H +#define NGTCP2_RATELIM_H + +#ifdef HAVE_CONFIG_H +# include +#endif /* HAVE_CONFIG_H */ + +#include + +typedef struct ngtcp2_ratelim { + /* burst is the maximum number of tokens. */ + uint64_t burst; + /* rate is the rate of token generation measured by token / + second. */ + uint64_t rate; + /* tokens is the amount of tokens available to drain. */ + uint64_t tokens; + /* carry is the partial token gained in sub-second period. It is + added to the computation in the next update round. */ + uint64_t carry; + /* ts is the last timestamp that is known to this object. */ + ngtcp2_tstamp ts; +} ngtcp2_ratelim; + +/* ngtcp2_ratelim_init initializes |rlim| with the given + parameters. */ +void ngtcp2_ratelim_init(ngtcp2_ratelim *rlim, uint64_t burst, uint64_t rate, + ngtcp2_tstamp ts); + +/* ngtcp2_ratelim_drain drains |n| from rlim->tokens. It returns 0 if + it succeeds, or -1. */ +int ngtcp2_ratelim_drain(ngtcp2_ratelim *rlim, uint64_t n, ngtcp2_tstamp ts); + +#endif /* !defined(NGTCP2_RATELIM_H) */ diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rob.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rob.c index 853f1d650eaf54..ef1938ea6326ca 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rob.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rob.c @@ -162,8 +162,8 @@ static int rob_write_data(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, return 0; } -int ngtcp2_rob_push(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, - size_t datalen) { +ngtcp2_ssize ngtcp2_rob_push(ngtcp2_rob *rob, uint64_t offset, + const uint8_t *data, size_t datalen) { int rv; ngtcp2_rob_gap *g; ngtcp2_range m, l, r; @@ -172,6 +172,8 @@ int ngtcp2_rob_push(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, .end = offset + datalen, }; ngtcp2_ksl_it it; + ngtcp2_ssize nwrite = 0; + size_t mlen; it = ngtcp2_ksl_lower_bound_search(&rob->gapksl, &q, ngtcp2_ksl_range_exclusive_search); @@ -180,7 +182,9 @@ int ngtcp2_rob_push(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, g = ngtcp2_ksl_it_get(&it); m = ngtcp2_range_intersect(&q, &g->range); - if (!ngtcp2_range_len(&m)) { + + mlen = (size_t)ngtcp2_range_len(&m); + if (mlen == 0) { break; } @@ -188,12 +192,13 @@ int ngtcp2_rob_push(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, ngtcp2_ksl_remove_hint(&rob->gapksl, &it, &it, &g->range); ngtcp2_rob_gap_del(g, rob->mem); - rv = rob_write_data(rob, m.begin, data + (m.begin - offset), - (size_t)ngtcp2_range_len(&m)); + rv = rob_write_data(rob, m.begin, data + (m.begin - offset), mlen); if (rv != 0) { return rv; } + nwrite += (ngtcp2_ssize)mlen; + continue; } @@ -222,16 +227,17 @@ int ngtcp2_rob_push(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, g->range = r; } - rv = rob_write_data(rob, m.begin, data + (m.begin - offset), - (size_t)ngtcp2_range_len(&m)); + rv = rob_write_data(rob, m.begin, data + (m.begin - offset), mlen); if (rv != 0) { return rv; } + nwrite += (ngtcp2_ssize)mlen; + ngtcp2_ksl_it_next(&it); } - return 0; + return nwrite; } void ngtcp2_rob_remove_prefix(ngtcp2_rob *rob, uint64_t offset) { diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rob.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rob.h index d53b5160b10230..60a1c5b46a0d67 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rob.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rob.h @@ -138,14 +138,14 @@ void ngtcp2_rob_free(ngtcp2_rob *rob); * ngtcp2_rob_push adds new data pointed by |data| of length |datalen| * at the stream offset |offset|. * - * This function returns 0 if it succeeds, or one of the following - * negative error codes: + * This function returns the number of data newly buffered if it + * succeeds, or one of the following negative error codes: * * NGTCP2_ERR_NOMEM * Out of memory */ -int ngtcp2_rob_push(ngtcp2_rob *rob, uint64_t offset, const uint8_t *data, - size_t datalen); +ngtcp2_ssize ngtcp2_rob_push(ngtcp2_rob *rob, uint64_t offset, + const uint8_t *data, size_t datalen); /* * ngtcp2_rob_remove_prefix removes gap up to |offset|, exclusive. It diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rtb.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rtb.c index b50f482bc7e8c0..7df1c197db79d7 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_rtb.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_rtb.c @@ -802,8 +802,8 @@ ngtcp2_ssize ngtcp2_rtb_recv_ack(ngtcp2_rtb *rtb, const ngtcp2_ack *fr, if (conn && (conn->flags & NGTCP2_CONN_FLAG_KEY_UPDATE_NOT_CONFIRMED) && (conn->flags & NGTCP2_CONN_FLAG_KEY_UPDATE_INITIATOR) && largest_ack >= conn->pktns.crypto.tx.ckm->pkt_num) { - conn->flags &= (uint32_t) ~(NGTCP2_CONN_FLAG_KEY_UPDATE_NOT_CONFIRMED | - NGTCP2_CONN_FLAG_KEY_UPDATE_INITIATOR); + conn->flags &= (uint32_t)~(NGTCP2_CONN_FLAG_KEY_UPDATE_NOT_CONFIRMED | + NGTCP2_CONN_FLAG_KEY_UPDATE_INITIATOR); conn->crypto.key_update.confirmed_ts = ts; ngtcp2_log_info(rtb->log, NGTCP2_LOG_EVENT_CRY, "key update confirmed"); diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.c index 77a68bd112e3b2..f774504282e6b1 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.c @@ -37,6 +37,10 @@ void ngtcp2_settings_default_versioned(int settings_version, switch (settings_version) { case NGTCP2_SETTINGS_VERSION: + settings->glitch_ratelim_burst = NGTCP2_DEFAULT_GLITCH_RATELIM_BURST; + settings->glitch_ratelim_rate = NGTCP2_DEFAULT_GLITCH_RATELIM_RATE; + /* fall through */ + case NGTCP2_SETTINGS_V2: case NGTCP2_SETTINGS_V1: settings->cc_algo = NGTCP2_CC_ALGO_CUBIC; settings->initial_rtt = NGTCP2_DEFAULT_INITIAL_RTT; @@ -82,6 +86,9 @@ size_t ngtcp2_settingslen_version(int settings_version) { switch (settings_version) { case NGTCP2_SETTINGS_VERSION: return sizeof(settings); + case NGTCP2_SETTINGS_V2: + return offsetof(ngtcp2_settings, pmtud_probeslen) + + sizeof(settings.pmtud_probeslen); case NGTCP2_SETTINGS_V1: return offsetof(ngtcp2_settings, initial_pkt_num) + sizeof(settings.initial_pkt_num); diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.h index 80466d43e47a7a..caa0fb58c58b13 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_settings.h @@ -31,6 +31,13 @@ #include +/* NGTCP2_DEFAULT_GLITCH_RATELIM_BURST is the maximum number of tokens + in glitch rate limiter. It is also the initial value. */ +#define NGTCP2_DEFAULT_GLITCH_RATELIM_BURST 1000 +/* NGTCP2_DEFAULT_GLITCH_RATELIM_RATE is the rate of tokens generated + per second for glitch rate limiter. */ +#define NGTCP2_DEFAULT_GLITCH_RATELIM_RATE 33 + /* * ngtcp2_settings_convert_to_latest converts |src| of version * |settings_version| to the latest version NGTCP2_SETTINGS_VERSION. diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.c index 70ec6ee2fe9ea7..faa417713224d5 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.c @@ -123,9 +123,10 @@ static int strm_rob_heavily_fragmented(const ngtcp2_rob *rob) { return ngtcp2_ksl_len(&rob->gapksl) >= 1000; } -int ngtcp2_strm_recv_reordering(ngtcp2_strm *strm, const uint8_t *data, - size_t datalen, uint64_t offset) { +ngtcp2_ssize ngtcp2_strm_recv_reordering(ngtcp2_strm *strm, const uint8_t *data, + size_t datalen, uint64_t offset) { int rv; + ngtcp2_ssize nwrite; if (strm->rx.rob == NULL) { rv = strm_rob_init(strm); @@ -138,16 +139,16 @@ int ngtcp2_strm_recv_reordering(ngtcp2_strm *strm, const uint8_t *data, } } - rv = ngtcp2_rob_push(strm->rx.rob, offset, data, datalen); - if (rv != 0) { - return rv; + nwrite = ngtcp2_rob_push(strm->rx.rob, offset, data, datalen); + if (nwrite < 0) { + return nwrite; } if (strm_rob_heavily_fragmented(strm->rx.rob)) { return NGTCP2_ERR_INTERNAL; } - return 0; + return nwrite; } void ngtcp2_strm_update_rx_offset(ngtcp2_strm *strm, uint64_t offset) { diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.h index c72f8b9dc89aca..1a1e8fd3b7d5c4 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_strm.h @@ -208,14 +208,14 @@ uint64_t ngtcp2_strm_rx_offset(const ngtcp2_strm *strm); /* * ngtcp2_strm_recv_reordering handles reordered data. * - * It returns 0 if it succeeds, or one of the following negative error - * codes: + * It returns the number of bytes newly buffered if it succeeds, or + * one of the following negative error codes: * * NGTCP2_ERR_NOMEM * Out of memory */ -int ngtcp2_strm_recv_reordering(ngtcp2_strm *strm, const uint8_t *data, - size_t datalen, uint64_t offset); +ngtcp2_ssize ngtcp2_strm_recv_reordering(ngtcp2_strm *strm, const uint8_t *data, + size_t datalen, uint64_t offset); /* * ngtcp2_strm_update_rx_offset tells that data up to |offset| bytes diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.c b/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.c index dbca8691d64888..ada027b9095f4e 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.c +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.c @@ -217,3 +217,14 @@ size_t ngtcp2_vec_copy_at_most(ngtcp2_vec *dst, size_t dstcnt, void ngtcp2_vec_copy(ngtcp2_vec *dst, const ngtcp2_vec *src, size_t cnt) { memcpy(dst, src, sizeof(ngtcp2_vec) * cnt); } + +void ngtcp2_vec_split_at(ngtcp2_vec *dst, ngtcp2_vec *src, size_t offset) { + assert(offset < src->len); + + *dst = (ngtcp2_vec){ + .base = src->base + offset, + .len = src->len - offset, + }; + + src->len = offset; +} diff --git a/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.h b/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.h index d90a3204a9f2b4..af9b4d644539cf 100644 --- a/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.h +++ b/deps/ngtcp2/ngtcp2/lib/ngtcp2_vec.h @@ -96,4 +96,18 @@ size_t ngtcp2_vec_copy_at_most(ngtcp2_vec *dst, size_t dstcnt, */ void ngtcp2_vec_copy(ngtcp2_vec *dst, const ngtcp2_vec *src, size_t cnt); +/* + * ngtcp2_vec_split_at splits |src| at the |offset|. Caller must + * ensure that offset < src->len. This function assigns the right + * part of vector into |dst|. + */ +void ngtcp2_vec_split_at(ngtcp2_vec *dst, ngtcp2_vec *src, size_t offset); + +/* + * ngtcp2_vec_end returns the one beyond the last offset of |v|. + */ +static inline uint8_t *ngtcp2_vec_end(const ngtcp2_vec *v) { + return v->base + v->len; +} + #endif /* !defined(NGTCP2_VEC_H) */ diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index cf8cb7baf78f5e..1b29a54a8c1199 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -11,8 +11,8 @@ #include #include #include +#include #include -#include #include "defs.h" namespace node::quic { @@ -172,7 +172,7 @@ class BindingData final // Purge the packet free list to free up memory. JS_METHOD(FlushPacketFreelist); - std::vector> packet_freelist; + std::list> packet_freelist; std::unordered_map> listening_endpoints; diff --git a/src/quic/defs.h b/src/quic/defs.h index ef2cc4d0b0352d..abbc0ed182b130 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -309,13 +309,16 @@ enum class DatagramStatus : uint8_t { CC_ALGOS(V) #undef V +using error_code = uint64_t; +using stream_id = int64_t; +using datagram_id = uint64_t; + constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE; constexpr uint64_t kMaxSizeT = std::numeric_limits::max(); constexpr uint64_t kMaxSafeJsInteger = 9007199254740991; constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS; constexpr size_t kMaxVectorCount = 16; - -using error_code = uint64_t; +constexpr size_t kMaxStreamId = std::numeric_limits::max(); class DebugIndentScope final { public: diff --git a/src/quic/packet.cc b/src/quic/packet.cc index 0d10c0ad6196e9..ab9a43817c2682 100644 --- a/src/quic/packet.cc +++ b/src/quic/packet.cc @@ -120,6 +120,8 @@ BaseObjectPtr Packet::Create(Environment* env, } BaseObjectPtr Packet::Clone() const { + // Cloning is copy-free. Our data_ is a shared_ptr so we can just + // share it with the cloned packet. auto& binding = BindingData::Get(env()); if (binding.packet_freelist.empty()) { JS_NEW_INSTANCE_OR_RETURN(env(), obj, {}); @@ -135,8 +137,8 @@ BaseObjectPtr Packet::FromFreeList(Environment* env, const SocketAddress& destination) { auto& binding = BindingData::Get(env); if (binding.packet_freelist.empty()) return {}; - auto obj = binding.packet_freelist.back(); - binding.packet_freelist.pop_back(); + auto obj = binding.packet_freelist.front(); + binding.packet_freelist.pop_front(); CHECK(obj); CHECK_EQ(env, obj->env()); auto packet = BaseObjectPtr(static_cast(obj.get())); @@ -185,6 +187,8 @@ void Packet::Done(int status) { // big, we don't want to accumulate these things forever. auto& binding = BindingData::Get(env()); if (binding.packet_freelist.size() >= kMaxFreeList) { + Debug(this, "Freelist full, destroying packet"); + data_.reset(); return; } @@ -195,6 +199,10 @@ void Packet::Done(int status) { binding.packet_freelist.push_back(std::move(self)); } +Packet::operator bool() const { + return data_ != nullptr; +} + std::string Packet::ToString() const { if (!data_) return "Packet ()"; return "Packet (" + data_->ToString() + ")"; @@ -202,7 +210,7 @@ std::string Packet::ToString() const { void Packet::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("destination", destination_); - tracker->TrackField("data", data_); + if (data_) tracker->TrackField("data", data_); } BaseObjectPtr Packet::CreateRetryPacket( diff --git a/src/quic/packet.h b/src/quic/packet.h index a7f27a28f26540..32296defe1a80b 100644 --- a/src/quic/packet.h +++ b/src/quic/packet.h @@ -28,22 +28,23 @@ struct PathDescriptor final { std::string ToString() const; }; -// A Packet encapsulates serialized outbound QUIC data. -// Packets must never be larger than the path MTU. The -// default QUIC packet maximum length is 1200 bytes, -// which we assume by default. The packet storage will -// be stack allocated up to this size. +// A Packet encapsulates serialized outbound QUIC data. Packets must never be +// larger than the path MTU. The default QUIC packet maximum length is 1200 +// bytes, which we assume by default. The packet storage will be stack allocated +// up to this size. // -// Packets are maintained in a freelist held by the -// BindingData instance. When using Create() to create -// a Packet, we'll check to see if there is a free -// packet in the freelist and use it instead of starting -// fresh with a new packet. The freelist can store at -// most kMaxFreeList packets +// Packets are maintained in a freelist held by the BindingData instance. When +// using Create() to create a Packet, we'll check to see if there is a free +// packet in the freelist and use it instead of starting fresh with a new +// packet. The freelist can store at most kMaxFreeList packets. This is a +// performance optimization to avoid excessive allocation churn when creating +// lots of packets since each one is ReqWrap and has a fair amount of associated +// overhead. However, we don't want to accumulate too many of these in the +// freelist either, so we cap the size. // -// Packets are always encrypted so their content should -// be considered opaque to us. We leave it entirely up -// to ngtcp2 how to encode QUIC frames into the packet. +// Packets are always encrypted so their content should be considered opaque +// to us. We leave it entirely up to ngtcp2 how to encode QUIC frames into +// the packet. class Packet final : public ReqWrap { private: struct Data; @@ -56,11 +57,9 @@ class Packet final : public ReqWrap { virtual void PacketDone(int status) = 0; }; - // Do not use the Packet constructors directly to create - // them. These are public only to support MakeBaseObject. - // Use the Create, or Create variants to create or - // acquire packet instances. - + // Do not use the Packet constructors directly to create them. These are + // public only to support MakeBaseObject. Use the Create, or Create variants + // to create or acquire packet instances. Packet(Environment* env, Listener* listener, v8::Local object, @@ -80,6 +79,7 @@ class Packet final : public ReqWrap { size_t length() const; operator uv_buf_t() const; operator ngtcp2_vec() const; + operator bool() const; // Modify the size of the packet after ngtcp2 has written // to it. len must be <= length(). We call this after we've @@ -87,6 +87,9 @@ class Packet final : public ReqWrap { // tells us how many of the packets bytes were used. void Truncate(size_t len); + // Create (or acquire from the freelist) a Packet with the given + // destination and length. The diagnostic_label is used to help + // identify the packet purpose in debugging output. static BaseObjectPtr Create( Environment* env, Listener* listener, diff --git a/src/quic/session.cc b/src/quic/session.cc index 0f64c4f20ac7d8..e546803fa881e5 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -71,7 +71,7 @@ namespace quic { V(STREAM_OPEN_ALLOWED, stream_open_allowed, uint8_t) \ V(PRIORITY_SUPPORTED, priority_supported, uint8_t) \ V(WRAPPED, wrapped, uint8_t) \ - V(LAST_DATAGRAM_ID, last_datagram_id, uint64_t) + V(LAST_DATAGRAM_ID, last_datagram_id, datagram_id) #define SESSION_STATS(V) \ V(CREATED_AT, created_at) \ @@ -123,7 +123,7 @@ STAT_STRUCT(Session, SESSION) class Http3Application; namespace { -std::string to_string(PreferredAddress::Policy policy) { +constexpr std::string to_string(PreferredAddress::Policy policy) { switch (policy) { case PreferredAddress::Policy::USE_PREFERRED: return "use"; @@ -133,7 +133,7 @@ std::string to_string(PreferredAddress::Policy policy) { return ""; } -std::string to_string(Side side) { +constexpr std::string to_string(Side side) { switch (side) { case Side::CLIENT: return "client"; @@ -143,7 +143,7 @@ std::string to_string(Side side) { return ""; } -std::string to_string(ngtcp2_encryption_level level) { +constexpr std::string to_string(ngtcp2_encryption_level level) { switch (level) { case NGTCP2_ENCRYPTION_LEVEL_1RTT: return "1rtt"; @@ -157,7 +157,7 @@ std::string to_string(ngtcp2_encryption_level level) { return ""; } -std::string to_string(ngtcp2_cc_algo cc_algorithm) { +constexpr std::string to_string(ngtcp2_cc_algo cc_algorithm) { #define V(name, label) \ case NGTCP2_CC_ALGO_##name: \ return #label; @@ -346,7 +346,7 @@ Session::Config::Config(Environment* env, // We currently do not support Path MTU Discovery. Once we do, unset this. settings.no_pmtud = 1; // Per the ngtcp2 documentation, when no_tx_udp_payload_size_shaping is set - // to a non-zero value, ngtcp2 not to limit the UDP payload size to + // to a non-zero value, it tells ngtcp2 not to limit the UDP payload size to // NGTCP2_MAX_UDP_PAYLOAD_SIZE` and will instead "use the minimum size among // the given buffer size, :member:`max_tx_udp_payload_size`, and the // received max_udp_payload_size QUIC transport parameter." For now, this @@ -543,6 +543,7 @@ struct Session::Impl final : public MemoryRetainer { timer_(session_->env(), [this] { session_->OnTimeout(); }) { timer_.Unref(); } + DISALLOW_COPY_AND_MOVE(Impl) inline bool is_closing() const { return state_->closing; } @@ -647,12 +648,18 @@ struct Session::Impl final : public MemoryRetainer { // JavaScript APIs + // TODO(@jasnell): Fast API alternatives for each of these + JS_METHOD(Destroy) { auto env = Environment::GetCurrent(args); Session* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); if (session->is_destroyed()) { - THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + // At this layer, Destroy should only be called once. At the + // JavaScript layer calling destroy() multiple times should be + // an idempotent operation. Be sure to check for that there + // as we strictly enforce it here. + return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); } session->Destroy(); } @@ -663,7 +670,7 @@ struct Session::Impl final : public MemoryRetainer { ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); if (session->is_destroyed()) { - THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); } auto address = session->remote_address(); @@ -678,7 +685,7 @@ struct Session::Impl final : public MemoryRetainer { ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); if (session->is_destroyed()) { - THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); } Local ret; @@ -692,7 +699,7 @@ struct Session::Impl final : public MemoryRetainer { ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); if (session->is_destroyed()) { - THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); } Local ret; @@ -707,7 +714,7 @@ struct Session::Impl final : public MemoryRetainer { ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); if (session->is_destroyed()) { - THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); } Local ret; @@ -721,7 +728,7 @@ struct Session::Impl final : public MemoryRetainer { ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); if (session->is_destroyed()) { - THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); } session->Close(CloseMethod::GRACEFUL); @@ -734,7 +741,7 @@ struct Session::Impl final : public MemoryRetainer { ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); if (session->is_destroyed()) { - THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); } session->Close(CloseMethod::SILENT); @@ -746,7 +753,7 @@ struct Session::Impl final : public MemoryRetainer { ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); if (session->is_destroyed()) { - THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); } // Initiating a key update may fail if it is done too early (either @@ -763,7 +770,7 @@ struct Session::Impl final : public MemoryRetainer { ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); if (session->is_destroyed()) { - THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); } DCHECK(args[0]->IsUint32()); @@ -790,7 +797,7 @@ struct Session::Impl final : public MemoryRetainer { ASSIGN_OR_RETURN_UNWRAP(&session, args.This()); if (session->is_destroyed()) { - THROW_ERR_INVALID_STATE(env, "Session is destroyed"); + return THROW_ERR_INVALID_STATE(env, "Session is destroyed"); } DCHECK(args[0]->IsArrayBufferView()); @@ -808,7 +815,7 @@ struct Session::Impl final : public MemoryRetainer { // Internal ngtcp2 callbacks static int on_acknowledge_stream_data_offset(ngtcp2_conn* conn, - int64_t stream_id, + stream_id stream_id, uint64_t offset, uint64_t datalen, void* user_data, @@ -828,7 +835,7 @@ struct Session::Impl final : public MemoryRetainer { } static int on_acknowledge_datagram(ngtcp2_conn* conn, - uint64_t dgram_id, + datagram_id dgram_id, void* user_data) { NGTCP2_CALLBACK_SCOPE(session) session->DatagramStatus(dgram_id, DatagramStatus::ACKNOWLEDGED); @@ -898,7 +905,7 @@ struct Session::Impl final : public MemoryRetainer { } static int on_extend_max_stream_data(ngtcp2_conn* conn, - int64_t stream_id, + stream_id stream_id, uint64_t max_data, void* user_data, void* stream_user_data) { @@ -931,7 +938,7 @@ struct Session::Impl final : public MemoryRetainer { } static int on_lost_datagram(ngtcp2_conn* conn, - uint64_t dgram_id, + datagram_id dgram_id, void* user_data) { NGTCP2_CALLBACK_SCOPE(session) session->DatagramStatus(dgram_id, DatagramStatus::LOST); @@ -1015,7 +1022,7 @@ struct Session::Impl final : public MemoryRetainer { static int on_receive_stream_data(ngtcp2_conn* conn, uint32_t flags, - int64_t stream_id, + stream_id stream_id, uint64_t offset, const uint8_t* data, size_t datalen, @@ -1098,8 +1105,8 @@ struct Session::Impl final : public MemoryRetainer { static int on_stream_close(ngtcp2_conn* conn, uint32_t flags, - int64_t stream_id, - uint64_t app_error_code, + stream_id stream_id, + error_code app_error_code, void* user_data, void* stream_user_data) { NGTCP2_CALLBACK_SCOPE(session) @@ -1114,9 +1121,9 @@ struct Session::Impl final : public MemoryRetainer { } static int on_stream_reset(ngtcp2_conn* conn, - int64_t stream_id, + stream_id stream_id, uint64_t final_size, - uint64_t app_error_code, + error_code app_error_code, void* user_data, void* stream_user_data) { NGTCP2_CALLBACK_SCOPE(session) @@ -1128,8 +1135,8 @@ struct Session::Impl final : public MemoryRetainer { } static int on_stream_stop_sending(ngtcp2_conn* conn, - int64_t stream_id, - uint64_t app_error_code, + stream_id stream_id, + error_code app_error_code, void* user_data, void* stream_user_data) { NGTCP2_CALLBACK_SCOPE(session) @@ -1474,47 +1481,47 @@ void Session::Destroy() { } PendingStream::PendingStreamQueue& Session::pending_bidi_stream_queue() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return impl_->pending_bidi_stream_queue_; } PendingStream::PendingStreamQueue& Session::pending_uni_stream_queue() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return impl_->pending_uni_stream_queue_; } size_t Session::max_packet_size() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return ngtcp2_conn_get_max_tx_udp_payload_size(*this); } uint32_t Session::version() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return impl_->config_.version; } Endpoint& Session::endpoint() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return *impl_->endpoint_; } TLSSession& Session::tls_session() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return *tls_session_; } Session::Application& Session::application() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return *impl_->application_; } const SocketAddress& Session::remote_address() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return impl_->remote_address_; } const SocketAddress& Session::local_address() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return impl_->local_address_; } @@ -1527,17 +1534,17 @@ std::string Session::diagnostic_name() const { } const Session::Config& Session::config() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return impl_->config_; } Session::Config& Session::config() { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return impl_->config_; } const Session::Options& Session::options() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return impl_->config_.options; } @@ -1558,17 +1565,17 @@ void Session::HandleQlog(uint32_t flags, const void* data, size_t len) { } const TransportParams Session::local_transport_params() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return ngtcp2_conn_get_local_transport_params(*this); } const TransportParams Session::remote_transport_params() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return ngtcp2_conn_get_remote_transport_params(*this); } void Session::SetLastError(QuicError&& error) { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); Debug(this, "Setting last error to %s", error); impl_->last_error_ = std::move(error); } @@ -1576,10 +1583,10 @@ void Session::SetLastError(QuicError&& error) { bool Session::Receive(Store&& store, const SocketAddress& local_address, const SocketAddress& remote_address) { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); impl_->remote_address_ = remote_address; - // When we are done processing thins packet, we arrange to send any + // When we are done processing this packet, we arrange to send any // pending data for this session. SendPendingDataScope send_scope(this); @@ -1690,7 +1697,7 @@ void Session::Send(const BaseObjectPtr& packet) { // That said, we should never be trying to send a packet when we're in // a draining period. - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); DCHECK(!is_in_draining_period()); if (!can_send_packets()) [[unlikely]] { @@ -1705,12 +1712,19 @@ void Session::Send(const BaseObjectPtr& packet) { void Session::Send(const BaseObjectPtr& packet, const PathStorage& path) { + DCHECK(!is_destroyed()); + DCHECK(!is_in_draining_period()); UpdatePath(path); Send(packet); } -uint64_t Session::SendDatagram(Store&& data) { - CHECK(!is_destroyed()); +datagram_id Session::SendDatagram(Store&& data) { + DCHECK(!is_destroyed()); + + // Sending a datagram is best effort. If we cannot send it for any reason, + // we just return 0 to indicate that the datagram was not sent an the + // data is dropped on the floor. + if (!can_send_packets()) { Debug(this, "Unable to send datagram"); return 0; @@ -1740,7 +1754,7 @@ uint64_t Session::SendDatagram(Store&& data) { ngtcp2_vec vec = data; PathStorage path; int flags = NGTCP2_WRITE_DATAGRAM_FLAG_MORE; - uint64_t did = impl_->state_->last_datagram_id + 1; + datagram_id did = impl_->state_->last_datagram_id + 1; Debug(this, "Sending %zu-byte datagram %" PRIu64, data.length(), did); @@ -1766,7 +1780,8 @@ uint64_t Session::SendDatagram(Store&& data) { ngtcp2_conn_get_max_tx_udp_payload_size(*this), "datagram"); // Typically sending datagrams is best effort, but if we cannot create - // the packet, then we handle it as a fatal error. + // the packet, then we handle it as a fatal error as that indicates + // something else is likely very wrong. if (!packet) { SetLastError(QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL)); Close(CloseMethod::SILENT); @@ -1794,34 +1809,60 @@ uint64_t Session::SendDatagram(Store&& data) { // We cannot send data because of congestion control or the data will // not fit. Since datagrams are best effort, we are going to abandon // the attempt and just return. - CHECK_EQ(accepted, 0); + DCHECK_EQ(accepted, 0); packet->CancelPacket(); return 0; } case NGTCP2_ERR_WRITE_MORE: { - // We keep on looping! Keep on sending! + // The library wants us to keep writing more data to the packet. + // This is typically an indication that the packet is not yet + // full enough. continue; } case NGTCP2_ERR_INVALID_STATE: { // The remote endpoint does not want to accept datagrams. That's ok, // just return 0. + DCHECK_EQ(accepted, 0); packet->CancelPacket(); return 0; } case NGTCP2_ERR_INVALID_ARGUMENT: { // The datagram is too large. That should have been caught above but // that's ok. We'll just abandon the attempt and return. + DCHECK_EQ(accepted, 0); packet->CancelPacket(); return 0; } case NGTCP2_ERR_PKT_NUM_EXHAUSTED: { // We've exhausted the packet number space. Sadly we have to treat it // as a fatal condition (which we will do after the switch) + DCHECK_EQ(accepted, 0); + Debug(this, + "ngtcp2_conn_writev_datagram failed: Packet number " + "exhausted"); break; } case NGTCP2_ERR_CALLBACK_FAILURE: { // There was an internal failure. Sadly we have to treat it as a fatal // condition. (which we will do after the switch) + Debug(this, + "ngtcp2_conn_writev_datagram failed: Callback " + "failure"); + break; + } + case NGTCP2_ERR_NOMEM: { + // Out of memory. Sadly we have to treat it as a fatal condition. + // (which we will do after the switch) + Debug(this, "ngtcp2_conn_writev_datagram failed: Out of memory"); + break; + } + default: { + // Some other unknown, and unexpected failure. + // We have to treat it as a fatal condition. + Debug(this, + "ngtcp2_conn_writev_datagram failed with an unexpected " + "error: %zd", + nwrite); break; } } @@ -1840,7 +1881,8 @@ uint64_t Session::SendDatagram(Store&& data) { if (accepted) { // Yay! The datagram was accepted into the packet we just sent and we can - // return the datagram ID. + // return the datagram ID. Note that per the spec, datagrams cannot be + // fragmented, so if it was accepted, the entire datagram was sent. Debug(this, "Datagram %" PRIu64 " sent", did); auto& stats_ = impl_->stats_; STAT_INCREMENT(Stats, datagrams_sent); @@ -1850,7 +1892,8 @@ uint64_t Session::SendDatagram(Store&& data) { } // We sent a packet, but it wasn't the datagram packet. That can happen. - // Let's loop around and try again. + // Let's loop around and try again. We will limit the number of retries + // we do here to avoid looping indefinitely. if (++attempts == kMaxAttempts) [[unlikely]] { Debug(this, "Too many attempts to send datagram. Canceling."); // Too many attempts to send the datagram. @@ -1865,12 +1908,12 @@ uint64_t Session::SendDatagram(Store&& data) { } void Session::UpdatePacketTxTime() { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); ngtcp2_conn_update_pkt_tx_time(*this, uv_hrtime()); } void Session::UpdatePath(const PathStorage& storage) { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); impl_->remote_address_.Update(storage.path.remote.addr, storage.path.remote.addrlen); impl_->local_address_.Update(storage.path.local.addr, @@ -1881,7 +1924,7 @@ void Session::UpdatePath(const PathStorage& storage) { impl_->remote_address_); } -BaseObjectPtr Session::FindStream(int64_t id) const { +BaseObjectPtr Session::FindStream(stream_id id) const { if (is_destroyed()) return {}; auto it = impl_->streams_.find(id); if (it == std::end(impl_->streams_)) return {}; @@ -1889,7 +1932,7 @@ BaseObjectPtr Session::FindStream(int64_t id) const { } BaseObjectPtr Session::CreateStream( - int64_t id, + stream_id id, CreateStreamOption option, std::shared_ptr data_source) { if (!can_create_streams()) [[unlikely]] @@ -1922,7 +1965,7 @@ MaybeLocal Session::OpenStream(Direction direction, return {}; } - int64_t id = -1; + stream_id id = -1; auto open = [&] { switch (direction) { case Direction::BIDIRECTIONAL: { @@ -1965,13 +2008,13 @@ MaybeLocal Session::OpenStream(Direction direction, void Session::AddStream(BaseObjectPtr stream, CreateStreamOption option) { - CHECK(!is_destroyed()); - CHECK(stream); + DCHECK(!is_destroyed()); + DCHECK(stream); auto id = stream->id(); auto direction = stream->direction(); - // Let's double check that a stream with the given id does not already + // Let's double check that a stream with the given id does not alreadys // exist. If it does, that means we've got a bug somewhere. DCHECK_EQ(impl_->streams_.find(id), impl_->streams_.end()); @@ -2018,8 +2061,8 @@ void Session::AddStream(BaseObjectPtr stream, } } -void Session::RemoveStream(int64_t id) { - CHECK(!is_destroyed()); +void Session::RemoveStream(stream_id id) { + DCHECK(!is_destroyed()); Debug(this, "Removing stream %" PRIi64 " from session", id); if (!is_in_draining_period() && !is_in_closing_period() && !ngtcp2_conn_is_local_stream(*this, id)) { @@ -2050,14 +2093,14 @@ void Session::RemoveStream(int64_t id) { } } -void Session::ResumeStream(int64_t id) { - CHECK(!is_destroyed()); +void Session::ResumeStream(stream_id id) { + DCHECK(!is_destroyed()); SendPendingDataScope send_scope(this); application().ResumeStream(id); } -void Session::ShutdownStream(int64_t id, QuicError error) { - CHECK(!is_destroyed()); +void Session::ShutdownStream(stream_id id, QuicError error) { + DCHECK(!is_destroyed()); Debug(this, "Shutting down stream %" PRIi64 " with error %s", id, error); SendPendingDataScope send_scope(this); ngtcp2_conn_shutdown_stream(*this, @@ -2068,8 +2111,8 @@ void Session::ShutdownStream(int64_t id, QuicError error) { : application().GetNoErrorCode()); } -void Session::ShutdownStreamWrite(int64_t id, QuicError code) { - CHECK(!is_destroyed()); +void Session::ShutdownStreamWrite(stream_id id, QuicError code) { + DCHECK(!is_destroyed()); Debug(this, "Shutting down stream %" PRIi64 " write with error %s", id, code); SendPendingDataScope send_scope(this); ngtcp2_conn_shutdown_stream_write(*this, @@ -2080,8 +2123,8 @@ void Session::ShutdownStreamWrite(int64_t id, QuicError code) { : application().GetNoErrorCode()); } -void Session::StreamDataBlocked(int64_t id) { - CHECK(!is_destroyed()); +void Session::StreamDataBlocked(stream_id id) { + DCHECK(!is_destroyed()); auto& stats_ = impl_->stats_; STAT_INCREMENT(Stats, block_count); application().BlockStream(id); @@ -2089,13 +2132,13 @@ void Session::StreamDataBlocked(int64_t id) { void Session::CollectSessionTicketAppData( SessionTicket::AppData* app_data) const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); application().CollectSessionTicketAppData(app_data); } SessionTicket::AppData::Status Session::ExtractSessionTicketAppData( const SessionTicket::AppData& app_data, Flag flag) { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return application().ExtractSessionTicketAppData(app_data, flag); } @@ -2113,12 +2156,12 @@ void Session::MemoryInfo(MemoryTracker* tracker) const { } bool Session::is_in_closing_period() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return ngtcp2_conn_in_closing_period(*this) != 0; } bool Session::is_in_draining_period() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return ngtcp2_conn_in_draining_period(*this) != 0; } @@ -2127,7 +2170,7 @@ bool Session::wants_session_ticket() const { } void Session::SetStreamOpenAllowed() { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); impl_->state_->stream_open_allowed = 1; } @@ -2149,39 +2192,39 @@ bool Session::can_open_streams() const { } uint64_t Session::max_data_left() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return ngtcp2_conn_get_max_data_left(*this); } uint64_t Session::max_local_streams_uni() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return ngtcp2_conn_get_streams_uni_left(*this); } uint64_t Session::max_local_streams_bidi() const { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); return ngtcp2_conn_get_local_transport_params(*this) ->initial_max_streams_bidi; } void Session::set_wrapped() { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); impl_->state_->wrapped = 1; } void Session::set_priority_supported(bool on) { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); impl_->state_->priority_supported = on ? 1 : 0; } -void Session::ExtendStreamOffset(int64_t id, size_t amount) { - CHECK(!is_destroyed()); +void Session::ExtendStreamOffset(stream_id id, size_t amount) { + DCHECK(!is_destroyed()); Debug(this, "Extending stream %" PRIi64 " offset by %zu bytes", id, amount); ngtcp2_conn_extend_max_stream_offset(*this, id, amount); } void Session::ExtendOffset(size_t amount) { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); Debug(this, "Extending offset by %zu bytes", amount); ngtcp2_conn_extend_max_offset(*this, amount); } @@ -2265,7 +2308,7 @@ void Session::SendConnectionClose() { } void Session::OnTimeout() { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); HandleScope scope(env()->isolate()); int ret = ngtcp2_conn_handle_expiry(*this, uv_hrtime()); if (NGTCP2_OK(ret) && !is_in_closing_period() && !is_in_draining_period()) { @@ -2278,7 +2321,7 @@ void Session::OnTimeout() { } void Session::UpdateTimer() { - CHECK(!is_destroyed()); + DCHECK(!is_destroyed()); // Both uv_hrtime and ngtcp2_conn_get_expiry return nanosecond units. uint64_t expiry = ngtcp2_conn_get_expiry(*this); uint64_t now = uv_hrtime(); @@ -2298,7 +2341,8 @@ void Session::UpdateTimer() { impl_->timer_.Update(timeout == 0 ? 1 : timeout); } -void Session::DatagramStatus(uint64_t datagramId, quic::DatagramStatus status) { +void Session::DatagramStatus(datagram_id datagramId, + quic::DatagramStatus status) { DCHECK(!is_destroyed()); auto& stats_ = impl_->stats_; switch (status) { @@ -2435,7 +2479,7 @@ void Session::ProcessPendingBidiStreams() { // It shouldn't be possible to get here if can_create_streams() is false. DCHECK(can_create_streams()); - int64_t id; + stream_id id; while (!impl_->pending_bidi_stream_queue_.IsEmpty()) { if (ngtcp2_conn_get_streams_bidi_left(*this) == 0) { @@ -2468,7 +2512,7 @@ void Session::ProcessPendingUniStreams() { // It shouldn't be possible to get here if can_create_streams() is false. DCHECK(can_create_streams()); - int64_t id; + stream_id id; while (!impl_->pending_uni_stream_queue_.IsEmpty()) { if (ngtcp2_conn_get_streams_uni_left(*this) == 0) { @@ -2540,7 +2584,7 @@ void Session::EmitDatagram(Store&& datagram, DatagramReceivedFlags flag) { argv); } -void Session::EmitDatagramStatus(uint64_t id, quic::DatagramStatus status) { +void Session::EmitDatagramStatus(datagram_id id, quic::DatagramStatus status) { DCHECK(!is_destroyed()); if (!env()->can_call_into_js()) return; diff --git a/src/quic/session.h b/src/quic/session.h index 8169cac63ffc41..ddaddb8d18a7a7 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -307,7 +307,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { private: struct Impl; - using StreamsMap = std::unordered_map>; + using StreamsMap = std::unordered_map>; using QuicConnectionPointer = DeleteFnPtr; struct PathValidationFlags final { @@ -324,7 +324,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { void Send(const BaseObjectPtr& packet); void Send(const BaseObjectPtr& packet, const PathStorage& path); - uint64_t SendDatagram(Store&& data); + datagram_id SendDatagram(Store&& data); // A non-const variation to allow certain modifications. Config& config(); @@ -333,18 +333,18 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { NOTIFY, DO_NOT_NOTIFY, }; - BaseObjectPtr FindStream(int64_t id) const; + BaseObjectPtr FindStream(stream_id id) const; BaseObjectPtr CreateStream( - int64_t id, + stream_id id, CreateStreamOption option = CreateStreamOption::NOTIFY, std::shared_ptr data_source = nullptr); void AddStream(BaseObjectPtr stream, CreateStreamOption option = CreateStreamOption::NOTIFY); - void RemoveStream(int64_t id); - void ResumeStream(int64_t id); - void StreamDataBlocked(int64_t id); - void ShutdownStream(int64_t id, QuicError error = QuicError()); - void ShutdownStreamWrite(int64_t id, QuicError code = QuicError()); + void RemoveStream(stream_id id); + void ResumeStream(stream_id id); + void StreamDataBlocked(stream_id id); + void ShutdownStream(stream_id id, QuicError error = QuicError()); + void ShutdownStreamWrite(stream_id id, QuicError code = QuicError()); // Use the configured CID::Factory to generate a new CID. CID new_cid(size_t len = CID::kMaxLength) const; @@ -364,7 +364,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { v8::MaybeLocal OpenStream( Direction direction, std::shared_ptr data_source = nullptr); - void ExtendStreamOffset(int64_t id, size_t amount); + void ExtendStreamOffset(stream_id id, size_t amount); void ExtendOffset(size_t amount); void SetLastError(QuicError&& error); uint64_t max_data_left() const; @@ -463,7 +463,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { void EmitClose(const QuicError& error = QuicError()); void EmitDatagram(Store&& datagram, DatagramReceivedFlags flag); - void EmitDatagramStatus(uint64_t id, DatagramStatus status); + void EmitDatagramStatus(datagram_id id, DatagramStatus status); void EmitHandshakeComplete(); void EmitKeylog(const char* line); @@ -481,7 +481,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { void EmitVersionNegotiation(const ngtcp2_pkt_hd& hd, const uint32_t* sv, size_t nsv); - void DatagramStatus(uint64_t datagramId, DatagramStatus status); + void DatagramStatus(datagram_id datagramId, DatagramStatus status); void DatagramReceived(const uint8_t* data, size_t datalen, DatagramReceivedFlags flag); diff --git a/src/quic/streams.cc b/src/quic/streams.cc index 32b979eb3fa779..8fe5b72ce1fe5b 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -35,7 +35,7 @@ using v8::Value; namespace quic { #define STREAM_STATE(V) \ - V(ID, id, int64_t) \ + V(ID, id, stream_id) \ V(PENDING, pending, uint8_t) \ V(FIN_SENT, fin_sent, uint8_t) \ V(FIN_RECEIVED, fin_received, uint8_t) \ @@ -107,7 +107,7 @@ PendingStream::~PendingStream() { } } -void PendingStream::fulfill(int64_t id) { +void PendingStream::fulfill(stream_id id) { CHECK(waiting_); waiting_ = false; stream_->NotifyStreamOpened(id); @@ -145,31 +145,76 @@ Maybe> Stream::GetDataQueueFromSource( DCHECK_IMPLIES(!value->IsUndefined(), value->IsObject()); std::vector> entries; if (value->IsUndefined()) { + // Return an empty DataQueue. return Just(std::shared_ptr()); } else if (value->IsArrayBuffer()) { + // DataQueue is created from an ArrayBuffer. auto buffer = value.As(); + // We require that the ArrayBuffer be detachable. This ensures that the + // underlying memory can be transferred to the DataQueue without risk + // of the memory being modified by JavaScript code while it is owned + // by the DataQueue. + if (!buffer->IsDetachable()) { + THROW_ERR_INVALID_ARG_TYPE(env, "Data source not detachable"); + return Nothing>(); + } + auto backing = buffer->GetBackingStore(); + uint64_t offset = 0; + uint64_t length = buffer->ByteLength(); + if (buffer->Detach(Local()).IsNothing()) { + THROW_ERR_INVALID_ARG_TYPE(env, "Data source not detachable"); + return Nothing>(); + } entries.push_back(DataQueue::CreateInMemoryEntryFromBackingStore( - buffer->GetBackingStore(), 0, buffer->ByteLength())); + std::move(backing), offset, length)); return Just(DataQueue::CreateIdempotent(std::move(entries))); } else if (value->IsSharedArrayBuffer()) { - auto buffer = value.As(); - entries.push_back(DataQueue::CreateInMemoryEntryFromBackingStore( - buffer->GetBackingStore(), 0, buffer->ByteLength())); - return Just(DataQueue::CreateIdempotent(std::move(entries))); + // We aren't going to allow use of SharedArrayBuffer as a data source. + // The reason is that SharedArrayBuffer memory is possibly shared with + // other JavaScript code and we cannot detach it, making it impossible + // for us to guarantee that the memory will not be modified while it + // is owned by the DataQueue. + THROW_ERR_INVALID_ARG_TYPE(env, "SharedArrayBuffer is not allowed"); + return Nothing>(); } else if (value->IsArrayBufferView()) { - auto entry = - DataQueue::CreateInMemoryEntryFromView(value.As()); - if (!entry) { + auto view = value.As(); + auto buffer = view->Buffer(); + if (buffer->IsSharedArrayBuffer()) { + // We aren't going to allow use of SharedArrayBuffer as a data source. + // The reason is that SharedArrayBuffer memory is possibly shared with + // other JavaScript code and we cannot detach it, making it impossible + // for us to guarantee that the memory will not be modified while it + // is owned by the DataQueue. + THROW_ERR_INVALID_ARG_TYPE(env, "SharedArrayBuffer is not allowed"); + return Nothing>(); + } + if (!buffer->IsDetachable()) { THROW_ERR_INVALID_ARG_TYPE(env, "Data source not detachable"); return Nothing>(); } - entries.push_back(std::move(entry)); + if (buffer->Detach(Local()).IsNothing()) { + THROW_ERR_INVALID_ARG_TYPE(env, "Data source not detachable"); + return Nothing>(); + } + auto backing = buffer->GetBackingStore(); + auto offset = view->ByteOffset(); + auto length = view->ByteLength(); + entries.push_back(DataQueue::CreateInMemoryEntryFromBackingStore( + std::move(backing), offset, length)); return Just(DataQueue::CreateIdempotent(std::move(entries))); } else if (Blob::HasInstance(env, value)) { Blob* blob; ASSIGN_OR_RETURN_UNWRAP( &blob, value, Nothing>()); return Just(blob->getDataQueue().slice(0)); + } else if (value->IsString()) { + Utf8Value str(env->isolate(), value); + JS_TRY_ALLOCATE_BACKING_OR_RETURN( + env, backing, str.length(), Nothing>()); + memcpy(backing->Data(), *str, str.length()); + entries.push_back(DataQueue::CreateInMemoryEntryFromBackingStore( + std::move(backing), 0, backing->ByteLength())); + return Just(DataQueue::CreateIdempotent(std::move(entries))); } // TODO(jasnell): Add streaming sources... THROW_ERR_INVALID_ARG_TYPE(env, "Invalid data source type"); @@ -182,15 +227,16 @@ struct Stream::Impl { // Attaches an outbound data source to the stream. JS_METHOD(AttachSource) { Environment* env = Environment::GetCurrent(args); + Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); std::shared_ptr dataqueue; if (GetDataQueueFromSource(env, args[0]).To(&dataqueue)) { - Stream* stream; - ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); stream->set_outbound(std::move(dataqueue)); } } + // Immediately and forcefully destroys the stream. JS_METHOD(Destroy) { Stream* stream; ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); @@ -204,6 +250,10 @@ struct Stream::Impl { } } + // Sends a block of headers to the peer. If the stream is not yet open, + // the headers will be queued and sent immediately when the stream is + // opened. If the application does not support sending headers on streams, + // they will be ignored and dropped on the floor. JS_METHOD(SendHeaders) { Stream* stream; ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); @@ -233,7 +283,7 @@ struct Stream::Impl { JS_METHOD(StopSending) { Stream* stream; ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); - uint64_t code = 0; + error_code code = 0; CHECK_IMPLIES(!args[0]->IsUndefined(), args[0]->IsBigInt()); if (!args[0]->IsUndefined()) { bool unused = false; // not used but still necessary. @@ -258,7 +308,7 @@ struct Stream::Impl { JS_METHOD(ResetStream) { Stream* stream; ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); - uint64_t code = 0; + error_code code = 0; CHECK_IMPLIES(!args[0]->IsUndefined(), args[0]->IsBigInt()); if (!args[0]->IsUndefined()) { bool lossless = false; // not used but still necessary. @@ -315,6 +365,8 @@ struct Stream::Impl { args.GetReturnValue().Set(static_cast(priority)); } + // Returns a Blob::Reader that can be used to read data that has been + // received on the stream. JS_METHOD(GetReader) { Stream* stream; ASSIGN_OR_RETURN_UNWRAP(&stream, args.This()); @@ -758,7 +810,7 @@ Stream* Stream::From(void* stream_user_data) { } BaseObjectPtr Stream::Create(Session* session, - int64_t id, + stream_id id, std::shared_ptr source) { DCHECK_GE(id, 0); DCHECK_NOT_NULL(session); @@ -778,7 +830,7 @@ BaseObjectPtr Stream::Create(Session* session, Stream::Stream(BaseObjectWeakPtr session, Local object, - int64_t id, + stream_id id, std::shared_ptr source) : AsyncWrap(session->env(), object, PROVIDER_QUIC_STREAM), stats_(env()->isolate()), @@ -787,6 +839,7 @@ Stream::Stream(BaseObjectWeakPtr session, inbound_(DataQueue::Create()), headers_(env()->isolate()) { MakeWeak(); + DCHECK(id < kMaxStreamId); state_->id = id; state_->pending = 0; // Allows us to be notified when data is actually read from the @@ -818,7 +871,7 @@ Stream::Stream(BaseObjectWeakPtr session, std::make_unique(direction, this, session_)), headers_(env()->isolate()) { MakeWeak(); - state_->id = -1; + state_->id = kMaxStreamId; state_->pending = 1; // Allows us to be notified when data is actually read from the @@ -841,8 +894,9 @@ Stream::~Stream() { DCHECK_NE(stats_->destroyed_at, 0); } -void Stream::NotifyStreamOpened(int64_t id) { +void Stream::NotifyStreamOpened(stream_id id) { CHECK(is_pending()); + DCHECK(id < kMaxStreamId); Debug(this, "Pending stream opened with id %" PRIi64, id); state_->pending = 0; state_->id = id; @@ -886,13 +940,13 @@ void Stream::NotifyStreamOpened(int64_t id) { if (outbound_) session().ResumeStream(id); } -void Stream::NotifyReadableEnded(uint64_t code) { +void Stream::NotifyReadableEnded(error_code code) { CHECK(!is_pending()); Session::SendPendingDataScope send_scope(&session()); ngtcp2_conn_shutdown_stream_read(session(), 0, id(), code); } -void Stream::NotifyWritableEnded(uint64_t code) { +void Stream::NotifyWritableEnded(error_code code) { CHECK(!is_pending()); Session::SendPendingDataScope send_scope(&session()); ngtcp2_conn_shutdown_stream_write(session(), 0, id(), code); @@ -910,7 +964,7 @@ bool Stream::is_pending() const { return state_->pending; } -int64_t Stream::id() const { +stream_id Stream::id() const { return state_->id; } diff --git a/src/quic/streams.h b/src/quic/streams.h index f67d6f830bedf9..c230815d78e4be 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -27,8 +27,8 @@ using Ngtcp2Source = bob::SourceImpl; // or concurrency limits are temporarily reached) then the request to open the // stream is represented as a queued PendingStream. // -// The PendingStream instance itself is held by the stream but sits in a linked -// list in the session. +// The PendingStream instance itself is owned by the stream created but a +// reference sits in a linked list in the session. // // The PendingStream request can be canceled by dropping the PendingStream // instance before it can be fulfilled, at which point it is removed from the @@ -45,7 +45,7 @@ class PendingStream final { // Called when the stream has been opened. Transitions the stream from a // pending state to an opened state. - void fulfill(int64_t id); + void fulfill(stream_id id); // Called when opening the stream fails or is canceled. Transitions the // stream into a closed/destroyed state. @@ -105,7 +105,13 @@ class PendingStream final { // data is buffered in memory makes it essential that the flow control for the // session and the stream are properly handled. For now, we are largely relying // on ngtcp2's default flow control mechanisms which generally should be doing -// the right thing. +// the right thing. From the JavaScript side, the application pushes data into +// the stream's outbound queue and ngtcp2 pulls data from that queue as it is +// able. The stream outbound has a high watermark. The JS side can choose to +// continue writing data even after the high watermark is reached but this +// risks using up large amounts of memory if the session is slow to send data +// or the peer is slow to acknowledge receipt. The JavaScript side needs to +// be aware of this risk and pay proper attention to the backpressure signals. // // A Stream may be in a fully closed state (No longer readable nor writable) // state but still have unacknowledged data in both the inbound and outbound @@ -115,23 +121,29 @@ class PendingStream final { // (b) all queued data has been acknowledged. // // The Stream may be forcefully closed immediately using destroy(err). This -// causes all queued outbound data and pending JavaScript writes are abandoned, -// and causes the Stream to be immediately closed at the ngtcp2 level without -// waiting for any outstanding acknowledgements. Keep in mind, however, that the -// peer is not notified that the stream is destroyed and may attempt to continue -// sending data and acknowledgements. +// causes all queued outbound data to be cleared, pending JavaScript writes +// to be abandoned, the Stream to be immediately closed at the ngtcp2 level +// without waiting for any outstanding acknowledgements. Keep in mind, however, +// that the peer is not notified that the stream is destroyed and may attempt +// to continue sending data and acknowledgements until it is able to determine +// that the stream is gone. Any data that has already been received and is in +// the inbound queue is preserved and may be read by the application. // // QUIC streams in general do not have headers. Some QUIC applications, however, -// may associate headers with the stream (HTTP/3 for instance). +// may associate headers with the stream (HTTP/3 for instance). As a +// convenience, the Stream class will hold onto these headers for the +// application. // // Streams may be created in a pending state. This means that while the Stream // object is created, it has not yet been opened in ngtcp2 and therefore has // no official status yet. Certain operations can still be performed on the -// stream object such as providing data and headers, and destroying the stream. +// stream object such as providing data, adding headers, or destroying the +// stream. // // When a stream is created the data source for the stream must be given. // If no data source is given, then the stream is assumed to not have any -// outbound data. The data source can be fixed length or may support +// outbound data. If the stream was created as bidirectional, the outbound +// side will be closed. The data source can be fixed length or may support // streaming. What this means practically is, when a stream is opened, // you must already have a sense of whether that will provide data or // not. When in doubt, specify a streaming data source, which can produce @@ -142,18 +154,24 @@ class Stream final : public AsyncWrap, public: using Header = NgHeaderBase; + // Acquire a DataQueue from the given value if it is valid. The return + // follows the typical V8 rules for Maybe types. If an error occurs, + // the Maybe will be empty and an exception will be set on the isolate. static v8::Maybe> GetDataQueueFromSource( Environment* env, v8::Local value); + // The stream_user_data field is from ngtcp2 and will point to the + // Stream instance associated with the stream_id. static Stream* From(void* stream_user_data); JS_CONSTRUCTOR(Stream); JS_BINDING_INIT_BOILERPLATE(); - // Creates a new non-pending stream. + // Creates a new non-pending stream. The directionality of the stream + // is inferred from the stream id. static BaseObjectPtr Create( Session* session, - int64_t id, + stream_id id, std::shared_ptr source = nullptr); // Creates a new pending stream. @@ -166,7 +184,7 @@ class Stream final : public AsyncWrap, // Call Create to create new instances of Stream. Stream(BaseObjectWeakPtr session, v8::Local obj, - int64_t id, + stream_id id, std::shared_ptr source); // Creates the stream in a pending state. The constructor is only public @@ -179,8 +197,9 @@ class Stream final : public AsyncWrap, DISALLOW_COPY_AND_MOVE(Stream) ~Stream() override; - // While the stream is still pending, the id will be -1. - int64_t id() const; + // While the stream is still pending, the id will be kMaxStreamId, + // inidicating the maximum possible stream id is kMaxStreamId - 1. + stream_id id() const; // While the stream is still pending, the origin will be invalid. Side origin() const; @@ -208,6 +227,12 @@ class Stream final : public AsyncWrap, // Called by the session/application to indicate that the specified number // of bytes have been acknowledged by the peer. void Acknowledge(size_t datalen); + + // Called by the session/application to indicate that the specified number + // of bytes have been transmitted to the peer. This is an initial + // indication occuring the first time data is sent. It does not indicate + // that the data has been retransmitted due to loss or has been + // acknowledged to have been received by the peer. void Commit(size_t datalen); void EndWritable(); @@ -215,6 +240,10 @@ class Stream final : public AsyncWrap, void EntryRead(size_t amount) override; // Pulls data from the internal outbound DataQueue configured for this stream. + // This is called by the session/application when it is preparing to send + // data to the peer. There is no guarantee that the requested amount of data + // will actually be sent. The amount of data actually sent is indicated + // by the datalen argument to the Commit() method. int DoPull(bob::Next next, int options, ngtcp2_vec* data, @@ -282,7 +311,8 @@ class Stream final : public AsyncWrap, void EmitReset(const QuicError& error); // Notifies the JavaScript side that the application is ready to receive - // trailing headers. + // trailing headers. Any trailing headers must be sent immediately, and + // synchronously when this callback is triggered. void EmitWantTrailers(); // Notifies the JavaScript side that sending data on the stream has been @@ -292,12 +322,12 @@ class Stream final : public AsyncWrap, // Delivers the set of inbound headers that have been collected. void EmitHeaders(); - void NotifyReadableEnded(uint64_t code); - void NotifyWritableEnded(uint64_t code); + void NotifyReadableEnded(error_code code); + void NotifyWritableEnded(error_code code); // When a pending stream is finally opened, the NotifyStreamOpened method // will be called and the id will be assigned. - void NotifyStreamOpened(int64_t id); + void NotifyStreamOpened(stream_id id); void EnqueuePendingHeaders(HeadersKind kind, v8::Local headers, HeadersFlags flags); @@ -314,8 +344,8 @@ class Stream final : public AsyncWrap, std::optional> maybe_pending_stream_ = std::nullopt; std::vector> pending_headers_queue_; - uint64_t pending_close_read_code_ = 0; - uint64_t pending_close_write_code_ = 0; + error_code pending_close_read_code_ = 0; + error_code pending_close_write_code_ = 0; struct PendingPriority { StreamPriority priority; diff --git a/src/quic/transportparams.cc b/src/quic/transportparams.cc index 6d257371394c80..f46e0dbcd7739d 100644 --- a/src/quic/transportparams.cc +++ b/src/quic/transportparams.cc @@ -34,22 +34,23 @@ TransportParams::Config::Config(Side side, Maybe TransportParams::Options::From( Environment* env, Local value) { if (value.IsEmpty()) { - THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); + THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object or undefined"); + return Nothing(); + } else if (value->IsUndefined()) { + return Just(kDefault); + } else if (!value->IsObject()) { + THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object or undefined"); return Nothing(); } Options options; - auto& state = BindingData::Get(env); - - if (value->IsUndefined()) { - return Just(options); - } - if (!value->IsObject()) { - THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); - return Nothing(); - } + // TODO(@jasnell): We currently only support version 1 of the transport + // parameters, so the options.transportParamsVersion is hardcoded to that. + // In the future, when we support multiple versions, we will need to + // expose this via the options object. + auto& state = BindingData::Get(env); auto params = value.As(); #define SET(name) \ @@ -68,6 +69,11 @@ Maybe TransportParams::Options::From( #undef SET + // TODO(@jasnell): We are not yet exposing the ability to set the preferred + // adddress via the options, tho the underlying support is here in the class. + options.preferred_address_ipv4 = std::nullopt; + options.preferred_address_ipv6 = std::nullopt; + return Just(options); } @@ -75,7 +81,8 @@ std::string TransportParams::Options::ToString() const { DebugIndentScope indent; auto prefix = indent.Prefix(); std::string res("{"); - res += prefix + "version: " + std::to_string(transportParamsVersion); + res += prefix + + "version: " + std::to_string(static_cast(transportParamsVersion)); if (preferred_address_ipv4.has_value()) { res += prefix + "preferred_address_ipv4: " + preferred_address_ipv4.value().ToString(); @@ -131,36 +138,39 @@ TransportParams::TransportParams(const ngtcp2_transport_params* ptr) TransportParams::TransportParams(const Config& config, const Options& options) : TransportParams() { ngtcp2_transport_params_default(¶ms_); - params_.active_connection_id_limit = options.active_connection_id_limit; - params_.initial_max_stream_data_bidi_local = - options.initial_max_stream_data_bidi_local; - params_.initial_max_stream_data_bidi_remote = - options.initial_max_stream_data_bidi_remote; - params_.initial_max_stream_data_uni = options.initial_max_stream_data_uni; - params_.initial_max_streams_bidi = options.initial_max_streams_bidi; - params_.initial_max_streams_uni = options.initial_max_streams_uni; - params_.initial_max_data = options.initial_max_data; - params_.max_idle_timeout = options.max_idle_timeout * NGTCP2_SECONDS; - params_.max_ack_delay = options.max_ack_delay; - params_.ack_delay_exponent = options.ack_delay_exponent; - params_.max_datagram_frame_size = options.max_datagram_frame_size; - params_.disable_active_migration = options.disable_active_migration ? 1 : 0; - params_.preferred_addr_present = 0; - params_.stateless_reset_token_present = 0; - params_.retry_scid_present = 0; +#define SET_PARAM(name) params_.name = options.name +#define SET_PARAM_V(name, value) params_.name = value + SET_PARAM(active_connection_id_limit); + SET_PARAM(initial_max_stream_data_bidi_local); + SET_PARAM(initial_max_stream_data_bidi_remote); + SET_PARAM(initial_max_stream_data_uni); + SET_PARAM(initial_max_streams_bidi); + SET_PARAM(initial_max_streams_uni); + SET_PARAM(initial_max_data); + SET_PARAM(max_ack_delay); + SET_PARAM(ack_delay_exponent); + SET_PARAM(max_datagram_frame_size); + SET_PARAM_V(max_idle_timeout, options.max_idle_timeout * NGTCP2_SECONDS); + SET_PARAM_V(disable_active_migration, + options.disable_active_migration ? 1 : 0); + SET_PARAM_V(preferred_addr_present, 0); + SET_PARAM_V(stateless_reset_token_present, 0); + SET_PARAM_V(retry_scid_present, 0); if (config.side == Side::SERVER) { // For the server side, the original dcid is always set. CHECK(config.ocid); - params_.original_dcid = config.ocid; - params_.original_dcid_present = 1; + SET_PARAM_V(original_dcid, config.ocid); + SET_PARAM_V(original_dcid_present, 1); // The retry_scid is only set if the server validated a retry token. if (config.retry_scid) { - params_.retry_scid = config.retry_scid; - params_.retry_scid_present = 1; + SET_PARAM_V(retry_scid, config.retry_scid); + SET_PARAM_V(retry_scid_present, 1); } } +#undef SET_PARAM +#undef SET_PARAM_V if (options.preferred_address_ipv4.has_value()) SetPreferredAddress(options.preferred_address_ipv4.value()); @@ -169,25 +179,28 @@ TransportParams::TransportParams(const Config& config, const Options& options) SetPreferredAddress(options.preferred_address_ipv6.value()); } -TransportParams::TransportParams(const ngtcp2_vec& vec, int version) +TransportParams::TransportParams(const ngtcp2_vec& vec, Version version) : TransportParams() { int ret = ngtcp2_transport_params_decode_versioned( - version, ¶ms_, vec.base, vec.len); + static_cast(version), ¶ms_, vec.base, vec.len); + // The only error we should see here is NGTCP2_ERR_MALFORMED_TRANSPORT_PARAM, + // which indicates that the provided data was not valid transport parameters. + // In that case, we set ptr_ to nullptr to indicate that the parameters + // could not be decoded. if (ret != 0) { ptr_ = nullptr; - error_ = QuicError::ForNgtcp2Error(ret); } } -Store TransportParams::Encode(Environment* env, int version) const { +Store TransportParams::Encode(Environment* env, Version version) const { if (ptr_ == nullptr) { return {}; } // Preflight to see how much storage we'll need. - ssize_t size = - ngtcp2_transport_params_encode_versioned(nullptr, 0, version, ¶ms_); + ssize_t size = ngtcp2_transport_params_encode_versioned( + nullptr, 0, static_cast(version), ¶ms_); if (size == 0) { return {}; } @@ -195,7 +208,10 @@ Store TransportParams::Encode(Environment* env, int version) const { JS_TRY_ALLOCATE_BACKING_OR_RETURN(env, result, size, {}); auto ret = ngtcp2_transport_params_encode_versioned( - static_cast(result->Data()), size, version, ¶ms_); + static_cast(result->Data()), + size, + static_cast(version), + ¶ms_); // The ret is the number of bytes written, or a negative error code. if (ret < 0) return {}; @@ -214,6 +230,7 @@ void TransportParams::SetPreferredAddress(const SocketAddress& address) { &src->sin_addr, sizeof(params_.preferred_addr.ipv4.sin_addr)); params_.preferred_addr.ipv4.sin_port = address.port(); + params_.preferred_addr.ipv4_present = 1; return; } case AF_INET6: { @@ -223,6 +240,7 @@ void TransportParams::SetPreferredAddress(const SocketAddress& address) { &src->sin6_addr, sizeof(params_.preferred_addr.ipv6.sin6_addr)); params_.preferred_addr.ipv6.sin6_port = address.port(); + params_.preferred_addr.ipv6_present = 1; return; } } @@ -273,10 +291,6 @@ TransportParams::operator bool() const { return ptr_ != nullptr; } -const QuicError& TransportParams::error() const { - return error_; -} - void TransportParams::Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_STREAM_DATA); NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_DATA); diff --git a/src/quic/transportparams.h b/src/quic/transportparams.h index 479f036ae88403..ff37299c27ddb5 100644 --- a/src/quic/transportparams.h +++ b/src/quic/transportparams.h @@ -24,9 +24,10 @@ class TransportParams final { public: static void Initialize(Environment* env, v8::Local target); - static constexpr int QUIC_TRANSPORT_PARAMS_V1 = NGTCP2_TRANSPORT_PARAMS_V1; - static constexpr int QUIC_TRANSPORT_PARAMS_VERSION = - NGTCP2_TRANSPORT_PARAMS_VERSION; + enum class Version : int { + V1 = NGTCP2_TRANSPORT_PARAMS_V1, + }; + static constexpr uint64_t DEFAULT_MAX_STREAM_DATA = 256 * 1024; static constexpr uint64_t DEFAULT_MAX_DATA = 1 * 1024 * 1024; static constexpr uint64_t DEFAULT_MAX_IDLE_TIMEOUT = 10; // seconds @@ -44,52 +45,62 @@ class TransportParams final { }; struct Options final : public MemoryRetainer { - int transportParamsVersion = QUIC_TRANSPORT_PARAMS_V1; + Version transportParamsVersion = Version::V1; // Set only on server Sessions, the preferred address communicates the IP // address and port that the server would prefer the client to use when // communicating with it. See the QUIC specification for more detail on how - // the preferred address mechanism works. + // the preferred address mechanism works: + // https://www.rfc-editor.org/rfc/rfc9000.html#name-servers-preferred-address std::optional preferred_address_ipv4{}; std::optional preferred_address_ipv6{}; // The initial size of the flow control window of locally initiated streams. // This is the maximum number of bytes that the *remote* endpoint can send // when the connection is started. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.16.1 uint64_t initial_max_stream_data_bidi_local = DEFAULT_MAX_STREAM_DATA; // The initial size of the flow control window of remotely initiated // streams. This is the maximum number of bytes that the remote endpoint can // send when the connection is started. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.18.1 uint64_t initial_max_stream_data_bidi_remote = DEFAULT_MAX_STREAM_DATA; // The initial size of the flow control window of remotely initiated // unidirectional streams. This is the maximum number of bytes that the // remote endpoint can send when the connection is started. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.20.1 uint64_t initial_max_stream_data_uni = DEFAULT_MAX_STREAM_DATA; // The initial size of the session-level flow control window. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.14.1 uint64_t initial_max_data = DEFAULT_MAX_DATA; // The initial maximum number of concurrent bidirectional streams the remote // endpoint is permitted to open. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.22.1 uint64_t initial_max_streams_bidi = DEFAULT_MAX_STREAMS_BIDI; // The initial maximum number of concurrent unidirectional streams the // remote endpoint is permitted to open. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.24.1 uint64_t initial_max_streams_uni = DEFAULT_MAX_STREAMS_UNI; // The maximum amount of time that a Session is permitted to remain idle // before it is silently closed and state is discarded. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.4.1 uint64_t max_idle_timeout = DEFAULT_MAX_IDLE_TIMEOUT; // The maximum number of Connection IDs that the peer can store. A single // Session may have several connection IDs over it's lifetime. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-6.2.1 uint64_t active_connection_id_limit = DEFAULT_ACTIVE_CONNECTION_ID_LIMIT; // Establishes the exponent used in ACK Delay field in the ACK frame. See // the QUIC specification for details. This is an advanced option that // should rarely be modified and only if there is really good reason. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.26.1 uint64_t ack_delay_exponent = NGTCP2_DEFAULT_ACK_DELAY_EXPONENT; // The maximum amount of time by which the endpoint will delay sending @@ -97,16 +108,19 @@ class TransportParams final { // modified and only if there is a really good reason. It is used to // determine how long a Session will wait to determine that a packet has // been lost. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.28.1 uint64_t max_ack_delay = NGTCP2_DEFAULT_MAX_ACK_DELAY; // The maximum size of DATAGRAM frames that the endpoint will accept. // Setting the value to 0 will disable DATAGRAM support. + // https://datatracker.ietf.org/doc/html/rfc9221#section-3 uint64_t max_datagram_frame_size = kDefaultMaxPacketLength; // When true, communicates that the Session does not support active // connection migration. See the QUIC specification for more details on // connection migration. - // TODO(@jasnell): We currently do not implementation active migration. + // https://www.rfc-editor.org/rfc/rfc9000.html#section-18.2-4.30.1 + // TODO(@jasnell): We currently do not implement active migration. bool disable_active_migration = true; static const Options kDefault; @@ -123,40 +137,37 @@ class TransportParams final { explicit TransportParams(); - // Creates an instance of TransportParams wrapping the existing const - // ngtcp2_transport_params pointer. + // Creates an instance of TransportParams wrapping an existing const + // ngtcp2_transport_params pointer. Instances created this way are + // immutable. TransportParams(const ngtcp2_transport_params* ptr); TransportParams(const Config& config, const Options& options); // Creates an instance of TransportParams by decoding the given buffer. - // If the parameters cannot be successfully decoded, the error() - // property will be set with an appropriate QuicError and the bool() - // operator will return false. - TransportParams(const ngtcp2_vec& buf, - int version = QUIC_TRANSPORT_PARAMS_V1); + // If the parameters cannot be successfully decoded, the bool() operator + // will be false. + TransportParams(const ngtcp2_vec& buf, Version version = Version::V1); void GenerateSessionTokens(Session* session); - void GenerateStatelessResetToken(const Endpoint& endpoint, const CID& cid); - void GeneratePreferredAddressToken(Session* session); - void SetPreferredAddress(const SocketAddress& address); operator const ngtcp2_transport_params&() const; operator const ngtcp2_transport_params*() const; operator bool() const; - const QuicError& error() const; - - // Returns an ArrayBuffer containing the encoded transport parameters. - // If an error occurs during encoding, an empty shared_ptr will be returned - // and the error() property will be set to an appropriate QuicError. - Store Encode(Environment* env, int version = QUIC_TRANSPORT_PARAMS_V1) const; + // Returns a Store containing the encoded transport parameters. + // If an error occurs during encoding, or if the parameters could + // not be encoded, an empty Store will be returned. + Store Encode(Environment* env, Version version = Version::V1) const; private: + void SetPreferredAddress(const SocketAddress& address); + void GeneratePreferredAddressToken(Session* session); + void GenerateStatelessResetToken(const Endpoint& endpoint, const CID& cid); + ngtcp2_transport_params params_{}; const ngtcp2_transport_params* ptr_; - QuicError error_ = QuicError::TRANSPORT_NO_ERROR; }; } // namespace node::quic diff --git a/test/common/index.mjs b/test/common/index.mjs index 2c752db65e7ac4..98a26e29ddad27 100644 --- a/test/common/index.mjs +++ b/test/common/index.mjs @@ -16,6 +16,7 @@ const { getBufferSources, getTTYfd, hasCrypto, + hasQuic, hasSQLite, hasIntl, hasIPv6, @@ -66,6 +67,7 @@ export { getPort, getTTYfd, hasCrypto, + hasQuic, hasSQLite, hasIntl, hasIPv6, diff --git a/test/parallel/test-quic-handshake.js b/test/parallel/test-quic-handshake.js deleted file mode 100644 index 51b1fde2b0c4cb..00000000000000 --- a/test/parallel/test-quic-handshake.js +++ /dev/null @@ -1,50 +0,0 @@ -// Flags: --experimental-quic --no-warnings -'use strict'; - -const common = require('../common'); -const { ok, strictEqual } = require('node:assert'); - -if (!common.hasQuic) { - common.skip('QUIC is not enabled'); -} - -const { createPrivateKey } = require('node:crypto'); -const fixtures = require('../common/fixtures'); -const keys = createPrivateKey(fixtures.readKey('agent1-key.pem')); -const certs = fixtures.readKey('agent1-cert.pem'); - -const { - listen, - connect, -} = require('node:quic'); - - -const p1 = Promise.withResolvers(); -const p2 = Promise.withResolvers(); - -(async () => { - const serverEndpoint = await listen((serverSession) => { - - serverSession.opened.then((info) => { - strictEqual(info.servername, 'localhost'); - strictEqual(info.protocol, 'h3'); - strictEqual(info.cipher, 'TLS_AES_128_GCM_SHA256'); - - p1.resolve(); - serverSession.close(); - }); - }, { keys, certs }); - - ok(serverEndpoint.address !== undefined); - - const clientSession = await connect(serverEndpoint.address); - clientSession.opened.then((info) => { - strictEqual(info.servername, 'localhost'); - strictEqual(info.protocol, 'h3'); - strictEqual(info.cipher, 'TLS_AES_128_GCM_SHA256'); - p2.resolve(); - }); - - await Promise.all([p1.promise, p2.promise]); - clientSession.close(); -})().then(common.mustCall()); diff --git a/test/parallel/test-quic-handshake.mjs b/test/parallel/test-quic-handshake.mjs new file mode 100644 index 00000000000000..faabdcd898af32 --- /dev/null +++ b/test/parallel/test-quic-handshake.mjs @@ -0,0 +1,50 @@ +// Flags: --experimental-quic --no-warnings + +import { hasQuic, skip } from '../common/index.mjs'; +import { ok, partialDeepStrictEqual } from 'node:assert'; +import { readKey } from '../common/fixtures.mjs'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +// Import after the hasQuic check +const { listen, connect } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); + +const keys = createPrivateKey(readKey('agent1-key.pem')); +const certs = readKey('agent1-cert.pem'); + +const check = { + // The SNI value + servername: 'localhost', + // The selected ALPN protocol + protocol: 'h3', + // The negotiated cipher suite + cipher: 'TLS_AES_128_GCM_SHA256', + cipherVersion: 'TLSv1.3', +}; + +// The opened promise should resolve when the handshake is complete. + +const serverOpened = Promise.withResolvers(); +const clientOpened = Promise.withResolvers(); + +const serverEndpoint = await listen(async (serverSession) => { + const info = await serverSession.opened; + partialDeepStrictEqual(info, check); + serverOpened.resolve(); + serverSession.close(); +}, { keys, certs }); + +// The server must have an address to connect to after listen resolves. +ok(serverEndpoint.address !== undefined); + +const clientSession = await connect(serverEndpoint.address); +clientSession.opened.then((info) => { + partialDeepStrictEqual(info, check); + clientOpened.resolve(); +}); + +await Promise.all([serverOpened.promise, clientOpened.promise]); +clientSession.close(); diff --git a/test/parallel/test-quic-internal-endpoint-listen-defaults.js b/test/parallel/test-quic-internal-endpoint-listen-defaults.js deleted file mode 100644 index d5a96c252298f2..00000000000000 --- a/test/parallel/test-quic-internal-endpoint-listen-defaults.js +++ /dev/null @@ -1,87 +0,0 @@ -// Flags: --expose-internals --no-warnings -'use strict'; - -const { hasQuic } = require('../common'); - -const { - describe, - it, -} = require('node:test'); - -describe('quic internal endpoint listen defaults', { skip: !hasQuic }, async () => { - const { - ok, - rejects, - strictEqual, - throws, - } = require('node:assert'); - - const { - kState, - } = require('internal/quic/symbols'); - - const { createPrivateKey } = require('node:crypto'); - const fixtures = require('../common/fixtures'); - const keys = createPrivateKey(fixtures.readKey('agent1-key.pem')); - const certs = fixtures.readKey('agent1-cert.pem'); - - const { - SocketAddress, - } = require('net'); - - const { - QuicEndpoint, - listen, - } = require('internal/quic/quic'); - - it('are reasonable and work as expected', async () => { - const endpoint = new QuicEndpoint(); - - ok(!endpoint[kState].isBound); - ok(!endpoint[kState].isReceiving); - ok(!endpoint[kState].isListening); - - strictEqual(endpoint.address, undefined); - - await rejects(listen(123, { keys, certs, endpoint }), { - code: 'ERR_INVALID_ARG_TYPE', - }); - - await rejects(listen(() => {}, 123), { - code: 'ERR_INVALID_ARG_TYPE', - }); - - await listen(() => {}, { keys, certs, endpoint }); - await rejects(listen(() => {}, { keys, certs, endpoint }), { - code: 'ERR_INVALID_STATE', - }); - - ok(endpoint[kState].isBound); - ok(endpoint[kState].isReceiving); - ok(endpoint[kState].isListening); - - const address = endpoint.address; - ok(address instanceof SocketAddress); - - strictEqual(address.address, '127.0.0.1'); - strictEqual(address.family, 'ipv4'); - strictEqual(address.flowlabel, 0); - ok(address.port !== 0); - - ok(!endpoint.destroyed); - endpoint.destroy(); - strictEqual(endpoint.closed, endpoint.close()); - await endpoint.closed; - ok(endpoint.destroyed); - - await rejects(listen(() => {}, { keys, certs, endpoint }), { - code: 'ERR_INVALID_STATE', - }); - throws(() => { endpoint.busy = true; }, { - code: 'ERR_INVALID_STATE', - }); - await endpoint[Symbol.asyncDispose](); - - strictEqual(endpoint.address, undefined); - }); -}); diff --git a/test/parallel/test-quic-internal-endpoint-listen-defaults.mjs b/test/parallel/test-quic-internal-endpoint-listen-defaults.mjs new file mode 100644 index 00000000000000..d62bdda5755e64 --- /dev/null +++ b/test/parallel/test-quic-internal-endpoint-listen-defaults.mjs @@ -0,0 +1,72 @@ +// Flags: --expose-internals --experimental-quic --no-warnings +import { hasQuic, skip } from '../common/index.mjs'; + +import { + ok, + rejects, + strictEqual, + throws, +} from 'node:assert'; +import { readKey } from '../common/fixtures.mjs'; +import { SocketAddress } from 'node:net'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +// Import after the hasQuic check +const { listen, QuicEndpoint } = await import('node:quic'); +const { createPrivateKey } = await import('node:crypto'); +const { kState } = (await import('internal/quic/symbols')).default; + +const keys = createPrivateKey(readKey('agent1-key.pem')); +const certs = readKey('agent1-cert.pem'); + +const endpoint = new QuicEndpoint(); + +ok(!endpoint[kState].isBound); +ok(!endpoint[kState].isReceiving); +ok(!endpoint[kState].isListening); + +strictEqual(endpoint.address, undefined); + +await rejects(listen(123, { keys, certs, endpoint }), { + code: 'ERR_INVALID_ARG_TYPE', +}); + +await rejects(listen(() => {}, 123), { + code: 'ERR_INVALID_ARG_TYPE', +}); + +await listen(() => {}, { keys, certs, endpoint }); +await rejects(listen(() => {}, { keys, certs, endpoint }), { + code: 'ERR_INVALID_STATE', +}); + +ok(endpoint[kState].isBound); +ok(endpoint[kState].isReceiving); +ok(endpoint[kState].isListening); + +const address = endpoint.address; +ok(address instanceof SocketAddress); + +strictEqual(address.address, '127.0.0.1'); +strictEqual(address.family, 'ipv4'); +strictEqual(address.flowlabel, 0); +ok(address.port !== 0); + +ok(!endpoint.destroyed); +endpoint.destroy(); +strictEqual(endpoint.closed, endpoint.close()); +await endpoint.closed; +ok(endpoint.destroyed); + +await rejects(listen(() => {}, { keys, certs, endpoint }), { + code: 'ERR_INVALID_STATE', +}); +throws(() => { endpoint.busy = true; }, { + code: 'ERR_INVALID_STATE', +}); +await endpoint[Symbol.asyncDispose](); + +strictEqual(endpoint.address, undefined); diff --git a/test/parallel/test-quic-internal-endpoint-options.js b/test/parallel/test-quic-internal-endpoint-options.js deleted file mode 100644 index db8b13fe4bdb10..00000000000000 --- a/test/parallel/test-quic-internal-endpoint-options.js +++ /dev/null @@ -1,191 +0,0 @@ -// Flags: --experimental-quic --no-warnings -'use strict'; - -const { hasQuic } = require('../common'); - -const { - describe, - it, -} = require('node:test'); - -describe('quic internal endpoint options', { skip: !hasQuic }, async () => { - const { - strictEqual, - throws, - } = require('node:assert'); - - const { - QuicEndpoint, - } = require('node:quic'); - - const { - inspect, - } = require('util'); - - it('invalid options', async () => { - ['a', null, false, NaN].forEach((i) => { - throws(() => new QuicEndpoint(i), { - code: 'ERR_INVALID_ARG_TYPE', - }); - }); - }); - - it('valid options', async () => { - // Just Works... using all defaults - new QuicEndpoint(); - }); - - it('various cases', async () => { - const cases = [ - { - key: 'retryTokenExpiration', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'tokenExpiration', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'maxConnectionsPerHost', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'maxConnectionsTotal', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'maxStatelessResetsPerHost', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'addressLRUSize', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'maxRetries', - valid: [ - 1, 10, 100, 1000, 10000, 10000n, - ], - invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] - }, - { - key: 'validateAddress', - valid: [true, false, 0, 1, 'a'], - invalid: [], - }, - { - key: 'disableStatelessReset', - valid: [true, false, 0, 1, 'a'], - invalid: [], - }, - { - key: 'ipv6Only', - valid: [true, false, 0, 1, 'a'], - invalid: [], - }, - { - key: 'udpReceiveBufferSize', - valid: [0, 1, 2, 3, 4, 1000], - invalid: [-1, 'a', null, false, true, {}, [], () => {}], - }, - { - key: 'udpSendBufferSize', - valid: [0, 1, 2, 3, 4, 1000], - invalid: [-1, 'a', null, false, true, {}, [], () => {}], - }, - { - key: 'udpTTL', - valid: [0, 1, 2, 3, 4, 255], - invalid: [-1, 256, 'a', null, false, true, {}, [], () => {}], - }, - { - key: 'resetTokenSecret', - valid: [ - new Uint8Array(16), - new Uint16Array(8), - new Uint32Array(4), - ], - invalid: [ - 'a', null, false, true, {}, [], () => {}, - new Uint8Array(15), - new Uint8Array(17), - new ArrayBuffer(16), - ], - }, - { - key: 'tokenSecret', - valid: [ - new Uint8Array(16), - new Uint16Array(8), - new Uint32Array(4), - ], - invalid: [ - 'a', null, false, true, {}, [], () => {}, - new Uint8Array(15), - new Uint8Array(17), - new ArrayBuffer(16), - ], - }, - { - // Unknown options are ignored entirely for any value type - key: 'ignored', - valid: ['a', null, false, true, {}, [], () => {}], - invalid: [], - }, - ]; - - for (const { key, valid, invalid } of cases) { - for (const value of valid) { - const options = {}; - options[key] = value; - new QuicEndpoint(options); - } - - for (const value of invalid) { - const options = {}; - options[key] = value; - throws(() => new QuicEndpoint(options), { - message: new RegExp(`${key}`), - }, value); - } - } - }); - - it('endpoint can be inspected', async () => { - const endpoint = new QuicEndpoint({}); - strictEqual(typeof inspect(endpoint), 'string'); - endpoint.close(); - await endpoint.closed; - }); - - it('endpoint with object address', () => { - new QuicEndpoint({ - address: { host: '127.0.0.1:0' }, - }); - new QuicEndpoint({ - address: '127.0.0.1:0', - }); - throws(() => new QuicEndpoint({ address: 123 }), { - code: 'ERR_INVALID_ARG_TYPE', - }); - }); - -}); diff --git a/test/parallel/test-quic-internal-endpoint-options.mjs b/test/parallel/test-quic-internal-endpoint-options.mjs new file mode 100644 index 00000000000000..951ad3cf032f99 --- /dev/null +++ b/test/parallel/test-quic-internal-endpoint-options.mjs @@ -0,0 +1,171 @@ +// Flags: --experimental-quic --no-warnings +import { hasQuic, skip } from '../common/index.mjs'; +import { strictEqual, throws } from 'node:assert'; +import { inspect } from 'node:util'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +// Import after the hasQuic check +const { QuicEndpoint } = await import('node:quic'); + +// Reject invalid options +['a', null, false, NaN].forEach((i) => { + throws(() => new QuicEndpoint(i), { + code: 'ERR_INVALID_ARG_TYPE', + }); +}); + +// Just Works... using all defaults +new QuicEndpoint(); + +// Test various option values +const cases = [ + { + key: 'retryTokenExpiration', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'tokenExpiration', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxConnectionsPerHost', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxConnectionsTotal', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxStatelessResetsPerHost', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'addressLRUSize', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxRetries', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'validateAddress', + valid: [true, false, 0, 1, 'a'], + invalid: [], + }, + { + key: 'disableStatelessReset', + valid: [true, false, 0, 1, 'a'], + invalid: [], + }, + { + key: 'ipv6Only', + valid: [true, false, 0, 1, 'a'], + invalid: [], + }, + { + key: 'udpReceiveBufferSize', + valid: [0, 1, 2, 3, 4, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'udpSendBufferSize', + valid: [0, 1, 2, 3, 4, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'udpTTL', + valid: [0, 1, 2, 3, 4, 255], + invalid: [-1, 256, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'resetTokenSecret', + valid: [ + new Uint8Array(16), + new Uint16Array(8), + new Uint32Array(4), + ], + invalid: [ + 'a', null, false, true, {}, [], () => {}, + new Uint8Array(15), + new Uint8Array(17), + new ArrayBuffer(16), + ], + }, + { + key: 'tokenSecret', + valid: [ + new Uint8Array(16), + new Uint16Array(8), + new Uint32Array(4), + ], + invalid: [ + 'a', null, false, true, {}, [], () => {}, + new Uint8Array(15), + new Uint8Array(17), + new ArrayBuffer(16), + ], + }, + { + // Unknown options are ignored entirely for any value type + key: 'ignored', + valid: ['a', null, false, true, {}, [], () => {}], + invalid: [], + }, +]; + +for (const { key, valid, invalid } of cases) { + for (const value of valid) { + const options = {}; + options[key] = value; + new QuicEndpoint(options); + } + + for (const value of invalid) { + const options = {}; + options[key] = value; + throws(() => new QuicEndpoint(options), { + message: new RegExp(`${key}`), + }, value); + } +} + +// It can be inspected +const endpoint = new QuicEndpoint({}); +strictEqual(typeof inspect(endpoint), 'string'); +endpoint.close(); +await endpoint.closed; + +// Various address forms +new QuicEndpoint({ + address: { host: '127.0.0.1:0' }, +}); +new QuicEndpoint({ + address: '127.0.0.1:0', +}); +throws(() => new QuicEndpoint({ address: 123 }), { + code: 'ERR_INVALID_ARG_TYPE', +}); diff --git a/test/parallel/test-quic-internal-endpoint-stats-state.js b/test/parallel/test-quic-internal-endpoint-stats-state.js deleted file mode 100644 index 0565eaa979a3ed..00000000000000 --- a/test/parallel/test-quic-internal-endpoint-stats-state.js +++ /dev/null @@ -1,227 +0,0 @@ -// Flags: --expose-internals -'use strict'; - -const { hasQuic } = require('../common'); - -const { - describe, - it, -} = require('node:test'); - -describe('quic internal endpoint stats and state', { skip: !hasQuic }, () => { - const { - QuicEndpoint, - } = require('internal/quic/quic'); - - const { - QuicSessionState, - QuicStreamState, - } = require('internal/quic/state'); - - const { - QuicSessionStats, - QuicStreamStats, - } = require('internal/quic/stats'); - - const { - kFinishClose, - kPrivateConstructor, - kState, - } = require('internal/quic/symbols'); - - const { - inspect, - } = require('util'); - - const { - deepStrictEqual, - strictEqual, - throws, - } = require('node:assert'); - - it('endpoint state', () => { - const endpoint = new QuicEndpoint(); - - strictEqual(endpoint[kState].isBound, false); - strictEqual(endpoint[kState].isReceiving, false); - strictEqual(endpoint[kState].isListening, false); - strictEqual(endpoint[kState].isClosing, false); - strictEqual(endpoint[kState].isBusy, false); - strictEqual(endpoint[kState].pendingCallbacks, 0n); - - deepStrictEqual(JSON.parse(JSON.stringify(endpoint[kState])), { - isBound: false, - isReceiving: false, - isListening: false, - isClosing: false, - isBusy: false, - pendingCallbacks: '0', - }); - - endpoint.busy = true; - strictEqual(endpoint[kState].isBusy, true); - endpoint.busy = false; - strictEqual(endpoint[kState].isBusy, false); - - it('state can be inspected without errors', () => { - strictEqual(typeof inspect(endpoint[kState]), 'string'); - }); - }); - - it('state is not readable after close', () => { - const endpoint = new QuicEndpoint(); - endpoint[kState][kFinishClose](); - strictEqual(endpoint[kState].isBound, undefined); - }); - - it('state constructor argument is ArrayBuffer', () => { - const endpoint = new QuicEndpoint(); - const Cons = endpoint[kState].constructor; - throws(() => new Cons(kPrivateConstructor, 1), { - code: 'ERR_INVALID_ARG_TYPE' - }); - }); - - it('endpoint stats', () => { - const endpoint = new QuicEndpoint(); - - strictEqual(typeof endpoint.stats.isConnected, 'boolean'); - strictEqual(typeof endpoint.stats.createdAt, 'bigint'); - strictEqual(typeof endpoint.stats.destroyedAt, 'bigint'); - strictEqual(typeof endpoint.stats.bytesReceived, 'bigint'); - strictEqual(typeof endpoint.stats.bytesSent, 'bigint'); - strictEqual(typeof endpoint.stats.packetsReceived, 'bigint'); - strictEqual(typeof endpoint.stats.packetsSent, 'bigint'); - strictEqual(typeof endpoint.stats.serverSessions, 'bigint'); - strictEqual(typeof endpoint.stats.clientSessions, 'bigint'); - strictEqual(typeof endpoint.stats.serverBusyCount, 'bigint'); - strictEqual(typeof endpoint.stats.retryCount, 'bigint'); - strictEqual(typeof endpoint.stats.versionNegotiationCount, 'bigint'); - strictEqual(typeof endpoint.stats.statelessResetCount, 'bigint'); - strictEqual(typeof endpoint.stats.immediateCloseCount, 'bigint'); - - deepStrictEqual(Object.keys(endpoint.stats.toJSON()), [ - 'connected', - 'createdAt', - 'destroyedAt', - 'bytesReceived', - 'bytesSent', - 'packetsReceived', - 'packetsSent', - 'serverSessions', - 'clientSessions', - 'serverBusyCount', - 'retryCount', - 'versionNegotiationCount', - 'statelessResetCount', - 'immediateCloseCount', - ]); - - it('stats can be inspected without errors', () => { - strictEqual(typeof inspect(endpoint.stats), 'string'); - }); - }); - - it('stats are still readble after close', () => { - const endpoint = new QuicEndpoint(); - strictEqual(typeof endpoint.stats.toJSON(), 'object'); - endpoint.stats[kFinishClose](); - strictEqual(endpoint.stats.isConnected, false); - strictEqual(typeof endpoint.stats.destroyedAt, 'bigint'); - strictEqual(typeof endpoint.stats.toJSON(), 'object'); - }); - - it('stats constructor argument is ArrayBuffer', () => { - const endpoint = new QuicEndpoint(); - const Cons = endpoint.stats.constructor; - throws(() => new Cons(kPrivateConstructor, 1), { - code: 'ERR_INVALID_ARG_TYPE', - }); - }); - - // TODO(@jasnell): The following tests are largely incomplete. - // This is largely here to boost the code coverage numbers - // temporarily while the rest of the functionality is being - // implemented. - it('stream and session states', () => { - const streamState = new QuicStreamState(kPrivateConstructor, new ArrayBuffer(1024)); - const sessionState = new QuicSessionState(kPrivateConstructor, new ArrayBuffer(1024)); - - strictEqual(streamState.pending, false); - strictEqual(streamState.finSent, false); - strictEqual(streamState.finReceived, false); - strictEqual(streamState.readEnded, false); - strictEqual(streamState.writeEnded, false); - strictEqual(streamState.paused, false); - strictEqual(streamState.reset, false); - strictEqual(streamState.hasReader, false); - strictEqual(streamState.wantsBlock, false); - strictEqual(streamState.wantsReset, false); - - strictEqual(sessionState.hasPathValidationListener, false); - strictEqual(sessionState.hasVersionNegotiationListener, false); - strictEqual(sessionState.hasDatagramListener, false); - strictEqual(sessionState.hasSessionTicketListener, false); - strictEqual(sessionState.isClosing, false); - strictEqual(sessionState.isGracefulClose, false); - strictEqual(sessionState.isSilentClose, false); - strictEqual(sessionState.isStatelessReset, false); - strictEqual(sessionState.isHandshakeCompleted, false); - strictEqual(sessionState.isHandshakeConfirmed, false); - strictEqual(sessionState.isStreamOpenAllowed, false); - strictEqual(sessionState.isPrioritySupported, false); - strictEqual(sessionState.isWrapped, false); - strictEqual(sessionState.lastDatagramId, 0n); - - strictEqual(typeof streamState.toJSON(), 'object'); - strictEqual(typeof sessionState.toJSON(), 'object'); - strictEqual(typeof inspect(streamState), 'string'); - strictEqual(typeof inspect(sessionState), 'string'); - }); - - it('stream and session stats', () => { - const streamStats = new QuicStreamStats(kPrivateConstructor, new ArrayBuffer(1024)); - const sessionStats = new QuicSessionStats(kPrivateConstructor, new ArrayBuffer(1024)); - strictEqual(streamStats.createdAt, 0n); - strictEqual(streamStats.openedAt, 0n); - strictEqual(streamStats.receivedAt, 0n); - strictEqual(streamStats.ackedAt, 0n); - strictEqual(streamStats.destroyedAt, 0n); - strictEqual(streamStats.bytesReceived, 0n); - strictEqual(streamStats.bytesSent, 0n); - strictEqual(streamStats.maxOffset, 0n); - strictEqual(streamStats.maxOffsetAcknowledged, 0n); - strictEqual(streamStats.maxOffsetReceived, 0n); - strictEqual(streamStats.finalSize, 0n); - strictEqual(typeof streamStats.toJSON(), 'object'); - strictEqual(typeof inspect(streamStats), 'string'); - streamStats[kFinishClose](); - - strictEqual(typeof sessionStats.createdAt, 'bigint'); - strictEqual(typeof sessionStats.closingAt, 'bigint'); - strictEqual(typeof sessionStats.handshakeCompletedAt, 'bigint'); - strictEqual(typeof sessionStats.handshakeConfirmedAt, 'bigint'); - strictEqual(typeof sessionStats.bytesReceived, 'bigint'); - strictEqual(typeof sessionStats.bytesSent, 'bigint'); - strictEqual(typeof sessionStats.bidiInStreamCount, 'bigint'); - strictEqual(typeof sessionStats.bidiOutStreamCount, 'bigint'); - strictEqual(typeof sessionStats.uniInStreamCount, 'bigint'); - strictEqual(typeof sessionStats.uniOutStreamCount, 'bigint'); - strictEqual(typeof sessionStats.maxBytesInFlights, 'bigint'); - strictEqual(typeof sessionStats.bytesInFlight, 'bigint'); - strictEqual(typeof sessionStats.blockCount, 'bigint'); - strictEqual(typeof sessionStats.cwnd, 'bigint'); - strictEqual(typeof sessionStats.latestRtt, 'bigint'); - strictEqual(typeof sessionStats.minRtt, 'bigint'); - strictEqual(typeof sessionStats.rttVar, 'bigint'); - strictEqual(typeof sessionStats.smoothedRtt, 'bigint'); - strictEqual(typeof sessionStats.ssthresh, 'bigint'); - strictEqual(typeof sessionStats.datagramsReceived, 'bigint'); - strictEqual(typeof sessionStats.datagramsSent, 'bigint'); - strictEqual(typeof sessionStats.datagramsAcknowledged, 'bigint'); - strictEqual(typeof sessionStats.datagramsLost, 'bigint'); - strictEqual(typeof sessionStats.toJSON(), 'object'); - strictEqual(typeof inspect(sessionStats), 'string'); - streamStats[kFinishClose](); - }); -}); diff --git a/test/parallel/test-quic-internal-endpoint-stats-state.mjs b/test/parallel/test-quic-internal-endpoint-stats-state.mjs new file mode 100644 index 00000000000000..c5a06ced49b523 --- /dev/null +++ b/test/parallel/test-quic-internal-endpoint-stats-state.mjs @@ -0,0 +1,208 @@ +// Flags: --expose-internals --experimental-quic --no-warnings +import { hasQuic, skip } from '../common/index.mjs'; +import { inspect } from 'node:util'; +import { + deepStrictEqual, + strictEqual, + throws, +} from 'node:assert'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +const { QuicEndpoint } = await import('node:quic'); +const { + QuicSessionState, + QuicStreamState, +} = (await import('internal/quic/state')).default; +const { + QuicSessionStats, + QuicStreamStats, +} = (await import('internal/quic/stats')).default; +const { + kFinishClose, + kPrivateConstructor, + kState, +} = (await import('internal/quic/symbols')).default; + +{ + const endpoint = new QuicEndpoint(); + + strictEqual(endpoint[kState].isBound, false); + strictEqual(endpoint[kState].isReceiving, false); + strictEqual(endpoint[kState].isListening, false); + strictEqual(endpoint[kState].isClosing, false); + strictEqual(endpoint[kState].isBusy, false); + strictEqual(endpoint[kState].pendingCallbacks, 0n); + + deepStrictEqual(JSON.parse(JSON.stringify(endpoint[kState])), { + isBound: false, + isReceiving: false, + isListening: false, + isClosing: false, + isBusy: false, + pendingCallbacks: '0', + }); + + endpoint.busy = true; + strictEqual(endpoint[kState].isBusy, true); + endpoint.busy = false; + strictEqual(endpoint[kState].isBusy, false); + strictEqual(typeof inspect(endpoint[kState]), 'string'); +} + +{ + // It is not bound after close. + const endpoint = new QuicEndpoint(); + endpoint[kState][kFinishClose](); + strictEqual(endpoint[kState].isBound, undefined); +} + +{ + // State constructor argument is ArrayBuffer + const endpoint = new QuicEndpoint(); + const StateCons = endpoint[kState].constructor; + throws(() => new StateCons(kPrivateConstructor, 1), { + code: 'ERR_INVALID_ARG_TYPE' + }); +} + +{ + // Endpoint stats are readable and have expected properties + const endpoint = new QuicEndpoint(); + + strictEqual(typeof endpoint.stats.isConnected, 'boolean'); + strictEqual(typeof endpoint.stats.createdAt, 'bigint'); + strictEqual(typeof endpoint.stats.destroyedAt, 'bigint'); + strictEqual(typeof endpoint.stats.bytesReceived, 'bigint'); + strictEqual(typeof endpoint.stats.bytesSent, 'bigint'); + strictEqual(typeof endpoint.stats.packetsReceived, 'bigint'); + strictEqual(typeof endpoint.stats.packetsSent, 'bigint'); + strictEqual(typeof endpoint.stats.serverSessions, 'bigint'); + strictEqual(typeof endpoint.stats.clientSessions, 'bigint'); + strictEqual(typeof endpoint.stats.serverBusyCount, 'bigint'); + strictEqual(typeof endpoint.stats.retryCount, 'bigint'); + strictEqual(typeof endpoint.stats.versionNegotiationCount, 'bigint'); + strictEqual(typeof endpoint.stats.statelessResetCount, 'bigint'); + strictEqual(typeof endpoint.stats.immediateCloseCount, 'bigint'); + + deepStrictEqual(Object.keys(endpoint.stats.toJSON()), [ + 'connected', + 'createdAt', + 'destroyedAt', + 'bytesReceived', + 'bytesSent', + 'packetsReceived', + 'packetsSent', + 'serverSessions', + 'clientSessions', + 'serverBusyCount', + 'retryCount', + 'versionNegotiationCount', + 'statelessResetCount', + 'immediateCloseCount', + ]); + strictEqual(typeof inspect(endpoint.stats), 'string'); +} + +{ + // Stats are still readable after close + const endpoint = new QuicEndpoint(); + strictEqual(typeof endpoint.stats.toJSON(), 'object'); + endpoint.stats[kFinishClose](); + strictEqual(endpoint.stats.isConnected, false); + strictEqual(typeof endpoint.stats.destroyedAt, 'bigint'); + strictEqual(typeof endpoint.stats.toJSON(), 'object'); +} + +{ + // Stats constructor argument is ArrayBuffer + const endpoint = new QuicEndpoint(); + const StatsCons = endpoint.stats.constructor; + throws(() => new StatsCons(kPrivateConstructor, 1), { + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +// TODO(@jasnell): The following tests are largely incomplete. +// This is largely here to boost the code coverage numbers +// temporarily while the rest of the functionality is being +// implemented. +const streamState = new QuicStreamState(kPrivateConstructor, new ArrayBuffer(1024)); +const sessionState = new QuicSessionState(kPrivateConstructor, new ArrayBuffer(1024)); + +strictEqual(streamState.pending, false); +strictEqual(streamState.finSent, false); +strictEqual(streamState.finReceived, false); +strictEqual(streamState.readEnded, false); +strictEqual(streamState.writeEnded, false); +strictEqual(streamState.paused, false); +strictEqual(streamState.reset, false); +strictEqual(streamState.hasReader, false); +strictEqual(streamState.wantsBlock, false); +strictEqual(streamState.wantsReset, false); + +strictEqual(sessionState.hasPathValidationListener, false); +strictEqual(sessionState.hasVersionNegotiationListener, false); +strictEqual(sessionState.hasDatagramListener, false); +strictEqual(sessionState.hasSessionTicketListener, false); +strictEqual(sessionState.isClosing, false); +strictEqual(sessionState.isGracefulClose, false); +strictEqual(sessionState.isSilentClose, false); +strictEqual(sessionState.isStatelessReset, false); +strictEqual(sessionState.isHandshakeCompleted, false); +strictEqual(sessionState.isHandshakeConfirmed, false); +strictEqual(sessionState.isStreamOpenAllowed, false); +strictEqual(sessionState.isPrioritySupported, false); +strictEqual(sessionState.isWrapped, false); +strictEqual(sessionState.lastDatagramId, 0n); + +strictEqual(typeof streamState.toJSON(), 'object'); +strictEqual(typeof sessionState.toJSON(), 'object'); +strictEqual(typeof inspect(streamState), 'string'); +strictEqual(typeof inspect(sessionState), 'string'); + +const streamStats = new QuicStreamStats(kPrivateConstructor, new ArrayBuffer(1024)); +const sessionStats = new QuicSessionStats(kPrivateConstructor, new ArrayBuffer(1024)); +strictEqual(streamStats.createdAt, 0n); +strictEqual(streamStats.openedAt, 0n); +strictEqual(streamStats.receivedAt, 0n); +strictEqual(streamStats.ackedAt, 0n); +strictEqual(streamStats.destroyedAt, 0n); +strictEqual(streamStats.bytesReceived, 0n); +strictEqual(streamStats.bytesSent, 0n); +strictEqual(streamStats.maxOffset, 0n); +strictEqual(streamStats.maxOffsetAcknowledged, 0n); +strictEqual(streamStats.maxOffsetReceived, 0n); +strictEqual(streamStats.finalSize, 0n); +strictEqual(typeof streamStats.toJSON(), 'object'); +strictEqual(typeof inspect(streamStats), 'string'); +streamStats[kFinishClose](); + +strictEqual(typeof sessionStats.createdAt, 'bigint'); +strictEqual(typeof sessionStats.closingAt, 'bigint'); +strictEqual(typeof sessionStats.handshakeCompletedAt, 'bigint'); +strictEqual(typeof sessionStats.handshakeConfirmedAt, 'bigint'); +strictEqual(typeof sessionStats.bytesReceived, 'bigint'); +strictEqual(typeof sessionStats.bytesSent, 'bigint'); +strictEqual(typeof sessionStats.bidiInStreamCount, 'bigint'); +strictEqual(typeof sessionStats.bidiOutStreamCount, 'bigint'); +strictEqual(typeof sessionStats.uniInStreamCount, 'bigint'); +strictEqual(typeof sessionStats.uniOutStreamCount, 'bigint'); +strictEqual(typeof sessionStats.maxBytesInFlights, 'bigint'); +strictEqual(typeof sessionStats.bytesInFlight, 'bigint'); +strictEqual(typeof sessionStats.blockCount, 'bigint'); +strictEqual(typeof sessionStats.cwnd, 'bigint'); +strictEqual(typeof sessionStats.latestRtt, 'bigint'); +strictEqual(typeof sessionStats.minRtt, 'bigint'); +strictEqual(typeof sessionStats.rttVar, 'bigint'); +strictEqual(typeof sessionStats.smoothedRtt, 'bigint'); +strictEqual(typeof sessionStats.ssthresh, 'bigint'); +strictEqual(typeof sessionStats.datagramsReceived, 'bigint'); +strictEqual(typeof sessionStats.datagramsSent, 'bigint'); +strictEqual(typeof sessionStats.datagramsAcknowledged, 'bigint'); +strictEqual(typeof sessionStats.datagramsLost, 'bigint'); +strictEqual(typeof sessionStats.toJSON(), 'object'); +strictEqual(typeof inspect(sessionStats), 'string'); +streamStats[kFinishClose](); diff --git a/test/parallel/test-quic-internal-setcallbacks.js b/test/parallel/test-quic-internal-setcallbacks.js deleted file mode 100644 index c503f5f4f25691..00000000000000 --- a/test/parallel/test-quic-internal-setcallbacks.js +++ /dev/null @@ -1,47 +0,0 @@ -// Flags: --expose-internals --no-warnings -'use strict'; - -const { hasQuic } = require('../common'); - -const { - describe, - it, -} = require('node:test'); - -describe('quic internal setCallbacks', { skip: !hasQuic }, () => { - const { internalBinding } = require('internal/test/binding'); - const quic = internalBinding('quic'); - - it('require all callbacks to be set', (t) => { - const callbacks = { - onEndpointClose() {}, - onSessionNew() {}, - onSessionClose() {}, - onSessionDatagram() {}, - onSessionDatagramStatus() {}, - onSessionHandshake() {}, - onSessionPathValidation() {}, - onSessionTicket() {}, - onSessionVersionNegotiation() {}, - onStreamCreated() {}, - onStreamBlocked() {}, - onStreamClose() {}, - onStreamReset() {}, - onStreamHeaders() {}, - onStreamTrailers() {}, - }; - // Fail if any callback is missing - for (const fn of Object.keys(callbacks)) { - // eslint-disable-next-line no-unused-vars - const { [fn]: _, ...rest } = callbacks; - t.assert.throws(() => quic.setCallbacks(rest), { - code: 'ERR_MISSING_ARGS', - }); - } - // If all callbacks are present it should work - quic.setCallbacks(callbacks); - - // Multiple calls should just be ignored. - quic.setCallbacks(callbacks); - }); -}); diff --git a/test/parallel/test-quic-internal-setcallbacks.mjs b/test/parallel/test-quic-internal-setcallbacks.mjs new file mode 100644 index 00000000000000..4626b160aaec1a --- /dev/null +++ b/test/parallel/test-quic-internal-setcallbacks.mjs @@ -0,0 +1,41 @@ +// Flags: --expose-internals --experimental-quic --no-warnings +import { hasQuic, skip } from '../common/index.mjs'; +import { throws } from 'node:assert'; + +if (!hasQuic) { + skip('QUIC is not enabled'); +} + +import { default as binding } from 'internal/test/binding'; +const quic = binding.internalBinding('quic'); + +const callbacks = { + onEndpointClose() {}, + onSessionNew() {}, + onSessionClose() {}, + onSessionDatagram() {}, + onSessionDatagramStatus() {}, + onSessionHandshake() {}, + onSessionPathValidation() {}, + onSessionTicket() {}, + onSessionVersionNegotiation() {}, + onStreamCreated() {}, + onStreamBlocked() {}, + onStreamClose() {}, + onStreamReset() {}, + onStreamHeaders() {}, + onStreamTrailers() {}, +}; +// Fail if any callback is missing +for (const fn of Object.keys(callbacks)) { + // eslint-disable-next-line no-unused-vars + const { [fn]: _, ...rest } = callbacks; + throws(() => quic.setCallbacks(rest), { + code: 'ERR_MISSING_ARGS', + }); +} +// If all callbacks are present it should work +quic.setCallbacks(callbacks); + +// Multiple calls should just be ignored. +quic.setCallbacks(callbacks);